Conversation
Adds a Postgres-backed WebhookDeliverySupervisor that fulfils issue #352: cross-process circuit-breaker state, durable retry queue with FOR UPDATE SKIP LOCKED worker leasing, and per-attempt delivery audit log. Three tables (bootstrap via create_schema(), idempotent): - adcp_webhook_circuit_state — shared breaker state, atomic half-open via UPDATE RETURNING - adcp_webhook_delivery_queue — BIGSERIAL PK is the sequence number; partial index on (status_str, scheduled_at) WHERE IN ('pending','retry') for FUSK queries - adcp_webhook_delivery_log — per-attempt audit trail Key design choices: - send_mcp() always returns None (fire-and-forget enqueue); run_worker() delivers - Retry path calls sender.resend() with stored sent_body/idempotency_key so the same idempotency_key is replayed per spec ("Publishers MUST reuse the same key") - breaker_key param for multi-tenant URL isolation (consistent with InMemory) - Safe-identifier regex for table-name overrides (same guard as PgReplayStore) - worker_started warning on first send_mcp() before run_worker() is running - DB connection held during HTTP send so crashed workers auto-release via rollback 28 new unit tests, all passing. ruff clean, no mypy errors in new files. https://claude.ai/code/session_01DoCRKpgu6jrVNARmazguok
… test - WebhookDeliverySupervisor Protocol was missing breaker_key and notification_type kwargs that exist on InMemoryWebhookDeliverySupervisor; typed call-sites against the Protocol were broken - Re-export PgWebhookDeliverySupervisor from adcp.webhooks so it is discoverable without knowing the internal module path - test_log_insert_failure_does_not_crash_worker was passing vacuously because fetchone on the log cursor was set to raise but _log_attempt_via_conn never calls fetchone; fix by raising directly from conn.execute side-effect - Remove dead _send inner function from test_warning_emitted_only_once https://claude.ai/code/session_01DoCRKpgu6jrVNARmazguok
871ce82 to
e8b2af8
Compare
5 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #352
Summary
src/adcp/webhook_supervisor_pg.py(new, ~800 LOC) —PgWebhookDeliverySupervisor, a Postgres-backed implementation of theWebhookDeliverySupervisorProtocol introduced in feat(decisioning): WebhookDeliverySupervisor + SQLAlchemy A2A stores example #348. Designed for multi-process/multi-worker deployments where the in-memory supervisor cannot provide durability.tests/test_webhook_supervisor_pg.py(new, ~650 LOC) — 28 unit tests using fully-mocked psycopg3 (no real database required).src/adcp/webhook_supervisor.py(patched) — Protocolsend_mcp()was missingbreaker_keyandnotification_typekwargs that existed onInMemoryWebhookDeliverySupervisor; fixed.src/adcp/webhooks.py(patched) — Re-exportsPgWebhookDeliverySupervisorfor discoverability (from adcp.webhooks import PgWebhookDeliverySupervisor).Design highlights
FOR UPDATE SKIP LOCKED LIMIT 1— connection held open during HTTP send; crash rolls back the leaseadcp_webhook_circuit_state; OPEN gates writes insend_mcp()without touching the queueINSERT … ON CONFLICT DO UPDATE … RETURNING— serializes concurrentsuccess_countincrements so exactly one worker triggers the CLOSED transitionsent_body+idempotency_keyin the queue row; retries callsender.resend(prev)to replay the exact same bytesPgReplayStorepattern); all runtime values use parameterized queriesPG_AVAILABLEflag; uses the existing[pg]extra (psycopg[binary]>=3.1.0,psycopg-pool>=3.2.0)Required DDL (included in
create_schema())What was tested
ruff check— cleanmypy src/adcp/— cleanpytest tests/test_webhook_supervisor_pg.py -v— 28/28 passed (fully mocked psycopg3, no DB)create_schema()DDL, circuit breaker gating, half-open timeout transition,FOR UPDATE SKIP LOCKEDpoll, success/failure/final-failure paths, resend-with-stored-body, sink error swallowing, log-write fault isolation,run_worker()lifecyclePre-PR expert review sign-offs
fetchoneraise path unreachable in_log_attempt_via_conn) and one dead inner function — both fixed in the final commit.FOR UPDATE SKIP LOCKEDlease-holds-through-HTTP pattern, atomic UPSERT RETURNING for half-open, and safe-identifier validation all confirmed correct.Generated by Claude Code