Skip to content

feat(serving): replay outbox + dead-letter behind ControlPlaneStore (ADR 0010 slice 3) - #133

Merged
brownjuly2003-code merged 1 commit into
mainfrom
feature/control-plane-slice3-outbox
Jul 2, 2026
Merged

brownjuly2003-code merged 1 commit into
mainfrom
feature/control-plane-slice3-outbox

Conversation

@brownjuly2003-code

Copy link
Copy Markdown
Owner

Summary

Third rollout slice of ADR 0010: the replay outbox and dead-letter status transitions move behind the ControlPlaneStore port, preserving invariant 8 verbatim (the outbox↔dead-letter transactional coupling). Pure extraction — table DDL and SQL shapes are byte-compatible; no behavior change on the embedded (default) profile.

  • ControlPlaneStore gains claim_due_outbox_entries / get_pending_outbox_entry (reads), mark_outbox_sent / schedule_outbox_retry (invariant 8, one transaction each), enqueue_outbox_replay (the other invariant-8 half), plus get_dead_letter_event_for_replay / dismiss_dead_letter_event / dead_letter_event_exists / get_dead_letter_event / list_dead_letter_events / get_dead_letter_stats for the router's read/write paths.
  • OutboxProcessor and EventReplayer gain an additive store: ControlPlaneStore | None = None kwarg; when omitted (every existing call site) they build a private embedded store from whichever conn/duckdb_path they already owned — preserving main.py's :memory:-vs-file dual-connection split verbatim. routers/deadletter.py resolves the app's shared store and passes it via store=, so it never reaches query_engine._conn directly.
  • ensure_outbox_table / ensure_dead_letter_table moved to control_plane/embedded.py. Unlike the webhook/alert log methods, the outbox/dead-letter write-transaction methods do NOT lazily ensure their tables — ensure_outbox_schema runs once at construction instead, matching pre-port timing exactly and keeping the fault-injection rollback tests (table-drop, connection-wrapper) meaningful.

Test plan

  • tests/unit/test_control_plane_store.py — new outbox/dead-letter coverage (claim ordering, mark-sent + rollback, retry/backoff/kafka-floor, replay enqueue + rollback, tenant-scoped reads, stats)
  • Structural ratchet extended to outbox.py / event_replayer.py / routers/deadletter.py
  • Full unit+property: 1575 passed
  • mypy strict: clean
  • ruff (check + format): clean
  • Targeted integration (test_outbox.py, test_deadletter.py, test_logical_correctness.py): 27 passed
  • Full integration suite (pytest tests/integration -m "not requires_docker and not live") — a re-run was interrupted by a tool error at the end of the authoring session. Rerun before merging; targeted runs of every touched file passed.

🤖 Generated with Claude Code

…ADR 0010 slice 3)

Third rollout slice of ADR 0010: the replay outbox and dead-letter status
transitions move behind the ControlPlaneStore port, preserving invariant 8
verbatim. Pure extraction — table DDL and SQL shapes are byte-compatible; no
behavior change on the embedded (default) profile.

- ControlPlaneStore gains claim_due_outbox_entries / get_pending_outbox_entry
  (reads), mark_outbox_sent / schedule_outbox_retry (invariant 8, one
  transaction each), enqueue_outbox_replay (the other invariant-8 half —
  EventReplayer.replay's dead-letter-update + outbox-insert), plus
  get_dead_letter_event_for_replay / dismiss_dead_letter_event /
  dead_letter_event_exists / get_dead_letter_event / list_dead_letter_events /
  get_dead_letter_stats for the router's read/write paths.
- OutboxProcessor and EventReplayer gain an additive `store:
  ControlPlaneStore | None = None` kwarg; when omitted (every existing call
  site) they build a private embedded store from whichever conn/duckdb_path
  they already owned, preserving main.py's :memory:-vs-file dual-connection
  split verbatim. routers/deadletter.py resolves the app's shared store and
  passes it via store=, so it never reaches query_engine._conn directly.
- ensure_outbox_table / ensure_dead_letter_table moved to
  control_plane/embedded.py. Unlike the webhook/alert log methods, the
  outbox/dead-letter write-transaction methods do NOT lazily ensure their
  tables — ensure_outbox_schema runs once at construction instead, matching
  pre-port timing exactly and keeping the fault-injection rollback tests
  (table-drop, connection-wrapper) meaningful.

Tests: tests/unit/test_control_plane_store.py gains outbox/dead-letter
coverage (claim ordering, mark-sent + rollback, retry/backoff/kafka-floor,
replay enqueue + rollback, tenant-scoped reads, stats); structural ratchet
extended to outbox.py/event_replayer.py/routers/deadletter.py; 6 test files'
ensure_dead_letter_table imports rewired to the new module.

Verified: full unit+property 1575 passed, mypy strict clean, ruff clean,
targeted integration (test_outbox/test_deadletter/test_logical_correctness)
27 passed. Full integration suite re-run was interrupted by a tool error at
session end — rerun tests/integration -m "not requires_docker and not live"
before merging.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@brownjuly2003-code
brownjuly2003-code force-pushed the feature/control-plane-slice3-outbox branch from 776e776 to 2919a5d Compare July 2, 2026 18:14
@github-actions

github-actions Bot commented Jul 2, 2026

Copy link
Copy Markdown

DORA Metrics

  • Window: last 30 days
  • Branch: main
  • Deployment frequency: 127 total / 29.63 per week
  • Lead time for changes: avg 0.41h / median 0.0h
  • Change failure rate: 76.38% (97/127)
  • MTTR: 0.25h across 3 incident(s)

@brownjuly2003-code
brownjuly2003-code merged commit 4a97cf5 into main Jul 2, 2026
23 checks passed
@brownjuly2003-code
brownjuly2003-code deleted the feature/control-plane-slice3-outbox branch July 2, 2026 18:57
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.

2 participants