The gap
There is no supported way to ask a Marten database how far behind each async projection's current version is, per tenant, when the store uses sharded/per-tenant databases.
Advanced.ProjectionProgressFor and Advanced.AllProjectionProgress throw under a sharded tenancy — there is no store-global high-water and no default tenant to resolve. IMartenDatabase.AllProjectionProgress(token) does work per database, but it hands back raw ShardState rows whose names are the framework's own encoding:
HighWaterMark:<tenant>
<source>:V<n>:All:<tenant>
So a caller that wants "is projection X's current version caught up for tenant T on this database" has to:
- know that the high-water row is prefixed
HighWaterMark: and split the tenant off it;
- know that a projection shard identity is
<name>:V<version>:All with the tenant appended, and split that apart;
- know that a
V{n} segment is absent for version 1;
- know that a missing row for the current version means "has not started", not "caught up" — the distinction that matters during a version bump, when the previous version's row still sits at the old mark;
- and know to compare against that tenant's own high-water, not a global one.
We have all five of those in application code, and getting (4) wrong is what made our readiness signal latch early on a rollout. Points 1-3 are re-implementing ShardName parsing that JasperFx already owns — the same knowledge ShardName.TryParse and EventSubscriptionAgentFamily.TenantNeutralKeyOf encode internally.
What I would like
Something that answers the question per database, with the version and tenant semantics inside Marten rather than in every caller:
// on IMartenDatabase, or via Advanced
Task<IReadOnlyList<ProjectionLag>> FetchProjectionLagAsync(CancellationToken token);
public readonly record struct ProjectionLag(
string ProjectionName,
uint Version, // the version the store currently registers
string? TenantId, // null for a store-global shard
long Sequence, // 0 when the current version has no progression row yet
long HighWaterMark); // that tenant's high-water, not a global one
Anchored on the registered async sources at their current version, so a lingering previous-version row is not mistaken for progress and an absent row reads as fully behind. That single guarantee is the part that is easy to get wrong and impossible to discover from the row names.
Two smaller things in the same area, if they are cheap:
ShardName's parsing is public enough to build an identity but not to take one apart. A public way to get (name, version, tenant) out of a shard identity would remove the string splitting from callers.
- Wolverine's
EventSubscriptionAgentFamily.DatabaseKeyOf is internal, so a test that reasons about agent placement per database has to copy the URI grammar. Making it public would let consumers group agents by database without duplicating that.
Where this comes from
A 512-shard-database, ~854-tenant deployment. We use this to gate a readiness probe, to drive an admin projection-status endpoint, and now to decide when a warm-up job may exit — three callers, all of which had to learn the same encoding. Happy to open a PR if you tell me where you would want it to live (IMartenDatabase extension, Advanced, or a small service); the shape above is what our own helper converged on after two bugs, so I have a decent idea of the edge cases, but the API placement is your call.
The gap
There is no supported way to ask a Marten database how far behind each async projection's current version is, per tenant, when the store uses sharded/per-tenant databases.
Advanced.ProjectionProgressForandAdvanced.AllProjectionProgressthrow under a sharded tenancy — there is no store-global high-water and no default tenant to resolve.IMartenDatabase.AllProjectionProgress(token)does work per database, but it hands back rawShardStaterows whose names are the framework's own encoding:So a caller that wants "is projection X's current version caught up for tenant T on this database" has to:
HighWaterMark:and split the tenant off it;<name>:V<version>:Allwith the tenant appended, and split that apart;V{n}segment is absent for version 1;We have all five of those in application code, and getting (4) wrong is what made our readiness signal latch early on a rollout. Points 1-3 are re-implementing
ShardNameparsing that JasperFx already owns — the same knowledgeShardName.TryParseandEventSubscriptionAgentFamily.TenantNeutralKeyOfencode internally.What I would like
Something that answers the question per database, with the version and tenant semantics inside Marten rather than in every caller:
Anchored on the registered async sources at their current version, so a lingering previous-version row is not mistaken for progress and an absent row reads as fully behind. That single guarantee is the part that is easy to get wrong and impossible to discover from the row names.
Two smaller things in the same area, if they are cheap:
ShardName's parsing is public enough to build an identity but not to take one apart. A public way to get(name, version, tenant)out of a shard identity would remove the string splitting from callers.EventSubscriptionAgentFamily.DatabaseKeyOfisinternal, so a test that reasons about agent placement per database has to copy the URI grammar. Making it public would let consumers group agents by database without duplicating that.Where this comes from
A 512-shard-database, ~854-tenant deployment. We use this to gate a readiness probe, to drive an admin projection-status endpoint, and now to decide when a warm-up job may exit — three callers, all of which had to learn the same encoding. Happy to open a PR if you tell me where you would want it to live (
IMartenDatabaseextension,Advanced, or a small service); the shape above is what our own helper converged on after two bugs, so I have a decent idea of the edge cases, but the API placement is your call.