Skip to content

refactor: dedupe outbox rules between real and fake clients#109

Merged
lesnik512 merged 6 commits into
mainfrom
refactor/client-rules-kernel
Jun 23, 2026
Merged

refactor: dedupe outbox rules between real and fake clients#109
lesnik512 merged 6 commits into
mainfrom
refactor/client-rules-kernel

Conversation

@lesnik512

Copy link
Copy Markdown
Member

Summary

AbstractOutboxClient has two real adapters — OutboxClient (SQL/Postgres) and FakeOutboxClient (in-memory) — and the outbox rules (eligibility, lease, retry timing, scheduling, the DLQ projection) were written down twice: once as SQLAlchemy in client.py/producer.py, once as Python in testing.py, kept in parity by hand and by comment (P9, B10). This change removes the duplication where the implementations can be shared and replaces hand-parity with a machine-checked contract where they cannot.

Spec/plan: planning/changes/2026-06-23.01-client-rules-kernel/.

What changed

  • _scheduling.py (new stdlib-only leaf) — is_future_dated, resolve_next_attempt_client_side, validate_activate_args, the pure activate-args helpers shared by the real and fake publish paths (previously scattered across producer.py/broker.py, with publish_batch inlining one of them).
  • _DLQ_PROJECTION in schema.py — one declarative (outbox_col, dlq_col) map that both the real DLQ CTE and the fake build their column lists from. A DLQ column change is now one edit, not hand-kept parity in two languages.
  • tests/test_client_contract.py — one parametrized scenario module pins the shared AbstractOutboxClient surface (fetch / delete_with_lease / mark_pending_with_lease + DLQ) against both adapters: fake everywhere, real Postgres auto-skipped when unreachable. Drift fails a test instead of shipping.
  • Replace-don't-layer — removed ~220 lines of subsumed per-adapter tests (basic fetch/delete/mark, the manual fake-vs-real predicate-parity test). Real-specific coverage (SKIP LOCKED concurrency, autocommit round-trip, DB-clock retry timing, DLQ CTE) stays.

Scope decisions made during execution

  • The eligibility/lease-cutoff/retry-timing rules are left as two implementations (the real client runs them server-side in Postgres for atomicity + clock authority; a pure version would be fake-only — the one-adapter indirection trap). They are co-verified, not co-implemented.
  • cancel_timer and timer_id insert-dedup are broker/producer concerns, not on AbstractOutboxClient, so they stay in test_integration.py / test_fake.py.
  • Within-batch fetch return order is unspecified (F2-09), so the suite asserts FIFO selection under LIMIT, not return order.
  • Documented residual: an in-process test can't manufacture cross-host DB-vs-worker clock skew, so the real client's server-side make_interval clock authority stays a documented invariant, not an assertion.

Verification

  • just test: 543 passed, 100% coverage.
  • just lint-ci: ruff + ty clean.

🤖 Generated with Claude Code

lesnik512 and others added 6 commits June 23, 2026 16:22
…ing.py

Move is_future_dated, resolve_next_attempt_client_side and validate_activate_args
into a new stdlib-only leaf module so the real and fake publish paths share one
copy. publish_batch now calls the resolver instead of inlining it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
One parametrized scenario module pins the shared AbstractOutboxClient surface
(fetch / delete_with_lease / mark_pending_with_lease + DLQ) against both the
in-memory fake and real Postgres (auto-skipped without a DB). A per-adapter
harness hides substrate differences; scheduling is seeded as server-side
make_interval offsets, matching the existing predicate-parity idiom.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
_DLQ_PROJECTION (+ _DLQ_INJECTED_COLUMNS) in schema.py is now the single source
the real DLQ CTE and the fake's delete_with_lease both build from, replacing the
hand-kept parity (the P9 comment) between the two substrates.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Remove the basic single-adapter fetch/delete/mark scenarios and the manual
fake-vs-real predicate-parity test that test_client_contract.py now subsumes on
both adapters. Real-specific coverage (SKIP LOCKED concurrency, autocommit
round-trip, DB-clock retry timing, DLQ CTE) stays. Coverage holds at 100%.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Promote the conclusions into CLAUDE.md (User-owned schema / DLQ / Test broker)
and architecture/{dlq,test-broker}.md alongside the code, and mark the
client-rules-kernel change shipped.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@lesnik512 lesnik512 merged commit 41dddcb into main Jun 23, 2026
3 checks passed
@lesnik512 lesnik512 deleted the refactor/client-rules-kernel branch June 23, 2026 14:13
lesnik512 added a commit that referenced this pull request Jun 23, 2026
Patch release: internal refactors (#109-#111, no behavior change) + richer PyPI
metadata (#108). Drop-in upgrade. Tag/publish gated on explicit go per the
release procedure.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.

1 participant