Skip to content

feat(decisioning): add registry observer removal#861

Merged
bokelley merged 1 commit into
mainfrom
bokelley/issue-696-registry-observer-removal
May 26, 2026
Merged

feat(decisioning): add registry observer removal#861
bokelley merged 1 commit into
mainfrom
bokelley/issue-696-registry-observer-removal

Conversation

@bokelley
Copy link
Copy Markdown
Contributor

@bokelley bokelley commented May 26, 2026

Summary

Replaces #764 so the required repo-owned checks can run with the normal base-repo secrets/check suite.

Closes #696.

This adds lifecycle management for PgBuyerAgentRegistry mutation observers. Observers can now be removed explicitly, and the observer registry is protected by a lock so add/remove operations do not race with mutation notification setup.

What changed

  • Added remove_mutation_observer(observer) -> bool.
  • Guarded mutation observer registration/removal with a threading.Lock.
  • Changed mutation notification to dispatch against a snapshot of observers, with callbacks still executed outside the lock.
  • Documented snapshot semantics for observers added or removed while a notification is in flight.
  • Added conformance coverage for unregistering observers and self-removal during notification.

Local validation

  • PYTHONPATH=src ruff check src/adcp/decisioning/pg/buyer_agent_registry.py tests/conformance/decisioning/test_pg_buyer_agent_registry.py
  • PYTHONPATH=src pytest tests/conformance/decisioning/test_pg_buyer_agent_registry.py -q (skips locally: no Postgres fixture in this workspace)

@bokelley bokelley enabled auto-merge (squash) May 26, 2026 01:23
Copy link
Copy Markdown

@aao-ipr-bot aao-ipr-bot Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. Lock-protected snapshot copy with dispatch outside the lock is the right shape — re-entrant add/remove from inside an observer can't deadlock, and the "removal during in-flight notification still fires this round" contract is exercised by the self-remove test.

Things I checked

  • Snapshot semantics at src/adcp/decisioning/pg/buyer_agent_registry.py:422-424tuple(self._mutation_observers) taken under the lock, iteration outside. Observers added mid-flight skip this round; observers removed mid-flight still fire this round. Matches the assertion sequence ["self-removing", "persistent", "persistent"] at tests/conformance/decisioning/test_pg_buyer_agent_registry.py:387.
  • Re-entrancy: threading.Lock is non-reentrant, but the with block at _notify_mutation exits before iteration starts, so an observer calling remove_mutation_observer re-acquires cleanly. Self-remove test confirms.
  • remove_mutation_observer return contract: list.remove()'s ValueError is caught → False; success → True. Idempotent double-remove asserted at test_pg_buyer_agent_registry.py:350-351.
  • Conventional-commit prefix: feat(decisioning): is right — additive public method on PgBuyerAgentRegistry, no signature change, no ! needed.
  • Lock scope is minimal — held only across append / remove / tuple-copy. Observer dispatch never runs under the lock.

Follow-ups (non-blocking — file as issues)

  • with_caching at src/adcp/decisioning/pg/buyer_agent_registry.py:417 registers an anonymous lambda observer that cannot be removed by identity. Pre-existing, but now that removal is on the wire it's worth exposing the registered callback (or returning (cache, observer)) so callers can unwire. Out of scope here.

Minor nits (non-blocking)

  1. Docstring on add_mutation_observer blurs the add vs. remove semantics. src/adcp/decisioning/pg/buyer_agent_registry.py:359-362 says "observers added or removed while a notification is in flight apply to the next mutation." The remove path is the opposite — a removed observer still fires this round (which is exactly what the self-remove test locks in). remove_mutation_observer's docstring at L374-376 gets it right; the add_mutation_observer blurb should match: "observers added during a notification fire on the next mutation; observers removed during a notification still fire for that in-flight notification but not subsequent ones."
  2. Test could lock the N-registrations contract. tests/conformance/decisioning/test_pg_buyer_agent_registry.py:342-360 verifies double-remove returns False but doesn't assert the "one registration removed per call" promise at buyer_agent_registry.py:371-372. Two extra lines (add twice, remove once, mutate, expect one call) would pin that behaviorally. Optional.

A re-roll of #764 to get the repo-owned check suite — fine, but worth noting in the body so the next reviewer doesn't go hunting for the diff between them.

LGTM. Follow-ups noted below.

@bokelley bokelley merged commit 2657a8b into main May 26, 2026
23 checks passed
@bokelley bokelley deleted the bokelley/issue-696-registry-observer-removal branch May 26, 2026 01:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

2 participants