Repository navigation
Carry the correlation a slice's events agree on onto its side effects (#1101) - #1105
Merged
Merged
Conversation
…#1101) A message published with slice.PublishMessage(message) went out with its tenant alone, even though the slice held the correlation id of the event that caused it. The bus then fell back to its own context, which under the daemon is one context per batch, so two unrelated flows landing in one page were given the same correlation id. Inline, each flow lost its id. The override from #186 works, but it needs every projection author to know about it, and the people who need the correlation usually don't own the projection. So this is now the default rather than an option. The envelope's correlation id changes for existing side effects whenever event metadata is on. PublishMessage(message) doesn't say which event it came from, so the slice only vouches for what all of its events agree on. A null on an appended event, a disagreement or an empty slice carries nothing, and taking the last event's value would be a guess. With no agreement the call is the tenant-only one as before, so a store or sink sees no change with event metadata off. Synthetic events don't count. slice.Reference<T>() and a composite's upstream stages put stand-ins beside the appended events, and those were never appended, so their null ids say nothing about the flow. Counting them would have left every enriched slice with the bug. ICanWrapEvent can't recognise them because plain References<T> doesn't implement it, so they get a marker. Causation is deliberately left out. Wolverine lets a causation-id header win over the conversation id when it sets up a handler's session, and that conversation id is the handler's own envelope id. Copying the event's causation would have a side effect's handler name the root message as the cause of what it appends, and flatten the chain from there. MessageMetadata.CausationId stays available to anyone who sets it on purpose. The decision lives on EventSlice so the daemon and both inline paths cannot drift. MessageMetadata.From(IEvent) covers the mixed case, where only the projection knows which event a message belongs to, and gives the same answer as the default. It takes the event's tenant, because the message answers to that one event. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Nov 2, 2026
erdtsieck
pushed a commit
to erdtsieck/jasperfx
that referenced
this pull request
Nov 3, 2026
CloneEventWithNewData and WithData dropped CorrelationId and CausationId, so a slice built by fan out or ReplaceEvent held nulls and PublishMessage(message) fell back to the tenant-only call that JasperFx#1105 fixed for every other slice. There are two clone methods and two FanOut extensions. A slicer's fan out rules and ReplaceEvent use CloneEventWithNewData, while EventSlice.FanOut, and so SliceGroup.FanOutOnEach, uses WithData. WithData now delegates so the two cannot drift apart again. No store writes these ids from an event: Marten and Fisher read only Version and Sequence from the last event, Polecat reads nothing, and the document columns and ITracked come from the session. A projection that reads them off a clone itself now sees the source's value instead of null. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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 #1101.
A message published with
slice.PublishMessage(message)went out with its tenant alone, even though the slice held the correlation id of the event that caused it. Under the async daemon the bus then fell back to one id per page, so two unrelated flows landing in one page shared a correlation id. Inline, each flow lost its id.What changes
When a message has no explicit metadata, its slice now supplies the correlation id if every appended event in the slice agrees on one. This applies on the async path and both inline paths, which all go through one method on
EventSlice, so they can't drift apart.slice.Reference<T>()and composite stages add events nobody appended, and their null ids say nothing about the flow. Counting them would have left every enriched slice with the bug.References<T>(and soUpdated<T>) andProjectionDeleted<TDoc, TId>now carry an internalISyntheticEventmarker.MessageMetadata.From(IEvent)is new, so the mixed case is one line:slice.PublishMessage(msg, MessageMetadata.From(e)). It takes the event's tenant and correlation id.slice.PublishMessage(message, metadata)still wins, and the order between the two lists is unchanged.Decisions worth checking
From. The issue suggestedIEvent.CausationId, but it does damage here. Wolverine setsConversationId = originalEnvelope.Idwhile handling a message, so today a side effect handler's events correctly name that side effect as their cause. Acausation-idheader takes precedence over that inOutboxedSessionFactory, so copying the event's causation would make those events name the root message instead, and flatten the chain.MessageMetadata.CausationIdstays available to anyone who sets it on purpose. A test pins this.Release note
With event metadata on, side effects from
slice.PublishMessage(message)now carry the correlation id that all of the slice's appended events agree on. The id used to be the daemon page's id (async) or a fresh one (inline). This changes the envelope's correlation id for existing side effects. A customIMessageSinkorIProjectionBatchthat overrides the metadata overload now receives these calls instead of the tenant-only overload.With event metadata off, or when nothing agrees, nothing changes. New public
MessageMetadata.From(IEvent).Known limits
ReplaceEventslices, becauseCloneEventWithNewDatadoesn't copyCorrelationId, despite its doc comment. Fixing that may change what stores write on projected documents, so it needs its own issue.Updated<T>events has no appended event to read, so its side effects fall back too.Testing
Full gate on this branch, locally on net9.0 and net10.0: 7,788 passed, 0 failed, 62 skipped. With the call sites or the stand-in skip reverted, the new tests failed, and they also failed against a "take the last value" mutation.
That was the third gate run on this commit. The first two each failed on a test this change doesn't touch:
CommandLineTests.DescribeCommandTests.write_file_smoke_test_as_htm:Collection was modifiedinsideAnsiConsole.ExportHtml, 2 of 12 in a loop.CommandLineTestshas no reference toJasperFx.Events.SideEffectGateWarmupThrottleTests.the_gate_flip_is_driven_by_committed_progression_not_by_what_was_loaded, at a load average around 60. By reading the code it's a race betweenLastCommittedand the gate flip, but that wasn't reproduced.Marten, Polecat and Fisher need nothing: each
IProjectionBatchoverrides the metadata overload and forwards it, and all three Wolverine bridges map the correlation id. That comes from reading their source. This has not been run end to end against a store or Wolverine, and no compliance fact proves that a store forwards metadata.🤖 Generated with Claude Code