Defer aggregate cache compaction during composite projection execution#206
Merged
Conversation
The per-projection RecentlyUsedCache was being compacted at the end of every projection's BuildBatchAsync, including upstream stages of a composite. With a small CacheLimitPerTenant the compaction at end of stage 1 evicted entities that downstream stages were about to look up via EnrichWith<T>().AddReferences() or the public TryFindUpstreamCache<TId, T> helper. Those evicted ids fell through to storage.LoadManyAsync, which cannot see the upstream's queued in-flight writes (the IProjectionBatch hasn't been flushed yet), so the downstream lookup silently returned no entity and the slice was processed without enrichment. Reproducible from the Marten side by setting Options.CacheLimitPerTenant = 1 on AppointmentProjection and running multi_stage_projections.end_to_end — AppointmentByExternalIdentifier ends up empty because nearly every stage-2 lookup misses both the cache and storage. Fix: composite-aware compaction. - AggregationRunner.BuildBatchAsync now skips end-of-batch CompactIfNecessary when range.BatchBehavior == BatchBehavior.Composite. - New AggregationRunner.CompactCachesAsync iterates _caches and applies CompactIfNecessary to each per-tenant cache. - New default-implemented Task CompactCachesAsync() on ISubscriptionExecution and IGroupedProjectionRunner so callers can opt in without breaking existing implementations. - GroupedProjectionExecution forwards CompactCachesAsync to the runner. - CompositeExecution.buildBatchWithNoSkippingAsync calls CompactCachesAsync on every child execution after all stages finish, restoring per-tenant bound at the composite boundary. Refs JasperFx/marten#4329. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
jeremydmiller
added a commit
that referenced
this pull request
May 7, 2026
Two changes ride this release: 1. Composite-projection cache eviction fix (#206): downstream stages of a composite no longer lose access to upstream in-flight entities when CacheLimitPerTenant is small relative to per-batch fan-out. 2. ForEntityIds (#208): first-class 1-event-to-N-entities enrichment via group.EnrichWith<T>().ForEvent<E>().ForEntityIds(selector).AddReferences(). Refs JasperFx/marten#4329, #206, #208. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
jeremydmiller
added a commit
to JasperFx/marten
that referenced
this pull request
May 7, 2026
JasperFx.Events 1.35.0 ships two changes that close out the usability gap @xander-abn surfaced in #4329: 1. Composite-projection cache eviction fix (JasperFx/jasperfx#206) — AppointmentByExternalIdentifier (and similar EnrichWith<UpstreamDoc> chains) no longer drop downstream lookups when the upstream's CacheLimitPerTenant is small relative to per-batch fan-out. 2. ForEntityIds (JasperFx/jasperfx#208) — first-class declarative shape for 1-event-to-N-entities enrichment. This commit: - Bumps JasperFx.Events 1.34.0 → 1.35.0 in Directory.Packages.props. - Adds Bug_4329_fan_out_and_cache_limit with two facts: a fan-out scenario using ForEntityIds (one OrderPlacedWithLineItems event referencing 5 upstream Product documents), and a cache-eviction regression that intentionally sets ProductProjection.CacheLimitPerTenant = 1 and verifies 20 distinct upstream lookups all resolve to the in-flight cache. - Wires the new patterns into composite.md and enrichment.md, removes the now-stale "cache-limit is load-bearing for correctness" caveat, and adds a "Fan-out enrichment with ForEntityIds" section sourced from #region sample_for_entity_ids_fan_out in the new test. Refs #4329, JasperFx/jasperfx#206, JasperFx/jasperfx#208. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This was referenced May 7, 2026
Closed
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.
Summary
Companion fix for JasperFx/marten#4329. When a composite projection's upstream stage finishes, its per-tenant
RecentlyUsedCachewas being compacted before downstream stages had a chance to read it. With a smallCacheLimitPerTenantthe compaction at end of stage 1 evicted entities that downstream stages were about to look up viaEnrichWith<T>().AddReferences()or the new publicTryFindUpstreamCache<TId, T>. Evicted ids fell through tostorage.LoadManyAsync, which cannot see the upstream's queued in-flight writes — so the downstream lookup silently dropped them.Reproducible from the Marten side by setting `Options.CacheLimitPerTenant = 1` on `AppointmentProjection` and running `multi_stage_projections.end_to_end` — `AppointmentByExternalIdentifier` ends up empty because nearly every stage-2 lookup misses both the cache and storage. See the diagnosis comment on #4329.
What changes
Composite-aware compaction:
Within a composite batch, every projection's cache stays at full size (one batch's worth of in-flight aggregates), then is compacted as a unit at composite end. Outside composites, behavior is unchanged.
Test plan
🤖 Generated with Claude Code