Skip to content

Possible bug: async_initialize's except Exception doesn't catch CancelledError, leaving the gateway half-up after HA bootstrap timeout #769

Description

@zigpy-review-bot

⚠️ Investigation, not a fresh confirmed reproduction. Reading from a user-supplied debug log against zha==1.3.1 / bellows==0.49.1 / zigpy==1.4.1 on Python 3.14.2, confirmed against the 1.3.1 tag.

home-assistant/core#172247 attached a full ZHA debug log covering one HA boot. The captured failure mode is a CancelledError from Home Assistant's STAGE_2_TIMEOUT (homeassistant/bootstrap.py:141, 300 s) cancelling ZHA's setup while it is inside bellows.zigbee.application.start_network_restore_route_tablexncp_set_route_table_entry. The relevant log fragment:

2026-05-26 16:13:28.144 ERROR (MainThread) [homeassistant.config_entries] Setup of config entry '… ZBT-1 …' for zha integration cancelled
Traceback (most recent call last):
  File "…/homeassistant/components/zha/__init__.py", line 188, in async_setup_entry
    await zha_gateway.async_initialize()
  File "…/zha/application/gateway.py", line 273, in async_initialize
    await self._async_initialize()
  File "…/zha/application/gateway.py", line 256, in _async_initialize
    await self.application_controller.startup(auto_form=True)
  …
  File "…/bellows/zigbee/application.py", line 299, in start_network
    await self._restore_route_table(backup.network_info.route_table)
  …
asyncio.exceptions.CancelledError: Global task timeout: Bootstrap stage 2 timeout

The relevant async_initialize in zha/application/gateway.py:270-276:

async def async_initialize(self) -> None:
    """Initialize controller and connect radio."""
    try:
        await self._async_initialize()
    except Exception:
        await self.shutdown()
        raise

asyncio.CancelledError is a subclass of BaseException (not Exception) since Python 3.8, so this except clause does not catch it. The cancellation propagates upward without self.shutdown() running, leaving the gateway half-initialised:

  • controller_event.set() happened earlier in start_network (at L244 in bellows 0.49.1), so is_controller_running later evaluates True at the bellows level.
  • _watchdog_task was started by zigpy.application.initialize at L217 and is never cancelled. In the linked debug log it continues to feed every ~10–15 s for the remaining ~5 minutes of capture.
  • _async_initialize never reaches load_devices() / load_groups() / add_listener(self) / platform setup — so the ZHA gateway is alive on the radio side but the entity layer is not wired up.

The user-visible symptom matches what home-assistant/core#172247 (and several comments in home-assistant/core#130548) describe: devices show up from the entity registry but commands fail. There is no recovery — the watchdog stays happy, so nothing triggers a reload.

Possible directions for a fix

Sketching, none committed:

  • Broaden the handler so shutdown() runs on cancellation too — e.g. except (Exception, asyncio.CancelledError): (or except BaseException: if also wanting to clean up on KeyboardInterrupt, with the usual caveats). Re-raising at the end preserves the cancellation up the stack.
  • Alternative: wrap the cleanup in try / finally instead of try / except / raise, so it always runs.
  • Either way, shutdown() needs to be safe to call mid-_async_initialize (it appears so: zigpy.application.shutdown() cancels _watchdog_task and tolerates missing optional state).

What I have not verified

Happy to fold in any additional context the maintainers have.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions