Skip to content

Commit 321b692

Browse files
agnersclaude
andauthored
Consolidate Supervisor auto-update into updater reload task (#6705)
* Consolidate Supervisor auto-update into updater reload task The separate _update_supervisor task (24h interval) was redundant since _reload_updater (previously ~7.5h interval) already triggered the same update when a new version was found. This meant Supervisor updates were effectively checked every 7.5h, undermining the intent of #6633 and #6638 to reduce update frequency and registry pressure. Merge the two code paths into one: _reload_updater now directly calls _auto_update_supervisor (with the same job conditions) when a new version is detected. The reload interval is increased from ~7.5h to 24h to match the originally intended update cadence. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Add regression test for scheduled supervisor auto-update Verify that the scheduled reload_updater task triggers exactly one supervisor update when a new version is available. Uses event loop time patching to simulate the 24h interval firing. This guards against the previous bug where a separate _update_supervisor task ran on its own schedule, causing duplicate update attempts. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 1fcfede commit 321b692

File tree

2 files changed

+83
-33
lines changed

2 files changed

+83
-33
lines changed

supervisor/misc/tasks.py

Lines changed: 28 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@
3131
HASS_WATCHDOG_MAX_API_ATTEMPTS = 2
3232
HASS_WATCHDOG_MAX_REANIMATE_ATTEMPTS = 5
3333

34-
RUN_UPDATE_SUPERVISOR = 86400 # 24h
3534
RUN_UPDATE_ADDONS = 57600
3635
RUN_UPDATE_CLI = 43200 # 12h, staggered +2min per plugin
3736
RUN_UPDATE_DNS = 43320
@@ -42,7 +41,7 @@
4241
RUN_RELOAD_ADDONS = 10800
4342
RUN_RELOAD_BACKUPS = 72000
4443
RUN_RELOAD_HOST = 7600
45-
RUN_RELOAD_UPDATER = 27100
44+
RUN_RELOAD_UPDATER = 86400 # 24h
4645
RUN_RELOAD_INGRESS = 930
4746
RUN_RELOAD_MOUNTS = 900
4847

@@ -73,7 +72,6 @@ async def load(self):
7372
"""Add Tasks to scheduler."""
7473
# Update
7574
self.sys_scheduler.register_task(self._update_addons, RUN_UPDATE_ADDONS)
76-
self.sys_scheduler.register_task(self._update_supervisor, RUN_UPDATE_SUPERVISOR)
7775
self.sys_scheduler.register_task(self._update_cli, RUN_UPDATE_CLI)
7876
self.sys_scheduler.register_task(self._update_dns, RUN_UPDATE_DNS)
7977
self.sys_scheduler.register_task(self._update_audio, RUN_UPDATE_AUDIO)
@@ -164,34 +162,6 @@ async def _update_addons(self):
164162
err,
165163
)
166164

