Round-trip per-tenant HighWaterMark progression rows through ShardName (#618) - #623
Merged
Merged
Conversation
…rdName (#618) `ShardName.TryParse("HighWaterMark:acme", ...)` returned true and a wrong answer: the tenant id landed in the `ShardKey` slot, `TenantId` came back null, and the constructor's high-water special case collapsed `Identity` back to the bare store-global constant. A caller enumerating `AllProjectionProgress` and grouping by tenant got a name claiming to be the store-global mark for every tenant, with nothing to signal it. `HighWaterMark:{tenant}` is a real persisted row shape -- Marten writes one per tenant on every vectorized high-water poll and on the bulk-append path, and they come back interleaved with the projection shard rows. - Recognize the shape in `TryParse` and preserve the tenant, and stop the constructor flattening `Identity` when a tenant is present, so it round-trips. - Reject every other `HighWaterMark`-prefixed string rather than forcing it through the generic grammar into a name that renders a different identity. - Add `ShardName.HighWaterMarkFor(tenantId)` to compose the identity and `IsHighWaterMark` to recognize the bookkeeping rows, so the knowledge stops being Marten-private (`HighWaterShardIdentity`). Behavior change worth calling out: per-tenant high-water names are now distinct values, where they previously all compared equal to the store-global mark. That was the bug. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Fuk1GybEEmohFmboJuM4Po
This was referenced Aug 5, 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.
Closes #618.
The problem
ShardName.TryParse("HighWaterMark:acme", out var parsed)returnedtrueand a wrong answer:TryParsespecial-cased only the exact stringShardState.HighWaterMark, so the per-tenant form fell through to the generic two-segment branch; the constructor's own high-water case then collapsedIdentityto the bare store-global constant.HighWaterMark:{tenant}is a real persisted row shape, not hypothetical — Marten writes one per tenant on every vectorized high-water poll (marten#4717) and on the bulk-append path, and those rows come back fromAllProjectionProgressinterleaved with the projection shard rows. So the obvious consumer — enumerate progress,TryParseeach name, group by tenant — got a name claiming to be the store-global mark for every tenant, with no error and nofalseto signal it.The fix
Option 1 from the issue, plus the recognizer from option 2:
TryParserecognizesHighWaterMark:{tenant}and preserves the tenant.Identityto the bare constant when the name is store-global, so a tenant-bearing high-water name rendersHighWaterMark:{tenant}and round-trips.HighWaterMark-prefixed string (HighWaterMark:,HighWaterMark:a:b,HighWaterMark:V2:All) is now rejected rather than forced through the generic grammar into a name whoseIdentityis a different string.ShardName.HighWaterMarkFor(tenantId)composes the identity andShardName.IsHighWaterMarkrecognizes the bookkeeping rows — so the knowledge stops being Marten-private (HighWaterShardIdentity) and lag/status/readiness consumers have a supported path.Behavior change
Per-tenant high-water names are now distinct values.
ShardNameequality is identity-only, so before this change every tenant's mark compared equal to the store-global mark — which is precisely the bug. Nothing in JasperFx constructs aShardNamewithname == HighWaterMarkand a tenant today (grep: the only constructions are the bare store-global form), so nothing in-tree keys on the old collapsed value.RelativeUrlis deliberately untouched.Tests
src/EventTests/ShardNameTests.cs: round-trip of the per-tenant form, distinctness of two tenants' marks and the global mark, the tenant staying out of theShardKeyslot,IsHighWaterMark, and rejection of the unrecognized shapes. FullEventTestssuite passes (685).Follow-on
Unblocks #619 (per-tenant projection lag) — a lag implementation routing high-water rows through
ShardNametoday would attribute every tenant's mark to the store-global one.🤖 Generated with Claude Code
https://claude.ai/code/session_01Fuk1GybEEmohFmboJuM4Po