Skip to content

Inbox gates only the delivery's own message (#534) - #540

Merged
brenpike merged 11 commits into
masterfrom
bugfix/534-inbox-gates-the-delivery-entry
Sep 26, 2026
Merged

brenpike merged 11 commits into
masterfrom
bugfix/534-inbox-gates-the-delivery-entry

Conversation

@brenpike

Copy link
Copy Markdown
Owner

Closes #534.

Problem

InboxBehavior<> gated every Command dispatched with a delivery's IMessageBrokerContext. A Command that a received handler dispatches in-process with context.InMemory() inherits that context, so the Inbox treated it as the delivery itself:

  • in-memory Inbox (the default): the nested Command was silently skipped;
  • relational (EF) Inbox: the id was re-claimed, and a second nested Command in the same delivery was skipped;
  • standalone Cosmos Inbox: the nested Command completed the delivery's marker before the outer handler finished, so if the outer handler then failed, the redelivery was skipped and its work lost.

Fix

The Inbox now gates only the Delivery Entry: the first message it sees on the delivery's context in the current receive attempt. BrokeredMessageReceiver installs a fresh entry at the start of each recovery attempt (BeginReceiveAttempt), above the virtual DispatchReceivedMessageAsync, so receiver overrides such as ChangeFeedReceiver are covered. Every other message dispatched in-process in that attempt passes through the Inbox and runs. One internal change fixes all three Inbox tiers; no store contract and no public API change.

Interim guarantee (ADR-0041 scope limits)

  • A delivered Command, and every Command it dispatches in-process, is fully covered by the Inbox. Whether that work commits atomically with the receipt depends on the tier (ADR-0006, ADR-0009).
  • A delivered Event is not itself deduplicated. The first Command its handler(s) dispatch in each receive attempt is gated; later Commands in that attempt are at-least-once on retry or redelivery (previously they were silently dropped).
  • A direct call to the public DispatchReceivedMessageAsync does not start a new receive attempt and is outside the guarantee.

The correct long-term design is an idempotent receiver at the delivery boundary (unit of work, Inbox and outbox around the whole delivery). That is epic #539, which also absorbs #538 (the Cosmos document tier's separate gate).

Consumer impact

No code change. A Command a received handler dispatches in-process now runs. The Inbox is not a loop guard: a handler that dispatches a new Command back into itself now loops on every Inbox tier.

Also

  • Corrected the InMemoryBrokeredMessageOutbox comment on when the processed stamp is written relative to publishing.

Design record

  • ADR-0041 (new): the decision, rejected options (id+type keying, gating at the receive seam, a non-broker nested context, a re-entrancy flag, an attempt ordinal, keying on the deserialized payload), the closed-by-construction argument, and the scope limits above.
  • CONTEXT.md gains the Delivery Entry term; the MessageBrokers README Inbox section is updated.

Validation

  • dotnet test on Chatter.sln (net10.0) at b89ef7e: all projects green (4423 passed, 2 known Azure Service Bus skips; one known CircuitBreaker timing flake passed on re-run). Later commits change docs and comments only; MessageBrokers 1413/1413, Reliability.EntityFramework 250/250, SqlChangeFeed 472/472, Reliability.Cosmos 580/580 at 4818315.
  • All production code written test-first. The retry fix is pinned by two tests that drive the real receiver recovery loop with a handler that builds a fresh Command per attempt, one of them through a receiver that overrides dispatch.
  • Local adversarial review: three passes. Pass 1 found the retry bypass (fixed by the attempt-scoped entry). Pass 2 found the event fan-out and direct-dispatch limits (stated as scope limits; redesign tracked in [Epic] Idempotent receiver: move the unit of work, inbox and outbox processing to the delivery boundary #539). Pass 3 fixed two wording overstatements; its remaining finding (the Inbox keys on the producer-supplied MessageId) is pre-existing and governed by ADR-0009 and ADR-0015.

Versioning

  • Chatter.MessageBrokers 0.35.2 -> 0.35.3 (0.35.2 was never tagged). No other package changes.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 481831564c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@brenpike
brenpike merged commit 5d2c26f into master Sep 26, 2026
16 checks passed
@brenpike
brenpike deleted the bugfix/534-inbox-gates-the-delivery-entry branch September 26, 2026 16:14
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.

In-memory Inbox silently skips a Command dispatched with context.InMemory() from inside a received handler

1 participant