Skip to content

feat(buyer-agent-registry): observer lifecycle — remove_mutation_observer + thread-safe registry #696

@bokelley

Description

@bokelley

Follow-up to #380 / #692.

Gap

PgBuyerAgentRegistry.add_mutation_observer() ships in #692 but the registry is append-only and unlocked:

self._mutation_observers: list[MutationObserver] = []

def add_mutation_observer(self, observer):
    self._mutation_observers.append(observer)

Two consequences:

  1. No removal. Tests that wire fixtures across reused registry instances (uncommon in production, common in test suites) accumulate observers across the suite. A test that asserts "observer fires once" will leak into the next test that asserts "observer fires once."
  2. Append race with iteration. _notify_mutation iterates self._mutation_observers on the mutation hot path. A late-registered observer added concurrently with a commit can miss its first mutation. In practice all wiring happens at boot, so the window is theoretical — but the lack of contract is worth documenting either way.

Proposed

def add_mutation_observer(self, observer: MutationObserver) -> None: ...
def remove_mutation_observer(self, observer: MutationObserver) -> bool:
    \"\"\"Returns True if observer was registered and removed.\"\"\"
    ...

Either:

  • (a) document that observers must be wired before any mutation traffic and the registry is append-only at that point — accept the test-fixture footgun, OR
  • (b) guard `_mutation_observers` with a lock and provide `remove_mutation_observer`.

The cache-invalidation use case from #692 only ever needs one observer for the lifetime of the registry, so (a) is defensible. But adopters building observability hooks (audit emit, metrics counter) will want to wire and unwire per request scope — those will need (b).

Why now

Closing the API surface in a follow-up is cheaper than adding remove later when adopters have already started subclassing or wrapping the registry.

Acceptance

  • remove_mutation_observer API + conformance test, OR explicit documented constraint that observers are register-only at boot
  • No regression on existing add_mutation_observer semantics

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type
    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