Fail recorder setup with unsupported dialect or version (#70888)

This commit is contained in:
Erik Montnemery 2022-05-18 16:52:46 +02:00 committed by GitHub
parent f3c582815c
commit 037f6947d8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 47 additions and 34 deletions

View file

@ -56,6 +56,7 @@ from .models import (
StatisticData, StatisticData,
StatisticMetaData, StatisticMetaData,
StatisticsRuns, StatisticsRuns,
UnsupportedDialect,
process_timestamp, process_timestamp,
) )
from .pool import POOL_SIZE, MutexPool, RecorderPool from .pool import POOL_SIZE, MutexPool, RecorderPool
@ -606,6 +607,8 @@ class Recorder(threading.Thread):
try: try:
self._setup_connection() self._setup_connection()
return migration.get_schema_version(self.get_session) return migration.get_schema_version(self.get_session)
except UnsupportedDialect:
break
except Exception as err: # pylint: disable=broad-except except Exception as err: # pylint: disable=broad-except
_LOGGER.exception( _LOGGER.exception(
"Error during connection setup: %s (retrying in %s seconds)", "Error during connection setup: %s (retrying in %s seconds)",

View file

@ -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)} 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] class Events(Base): # type: ignore[misc,valid-type]
"""Event history data.""" """Event history data."""

View file

