mirror of
https://github.com/home-assistant/core.git
synced 2025-07-24 21:57:51 +00:00
Improve error handling when recorder schema migration fails (#122397)
This commit is contained in:
parent
02c34ba3f8
commit
76cd53a864
@ -725,6 +725,10 @@ class Recorder(threading.Thread):
|
|||||||
"recorder_database_migration",
|
"recorder_database_migration",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _dismiss_migration_in_progress(self) -> None:
|
||||||
|
"""Dismiss notification about migration in progress."""
|
||||||
|
persistent_notification.dismiss(self.hass, "recorder_database_migration")
|
||||||
|
|
||||||
def _run(self) -> None:
|
def _run(self) -> None:
|
||||||
"""Start processing events to save."""
|
"""Start processing events to save."""
|
||||||
thread_id = threading.get_ident()
|
thread_id = threading.get_ident()
|
||||||
@ -787,9 +791,16 @@ class Recorder(threading.Thread):
|
|||||||
# was True, we need to reinitialize the listener.
|
# was True, we need to reinitialize the listener.
|
||||||
self.hass.add_job(self.async_initialize)
|
self.hass.add_job(self.async_initialize)
|
||||||
else:
|
else:
|
||||||
|
self.migration_in_progress = False
|
||||||
|
self._dismiss_migration_in_progress()
|
||||||
self._notify_migration_failed()
|
self._notify_migration_failed()
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Schema migration and repair is now completed
|
||||||
|
if self.migration_in_progress:
|
||||||
|
self.migration_in_progress = False
|
||||||
|
self._dismiss_migration_in_progress()
|
||||||
|
|
||||||
# Catch up with missed statistics
|
# Catch up with missed statistics
|
||||||
self._schedule_compile_missing_statistics()
|
self._schedule_compile_missing_statistics()
|
||||||
_LOGGER.debug("Recorder processing the queue")
|
_LOGGER.debug("Recorder processing the queue")
|
||||||
@ -984,14 +995,10 @@ class Recorder(threading.Thread):
|
|||||||
"recorder_database_migration",
|
"recorder_database_migration",
|
||||||
)
|
)
|
||||||
self.hass.add_job(self._async_migration_started)
|
self.hass.add_job(self._async_migration_started)
|
||||||
try:
|
migration_result, schema_status = self._migrate_schema(schema_status, True)
|
||||||
migration_result, schema_status = self._migrate_schema(schema_status, True)
|
if migration_result:
|
||||||
if migration_result:
|
self._setup_run()
|
||||||
self._setup_run()
|
return migration_result, schema_status
|
||||||
return migration_result, schema_status
|
|
||||||
finally:
|
|
||||||
self.migration_in_progress = False
|
|
||||||
persistent_notification.dismiss(self.hass, "recorder_database_migration")
|
|
||||||
|
|
||||||
def _migrate_schema(
|
def _migrate_schema(
|
||||||
self,
|
self,
|
||||||
@ -1010,7 +1017,15 @@ class Recorder(threading.Thread):
|
|||||||
)
|
)
|
||||||
except exc.DatabaseError as err:
|
except exc.DatabaseError as err:
|
||||||
if self._handle_database_error(err):
|
if self._handle_database_error(err):
|
||||||
return (True, schema_status)
|
# If _handle_database_error returns True, we have a new database
|
||||||
|
# which does not need migration or repair.
|
||||||
|
new_schema_status = migration.SchemaValidationStatus(
|
||||||
|
current_version=SCHEMA_VERSION,
|
||||||
|
migration_needed=False,
|
||||||
|
schema_errors=set(),
|
||||||
|
start_version=SCHEMA_VERSION,
|
||||||
|
)
|
||||||
|
return (True, new_schema_status)
|
||||||
_LOGGER.exception("Database error during schema migration")
|
_LOGGER.exception("Database error during schema migration")
|
||||||
return (False, schema_status)
|
return (False, schema_status)
|
||||||
except Exception:
|
except Exception:
|
||||||
|
@ -160,7 +160,7 @@ async def test_database_migration_failed(
|
|||||||
|
|
||||||
@pytest.mark.skip_on_db_engine(["mysql", "postgresql"])
|
@pytest.mark.skip_on_db_engine(["mysql", "postgresql"])
|
||||||
@pytest.mark.usefixtures("skip_by_db_engine")
|
@pytest.mark.usefixtures("skip_by_db_engine")
|
||||||
async def test_database_migration_encounters_corruption(
|
async def test_live_database_migration_encounters_corruption(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
recorder_db_url: str,
|
recorder_db_url: str,
|
||||||
async_setup_recorder_instance: RecorderInstanceGenerator,
|
async_setup_recorder_instance: RecorderInstanceGenerator,
|
||||||
@ -183,6 +183,51 @@ async def test_database_migration_encounters_corruption(
|
|||||||
"homeassistant.components.recorder.migration._schema_is_current",
|
"homeassistant.components.recorder.migration._schema_is_current",
|
||||||
side_effect=[False],
|
side_effect=[False],
|
||||||
),
|
),
|
||||||
|
patch(
|
||||||
|
"homeassistant.components.recorder.migration.migrate_schema_live",
|
||||||
|
side_effect=sqlite3_exception,
|
||||||
|
),
|
||||||
|
patch(
|
||||||
|
"homeassistant.components.recorder.core.move_away_broken_database"
|
||||||
|
) as move_away,
|
||||||
|
):
|
||||||
|
await async_setup_recorder_instance(hass)
|
||||||
|
hass.states.async_set("my.entity", "on", {})
|
||||||
|
hass.states.async_set("my.entity", "off", {})
|
||||||
|
await async_wait_recording_done(hass)
|
||||||
|
|
||||||
|
assert recorder.util.async_migration_in_progress(hass) is False
|
||||||
|
move_away.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skip_on_db_engine(["mysql", "postgresql"])
|
||||||
|
@pytest.mark.usefixtures("skip_by_db_engine")
|
||||||
|
async def test_non_live_database_migration_encounters_corruption(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
recorder_db_url: str,
|
||||||
|
async_setup_recorder_instance: RecorderInstanceGenerator,
|
||||||
|
) -> None:
|
||||||
|
"""Test we move away the database if its corrupt.
|
||||||
|
|
||||||
|
This test is specific for SQLite, wiping the database on error only happens
|
||||||
|
with SQLite.
|
||||||
|
"""
|
||||||
|
|
||||||
|
assert recorder.util.async_migration_in_progress(hass) is False
|
||||||
|
|
||||||
|
sqlite3_exception = DatabaseError("statement", {}, [])
|
||||||
|
sqlite3_exception.__cause__ = sqlite3.DatabaseError(
|
||||||
|
"database disk image is malformed"
|
||||||
|
)
|
||||||
|
|
||||||
|
with (
|
||||||
|
patch(
|
||||||
|
"homeassistant.components.recorder.migration._schema_is_current",
|
||||||
|
side_effect=[False],
|
||||||
|
),
|
||||||
|
patch(
|
||||||
|
"homeassistant.components.recorder.migration.migrate_schema_live",
|
||||||
|
) as migrate_schema_live,
|
||||||
patch(
|
patch(
|
||||||
"homeassistant.components.recorder.migration.migrate_schema_non_live",
|
"homeassistant.components.recorder.migration.migrate_schema_non_live",
|
||||||
side_effect=sqlite3_exception,
|
side_effect=sqlite3_exception,
|
||||||
@ -197,7 +242,8 @@ async def test_database_migration_encounters_corruption(
|
|||||||
await async_wait_recording_done(hass)
|
await async_wait_recording_done(hass)
|
||||||
|
|
||||||
assert recorder.util.async_migration_in_progress(hass) is False
|
assert recorder.util.async_migration_in_progress(hass) is False
|
||||||
assert move_away.called
|
move_away.assert_called_once()
|
||||||
|
migrate_schema_live.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
|
Loading…
x
Reference in New Issue
Block a user