Support non-live database migration (#72433)

* Support non-live database migration

* Tweak startup order, add test

* Address review comments

* Fix typo

* Clarify comment about promoting dependencies

* Tweak

* Fix merge mistake

* Fix some tests

* Fix additional test

* Fix additional test

* Adjust tests

* Improve test coverage
This commit is contained in:
Erik Montnemery 2022-07-22 15:11:34 +02:00 committed by GitHub
parent 9d0a252ca7
commit fd6ffef52f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 993 additions and 61 deletions

View file

@ -27,6 +27,7 @@ from homeassistant.components.recorder.db_schema import (
States,
)
from homeassistant.components.recorder.util import session_scope
from homeassistant.helpers import recorder as recorder_helper
import homeassistant.util.dt as dt_util
from .common import async_wait_recording_done, create_engine_test
@ -53,6 +54,7 @@ async def test_schema_update_calls(hass):
"homeassistant.components.recorder.migration._apply_update",
wraps=migration._apply_update,
) as update:
recorder_helper.async_initialize_recorder(hass)
await async_setup_component(
hass, "recorder", {"recorder": {"db_url": "sqlite://"}}
)
@ -74,10 +76,11 @@ async def test_migration_in_progress(hass):
"""Test that we can check for migration in progress."""
assert recorder.util.async_migration_in_progress(hass) is False
with patch("homeassistant.components.recorder.ALLOW_IN_MEMORY_DB", True,), patch(
with patch("homeassistant.components.recorder.ALLOW_IN_MEMORY_DB", True), patch(
"homeassistant.components.recorder.core.create_engine",
new=create_engine_test,
):
recorder_helper.async_initialize_recorder(hass)
await async_setup_component(
hass, "recorder", {"recorder": {"db_url": "sqlite://"}}
)
@ -105,6 +108,7 @@ async def test_database_migration_failed(hass):
"homeassistant.components.persistent_notification.dismiss",
side_effect=pn.dismiss,
) as mock_dismiss:
recorder_helper.async_initialize_recorder(hass)
await async_setup_component(
hass, "recorder", {"recorder": {"db_url": "sqlite://"}}
)
@ -136,6 +140,7 @@ async def test_database_migration_encounters_corruption(hass):
), patch(
"homeassistant.components.recorder.core.move_away_broken_database"
) as move_away:
recorder_helper.async_initialize_recorder(hass)
await async_setup_component(
hass, "recorder", {"recorder": {"db_url": "sqlite://"}}
)
@ -165,6 +170,7 @@ async def test_database_migration_encounters_corruption_not_sqlite(hass):
"homeassistant.components.persistent_notification.dismiss",
side_effect=pn.dismiss,
) as mock_dismiss:
recorder_helper.async_initialize_recorder(hass)
await async_setup_component(
hass, "recorder", {"recorder": {"db_url": "sqlite://"}}
)
@ -189,6 +195,7 @@ async def test_events_during_migration_are_queued(hass):
"homeassistant.components.recorder.core.create_engine",
new=create_engine_test,
):
recorder_helper.async_initialize_recorder(hass)
await async_setup_component(
hass,
"recorder",
@ -219,6 +226,7 @@ async def test_events_during_migration_queue_exhausted(hass):
"homeassistant.components.recorder.core.create_engine",
new=create_engine_test,
), patch.object(recorder.core, "MAX_QUEUE_BACKLOG", 1):
recorder_helper.async_initialize_recorder(hass)
await async_setup_component(
hass,
"recorder",
@ -247,8 +255,11 @@ async def test_events_during_migration_queue_exhausted(hass):
assert len(db_states) == 2
@pytest.mark.parametrize("start_version", [0, 16, 18, 22])
async def test_schema_migrate(hass, start_version):
@pytest.mark.parametrize(
"start_version,live",
[(0, True), (16, True), (18, True), (22, True), (25, True)],
)
async def test_schema_migrate(hass, start_version, live):
"""Test the full schema migration logic.
We're just testing that the logic can execute successfully here without
@ -259,7 +270,8 @@ async def test_schema_migrate(hass, start_version):
migration_done = threading.Event()
migration_stall = threading.Event()
migration_version = None
real_migration = recorder.migration.migrate_schema
real_migrate_schema = recorder.migration.migrate_schema
real_apply_update = recorder.migration._apply_update
def _create_engine_test(*args, **kwargs):
"""Test version of create_engine that initializes with old schema.
@ -284,14 +296,12 @@ async def test_schema_migrate(hass, start_version):
start=self.run_history.recording_start, created=dt_util.utcnow()
)
def _instrument_migration(*args):
def _instrument_migrate_schema(*args):
"""Control migration progress and check results."""
nonlocal migration_done
nonlocal migration_version
nonlocal migration_stall
migration_stall.wait()
try:
real_migration(*args)
real_migrate_schema(*args)
except Exception:
migration_done.set()
raise
@ -307,6 +317,12 @@ async def test_schema_migrate(hass, start_version):
migration_version = res.schema_version
migration_done.set()
def _instrument_apply_update(*args):
"""Control migration progress."""
nonlocal migration_stall
migration_stall.wait()
real_apply_update(*args)
with patch("homeassistant.components.recorder.ALLOW_IN_MEMORY_DB", True), patch(
"homeassistant.components.recorder.core.create_engine",
new=_create_engine_test,
@ -316,12 +332,21 @@ async def test_schema_migrate(hass, start_version):
autospec=True,
) as setup_run, patch(
"homeassistant.components.recorder.migration.migrate_schema",
wraps=_instrument_migration,
wraps=_instrument_migrate_schema,
), patch(
"homeassistant.components.recorder.migration._apply_update",
wraps=_instrument_apply_update,
):
await async_setup_component(
hass, "recorder", {"recorder": {"db_url": "sqlite://"}}
recorder_helper.async_initialize_recorder(hass)
hass.async_create_task(
async_setup_component(
hass, "recorder", {"recorder": {"db_url": "sqlite://"}}
)
)
await recorder_helper.async_wait_recorder(hass)
assert recorder.util.async_migration_in_progress(hass) is True
assert recorder.util.async_migration_is_live(hass) == live
migration_stall.set()
await hass.async_block_till_done()
await hass.async_add_executor_job(migration_done.wait)