Skip to content

Commit 324b059

Browse files
authored
Move write of core state to executor (home-assistant#5720)
1 parent 76e916a commit 324b059

File tree

57 files changed

+288
-274
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

57 files changed

+288
-274
lines changed

supervisor/backups/manager.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -496,7 +496,7 @@ async def _do_backup(
496496
addon_start_tasks: list[Awaitable[None]] | None = None
497497

498498
try:
499-
self.sys_core.state = CoreState.FREEZE
499+
await self.sys_core.set_state(CoreState.FREEZE)
500500

501501
async with backup.create():
502502
# HomeAssistant Folder is for v1
@@ -549,7 +549,7 @@ async def _do_backup(
549549

550550
return backup
551551
finally:
552-
self.sys_core.state = CoreState.RUNNING
552+
await self.sys_core.set_state(CoreState.RUNNING)
553553

554554
@Job(
555555
name="backup_manager_full_backup",
@@ -808,7 +808,7 @@ async def do_restore_full(
808808
)
809809

810810
_LOGGER.info("Full-Restore %s start", backup.slug)
811-
self.sys_core.state = CoreState.FREEZE
811+
await self.sys_core.set_state(CoreState.FREEZE)
812812

813813
try:
814814
# Stop Home-Assistant / Add-ons
@@ -823,7 +823,7 @@ async def do_restore_full(
823823
location=location,
824824
)
825825
finally:
826-
self.sys_core.state = CoreState.RUNNING
826+
await self.sys_core.set_state(CoreState.RUNNING)
827827

828828
if success:
829829
_LOGGER.info("Full-Restore %s done", backup.slug)
@@ -878,7 +878,7 @@ async def do_restore_partial(
878878
)
879879

880880
_LOGGER.info("Partial-Restore %s start", backup.slug)
881-
self.sys_core.state = CoreState.FREEZE
881+
await self.sys_core.set_state(CoreState.FREEZE)
882882

883883
try:
884884
success = await self._do_restore(
@@ -890,7 +890,7 @@ async def do_restore_partial(
890890
location=location,
891891
)
892892
finally:
893-
self.sys_core.state = CoreState.RUNNING
893+
await self.sys_core.set_state(CoreState.RUNNING)
894894

895895
if success:
896896
_LOGGER.info("Partial-Restore %s done", backup.slug)
@@ -904,7 +904,7 @@ async def do_restore_partial(
904904
)
905905
async def freeze_all(self, timeout: float = DEFAULT_FREEZE_TIMEOUT) -> None:
906906
"""Freeze system to prepare for an external backup such as an image snapshot."""
907-
self.sys_core.state = CoreState.FREEZE
907+
await self.sys_core.set_state(CoreState.FREEZE)
908908

909909
# Determine running addons
910910
installed = self.sys_addons.installed.copy()
@@ -957,7 +957,7 @@ async def _thaw_all(
957957
if task
958958
]
959959
finally:
960-
self.sys_core.state = CoreState.RUNNING
960+
await self.sys_core.set_state(CoreState.RUNNING)
961961
self._thaw_event.clear()
962962
self._thaw_task = None
963963

supervisor/core.py

Lines changed: 23 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from contextlib import suppress
66
from datetime import timedelta
77
import logging
8+
from typing import Self
89

910
from .const import (
1011
ATTR_STARTUP,
@@ -39,7 +40,6 @@ def __init__(self, coresys: CoreSys):
3940
"""Initialize Supervisor object."""
4041
self.coresys: CoreSys = coresys
4142
self._state: CoreState = CoreState.INITIALIZE
42-
self._write_run_state(self._state)
4343
self.exit_code: int = 0
4444

4545
@property
@@ -57,32 +57,38 @@ def healthy(self) -> bool:
5757
"""Return true if the installation is healthy."""
5858
return len(self.sys_resolution.unhealthy) == 0
5959

60-
def _write_run_state(self, new_state: CoreState):
60+
async def _write_run_state(self):
6161
"""Write run state for s6 service supervisor."""
6262
try:
63-
RUN_SUPERVISOR_STATE.write_text(str(new_state), encoding="utf-8")
63+
await self.sys_run_in_executor(
64+
RUN_SUPERVISOR_STATE.write_text, str(self._state), encoding="utf-8"
65+
)
6466
except OSError as err:
6567
_LOGGER.warning(
66-
"Can't update the Supervisor state to %s: %s", new_state, err
68+
"Can't update the Supervisor state to %s: %s", self._state, err
6769
)
6870

69-
@state.setter
70-
def state(self, new_state: CoreState) -> None:
71+
async def post_init(self) -> Self:
72+
"""Post init actions that must be done in event loop."""
73+
await self._write_run_state()
74+
return self
75+
76+
async def set_state(self, new_state: CoreState) -> None:
7177
"""Set core into new state."""
7278
if self._state == new_state:
7379
return
7480

75-
self._write_run_state(new_state)
7681
self._state = new_state
82+
await self._write_run_state()
7783

7884
# Don't attempt to notify anyone on CLOSE as we're about to stop the event loop
79-
if new_state != CoreState.CLOSE:
80-
self.sys_bus.fire_event(BusEvent.SUPERVISOR_STATE_CHANGE, new_state)
85+
if self._state != CoreState.CLOSE:
86+
self.sys_bus.fire_event(BusEvent.SUPERVISOR_STATE_CHANGE, self._state)
8187

8288
# These will be received by HA after startup has completed which won't make sense
83-
if new_state not in STARTING_STATES:
89+
if self._state not in STARTING_STATES:
8490
self.sys_homeassistant.websocket.supervisor_update_event(
85-
"info", {"state": new_state}
91+
"info", {"state": self._state}
8692
)
8793

8894
async def connect(self):
@@ -116,7 +122,7 @@ async def connect(self):
116122

117123
async def setup(self):
118124
"""Start setting up supervisor orchestration."""
119-
self.state = CoreState.SETUP
125+
await self.set_state(CoreState.SETUP)
120126

121127
# Check internet on startup
122128
await self.sys_supervisor.check_connectivity()
@@ -196,7 +202,7 @@ async def setup(self):
196202

197203
async def start(self):
198204
"""Start Supervisor orchestration."""
199-
self.state = CoreState.STARTUP
205+
await self.set_state(CoreState.STARTUP)
200206

201207
# Check if system is healthy
202208
if not self.supported:
@@ -282,7 +288,7 @@ async def start(self):
282288
self.sys_create_task(self.sys_updater.reload())
283289
self.sys_create_task(self.sys_resolution.healthcheck())
284290

285-
self.state = CoreState.RUNNING
291+
await self.set_state(CoreState.RUNNING)
286292
self.sys_homeassistant.websocket.supervisor_update_event(
287293
"supervisor", {ATTR_STARTUP: "complete"}
288294
)
@@ -297,7 +303,7 @@ async def stop(self):
297303
return
298304

299305
# don't process scheduler anymore
300-
self.state = CoreState.STOPPING
306+
await self.set_state(CoreState.STOPPING)
301307

302308
# Stage 1
303309
try:
@@ -332,15 +338,15 @@ async def stop(self):
332338
except TimeoutError:
333339
_LOGGER.warning("Stage 2: Force Shutdown!")
334340

335-
self.state = CoreState.CLOSE
341+
await self.set_state(CoreState.CLOSE)
336342
_LOGGER.info("Supervisor is down - %d", self.exit_code)
337343
self.sys_loop.stop()
338344

339345
async def shutdown(self, *, remove_homeassistant_container: bool = False):
340346
"""Shutdown all running containers in correct order."""
341347
# don't process scheduler anymore
342348
if self.state == CoreState.RUNNING:
343-
self.state = CoreState.SHUTDOWN
349+
await self.set_state(CoreState.SHUTDOWN)
344350

345351
# Shutdown Application Add-ons, using Home Assistant API
346352
await self.sys_addons.shutdown(AddonStartup.APPLICATION)

tests/api/middleware/test_security.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ async def api_token_validation(aiohttp_client, coresys: CoreSys) -> TestClient:
5858
@pytest.mark.asyncio
5959
async def test_api_security_system_initialize(api_system: TestClient, coresys: CoreSys):
6060
"""Test security."""
61-
coresys.core.state = CoreState.INITIALIZE
61+
await coresys.core.set_state(CoreState.INITIALIZE)
6262

6363
resp = await api_system.get("/supervisor/ping")
6464
result = await resp.json()
@@ -69,7 +69,7 @@ async def test_api_security_system_initialize(api_system: TestClient, coresys: C
6969
@pytest.mark.asyncio
7070
async def test_api_security_system_setup(api_system: TestClient, coresys: CoreSys):
7171
"""Test security."""
72-
coresys.core.state = CoreState.SETUP
72+
await coresys.core.set_state(CoreState.SETUP)
7373

7474
resp = await api_system.get("/supervisor/ping")
7575
result = await resp.json()
@@ -80,7 +80,7 @@ async def test_api_security_system_setup(api_system: TestClient, coresys: CoreSy
8080
@pytest.mark.asyncio
8181
async def test_api_security_system_running(api_system: TestClient, coresys: CoreSys):
8282
"""Test security."""
83-
coresys.core.state = CoreState.RUNNING
83+
await coresys.core.set_state(CoreState.RUNNING)
8484

8585
resp = await api_system.get("/supervisor/ping")
8686
assert resp.status == 200
@@ -89,7 +89,7 @@ async def test_api_security_system_running(api_system: TestClient, coresys: Core
8989
@pytest.mark.asyncio
9090
async def test_api_security_system_startup(api_system: TestClient, coresys: CoreSys):
9191
"""Test security."""
92-
coresys.core.state = CoreState.STARTUP
92+
await coresys.core.set_state(CoreState.STARTUP)
9393

9494
resp = await api_system.get("/supervisor/ping")
9595
assert resp.status == 200

tests/api/test_backups.py

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ async def test_backup_to_location(
137137
await coresys.mounts.create_mount(mount)
138138
coresys.mounts.default_backup_mount = mount
139139

140-
coresys.core.state = CoreState.RUNNING
140+
await coresys.core.set_state(CoreState.RUNNING)
141141
coresys.hardware.disk.get_disk_free_space = lambda x: 5000
142142
resp = await api_client.post(
143143
"/backups/new/full",
@@ -178,7 +178,7 @@ async def test_backup_to_default(api_client: TestClient, coresys: CoreSys):
178178
await coresys.mounts.create_mount(mount)
179179
coresys.mounts.default_backup_mount = mount
180180

181-
coresys.core.state = CoreState.RUNNING
181+
await coresys.core.set_state(CoreState.RUNNING)
182182
coresys.hardware.disk.get_disk_free_space = lambda x: 5000
183183
resp = await api_client.post(
184184
"/backups/new/full",
@@ -196,7 +196,7 @@ async def test_api_freeze_thaw(
196196
api_client: TestClient, coresys: CoreSys, ha_ws_client: AsyncMock
197197
):
198198
"""Test manual freeze and thaw for external backup via API."""
199-
coresys.core.state = CoreState.RUNNING
199+
await coresys.core.set_state(CoreState.RUNNING)
200200
coresys.hardware.disk.get_disk_free_space = lambda x: 5000
201201
ha_ws_client.ha_version = AwesomeVersion("2022.1.0")
202202

@@ -230,7 +230,7 @@ async def test_api_backup_exclude_database(
230230
exclude_db_setting: bool,
231231
):
232232
"""Test backups exclude the database when specified."""
233-
coresys.core.state = CoreState.RUNNING
233+
await coresys.core.set_state(CoreState.RUNNING)
234234
coresys.hardware.disk.get_disk_free_space = lambda x: 5000
235235
coresys.homeassistant.version = AwesomeVersion("2023.09.0")
236236
coresys.homeassistant.backups_exclude_database = exclude_db_setting
@@ -278,7 +278,7 @@ async def test_api_backup_restore_background(
278278
tmp_supervisor_data: Path,
279279
):
280280
"""Test background option on backup/restore APIs."""
281-
coresys.core.state = CoreState.RUNNING
281+
await coresys.core.set_state(CoreState.RUNNING)
282282
coresys.hardware.disk.get_disk_free_space = lambda x: 5000
283283
coresys.homeassistant.version = AwesomeVersion("2023.09.0")
284284
(tmp_supervisor_data / "addons/local").mkdir(parents=True)
@@ -364,7 +364,7 @@ async def test_api_backup_errors(
364364
tmp_supervisor_data: Path,
365365
):
366366
"""Test error reporting in backup job."""
367-
coresys.core.state = CoreState.RUNNING
367+
await coresys.core.set_state(CoreState.RUNNING)
368368
coresys.hardware.disk.get_disk_free_space = lambda x: 5000
369369
coresys.homeassistant.version = AwesomeVersion("2023.09.0")
370370
(tmp_supervisor_data / "addons/local").mkdir(parents=True)
@@ -435,15 +435,15 @@ async def test_api_backup_errors(
435435

436436
async def test_backup_immediate_errors(api_client: TestClient, coresys: CoreSys):
437437
"""Test backup errors that return immediately even in background mode."""
438-
coresys.core.state = CoreState.FREEZE
438+
await coresys.core.set_state(CoreState.FREEZE)
439439
resp = await api_client.post(
440440
"/backups/new/full",
441441
json={"name": "Test", "background": True},
442442
)
443443
assert resp.status == 400
444444
assert "freeze" in (await resp.json())["message"]
445445

446-
coresys.core.state = CoreState.RUNNING
446+
await coresys.core.set_state(CoreState.RUNNING)
447447
coresys.hardware.disk.get_disk_free_space = lambda x: 0.5
448448
resp = await api_client.post(
449449
"/backups/new/partial",
@@ -460,7 +460,7 @@ async def test_restore_immediate_errors(
460460
mock_partial_backup: Backup,
461461
):
462462
"""Test restore errors that return immediately even in background mode."""
463-
coresys.core.state = CoreState.RUNNING
463+
await coresys.core.set_state(CoreState.RUNNING)
464464
coresys.hardware.disk.get_disk_free_space = lambda x: 5000
465465

466466
resp = await api_client.post(
@@ -634,7 +634,7 @@ async def test_backup_to_multiple_locations(
634634
inputs: dict[str, Any],
635635
):
636636
"""Test making a backup to multiple locations."""
637-
coresys.core.state = CoreState.RUNNING
637+
await coresys.core.set_state(CoreState.RUNNING)
638638
coresys.hardware.disk.get_disk_free_space = lambda x: 5000
639639

640640
resp = await api_client.post(
@@ -669,7 +669,7 @@ async def test_backup_with_extras(
669669
inputs: dict[str, Any],
670670
):
671671
"""Test backup including extra metdata."""
672-
coresys.core.state = CoreState.RUNNING
672+
await coresys.core.set_state(CoreState.RUNNING)
673673
coresys.hardware.disk.get_disk_free_space = lambda x: 5000
674674

675675
resp = await api_client.post(
@@ -909,7 +909,7 @@ async def test_partial_backup_all_addons(
909909
install_addon_ssh: Addon,
910910
):
911911
"""Test backup including extra metdata."""
912-
coresys.core.state = CoreState.RUNNING
912+
await coresys.core.set_state(CoreState.RUNNING)
913913
coresys.hardware.disk.get_disk_free_space = lambda x: 5000
914914

915915
with patch.object(Backup, "store_addons") as store_addons:
@@ -928,7 +928,7 @@ async def test_restore_backup_from_location(
928928
local_location: str | None,
929929
):
930930
"""Test restoring a backup from a specific location."""
931-
coresys.core.state = CoreState.RUNNING
931+
await coresys.core.set_state(CoreState.RUNNING)
932932
coresys.hardware.disk.get_disk_free_space = lambda x: 5000
933933

934934
# Make a backup and a file to test with
@@ -1000,7 +1000,7 @@ async def test_restore_backup_unencrypted_after_encrypted(
10001000
# We punt the ball on this one for this PR since this is a rare edge case.
10011001
backup.restore_dockerconfig = MagicMock()
10021002

1003-
coresys.core.state = CoreState.RUNNING
1003+
await coresys.core.set_state(CoreState.RUNNING)
10041004
coresys.hardware.disk.get_disk_free_space = lambda x: 5000
10051005

10061006
# Restore encrypted backup
@@ -1050,7 +1050,7 @@ async def test_restore_homeassistant_adds_env(
10501050
):
10511051
"""Test restoring home assistant from backup adds env to container."""
10521052
event = asyncio.Event()
1053-
coresys.core.state = CoreState.RUNNING
1053+
await coresys.core.set_state(CoreState.RUNNING)
10541054
coresys.hardware.disk.get_disk_free_space = lambda x: 5000
10551055
coresys.homeassistant.version = AwesomeVersion("2025.1.0")
10561056
backup = await coresys.backups.do_backup_full()
@@ -1134,7 +1134,7 @@ async def test_protected_backup(
11341134
api_client: TestClient, coresys: CoreSys, backup_type: str, options: dict[str, Any]
11351135
):
11361136
"""Test creating a protected backup."""
1137-
coresys.core.state = CoreState.RUNNING
1137+
await coresys.core.set_state(CoreState.RUNNING)
11381138
coresys.hardware.disk.get_disk_free_space = lambda x: 5000
11391139

11401140
resp = await api_client.post(
@@ -1246,7 +1246,7 @@ async def test_missing_file_removes_location_from_cache(
12461246
backup_file: str,
12471247
):
12481248
"""Test finding a missing file removes the location from cache."""
1249-
coresys.core.state = CoreState.RUNNING
1249+
await coresys.core.set_state(CoreState.RUNNING)
12501250
coresys.hardware.disk.get_disk_free_space = lambda x: 5000
12511251

12521252
backup_file = get_fixture_path(backup_file)
@@ -1305,7 +1305,7 @@ async def test_missing_file_removes_backup_from_cache(
13051305
backup_file: str,
13061306
):
13071307
"""Test finding a missing file removes the backup from cache if its the only one."""
1308-
coresys.core.state = CoreState.RUNNING
1308+
await coresys.core.set_state(CoreState.RUNNING)
13091309
coresys.hardware.disk.get_disk_free_space = lambda x: 5000
13101310

13111311
backup_file = get_fixture_path(backup_file)
@@ -1331,7 +1331,7 @@ async def test_immediate_list_after_missing_file_restore(
13311331
api_client: TestClient, coresys: CoreSys
13321332
):
13331333
"""Test race with reload for missing file on restore does not error."""
1334-
coresys.core.state = CoreState.RUNNING
1334+
await coresys.core.set_state(CoreState.RUNNING)
13351335
coresys.hardware.disk.get_disk_free_space = lambda x: 5000
13361336

13371337
backup_file = get_fixture_path("backup_example.tar")

0 commit comments

Comments
 (0)