@ -34,6 +34,7 @@ from .models import (
TABLE_SCHEMA_CHANGES, TABLE_SCHEMA_CHANGES,
TABLES_TO_CHECK, TABLES_TO_CHECK,
RecorderRuns, RecorderRuns,
UnsupportedDialect,
process_timestamp, process_timestamp,
) )
@ -328,29 +329,31 @@ def query_on_connection(dbapi_connection: Any, statement: str) -> Any:
return result return result
def _warn_unsupported_dialect(dialect_name: str) -> None: def _fail_unsupported_dialect(dialect_name: str) -> None:
"""Warn about unsupported database version.""" """Warn about unsupported database version."""
_LOGGER.warning( _LOGGER.error(
"Database %s is not supported; Home Assistant supports %s. " "Database %s is not supported; Home Assistant supports %s. "
"Starting with Home Assistant 2022.2 this will prevent the recorder from " "Starting with Home Assistant 2022.6 this prevents the recorder from "
"starting. Please migrate your database to a supported software before then", "starting. Please migrate your database to a supported software",
dialect_name, dialect_name,
"MariaDB ≥ 10.3, MySQL ≥ 8.0, PostgreSQL ≥ 12, SQLite ≥ 3.31.0", "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 server_version: str, dialect_name: str, minimum_version: str
) -> None: ) -> None:
"""Warn about unsupported database version.""" """Warn about unsupported database version."""
_LOGGER.warning( _LOGGER.error(
"Version %s of %s is not supported; minimum supported version is %s. " "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 with Home Assistant 2022.6 this prevents the recorder from "
"starting. Please upgrade your database software before then", "starting. Please upgrade your database software",
server_version, server_version,
dialect_name, dialect_name,
minimum_version, minimum_version,
) )
raise UnsupportedDialect
def _extract_version_from_server_response( def _extract_version_from_server_response(
@ -398,9 +401,6 @@ def setup_connection_for_dialect(
first_connection: bool, first_connection: bool,
) -> None: ) -> None:
"""Execute statements needed for dialect connection.""" """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 dialect_name == SupportedDialect.SQLITE:
if first_connection: if first_connection:
old_isolation = dbapi_connection.isolation_level old_isolation = dbapi_connection.isolation_level
@ -419,7 +419,7 @@ def setup_connection_for_dialect(
False False
) )
if not version or version < MIN_VERSION_SQLITE: if not version or version < MIN_VERSION_SQLITE:
_warn_unsupported_version( _fail_unsupported_version(
version or version_string, "SQLite", MIN_VERSION_SQLITE version or version_string, "SQLite", MIN_VERSION_SQLITE
) )
@ -453,7 +453,7 @@ def setup_connection_for_dialect(
False False
) )
if not version or version < MIN_VERSION_MARIA_DB: if not version or version < MIN_VERSION_MARIA_DB:
_warn_unsupported_version( _fail_unsupported_version(
version or version_string, "MariaDB", MIN_VERSION_MARIA_DB version or version_string, "MariaDB", MIN_VERSION_MARIA_DB
) )
else: else:
@ -462,7 +462,7 @@ def setup_connection_for_dialect(
False False
) )
if not version or version < MIN_VERSION_MYSQL: if not version or version < MIN_VERSION_MYSQL:
_warn_unsupported_version( _fail_unsupported_version(
version or version_string, "MySQL", MIN_VERSION_MYSQL version or version_string, "MySQL", MIN_VERSION_MYSQL
) )
@ -473,12 +473,12 @@ def setup_connection_for_dialect(
version_string = result[0][0] version_string = result[0][0]
version = _extract_version_from_server_response(version_string) version = _extract_version_from_server_response(version_string)
if not version or version < MIN_VERSION_PGSQL: if not version or version < MIN_VERSION_PGSQL:
_warn_unsupported_version( _fail_unsupported_version(
version or version_string, "PostgreSQL", MIN_VERSION_PGSQL version or version_string, "PostgreSQL", MIN_VERSION_PGSQL
) )
else: else:
_warn_unsupported_dialect(dialect_name) _fail_unsupported_dialect(dialect_name)
def end_incomplete_runs(session: Session, start_time: datetime) -> None: def end_incomplete_runs(session: Session, start_time: datetime) -> None:

View file

@ -14,7 +14,7 @@ from sqlalchemy.sql.lambdas import StatementLambdaElement
from homeassistant.components import recorder from homeassistant.components import recorder
from homeassistant.components.recorder import history, util from homeassistant.components.recorder import history, util
from homeassistant.components.recorder.const import DATA_INSTANCE, SQLITE_URL_PREFIX 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 ( from homeassistant.components.recorder.util import (
end_incomplete_runs, end_incomplete_runs,
is_second_sunday, is_second_sunday,
@ -168,10 +168,8 @@ async def test_last_run_was_recently_clean(
@pytest.mark.parametrize( @pytest.mark.parametrize(
"mysql_version, db_supports_row_number", "mysql_version, db_supports_row_number",
[ [
("10.2.0-MariaDB", True), ("10.3.0-MariaDB", True),
("10.1.0-MariaDB", False), ("8.0.0", True),
("5.8.0", True),
("5.7.0", False),
], ],
) )
def test_setup_connection_for_dialect_mysql(mysql_version, db_supports_row_number): 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( @pytest.mark.parametrize(
"sqlite_version, db_supports_row_number", "sqlite_version, db_supports_row_number",
[ [
("3.25.0", True), ("3.31.0", True),
("3.24.0", False),
], ],
) )
def test_setup_connection_for_dialect_sqlite(sqlite_version, db_supports_row_number): 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( @pytest.mark.parametrize(
"sqlite_version, db_supports_row_number", "sqlite_version, db_supports_row_number",
[ [
("3.25.0", True), ("3.31.0", True),
("3.24.0", False),
], ],
) )
def test_setup_connection_for_dialect_sqlite_zero_commit_interval( 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.""" """Test setting up the connection for an outdated mysql version."""
instance_mock = MagicMock(_db_supports_row_number=True) instance_mock = MagicMock(_db_supports_row_number=True)
execute_args = [] execute_args = []
@ -340,7 +336,10 @@ def test_warn_outdated_mysql(caplog, mysql_version, message):
dbapi_connection = MagicMock(cursor=_make_cursor_mock) 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 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.""" """Test setting up the connection for an outdated PostgreSQL version."""
instance_mock = MagicMock(_db_supports_row_number=True) instance_mock = MagicMock(_db_supports_row_number=True)
execute_args = [] execute_args = []
@ -416,9 +415,10 @@ def test_warn_outdated_pgsql(caplog, pgsql_version, message):
dbapi_connection = MagicMock(cursor=_make_cursor_mock) dbapi_connection = MagicMock(cursor=_make_cursor_mock)
util.setup_connection_for_dialect( with pytest.raises(UnsupportedDialect):
instance_mock, "postgresql", dbapi_connection, True util.setup_connection_for_dialect(
) instance_mock, "postgresql", dbapi_connection, True
)
assert message in caplog.text 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.""" """Test setting up the connection for an outdated sqlite version."""
instance_mock = MagicMock(_db_supports_row_number=True) instance_mock = MagicMock(_db_supports_row_number=True)
execute_args = [] execute_args = []
@ -493,7 +493,10 @@ def test_warn_outdated_sqlite(caplog, sqlite_version, message):
dbapi_connection = MagicMock(cursor=_make_cursor_mock) 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 assert message in caplog.text
@ -544,7 +547,10 @@ def test_warn_unsupported_dialect(caplog, dialect, message):
instance_mock = MagicMock() instance_mock = MagicMock()
dbapi_connection = 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 assert message in caplog.text