Skip to content

Preserve context fields on scheduled sends to non-native transports - #2572

Closed
ArieGato wants to merge 1 commit into
JasperFx:mainfrom
ArieGato:fix/preserve-context-on-scheduled-wrap
Closed

ArieGato wants to merge 1 commit into
JasperFx:mainfrom
ArieGato:fix/preserve-context-on-scheduled-wrap

Conversation

@ArieGato

Copy link
Copy Markdown
Contributor

Fixes #2571.

Summary

  • When an envelope is scheduled to a transport without native scheduled-send (RabbitMQ, Kafka, SharedMemory, ...), MessageRoute.ForScheduledSend wraps it and previously eagerly serialized the inner into outer.Data before TrackEnvelopeCorrelation could stamp context fields. The inner therefore arrived on the receive side with TenantId = null, SagaId = null, CorrelationId/UserName unset — breaking multi-tenanted saga timeouts and observability chains.
  • Defers serialization to EnvelopeReaderWriter.Write, which now unwraps model.Message to serialize the inner.
  • Extracts a virtual MessageBus.StampEnvelope with the idempotent context-derived stamps (Source, CorrelationId, TenantId, UserName); MessageContext overrides it to add SagaId. TrackEnvelopeCorrelation calls it on the outer and, when the envelope is a scheduled wrap, also on the inner.

Test plan

  • CoreTests.Runtime.Scheduled.inner_envelope_is_stamped_before_serialization — in-process via SharedMemory wrap path; asserts TenantId, CorrelationId, UserName on the delivered inner.
  • Wolverine.RabbitMQ.Tests.scheduled_saga_timeout_preserves_tenant — end-to-end multi-tenant saga with Marten + RabbitMQ + durable outbox; verifies the handler runs under the original tenant and the saga completes.
  • Full CoreTests suite (1347/1347) passes on net9.0.
  • RabbitMQ suite: new test passes; unrelated DLQ/tenancy failures are pre-existing flakes (reproduce on main, pass in isolation).

When an envelope is scheduled to a transport without native scheduled-send
support (RabbitMQ, Kafka, SharedMemory, ...), MessageRoute wraps it via
ForScheduledSend and the transport ultimately serializes the inner envelope.
Previously context-derived fields (TenantId, SagaId, CorrelationId, UserName)
were only stamped on the outer wrapper, so the inner lost them by the time
it reached the receiving handler.

Extracts a virtual StampEnvelope method on MessageBus containing the
idempotent context-derived stamps and applies it to both the outer envelope
and the inner envelope of a scheduled wrap. MessageContext overrides it to
add SagaId.

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

Copy link
Copy Markdown
Member

Hey, I'm going to want a little more review on this one, so it's not going to make the release this morning

@ArieGato

Copy link
Copy Markdown
Contributor Author

Should I put any effort in this to resolve the merge conflict?

@jeremydmiller

Copy link
Copy Markdown
Member

I took your test, but went a little different way to keep overhead out of the hot path: #2605

@ArieGato

Copy link
Copy Markdown
Contributor Author

Great you fixed it, thnx!

jeremydmiller added a commit that referenced this pull request May 8, 2026
….CopyContextCorrelationFrom()

Supersedes the implementation in #2709 while keeping the test contract
Lyall established (cherry-picked here as the parent commit so attribution
is preserved end-to-end).

Behavioural shape is identical to Lyall's PR — re-routed envelopes
inherit the inbound's correlation, tenant id, and distributed-trace
parent — but the field copying is delegated to the existing
`Envelope.CopyContextCorrelationFrom` helper. That method was added in
GH-2571 / PR #2572 to give the durable scheduled-send unwrap path
(`ScheduledSendEnvelopeHandler`) and the in-memory tracked-session
replay path (`TrackedSession.ReplayAll`) one canonical place to copy
the six context-correlation fields:

    CorrelationId, ConversationId, TenantId, UserName, ParentId, SagaId

The global-partitioning re-route is the same forwarding shape — same
logical message, different shard — so it should reuse the same helper
rather than re-state the field list inline. This PR adds it as the
third caller.

Implementation:

* `RouteBus` (private subclass of `MessageBus`) still seeds
  `Envelope = inbound` so `IEnvelopeRule.ApplyCorrelation` consumers
  like `PropagateHeadersRule` continue to see the originator and
  honour the `PropagateIncomingHeadersToOutgoing` allowlist.

* `RouteBus` overrides `TrackEnvelopeCorrelation` and replaces the
  base bus's narrower per-field pull-from-bus logic with a single
  `outbound.CopyContextCorrelationFrom(_inbound)` call. The base
  copies `CorrelationId` / `TenantId` / `UserName` from the bus's
  own properties and writes a fresh `ParentId` from
  `Activity.Current` — both wrong for a forwarded envelope:
  `ConversationId` would restart, `SagaId` would drop, and the
  trace would re-root at the interceptor hop instead of continuing
  the inbound's parent.

* The interceptor's `DeliveryOptions` is reduced to just `GroupId`,
  which is the only piece routing itself needs to read — every
  context field flows through the bus's overridden tracker.

Test coverage: same suite, lifted from #2709 unchanged. The two
`ShouldNotContainKey` calls were tweaked to disambiguate Shouldly's
`IDictionary` / `IReadOnlyDictionary` overloads (`Envelope.Headers`
is a concrete `Dictionary`); behavioural assertions are otherwise
verbatim.

Tests: full `Runtime.Partitioning` suite green (78/78) on net9.0,
including the 16 GP-interceptor cases with the four new context
behaviours from #2709.

Co-Authored-By: Lyall Guiney <lyall@sportcontext.com>
Co-Authored-By: Claude Opus 4.7 (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.

Scheduled sends to non-native-scheduling transports lose context fields (TenantId, SagaId, CorrelationId, UserName)

2 participants