From 037f6947d88f0754b15d156180cdffb053a25b1a Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 18 May 2022 16:52:46 +0200 Subject: [PATCH] Fail recorder setup with unsupported dialect or version (#70888) --- homeassistant/components/recorder/core.py | 3 ++ homeassistant/components/recorder/models.py | 4 ++ homeassistant/components/recorder/util.py | 32 ++++++++-------- tests/components/recorder/test_util.py | 42 ++++++++++++--------- 4 files changed, 47 insertions(+), 34 deletions(-) diff --git a/homeassistant/components/recorder/core.py b/homeassistant/components/recorder/core.py index 91dc0a27ead..5a3a41568a1 100644 --- a/homeassistant/components/recorder/core.py +++ b/homeassistant/components/recorder/core.py @@ -56,6 +56,7 @@ from .models import ( StatisticData, StatisticMetaData, StatisticsRuns, + UnsupportedDialect, process_timestamp, ) from .pool import POOL_SIZE, MutexPool, RecorderPool @@ -606,6 +607,8 @@ class Recorder(threading.Thread): try: self._setup_connection() return migration.get_schema_version(self.get_session) + except UnsupportedDialect: + break except Exception as err: # pylint: disable=broad-except _LOGGER.exception( "Error during connection setup: %s (retrying in %s seconds)", diff --git a/homeassistant/components/recorder/models.py b/homeassistant/components/recorder/models.py index 9d16541e398..4dabd7899e0 100644 --- a/homeassistant/components/recorder/models.py +++ b/homeassistant/components/recorder/models.py @@ -123,6 +123,10 @@ EVENT_ORIGIN_ORDER = [EventOrigin.local, EventOrigin.remote] EVENT_ORIGIN_TO_IDX = {origin: idx for idx, origin in enumerate(EVENT_ORIGIN_ORDER)} +class UnsupportedDialect(Exception): + """The dialect or its version is not supported.""" + + class Events(Base): # type: ignore[misc,valid-type] """Event history data.""" diff --git a/homeassistant/components/recorder/util.py b/homeassistant/components/recorder/util.py index f086119f7f9..ce8812a653e 100644 --- a/homeassistant/components/recorder/util.py +++ b/homeassistant/components/recorder/util.py @@ -34,6 +34,7 @@ from .models import ( TABLE_SCHEMA_CHANGES, TABLES_TO_CHECK, RecorderRuns, + UnsupportedDialect, process_timestamp, ) @@ -328,29 +329,31 @@ def query_on_connection(dbapi_connection: Any, statement: str) -> Any: return result -def _warn_unsupported_dialect(dialect_name: str) -> None: +def _fail_unsupported_dialect(dialect_name: str) -> None: """Warn about unsupported database version.""" - _LOGGER.warning( + _LOGGER.error( "Database %s is not supported; Home Assistant supports %s. " - "Starting with Home Assistant 2022.2 this will prevent the recorder from " - "starting. Please migrate your database to a supported software before then", + "Starting with Home Assistant 2022.6 this prevents the recorder from " + "starting. Please migrate your database to a supported software", dialect_name, "MariaDB ≥ 10.3, MySQL ≥ 8.0, PostgreSQL ≥ 12, SQLite ≥ 3.31.0", ) + raise UnsupportedDialect -def _warn_unsupported_version( +def _fail_unsupported_version( server_version: str, dialect_name: str, minimum_version: str ) -> None: """Warn about unsupported database version.""" - _LOGGER.warning( + _LOGGER.error( "Version %s of %s is not supported; minimum supported version is %s. " - "Starting with Home Assistant 2022.2 this will prevent the recorder from " - "starting. Please upgrade your database software before then", + "Starting with Home Assistant 2022.6 this prevents the recorder from " + "starting. Please upgrade your database software", server_version, dialect_name, minimum_version, ) + raise UnsupportedDialect def _extract_version_from_server_response( @@ -398,9 +401,6 @@ def setup_connection_for_dialect( first_connection: bool, ) -> None: """Execute statements needed for dialect connection.""" - # Returns False if the the connection needs to be setup - # on the next connection, returns True if the connection - # never needs to be setup again. if dialect_name == SupportedDialect.SQLITE: if first_connection: old_isolation = dbapi_connection.isolation_level @@ -419,7 +419,7 @@ def setup_connection_for_dialect( False ) if not version or version < MIN_VERSION_SQLITE: - _warn_unsupported_version( + _fail_unsupported_version( version or version_string, "SQLite", MIN_VERSION_SQLITE ) @@ -453,7 +453,7 @@ def setup_connection_for_dialect( False ) if not version or version < MIN_VERSION_MARIA_DB: - _warn_unsupported_version( + _fail_unsupported_version( version or version_string, "MariaDB", MIN_VERSION_MARIA_DB ) else: @@ -462,7 +462,7 @@ def setup_connection_for_dialect( False ) if not version or version < MIN_VERSION_MYSQL: - _warn_unsupported_version( + _fail_unsupported_version( version or version_string, "MySQL", MIN_VERSION_MYSQL ) @@ -473,12 +473,12 @@ def setup_connection_for_dialect( version_string = result[0][0] version = _extract_version_from_server_response(version_string) if not version or version < MIN_VERSION_PGSQL: - _warn_unsupported_version( + _fail_unsupported_version( version or version_string, "PostgreSQL", MIN_VERSION_PGSQL ) else: - _warn_unsupported_dialect(dialect_name) + _fail_unsupported_dialect(dialect_name) def end_incomplete_runs(session: Session, start_time: datetime) -> None: diff --git a/tests/components/recorder/test_util.py b/tests/components/recorder/test_util.py index fea255647ec..e7685a93ff2 100644 --- a/tests/components/recorder/test_util.py +++ b/tests/components/recorder/test_util.py @@ -14,7 +14,7 @@ from sqlalchemy.sql.lambdas import StatementLambdaElement from homeassistant.components import recorder from homeassistant.components.recorder import history, util from homeassistant.components.recorder.const import DATA_INSTANCE, SQLITE_URL_PREFIX -from homeassistant.components.recorder.models import RecorderRuns +from homeassistant.components.recorder.models import RecorderRuns, UnsupportedDialect from homeassistant.components.recorder.util import ( end_incomplete_runs, is_second_sunday, @@ -168,10 +168,8 @@ async def test_last_run_was_recently_clean( @pytest.mark.parametrize( "mysql_version, db_supports_row_number", [ - ("10.2.0-MariaDB", True), - ("10.1.0-MariaDB", False), - ("5.8.0", True), - ("5.7.0", False), + ("10.3.0-MariaDB", True), + ("8.0.0", True), ], ) def test_setup_connection_for_dialect_mysql(mysql_version, db_supports_row_number): @@ -207,8 +205,7 @@ def test_setup_connection_for_dialect_mysql(mysql_version, db_supports_row_numbe @pytest.mark.parametrize( "sqlite_version, db_supports_row_number", [ - ("3.25.0", True), - ("3.24.0", False), + ("3.31.0", True), ], ) def test_setup_connection_for_dialect_sqlite(sqlite_version, db_supports_row_number): @@ -255,8 +252,7 @@ def test_setup_connection_for_dialect_sqlite(sqlite_version, db_supports_row_num @pytest.mark.parametrize( "sqlite_version, db_supports_row_number", [ - ("3.25.0", True), - ("3.24.0", False), + ("3.31.0", True), ], ) def test_setup_connection_for_dialect_sqlite_zero_commit_interval( @@ -319,7 +315,7 @@ def test_setup_connection_for_dialect_sqlite_zero_commit_interval( ), ], ) -def test_warn_outdated_mysql(caplog, mysql_version, message): +def test_fail_outdated_mysql(caplog, mysql_version, message): """Test setting up the connection for an outdated mysql version.""" instance_mock = MagicMock(_db_supports_row_number=True) execute_args = [] @@ -340,7 +336,10 @@ def test_warn_outdated_mysql(caplog, mysql_version, message): dbapi_connection = MagicMock(cursor=_make_cursor_mock) - util.setup_connection_for_dialect(instance_mock, "mysql", dbapi_connection, True) + with pytest.raises(UnsupportedDialect): + util.setup_connection_for_dialect( + instance_mock, "mysql", dbapi_connection, True + ) assert message in caplog.text @@ -395,7 +394,7 @@ def test_supported_mysql(caplog, mysql_version): ), ], ) -def test_warn_outdated_pgsql(caplog, pgsql_version, message): +def test_fail_outdated_pgsql(caplog, pgsql_version, message): """Test setting up the connection for an outdated PostgreSQL version.""" instance_mock = MagicMock(_db_supports_row_number=True) execute_args = [] @@ -416,9 +415,10 @@ def test_warn_outdated_pgsql(caplog, pgsql_version, message): dbapi_connection = MagicMock(cursor=_make_cursor_mock) - util.setup_connection_for_dialect( - instance_mock, "postgresql", dbapi_connection, True - ) + with pytest.raises(UnsupportedDialect): + util.setup_connection_for_dialect( + instance_mock, "postgresql", dbapi_connection, True + ) assert message in caplog.text @@ -472,7 +472,7 @@ def test_supported_pgsql(caplog, pgsql_version): ), ], ) -def test_warn_outdated_sqlite(caplog, sqlite_version, message): +def test_fail_outdated_sqlite(caplog, sqlite_version, message): """Test setting up the connection for an outdated sqlite version.""" instance_mock = MagicMock(_db_supports_row_number=True) execute_args = [] @@ -493,7 +493,10 @@ def test_warn_outdated_sqlite(caplog, sqlite_version, message): dbapi_connection = MagicMock(cursor=_make_cursor_mock) - util.setup_connection_for_dialect(instance_mock, "sqlite", dbapi_connection, True) + with pytest.raises(UnsupportedDialect): + util.setup_connection_for_dialect( + instance_mock, "sqlite", dbapi_connection, True + ) assert message in caplog.text @@ -544,7 +547,10 @@ def test_warn_unsupported_dialect(caplog, dialect, message): instance_mock = MagicMock() dbapi_connection = MagicMock() - util.setup_connection_for_dialect(instance_mock, dialect, dbapi_connection, True) + with pytest.raises(UnsupportedDialect): + util.setup_connection_for_dialect( + instance_mock, dialect, dbapi_connection, True + ) assert message in caplog.text