167-
@Job(
168-
name="tasks_update_supervisor",
169-
conditions=[
170-
JobCondition.AUTO_UPDATE,
171-
JobCondition.FREE_SPACE,
172-
JobCondition.HEALTHY,
173-
JobCondition.INTERNET_HOST,
174-
JobCondition.OS_SUPPORTED,
175-
JobCondition.RUNNING,
176-
JobCondition.ARCHITECTURE_SUPPORTED,
177-
],
178-
concurrency=JobConcurrency.REJECT,
179-
)
180-
async def _update_supervisor(self):
181-
"""Check and run update of Supervisor Supervisor."""
182-
if not self.sys_supervisor.need_update:
183-
return
184-
185-
_LOGGER.info(
186-
"Found new Supervisor version %s, updating",
187-
self.sys_supervisor.latest_version,
188-
)
189-
190-
# Errors are logged by the exceptions, we can't really do something
191-
# if an update fails here.
192-
with suppress(SupervisorUpdateError):
193-
await self.sys_supervisor.update()
194-
195165
async def _watchdog_homeassistant_api(self):
196166
"""Create scheduler task for monitoring running state of API.
197167
@@ -390,9 +360,34 @@ async def _reload_updater(self) -> None:
390360
"""Check for new versions of Home Assistant, Supervisor, OS, etc."""
391361
await self.sys_updater.reload()
392362

393-
# If there's a new version of supervisor, start update immediately
363+
# If there's a new version of supervisor, update immediately
394364
if self.sys_supervisor.need_update:
395-
await self._update_supervisor()
365+
await self._auto_update_supervisor()
366+
367+
@Job(
368+
name="tasks_update_supervisor",
369+
conditions=[
370+
JobCondition.AUTO_UPDATE,
371+
JobCondition.FREE_SPACE,
372+
JobCondition.HEALTHY,
373+
JobCondition.INTERNET_HOST,
374+
JobCondition.OS_SUPPORTED,
375+
JobCondition.RUNNING,
376+
JobCondition.ARCHITECTURE_SUPPORTED,
377+
],
378+
concurrency=JobConcurrency.REJECT,
379+
)
380+
async def _auto_update_supervisor(self):
381+
"""Auto update Supervisor if enabled."""
382+
if not self.sys_supervisor.need_update:
383+
return
384+
385+
_LOGGER.info(
386+
"Found new Supervisor version %s, updating",
387+
self.sys_supervisor.latest_version,
388+
)
389+
with suppress(SupervisorUpdateError):
390+
await self.sys_supervisor.update()
396391

397392
@Job(name="tasks_core_backup_cleanup", conditions=[JobCondition.HEALTHY])
398393
async def _core_backup_cleanup(self) -> None:

tests/misc/test_tasks.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""Test scheduled tasks."""
22

3+
import asyncio
34
from collections.abc import AsyncGenerator
45
from shutil import copy
56
from unittest.mock import AsyncMock, Mock, PropertyMock, patch
@@ -244,6 +245,60 @@ async def test_update_dns_skipped_when_auto_update_disabled(
244245
update.assert_not_called()
245246

246247

248+
@pytest.mark.usefixtures("no_job_throttle", "supervisor_internet")
249+
async def test_scheduled_reload_updater_triggers_one_supervisor_update(
250+
tasks: Tasks, coresys: CoreSys, mock_update_data: MockResponse
251+
):
252+
"""Test scheduled reload updater triggers exactly one supervisor update.
253+
254+
Regression test: previously _update_supervisor ran on a separate schedule
255+
in addition to being called from _reload_updater, causing duplicate updates.
256+
Now only _reload_updater triggers the supervisor auto-update.
257+
"""
258+
coresys.hardware.disk.get_disk_free_space = lambda x: 5000
259+
await coresys.core.set_state(CoreState.RUNNING)
260+
261+
# Make version data show a newer supervisor version
262+
version_data = await mock_update_data.text()
263+
mock_update_data.update_text(version_data.replace("2024.10.0", "2024.10.1"))
264+
265+
with (
266+
patch.object(
267+
Supervisor,
268+
"version",
269+
new=PropertyMock(return_value=AwesomeVersion("2024.10.0")),
270+
),
271+
patch.object(Supervisor, "update") as update,
272+
):
273+
await tasks.load()
274+
update.assert_not_called()
275+
276+
# Advance the event loop clock by 24h+ so scheduled tasks fire.
277+
# Patching loop.time makes all call_later callbacks appear due;
278+
# a tiny real sleep lets _run_once re-evaluate and execute them.
279+
loop = asyncio.get_event_loop()
280+
original_time = loop.time
281+
loop.time = lambda: original_time() + 86401
282+
283+
try:
284+
# Busy-wait until call_later callbacks fire and create jobs
285+
while not any(t.job and not t.job.done() for t in coresys.scheduler._tasks):
286+
await asyncio.sleep(0)
287+
288+
# Wait for all scheduler-created tasks to finish
289+
pending = [
290+
t.job for t in coresys.scheduler._tasks if t.job and not t.job.done()
291+
]
292+
await asyncio.gather(*pending)
293+
294+
# Verify update was triggered exactly once
295+
update.assert_called_once()
296+
finally:
297+
loop.time = original_time
298+
299+
await coresys.scheduler.shutdown()
300+
301+
247302
@pytest.mark.usefixtures("tmp_supervisor_data")
248303
async def test_update_addons_auto_update_success(
249304
tasks: Tasks,

0 commit comments

Comments
 (0)