Skip to content

Supported per-tenant projection lag read (#619) - #627

Merged
jeremydmiller merged 1 commit into
mainfrom
gh/619-projection-lag
Aug 4, 2026
Merged

Supported per-tenant projection lag read (#619)#627
jeremydmiller merged 1 commit into
mainfrom
gh/619-projection-lag

Conversation

@jeremydmiller

Copy link
Copy Markdown
Member

Closes #619. Supersedes #624, which GitHub auto-closed when its base branch (the #618 fix, now merged as #623) was deleted. Rebased onto main; content unchanged.

Depends on #623 — without it, high-water rows routed through ShardName attribute every tenant's mark to the store-global one.

What ships

ProjectionLag — one value per (shard, tenant) cell, shaped per the issue:

public readonly record struct ProjectionLag(
    ShardName Shard, string? DatabaseIdentifier, long Sequence, long HighWaterMark, bool HasProgressionRow)
{
    public long Lag => Math.Max(0, HighWaterMark - Sequence);
    public bool IsCaughtUp => HasProgressionRow && Sequence >= HighWaterMark;
}

Carries the whole ShardName (a projection sliced across custom shard keys reports one cell per slice instead of collapsing to an indistinguishable tuple); HasProgressionRow is a real field, not a Sequence == 0 sentinel; DatabaseIdentifier keeps a 512-database fan-out attributable.

ProjectionLagCalculator — the provider-neutral correlation, public so a caller that already holds both halves can run it without a second round trip. The rules, each pinned by a test naming the incident it comes from:

  • Anchored on registered sources at their current version — a prior version's row is never borrowed (blue/green), and non-shard bookkeeping rows can never masquerade as a projection that never advances (marten#5161).
  • A registered cell with no row is fully behind, not caught up.
  • Tenants discovered from the HighWaterMark:{tenant} rows; each cell measured against its own tenant's mark (marten#4761).
  • A store-global :All agent under a tenanted store keeps its own cell against the store-global mark — without it that agent vanishes from the report (the marten#4761 follow-up).

The read surfaceIEventDatabase.FetchProjectionLagAsync(...) plus a ShardName-scoped overload, as default interface methods over the single AllProjectionProgress round trip that already exists. No new SQL, and every existing store gets it without implementing anything.

Deviation from the issue's proposed signature

The issue put both overloads on IEventDatabase with no registry argument. A database has no registry — "registered sources at their current version" lives on the store — so the database-level methods take the registered ShardNames, and IEventStore<TOperations,TQuerySession> carries overloads that supply AllShards() for you:

await store.FetchProjectionLagAsync(database, token);
await store.FetchProjectionLagAsync(database, ShardName.Compose("Trips", tenantId: "acme"), token);

The scoped overload takes a ShardName, not a string? tenantId, as the issue asked. ShardKey == All means every slice, a null tenant means every tenant, and Version is deliberately not matched — the registry only ever holds the current version, so a caller doesn't have to know what it is.

Documented limitation

Stated in the XML docs, because the API can't fix it: the tenant set is discovered from the HighWaterMark:{tenant} rows, so a tenant that exists but has never had one written produces no result at all — invisible rather than "fully behind". Cross-reference a store-supplied tenant list where you have one.

Not done here

The acceptance criterion — rewriting Marten's WaitForNonStaleDataAsync isCaughtUp on top of this — is a Marten-side change and needs a JasperFx release first. Worth filing against Marten once this ships; resolvePriorVersionProgressAsync in this repo is the second candidate consumer and is a follow-up.

Tests

src/EventTests/Daemon/ProjectionLagTests.cs — 17 tests covering each rule above plus the two default-interface read paths. Full EventTests suite passes (702).

🤖 Generated with Claude Code

https://claude.ai/code/session_01Fuk1GybEEmohFmboJuM4Po

"How far behind is projection X, at its current version, for tenant T on
this database?" had no supported answer, so the correlation had been
reinvented three times -- the daemon's blue/green side-effect gate, Marten's
WaitForNonStaleDataAsync, and application code in the field -- each
rediscovering the same traps (marten#4761, marten#5161, marten#4797,
marten#5170).

- `ProjectionLag`: one value per (shard, tenant) cell, carrying the whole
  `ShardName` (so a sliced projection doesn't collapse to one row), the
  database identifier (so a 512-database fan-out stays attributable), and
  `HasProgressionRow` as a real field rather than a `Sequence == 0` sentinel.
- `ProjectionLagCalculator`: the provider-neutral correlation, anchored on
  registered sources at their current version. A missing row is fully behind,
  not caught up; prior-version rows are never borrowed; non-shard bookkeeping
  rows are excluded; each tenant is measured against its own high-water mark;
  a store-global `:All` agent under a tenanted store keeps its own cell.
- `IEventDatabase.FetchProjectionLagAsync` (+ a ShardName-scoped overload) as
  default interface methods over the single AllProjectionProgress round trip
  that already exists -- no new SQL -- and `IEventStore<,>` overloads that
  supply `AllShards()` as the registry.

The one thing the API cannot fix is documented on it: the tenant set is
discovered from the HighWaterMark:{tenant} rows, so a tenant that has never
had one written is invisible rather than "fully behind".

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fuk1GybEEmohFmboJuM4Po
@jeremydmiller
jeremydmiller merged commit 15f9580 into main Aug 4, 2026
1 check passed
@jeremydmiller
jeremydmiller deleted the gh/619-projection-lag branch August 4, 2026 12:11
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.

Supported per-tenant projection lag read anchored on registered sources at their current version

1 participant