Skip to content

Skip the inline snapshot lookup when a commit only starts streams - #5671

Merged
jeremydmiller merged 1 commit into
JasperFx:masterfrom
lahma:skip-empty-projection-load
Oct 15, 2026
Merged

jeremydmiller merged 1 commit into
JasperFx:masterfrom
lahma:skip-empty-projection-load

Conversation

@lahma

@lahma lahma commented Oct 15, 2026

Copy link
Copy Markdown
Contributor

What was wasted

When a session that only starts streams is saved with an inline single-stream snapshot, every commit still ran a separate query before the write batch:

select d.id, d.data, d.mt_version from <schema>.mt_doc_<aggregate> as d where id = ANY($1)

That query always returns no rows. It costs one round trip for each commit that starts a stream. With UseIdentityMapForAggregates off, which is the default, it also opens a second connection, because the load goes through LoadManyProjectedAsync.

Why

JasperFxSingleStreamProjectionBase (in JasperFx.Events) asks projection storage only for the ids of streams with StreamActionType.Append. A commit that only starts streams therefore passes an empty id array. ProjectionStorage.LoadManyAsync passes the empty array on to LightweightDocumentStorage.LoadManyAsync or to LoadManyProjectedAsync. Neither of them short-circuits an empty array. IdentityMapDocumentStorage already returned early in that case (#5258), but lightweight sessions and the projected path did not.

The change

ProjectionStorage.LoadManyAsync now returns an empty dictionary when it is given no ids. That one place covers every caller: single-stream and multi-stream inline projections and the daemon's AggregationRunner, with UseIdentityMapForAggregates on or off.

Why it is safe

The set of streams that get looked up does not change. The empty array was never going to find anything, so skipping it changes no result:

  • Appends still load their existing snapshot.
  • In a batch that mixes a start and an append, only the appended stream is loaded.
  • Starting a stream whose id already exists still throws ExistingStreamIdCollisionException at commit, and the existing snapshot is left unchanged. That id was already excluded from the lookup before this change; only the empty query is gone.

Tests

New file: EventSourcingTests/Projections/inline_snapshot_reads_when_starting_streams.cs. It counts the selects against the aggregate's table through Npgsql's ActivitySource, so the separate connection used with the flag off is counted too. Every case runs with UseIdentityMapForAggregates both on and off:

Case Reads
Start a stream 0 (was 1)
Start string-keyed streams in two conjoined tenants 0 (was 2)
Append to an existing stream 1
Start one stream and append to another in the same commit 1
Start a stream with an id that already exists throws ExistingStreamIdCollisionException; snapshot unchanged

Before the fix, 4 of the 10 cases failed: the start-only and two-tenant cases, with each flag setting. After the fix, all of them pass, and so does inline_cache_hit_query_counts. The full EventSourcingTests run on net10.0 passes, except for the five testing_projections tests that hard-code localhost:5432; my local database was on a different port.

A single-stream inline projection asks its projection storage only for the
snapshots of streams being appended to, so a commit that only starts streams
asks for an empty id array. ProjectionStorage.LoadManyAsync passed that on to
LightweightDocumentStorage.LoadManyAsync or LoadManyProjectedAsync, neither of
which short-circuits it, so every such commit still ran
`select ... where id = ANY($1)` -- one round trip, plus a connection of its own
with UseIdentityMapForAggregates off, for a query that can only return no rows.
Only IdentityMapDocumentStorage already skipped it (JasperFx#5258).

ProjectionStorage.LoadManyAsync now returns an empty dictionary for an empty
id array. Which streams are looked up is unchanged: appends still load their
snapshot, and starting a stream whose id already exists still fails with
ExistingStreamIdCollisionException.
@jeremydmiller

Copy link
Copy Markdown
Member

@lahma Good catch. I thought we already had that optimization

@lahma
lahma marked this pull request as ready for review October 15, 2026 12:04
@jeremydmiller
jeremydmiller merged commit 7b4b409 into JasperFx:master Oct 15, 2026
32 checks passed
@lahma
lahma deleted the skip-empty-projection-load branch October 15, 2026 18:20
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.

2 participants