Ensure a wal checkpoint is scheduled nightly (#50746)
parent
b1ff9dc45e
commit
e7f7e61e88
|
@ -54,6 +54,7 @@ from .util import (
|
||||||
dburl_to_path,
|
dburl_to_path,
|
||||||
end_incomplete_runs,
|
end_incomplete_runs,
|
||||||
move_away_broken_database,
|
move_away_broken_database,
|
||||||
|
perodic_db_cleanups,
|
||||||
session_scope,
|
session_scope,
|
||||||
setup_connection_for_dialect,
|
setup_connection_for_dialect,
|
||||||
validate_or_move_away_sqlite_database,
|
validate_or_move_away_sqlite_database,
|
||||||
|
@ -278,6 +279,10 @@ class PurgeTask(NamedTuple):
|
||||||
apply_filter: bool
|
apply_filter: bool
|
||||||
|
|
||||||
|
|
||||||
|
class PerodicCleanupTask:
|
||||||
|
"""An object to insert into the recorder to trigger cleanup tasks when auto purge is disabled."""
|
||||||
|
|
||||||
|
|
||||||
class StatisticsTask(NamedTuple):
|
class StatisticsTask(NamedTuple):
|
||||||
"""An object to insert into the recorder queue to run a statistics task."""
|
"""An object to insert into the recorder queue to run a statistics task."""
|
||||||
|
|
||||||
|
@ -484,9 +489,15 @@ class Recorder(threading.Thread):
|
||||||
self.async_recorder_ready.set()
|
self.async_recorder_ready.set()
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def async_purge(self, now):
|
def async_nightly_tasks(self, now):
|
||||||
"""Trigger the purge."""
|
"""Trigger the purge."""
|
||||||
|
if self.auto_purge:
|
||||||
|
# Purge will schedule the perodic cleanups
|
||||||
|
# after it completes to ensure it does not happen
|
||||||
|
# until after the database is vacuumed
|
||||||
self.queue.put(PurgeTask(self.keep_days, repack=False, apply_filter=False))
|
self.queue.put(PurgeTask(self.keep_days, repack=False, apply_filter=False))
|
||||||
|
else:
|
||||||
|
self.queue.put(PerodicCleanupTask())
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def async_hourly_statistics(self, now):
|
def async_hourly_statistics(self, now):
|
||||||
|
@ -496,10 +507,9 @@ class Recorder(threading.Thread):
|
||||||
|
|
||||||
def _async_setup_periodic_tasks(self):
|
def _async_setup_periodic_tasks(self):
|
||||||
"""Prepare periodic tasks."""
|
"""Prepare periodic tasks."""
|
||||||
if self.auto_purge:
|
# Run nightly tasks at 4:12am
|
||||||
# Purge every night at 4:12am
|
|
||||||
async_track_time_change(
|
async_track_time_change(
|
||||||
self.hass, self.async_purge, hour=4, minute=12, second=0
|
self.hass, self.async_nightly_tasks, hour=4, minute=12, second=0
|
||||||
)
|
)
|
||||||
# Compile hourly statistics every hour at *:12
|
# Compile hourly statistics every hour at *:12
|
||||||
async_track_time_change(
|
async_track_time_change(
|
||||||
|
@ -646,6 +656,10 @@ class Recorder(threading.Thread):
|
||||||
def _run_purge(self, keep_days, repack, apply_filter):
|
def _run_purge(self, keep_days, repack, apply_filter):
|
||||||
"""Purge the database."""
|
"""Purge the database."""
|
||||||
if purge.purge_old_data(self, keep_days, repack, apply_filter):
|
if purge.purge_old_data(self, keep_days, repack, apply_filter):
|
||||||
|
# We always need to do the db cleanups after a purge
|
||||||
|
# is finished to ensure the WAL checkpoint and other
|
||||||
|
# tasks happen after a vacuum.
|
||||||
|
perodic_db_cleanups(self)
|
||||||
return
|
return
|
||||||
# Schedule a new purge task if this one didn't finish
|
# Schedule a new purge task if this one didn't finish
|
||||||
self.queue.put(PurgeTask(keep_days, repack, apply_filter))
|
self.queue.put(PurgeTask(keep_days, repack, apply_filter))
|
||||||
|
@ -662,6 +676,9 @@ class Recorder(threading.Thread):
|
||||||
if isinstance(event, PurgeTask):
|
if isinstance(event, PurgeTask):
|
||||||
self._run_purge(event.keep_days, event.repack, event.apply_filter)
|
self._run_purge(event.keep_days, event.repack, event.apply_filter)
|
||||||
return
|
return
|
||||||
|
if isinstance(event, PerodicCleanupTask):
|
||||||
|
perodic_db_cleanups(self)
|
||||||
|
return
|
||||||
if isinstance(event, StatisticsTask):
|
if isinstance(event, StatisticsTask):
|
||||||
self._run_statistics(event.start)
|
self._run_statistics(event.start)
|
||||||
return
|
return
|
||||||
|
|
|
@ -318,3 +318,15 @@ def retryable_database_job(description: str):
|
||||||
return wrapper
|
return wrapper
|
||||||
|
|
||||||
return decorator
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
|
def perodic_db_cleanups(instance: Recorder):
|
||||||
|
"""Run any database cleanups that need to happen perodiclly.
|
||||||
|
|
||||||
|
These cleanups will happen nightly or after any purge.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if instance.engine.dialect.name == "sqlite":
|
||||||
|
# Execute sqlite to create a wal checkpoint and free up disk space
|
||||||
|
_LOGGER.debug("WAL checkpoint")
|
||||||
|
instance.engine.execute("PRAGMA wal_checkpoint(TRUNCATE);")
|
||||||
|
|
|
@ -8,6 +8,7 @@ from sqlalchemy.exc import DatabaseError, OperationalError, SQLAlchemyError
|
||||||
|
|
||||||
from homeassistant.components import recorder
|
from homeassistant.components import recorder
|
||||||
from homeassistant.components.recorder import (
|
from homeassistant.components.recorder import (
|
||||||
|
CONF_AUTO_PURGE,
|
||||||
CONF_DB_URL,
|
CONF_DB_URL,
|
||||||
CONFIG_SCHEMA,
|
CONFIG_SCHEMA,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
|
@ -610,30 +611,73 @@ def test_auto_purge(hass_recorder):
|
||||||
|
|
||||||
with patch(
|
with patch(
|
||||||
"homeassistant.components.recorder.purge.purge_old_data", return_value=True
|
"homeassistant.components.recorder.purge.purge_old_data", return_value=True
|
||||||
) as purge_old_data:
|
) as purge_old_data, patch(
|
||||||
|
"homeassistant.components.recorder.perodic_db_cleanups"
|
||||||
|
) as perodic_db_cleanups:
|
||||||
# Advance one day, and the purge task should run
|
# Advance one day, and the purge task should run
|
||||||
test_time = test_time + timedelta(days=1)
|
test_time = test_time + timedelta(days=1)
|
||||||
run_tasks_at_time(hass, test_time)
|
run_tasks_at_time(hass, test_time)
|
||||||
assert len(purge_old_data.mock_calls) == 1
|
assert len(purge_old_data.mock_calls) == 1
|
||||||
|
assert len(perodic_db_cleanups.mock_calls) == 1
|
||||||
|
|
||||||
purge_old_data.reset_mock()
|
purge_old_data.reset_mock()
|
||||||
|
perodic_db_cleanups.reset_mock()
|
||||||
|
|
||||||
# Advance one day, and the purge task should run again
|
# Advance one day, and the purge task should run again
|
||||||
test_time = test_time + timedelta(days=1)
|
test_time = test_time + timedelta(days=1)
|
||||||
run_tasks_at_time(hass, test_time)
|
run_tasks_at_time(hass, test_time)
|
||||||
assert len(purge_old_data.mock_calls) == 1
|
assert len(purge_old_data.mock_calls) == 1
|
||||||
|
assert len(perodic_db_cleanups.mock_calls) == 1
|
||||||
|
|
||||||
purge_old_data.reset_mock()
|
purge_old_data.reset_mock()
|
||||||
|
perodic_db_cleanups.reset_mock()
|
||||||
|
|
||||||
# Advance less than one full day. The alarm should not yet fire.
|
# Advance less than one full day. The alarm should not yet fire.
|
||||||
test_time = test_time + timedelta(hours=23)
|
test_time = test_time + timedelta(hours=23)
|
||||||
run_tasks_at_time(hass, test_time)
|
run_tasks_at_time(hass, test_time)
|
||||||
assert len(purge_old_data.mock_calls) == 0
|
assert len(purge_old_data.mock_calls) == 0
|
||||||
|
assert len(perodic_db_cleanups.mock_calls) == 0
|
||||||
|
|
||||||
# Advance to the next day and fire the alarm again
|
# Advance to the next day and fire the alarm again
|
||||||
test_time = test_time + timedelta(hours=1)
|
test_time = test_time + timedelta(hours=1)
|
||||||
run_tasks_at_time(hass, test_time)
|
run_tasks_at_time(hass, test_time)
|
||||||
assert len(purge_old_data.mock_calls) == 1
|
assert len(purge_old_data.mock_calls) == 1
|
||||||
|
assert len(perodic_db_cleanups.mock_calls) == 1
|
||||||
|
|
||||||
|
dt_util.set_default_time_zone(original_tz)
|
||||||
|
|
||||||
|
|
||||||
|
def test_auto_purge_disabled(hass_recorder):
|
||||||
|
"""Test periodic db cleanup still run when auto purge is disabled."""
|
||||||
|
hass = hass_recorder({CONF_AUTO_PURGE: False})
|
||||||
|
|
||||||
|
original_tz = dt_util.DEFAULT_TIME_ZONE
|
||||||
|
|
||||||
|
tz = dt_util.get_time_zone("Europe/Copenhagen")
|
||||||
|
dt_util.set_default_time_zone(tz)
|
||||||
|
|
||||||
|
# Purging is scheduled to happen at 4:12am every day. We want
|
||||||
|
# to verify that when auto purge is disabled perodic db cleanups
|
||||||
|
# are still scheduled
|
||||||
|
#
|
||||||
|
# The clock is started at 4:15am then advanced forward below
|
||||||
|
now = dt_util.utcnow()
|
||||||
|
test_time = datetime(now.year + 2, 1, 1, 4, 15, 0, tzinfo=tz)
|
||||||
|
run_tasks_at_time(hass, test_time)
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.recorder.purge.purge_old_data", return_value=True
|
||||||
|
) as purge_old_data, patch(
|
||||||
|
"homeassistant.components.recorder.perodic_db_cleanups"
|
||||||
|
) as perodic_db_cleanups:
|
||||||
|
# Advance one day, and the purge task should run
|
||||||
|
test_time = test_time + timedelta(days=1)
|
||||||
|
run_tasks_at_time(hass, test_time)
|
||||||
|
assert len(purge_old_data.mock_calls) == 0
|
||||||
|
assert len(perodic_db_cleanups.mock_calls) == 1
|
||||||
|
|
||||||
|
purge_old_data.reset_mock()
|
||||||
|
perodic_db_cleanups.reset_mock()
|
||||||
|
|
||||||
dt_util.set_default_time_zone(original_tz)
|
dt_util.set_default_time_zone(original_tz)
|
||||||
|
|
||||||
|
|
|
@ -269,3 +269,11 @@ def test_end_incomplete_runs(hass_recorder, caplog):
|
||||||
assert run_info.end == now_without_tz
|
assert run_info.end == now_without_tz
|
||||||
|
|
||||||
assert "Ended unfinished session" in caplog.text
|
assert "Ended unfinished session" in caplog.text
|
||||||
|
|
||||||
|
|
||||||
|
def test_perodic_db_cleanups(hass_recorder):
|
||||||
|
"""Test perodic db cleanups."""
|
||||||
|
hass = hass_recorder()
|
||||||
|
with patch.object(hass.data[DATA_INSTANCE].engine, "execute") as execute_mock:
|
||||||
|
util.perodic_db_cleanups(hass.data[DATA_INSTANCE])
|
||||||
|
assert execute_mock.call_args[0][0] == "PRAGMA wal_checkpoint(TRUNCATE);"
|
||||||
|
|
Loading…
Reference in New Issue