Skip to content

Fix subscription/removal race in Bus.on() fire-and-forget pattern #451

@jessica-claude

Description

@jessica-claude

Description

Bus.on() spawns an async task for add_listener and immediately returns a Subscription. If cancel() is called before the add task completes, the remove finds nothing in the router, then the add task runs — leaving the listener permanently registered with no way to remove it individually.

remove_all_listeners by owner catches it during shutdown, but the individual subscription handle is broken.

Steps to Reproduce

  1. Call sub = bus.on_state_change("light.kitchen", handler=callback)
  2. Immediately call sub.cancel() before the event loop processes the add task
  3. The listener is added after the cancel, with no way to remove it

Expected Behavior

Cancelling a subscription should reliably remove the listener, even if called before the add task completes.

Acceptance Criteria

  • Subscription.cancel() reliably removes the listener even when called before add_listener completes
  • No permanently orphaned listeners after cancel
  • Existing tests pass

Context

Found during post-execution challenge review of bus/scheduler hardening (#437, #412, #414, #436). Pre-existing issue, not introduced by the hardening changes.

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:busEvent busbugSomething isn't workingsize:smallQuick win, < 1 hour

    Projects

    Status

    Refinement

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions