Gate Archived handling on single-stream projection ownership (#4093)#185
Merged
jeremydmiller merged 1 commit intomainfrom Apr 18, 2026
Merged
Gate Archived handling on single-stream projection ownership (#4093)#185jeremydmiller merged 1 commit intomainfrom
jeremydmiller merged 1 commit intomainfrom
Conversation
When multiple single-stream projections share a composite group, every
child sees every slice. An Archived event raised on one child's stream
was previously also routed through sibling projections, which could
either create phantom documents (via an accidental Create(Archived))
or issue redundant mt_archive_stream operations from projections that
do not own the stream.
Two symmetric guards, keyed on snapshot-presence ("owns the stream"):
* EvolveAsync: if snapshot is null and the event is Archived, treat
as a no-op. Apply(Archived, current) still runs once a snapshot
exists (either pre-loaded or materialized by a preceding event in
the same slice), preserving existing Apply(Archived) semantics.
* maybeArchiveStream (both inline and async paths): only issue
storage.ArchiveStream when the projection owns the stream, which
is signalled by a snapshot existing either before or after the
slice is applied.
Version bumped to 1.28.0.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
jeremydmiller
added a commit
to JasperFx/marten
that referenced
this pull request
Apr 18, 2026
Adds end-to-end tests that prove Archived is now safely scoped to the single-stream projection that owns the stream when multiple single-stream children run in the same composite group. The underlying behavior change lives in JasperFx.Events 1.28.0 (PR JasperFx/jasperfx#185) — both guards are keyed on snapshot presence so sibling projections that do not own the stream no longer create phantom documents or issue redundant mt_archive_stream operations. * Bug_4093_archived_in_composite_projections.cs exercises both scenarios: phantom Create(Archived) prevention and owning/non-owning archival. * docs/events/archiving.md gains a new section explaining the composite semantics and the snapshot-ownership rule. * JasperFx.Events bumped to 1.28.0; transitively requires JasperFx 1.24.1. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
4 tasks
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.
Addresses JasperFx/marten#4093.
Problem
When multiple single-stream projections share a composite group, every child sees every slice. An
Archivedevent raised on one child's stream was previously also routed through sibling projections, which could:Create(Archived)(legal but almost always accidental in a composite)mt_archive_streamoperations from siblings that have no snapshot for the stream idFix
Two symmetric guards, keyed on "does this projection own the stream" — measured by whether the projection has a snapshot either before or after the slice is applied:
JasperFxAggregationProjectionBase.Runtime.cs): inEvolveAsync, whensnapshot == nulland the event isArchived, skip. Once a snapshot exists — either pre-loaded or materialized by a preceding event in the same slice —Apply(Archived, current)hooks run normally.AggregationRunner.csasync path +JasperFxSingleStreamProjectionBase.csinline path):maybeArchiveStreamtakes a newownsStreamflag. Call sites passpre-snapshot != null || post-snapshot != null. If neither is set, the archive is skipped.Compatibility
Apply(Archived, current)patterns on genuine owners keep working (verified with Marten'sBug_3874regression and the full archiving + aggregation suite — 300/300 pass).Archivedon a fresh stream that also contains creating events still archives correctly: the first event materializes the snapshot, subsequentApply(Archived)and the archive call both see ownership.Deletes<Archived>()(explicit registration, rare) continues to work because the delete-type branch checks pre-load ownership only, which matches the prior behavior of requiring an existing snapshot.Testing (in Marten)
Companion PR JasperFx/marten#TBD adds
Bug_4093_archived_in_composite_projections.cswith two scenarios:Bug4093BarProjectionwithCreate(Archived)no longer materializes a phantomBug4093BarDocfor another child's stream id.Both pass on net8.0 / net9.0 / net10.0.
Version
JasperFx.Eventsbumped to1.28.0.🤖 Generated with Claude Code