Repository navigation
Conversation
…e effect MartenToWolverineMessageBatch always hands DeliveryOptions to PublishAsync, and MessageBus only lets an ISendMyself apply itself when there are none, so ToWebSocketGroup(), ToEndpoint() and friends were routed as the wrapper type and dropped as NoRoutes. Each such message is now applied against its own MessageContext, enlisted in the projection batch's Marten session and flushed after commit, which stamps the side effect's tenant, correlation id, causation id and headers on whatever it sends. Options the message sets itself win.
jeremydmiller
added a commit
that referenced
this pull request
Sep 23, 2026
`unknown_tenant_problem_details_4516.without_the_opt_in_the_unknown_tenant_still_escapes` went red on CI the moment #4551 merged, and it takes the whole `test` job with it -- so every open PR is red regardless of its contents (#4553, #4557, #4558 at the time of writing, two of them from contributors). The test was self contradictory. It asked Alba to assert a 404 inside a scenario it expected to THROW: await Should.ThrowAsync<UnknownTenantIdException>(async () => await host.Scenario(x => { x.Get.Url("/gh4516/tenanted?tenantId=ghost"); x.StatusCodeShouldBe(404); // <- asserted on a request expected to blow up })); Locally the UnknownTenantIdException propagated before Alba evaluated its assertions, so the expected exception won and the test passed. On CI the host turned the exception into a 500 first, so Alba's own 404 assertion fired and raised ScenarioAssertionException instead of UnknownTenantIdException -- a different type, so Should.ThrowAsync failed. The control's actual purpose is narrower than what it asserted: it exists to show that MapUnknownTenantToNotFound() is what produces the 404, not something else in the pipeline. It is not a claim about HOW an unmapped failure surfaces, and that is exactly the part that varies by environment. It now uses IgnoreStatusCode() and asserts only that the response is not a mapped 404, tolerating the other surfacing -- the exception escaping the scenario -- and nothing else. Claude-Session: https://claude.ai/code/session_01VDUrBeB4tTnKj4AExCS1nj Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Member
|
Thank you for this @erdtsieck — the diagnosis was exactly right, and the Closing in favour of #4575, which carries your commit authorship and adds the review follow-up:
One note on the test: the schema clean ran after |
jeremydmiller
added a commit
that referenced
this pull request
Sep 23, 2026
…, on all three stores (#4575) MessageBus only lets an ISendMyself apply itself when no DeliveryOptions are passed -- the guard that keeps TimeoutMessage from looping. A projection side effect always passes some, at minimum the tenant id, so a ToEndpoint(), DelayedFor() or SignalR ToWebSocketGroup() published out of RaiseSideEffects was routed as the wrapper type, found no subscriber, and was dropped as NoRoutes. The bridge now spots an ISendMyself and applies it against its own ProjectionSideEffectContext: a MessageContext enlisted in the same store session, with the batch's storage, so a durable send still commits with the projection. The metadata is stamped in TrackEnvelopeCorrelation -- the one point every outgoing envelope passes through -- rather than mapped onto a single DeliveryOptions that only the outermost send would have seen. Beyond the original diagnosis in #4557: - The correlation id is only taken over when the projection actually supplied one. MessageMetadata.CorrelationId is plain null otherwise, so stamping it unconditionally stripped the correlation id every other side effect message from the same batch carries. It falls back to the batch context's id now. - The side effect contexts are cleared once flushed. Marten only discards its cached message batch after a SUCCESSFUL commit -- SaveChangesAsync does not reset it in a finally -- so a session retrying a failed commit gets the same batch back and would otherwise accumulate, and re-flush, every context from every failed attempt for as long as the session lived. - Polecat and Fisher had the identical defect. The behaviour moved into the store agnostic ProjectionSideEffectSink and all three IMessageBatch implementations delegate to it, so the three bridges cannot drift apart again. - BroadcastToTopicAsync never called TrackEnvelopeCorrelation at all, so a topic broadcast went out with no Source, CorrelationId, ParentId or Store -- for every topic send in every application, not just projection side effects. Supersedes #4557. Closes #4556. Co-Authored-By: Anne Erdtsieck <anne.erdtsieck@topicus.nl> Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This was referenced Sep 24, 2026
This was referenced Sep 30, 2026
This was referenced Oct 8, 2026
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 #4556.
Problem
MartenToWolverineMessageBatchalways passesDeliveryOptions(at least the tenant id) toPublishAsync.MessageBus.PublishAsynconly lets anISendMyselfapply itself whenoptions == null. So aToWebSocketGroup(),ToEndpoint(),DelayedFor(), ... published fromRaiseSideEffectsgets routed as the wrapper type and ends up asNoRoutes.Fix
The batch now spots an
ISendMyselfand applies it against its ownProjectionSideEffectContext:MessageContextenlisted in the same Marten session throughMartenEnvelopeTransaction, with the batch's storage, so durable sends still commit with the projectionAfterCommitAsyncafter the batch's own contextTrackEnvelopeCorrelation, the point every outgoing envelope passes through (publish,EndpointFor, topics), to stamp the side effect's tenant, correlation id, causation id and headers. Anything the message set on its own options wins.I used a context per message instead of changing
TenantId/CorrelationIdon the shared batch context for the duration ofApplyAsync, because slices can publish concurrently.I left the
options == nullguard inMessageBusalone, since it protects against theTimeoutMessageloop.Tests
Bug_4556_projection_side_effects_honor_send_myselfcoversToEndpoint()from an async projection, with metadata (correlation, causation, custom header asserted on the received envelope) and without. Both failed onmainwithNoRoutesforRoutedToEndpointMessage<SendMyselfCommand>.Also still green locally:
Bug_2545,Bug_4268,Bug_2382,Bug_2887,end_to_end_publish_messages_through_marten_to_wolverine,multi_tenancy_queue_usage, and theMartenTests.AncillaryStoresnamespace (46 tests).