95bb8fe6ab Make Core.shutdown idempotent and safe to call concurrently (#6891)
* Make Core.shutdown idempotent and safe to call concurrently

After #6887, Core.shutdown() now runs in the SIGTERM path during host
shutdown in addition to the existing host.control reboot/shutdown and
backup restore paths. Multiple concurrent callers were possible (e.g.
SIGTERM arriving while a reboot API call is mid-flight), so __main__.py
debounced the signal handler by stashing the in-flight task in a single-
element list and bailing out on the second SIGTERM.

Move the idempotency into Core.shutdown() itself, where it belongs:

- A second call while shutdown is in progress awaits the in-flight
  shutdown via an asyncio.Event rather than re-running the sequence.
- Calls during STOPPING/CLOSE return early (Supervisor is already going
  away; the work is moot).
- Calls during STARTING_STATES (INITIALIZE/STARTUP/SETUP) return early
  too. There is nothing coherent to gracefully stop before startup
  completes, and on the SIGTERM-during-startup path the caller cancels
  startup_task first, so waiting for it to complete would deadlock.
- The sequence is wrapped in try/finally so the completion event is set
  even when an inner step raises.

With that in place the closure workaround in __main__.py collapses to a
plain coresys.create_task(stop_supervisor()): repeat SIGTERMs spawn
extra tasks but each just observes the in-flight shutdown and waits.

Tests cover the four state branches and confirm the event is reset
between repeated shutdown cycles (backup restore re-enters RUNNING).

* Split Core.shutdown() into teardown_services + shutdown

PR feedback (@mdegat01) flagged the "supports repeated use" comment on
_shutdown_event.clear() as describing a use case that does not exist.
Investigating, the real source of confusion is that the old shutdown()
did two different things stitched together:

  - Stop user-facing containers (add-ons + Home Assistant Core), which
    backup restore uses while leaving the host alone.
  - Run the full shutdown ceremony (state transition to SHUTDOWN, stop
    plugins), which only the SIGTERM signal handler and the host
    reboot/power-off API want.

That dispatch was implemented with an asymmetric state transition
("only set SHUTDOWN if state == RUNNING") and a plugin-shutdown gate
("only stop plugins if state in (STOPPING, SHUTDOWN)"). It worked but
made the intent of each branch hard to read, broke the reentrancy
guard on the restore path (state never reaches SHUTDOWN, so concurrent
callers fall through every early return), and forced the misleading
"repeated cycles" framing on the event handling.

Split into two methods with one job each:

  - teardown_services(): stop add-ons + Home Assistant Core. Does not
    change Core state and does not stop plugins. Backup restore calls
    this directly so HA Core's watchdog stays registered (it only
    disables on transitions into CLOSING_STATES) and plugins keep
    running for the restore body to use.
  - shutdown(): real shutdown ceremony. Unconditionally transitions to
    SHUTDOWN, calls teardown_services(), then stops plugins. The
    reentrancy guard (state == SHUTDOWN -> await event) now works
    correctly because every caller transitions state on entry. One-shot
    per process lifetime; no clear() needed.

Update backups/manager.py:867 to call teardown_services() instead.
remove_homeassistant_container moves to teardown_services() since
restore is the only caller that passes it.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Release shutdown waiters when set_state() is cancelled

PR feedback from Copilot: set_state() updates Core._state synchronously
(line 84 in core.py) before awaiting _write_run_state(). If the
shutdown task is cancelled while awaiting that write, in-memory state
is already SHUTDOWN but the function exits before entering the
try/finally that sets _shutdown_event. Any concurrent or later
shutdown() caller then sees state == SHUTDOWN and blocks forever on
_shutdown_event.wait().

Move the set_state(SHUTDOWN) call inside the try so finally always
runs and releases waiters. CancelledError still propagates to the
caller after finally as expected; we just no longer leak the lock.

Add a regression test that simulates cancellation inside
_write_run_state() and asserts both that state has moved to SHUTDOWN
and that _shutdown_event is set.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-17 17:21:03 +02:00
2021-03-16 15:47:40 +01:00
2025-10-08 10:44:49 +02:00
2020-07-29 14:45:37 +02:00
2024-09-30 18:42:08 +02:00

Home Assistant Supervisor

First private cloud solution for home automation

Home Assistant (former Hass.io) is a container-based system for managing your Home Assistant Core installation and related applications. The system is controlled via Home Assistant which communicates with the Supervisor. The Supervisor provides an API to manage the installation. This includes changing network settings or installing and updating software.

Installation

Installation instructions can be found at https://home-assistant.io/getting-started.

Development

For small changes and bugfixes you can just follow this, but for significant changes open a RFC first. Development instructions can be found here.

Release

Releases are done in 3 stages (channels) with this structure:

  1. Pull requests are merged to the main branch.
  2. A new build is pushed to the dev stage.
  3. Releases are published.
  4. A new build is pushed to the beta stage.
  5. The stable.json file is updated.
  6. The build that was pushed to beta will now be pushed to stable.

Home Assistant - A project from the Open Home Foundation

Languages
Python 96%
JavaScript 3.9%