Skip to content

No supported way to read per-tenant projection lag for the current version on a sharded store #5170

Description

@erdtsieck

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:

  1. know that the high-water row is prefixed HighWaterMark: and split the tenant off it;
  2. know that a projection shard identity is <name>:V<version>:All with the tenant appended, and split that apart;
  3. know that a V{n} segment is absent for version 1;
  4. 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;
  5. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions