Skip to content

feat: foreign-broker relay from OutboxSubscriber#44

Merged
lesnik512 merged 14 commits into
mainfrom
feat/foreign-broker-relay
Jun 4, 2026
Merged

feat: foreign-broker relay from OutboxSubscriber#44
lesnik512 merged 14 commits into
mainfrom
feat/foreign-broker-relay

Conversation

@lesnik512

Copy link
Copy Markdown
Member

Summary

  • Officially support the FastStream-native decorator-relay pattern (@kafka_pub @broker_outbox.subscriber(...)) so OutboxSubscriber can be the source of an outbox-to-foreign-broker chain. The chain mechanism is upstream FastStream — no dispatch-path changes were needed for the bare relay.
  • Three guardrails on top: refuse OutboxResponse + foreign-publisher dual-fire (G1), WARN on unstarted foreign brokers at OutboxBroker.start() (G2), opt-in propagate_inbound_headers: bool = False kwarg (G3).
  • Tutorial promoted to the top of Usage nav; README quickstart rewritten to lead with the relay example.
  • Concrete faststream-sqlbroker comparison documented in the spec and CLAUDE.md.

What changed

  • Code: OutboxSubscriber.process_message override (G3 + G1 hook), OutboxSubscriber.consume override (re-raise _OutboxConfigError), OutboxBroker.start extension (G2 warning), propagate_inbound_headers plumbed through OutboxSubscriberConfig → factory → registrator → router → fastapi router.
  • Tests: 8 new unit tests in tests/test_relay.py (against TestKafkaBroker) + 1 integration test in tests/test_integration.py (real Postgres + simulated foreign-publish failure proving at-least-once via retry).
  • Docs: new docs/usage/relay.md (promoted to top of Usage nav), README rewrite, intro callout, CLAUDE.md subsection.

Test plan

  • tests/test_relay.py — 8 tests: naked chain (with default-OFF header behavior), KafkaRouter publisher shape, OutboxRouter subscriber shape, header propagation TRUE, header propagation FALSE, explicit user-headers-win override, OutboxResponse + foreign publisher refused, unstarted foreign broker warns.
  • tests/test_integration.py::test_relay_at_least_once_under_foreign_publish_failure — real Postgres-backed OutboxBroker + TestKafkaBroker; first foreign publish raises, retry succeeds, row clears.
  • just lint-ci clean.
  • just test passes — 401 tests, 100% coverage.

Non-blocking follow-up notes (from final review)

  • G2's WARNING message lists queues from all subscribers, not just those that decorate the specific unstarted foreign publisher. Operator-visible but harmless; could be tightened.
  • _OutboxConfigError inherits RuntimeError so user except RuntimeError blocks observe it; documented in CLAUDE.md.

Spec: planning/specs/2026-06-04-foreign-broker-relay-design.md
Plan: planning/plans/2026-06-04-foreign-broker-relay-plan.md

🤖 Generated with Claude Code

lesnik512 and others added 14 commits June 4, 2026 18:23
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pins the FastStream-native cross-broker chain against OutboxSubscriber
with no guardrail code in place: a plain handler return relays to Kafka
via the @publisher decorator stack.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Confirms include_router-before-start() resolves the router-publisher's
ConfigComposition to the real producer (Kafka side) and that
broker_outbox.include_router wires foreign-decorated subscribers from
an OutboxRouter the same as broker-direct ones.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds opt-in inbound-header propagation on OutboxSubscriber so handlers
that return a plain value can forward outbox-row headers to the
foreign publisher chain. Default False matches FastStream's
broker-wide convention. The dispatch hook lives in a process_message
override that Task 5 (OutboxResponse + foreign-publisher guard) also
uses.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Replace body-only assertion with cmd.headers capture so the test
  actually verifies header propagation.
- Add complementary negative test for the default-False case.
- Switch # type: ignore[override] → project convention: suppression
  removed entirely since ty does not flag OutboxSubscriber.process_message.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
A handler that returns OutboxResponse(...) and is also decorated by a
foreign-broker publisher would both insert an outbox row AND publish
to the foreign broker on every dispatch. Detect the combo at dispatch
time and raise a RuntimeError pointing at the two valid patterns.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Preflight check during OutboxBroker.start() walks subscribers for
publishers whose _outer_config is foreign and logs one WARNING per
unstarted foreign broker. Operators see the cause immediately
instead of debugging an AttributeError on the first relay attempt.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Confirms the FastStream AckPolicy nack path (publisher-chain exception
-> AcknowledgementMiddleware.__aexit__ -> outbox row nack/retry) end
to end against a real Postgres-backed OutboxBroker plus TestKafkaBroker.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The outbox-to-foreign-broker relay is the canonical use case for this
package; promote it to the front page so readers see the payoff line
immediately. Keep the standalone-queue example as a secondary quickstart
for users without a downstream broker.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…rride

The consume() and process_message overrides reproduce upstream
SubscriberUsecase paths verbatim so _OutboxConfigError can be re-raised
without losing FastStream's existing behavior. The upstream-mirrored
branches (StopConsume/SystemExit in consume(); parser error and
no-matching-handler fall-through in process_message) are unreachable
from the outbox dispatch path. Internal outbox publishers in subscriber
decorator chains (filtered by "outer is self.config") are also unreachable
in normal usage — users should call broker.publish() directly instead.
Marking these branches # pragma: no cover keeps the 100% gate clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ders

Covers the spec contract that when propagate_inbound_headers=True,
the subscriber only fills headers when result_msg.headers is empty —
a handler that returns Response(value, headers=...) keeps its choice.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@lesnik512 lesnik512 self-assigned this Jun 4, 2026
@lesnik512 lesnik512 merged commit 0442b35 into main Jun 4, 2026
3 checks passed
@lesnik512 lesnik512 deleted the feat/foreign-broker-relay branch June 4, 2026 21:15
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