Stop publishing and saving from the start of unload; keep deletions final
Home Assistant marks a coordinator shut down only after the platforms are unloaded, and the model save checked for shutdown before waiting for the storage lock. A deletion could therefore be followed by a save that had been waiting for it, recreating the file. Also, unloading cancels a running scheduled update, which released the lock while the file write continued in a worker thread. - The coordinator is marked closing before platforms unload (restored if unloading fails); closing updates, relay callbacks and saves do nothing. - The save checks closing and a removed-model mark after acquiring the lock, and runs shielded so the lock is held until the write finishes. - Removal marks the model removed under the lock before deleting it. - A failed setup also shuts the coordinator down. Tests: unload and reload during a running optimization, removal during a save, a save waiting for a deletion, simultaneous saves, repeated setup/unload, shutdown ordering and failed setup. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
87437cbd48
commit
28ff953ddd
4 files changed
+281
-5
No files matched your search
@@ -12,7 +12,7 @@ from homeassistant.helpers.event import async_track_state_change_event
|
||||
|
||||
from .const import CONF_HEATER, CONF_ROOMS, CONF_WEIGHTS, DOMAIN, CONF_OUTSIDE, WEATHER_SOURCES
|
||||
from .weather_sources import sources_for
|
||||
from .coordinator import HeatingMPCCoordinator, storage_lock
|
||||
from .coordinator import HeatingMPCCoordinator, mark_model_removed, storage_lock
|
||||
from .zones import SUBENTRY_ZONE, ZONE_KEY, ZONE_NAME, legacy_zone_data
|
||||
|
||||
PLATFORMS: list[Platform] = [Platform.SENSOR, Platform.BINARY_SENSOR]
|
||||
@@ -29,9 +29,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
except BaseException:
|
||||
# HA skips unload callbacks for unexpected setup errors, so clean up here.
|
||||
# HA skips unload callbacks for unexpected setup errors, so clean up here,
|
||||
# including a refresh scheduled by entities that were already added.
|
||||
unsub_relay()
|
||||
hass.data.get(DOMAIN, {}).pop(entry.entry_id, None)
|
||||
await coordinator.async_shutdown()
|
||||
raise
|
||||
entry.async_on_unload(unsub_relay)
|
||||
# Remove only obsolete zone forecasts belonging to this entry/platform. Zones
|
||||
@@ -53,17 +55,25 @@ async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
coordinator = hass.data.get(DOMAIN, {}).get(entry.entry_id)
|
||||
if coordinator is not None:
|
||||
# Before anything is unloaded: an update still running must not publish or save.
|
||||
coordinator.async_begin_shutdown()
|
||||
ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
if ok:
|
||||
hass.data[DOMAIN].pop(entry.entry_id, None)
|
||||
elif coordinator is not None:
|
||||
coordinator.async_cancel_shutdown()
|
||||
return ok
|
||||
|
||||
|
||||
async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
"""Remove the learned model when this integration instance is deleted."""
|
||||
store = Store(hass, 1, f"{DOMAIN}.{entry.entry_id}")
|
||||
# Wait for a save still in progress, which would otherwise recreate the file.
|
||||
# Wait for a save still in progress, which would otherwise recreate the file;
|
||||
# a save still waiting for the lock sees the mark and does not write.
|
||||
async with storage_lock(hass, entry.entry_id):
|
||||
mark_model_removed(hass, entry.entry_id)
|
||||
await store.async_remove()
|
||||
hass.data.get(f"{DOMAIN}_storage_locks", {}).pop(entry.entry_id, None)
|
||||
|
||||
|
||||
Reference in new issue
Block a user