Skip to content

Gate Archived handling on single-stream projection ownership (#4093)#185

Merged
jeremydmiller merged 1 commit intomainfrom
fix/4093-archived-in-composite
Apr 18, 2026
Merged

Gate Archived handling on single-stream projection ownership (#4093)#185
jeremydmiller merged 1 commit intomainfrom
fix/4093-archived-in-composite

Conversation

@jeremydmiller
Copy link
Copy Markdown
Member

Addresses JasperFx/marten#4093.

Problem

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:

  1. Create a phantom document in a sibling that happens to declare Create(Archived) (legal but almost always accidental in a composite)
  2. Issue redundant mt_archive_stream operations from siblings that have no snapshot for the stream id

Fix

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:

  • Evolve-time (JasperFxAggregationProjectionBase.Runtime.cs): in EvolveAsync, when snapshot == null and the event is Archived, skip. Once a snapshot exists — either pre-loaded or materialized by a preceding event in the same slice — Apply(Archived, current) hooks run normally.
  • Archive-time (AggregationRunner.cs async path + JasperFxSingleStreamProjectionBase.cs inline path): maybeArchiveStream takes a new ownsStream flag. Call sites pass pre-snapshot != null || post-snapshot != null. If neither is set, the archive is skipped.

Compatibility

  • Existing Apply(Archived, current) patterns on genuine owners keep working (verified with Marten's Bug_3874 regression and the full archiving + aggregation suite — 300/300 pass).
  • Archived on a fresh stream that also contains creating events still archives correctly: the first event materializes the snapshot, subsequent Apply(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.cs with two scenarios:

  • Composite containing a contrived Bug4093BarProjection with Create(Archived) no longer materializes a phantom Bug4093BarDoc for another child's stream id.
  • An owning child still archives its stream; a non-owning child is a no-op.

Both pass on net8.0 / net9.0 / net10.0.

Version

JasperFx.Events bumped to 1.28.0.

🤖 Generated with Claude Code

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>
@jeremydmiller jeremydmiller merged commit 28697d7 into main Apr 18, 2026
1 check passed
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.

1 participant