#4667 Phase 1 — write-path *Projected variants bypass session-shared trackers#4669
Merged
Merged
Conversation
…trackers Async-daemon parallel slice workers share a single DocumentSessionBase; the projection write path was still reaching into _session.Versions / _session.Revisions / _session.ItemMap from inside the parallel block. #4658 closed StoreProjection; this phase closes the remaining write sites that ProjectionStorage's Store(snapshot) and Store(snapshot, id, tenantId) routed through. Surface change: IDocumentStorage<T>.UpsertProjected(T, string tenantId) IDocumentStorage<T>.InsertProjected(T, string tenantId) IDocumentStorage<T>.UpdateProjected(T, string tenantId) These mirror the existing OverwriteProjected pattern. The 9 writeable closed-shape storage leaves build their corresponding closed-shape op with null trackers; QueryOnly throws NotSupported; SubClass / ValueType / Event storages delegate or throw consistently with their existing OverwriteProjected implementations. Null-tolerance audit applied to all four ConcurrencyMode op classes that hold a tracker dictionary (Optimistic Insert/Update/Upsert + Numeric Insert/Update/ Upsert): _versions / _revisions are now nullable and the dict writes in PostprocessAsync are guarded. Optimistic Update/Upsert's ConfigureCommand treats a null tracker as "expected version unknown" and binds DBNull for the WHERE-guard slot. ProjectionStorage rewrites: * Store(snapshot) -> _storage.UpsertProjected(snapshot, TenantId) * Store(snapshot, id, tenantId) -> _storage.UpsertProjected(snapshot, tenantId) The GH-3850 identity-map maintenance (EjectAggregateFromIdentityMap + _storage.Store(_session, snapshot)) is preserved but gated on Options.EventGraph.UseIdentityMapForAggregates so the default (false) path takes the session-state-free write and the opt-in (true) path still gets correct inline-projection-rewriting-an-immutable-aggregate semantics. Per the #4667 Phase 3 design note, opt-in is documented as not safe for parallel projection workers — that race is accepted with the flag on. Verified locally: * DaemonTests net9.0 — 187 / 187 green * EventSourcingTests net9.0 — 1361 / 1368 green (7 pre-existing skips, including the GH-3850 regression which still passes) * CoreTests net9.0 — 421 / 422 green (1 pre-existing skip) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This was referenced Jun 5, 2026
This was referenced Jun 8, 2026
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.
Phase 1 of #4667 — eliminate
session-shared dictionary access from the projection write path.
Background
Async-daemon parallel slice workers share a single
DocumentSessionBase(10-wideBlock<EventSliceExecution>per(range, tenant)inAggregationRunner). Theprojection write path was still reaching into
_session.Versions/_session.Revisions/
_session.ItemMapfrom inside that parallel block.IMartenSessionis documentedas not thread-safe; #4658 closed
StoreProjection; this phase closes the remainingwrite sites that
ProjectionStorage'sStore(snapshot)andStore(snapshot, id, tenantId)routed through.What changes
New
IDocumentStorage<T>surface (mirrors the existingOverwriteProjectedpattern):Closed-shape storage leaves — the 9 writeable variants (Unversioned / Optimistic /
Numeric × Lightweight / IdentityMap / DirtyChecked) build their corresponding
closed-shape op with null trackers.
QueryOnlyClosedShapeStoragethrowsNotSupportedException;SubClassDocumentStorage/ValueTypeIdentifiedDocumentStoragedelegate;
EventDocumentStorage/EventMappingthrow — all consistent with theestablished
OverwriteProjectedshape.Null-tolerance audit on all four
ConcurrencyModeop classes that hold atracker dictionary (Optimistic Insert/Update/Upsert + Numeric Insert/Update/Upsert):
_versions/_revisionsare now nullable and the dict writes inPostprocessAsyncare guarded. Optimistic Update/Upsert's
ConfigureCommandtreats a null tracker as"expected version unknown" and binds
DBNullfor the WHERE-guard slot — callers thatgo through
*Projectedshould also setIgnoreConcurrencyViolation = trueif theywant silent no-op semantics on the conflict branch (the projection runtime already
does this via
IRevisionedOperationinStoreProjection).ProjectionStoragerewrites:Store(snapshot)line 86_storage.Upsert(snapshot, _session, TenantId)_storage.UpsertProjected(snapshot, TenantId)Store(snapshot, id, tenantId)line 117–135_storage.Store(_session, ...)+_storage.Upsert(snapshot, _session, tenantId)_storage.UpsertProjected(snapshot, tenantId)The GH-3850 identity-map maintenance (
EjectAggregateFromIdentityMap+_storage.Store(_session, snapshot)) is preserved but gated onOptions.EventGraph.UseIdentityMapForAggregates. The default (false) path now takesthe session-state-free write; the opt-in (true) path still gets correct
inline-projection-rewriting-an-immutable-aggregate semantics. Per the #4667 Phase 3
design note, opt-in is documented as not safe for parallel projection workers — that
race is accepted with the flag on.
Acceptance criteria
ProjectionStoragewrite method now routes through a*Projectedvariant. ✅grep -nE '_storage\.(Upsert|Insert|Update)\b' src/Marten/Internal/Sessions/DocumentSessionBase.ProjectionStorage.csreturns zero hits. ✅Postprocessmethods tolerate null trackers. ✅src/DaemonTests/pass. ✅Verification
Local on net9.0:
The GH-3850 regression test (
fetch_latest_immutable_aggregate_running_inline_and_identity_map)specifically covers the identity-map-maintenance path and is green.
Follow-ups
LoadProjectedAsync/LoadManyProjectedAsync+ a fifth projection-safe selectorflavor per closed-shape storage.
ProjectionDocumentSessionoverrides for user-code reads from insideEvolveAsync, gated byUseIdentityMapForAggregates.🤖 Generated with Claude Code