Fix marten#4665: CatchUpAsync dispatches on _tenantHighWater under per-tenant partitioning; bump to 2.8.2#418
Merged
jeremydmiller merged 1 commit intoJun 5, 2026
Conversation
…r-tenant partitioning; bump to 2.8.2
JasperFxAsyncDaemon.CatchUpAsync(CancellationToken) — the test-automation
catch-up path that ForceAllMartenDaemonActivityToCatchUpAsync drives —
used the store-global _highWater path even when _tenantHighWater was
non-null. Under per-tenant event partitioning the store-global
mt_events_sequence is never advanced (per-tenant mt_events_sequence_{suffix}
values power mt_events.seq_id), so _highWater.CheckNowAsync() leaves the
global high-water pinned at the unused sequence's last_value. Driving
catch-up off HighWaterMark() in that mode leaves every catch-up loop
stuck at zero — the helper returned "success" while every async projection
was still behind.
Fix: when _tenantHighWater is non-null AND the database implements
ICrossTenantRebuildSource (the partitioned-store contract — Marten's
MartenDatabase implements it), fan out per tenant. For each base shard:
* Discover every tenant the projection knows about via
ICrossTenantRebuildSource.FindRebuildTenantsAsync.
* Activate the tenants in _tenantHighWater.PolledTenants and drive one
vectorized poll to fetch fresh ceilings.
* For each (shard, tenant), build a tenant-scoped agent
(asyncShard with { Name = asyncShard.Name.ForTenant(tenantId) }) and
catch it up to that tenant's ceiling.
Mirrors the rebuildProjectionForTenant ceiling-lookup pattern that already
existed for per-tenant rebuilds. Single-tenant stores and non-partitioned
multi-tenant stores keep the byte-for-byte global path.
Marten side: src/TenantPartitionedEventsTests/Regressions/Bug_4665_catch_up_uses_global_high_water.cs
(JasperFx/marten#4673) is the failing reproduction shipped as Skip pin;
when this lands on NuGet and Marten bumps Directory.Packages.props to
2.8.2, the test gets unskipped.
Version bump: 2.8.0 → 2.8.2 (skipping 2.8.1 to avoid clashing with any
local prerelease lines).
Verified locally:
* dotnet build src/JasperFx.Events/JasperFx.Events.csproj -f net9.0 — clean.
* dotnet test src/EventStoreTests -f net9.0 — 72/72 ✅.
* dotnet test src/EventTests -f net9.0 — 351/351 ✅.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
06a130d to
92a9a16
Compare
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.
Fixes JasperFx/marten#4665.
Bug
JasperFxAsyncDaemon.CatchUpAsync(CancellationToken)— the test-automationcatch-up path that Marten's
ForceAllMartenDaemonActivityToCatchUpAsyncdrives — used the store-global
_highWateragent even when_tenantHighWaterwas non-null.Under per-tenant event partitioning the store-global `mt_events_sequence` is
never advanced (per-tenant `mt_events_sequence_{suffix}` values power
`mt_events.seq_id` instead). So
_highWater.CheckNowAsync()leaves theglobal high-water pinned at the unused sequence's
last_value. Drivingcatch-up off
HighWaterMark()in that mode leaves every catch-up loopstuck at zero — the test helper returned "success" while every async
projection was still behind.
The normally-running daemon already does the right thing (uses
_tenantHighWatervia the polling loop andrebuildProjectionForTenant);the test-automation
CatchUpAsyncwas the one path still on the globalroute.
Fix
When
_tenantHighWateris non-null and the database implementsICrossTenantRebuildSource(the partitioned-store contract — Marten'sMartenDatabaseimplements it), fan out per tenant. For each base shard:ICrossTenantRebuildSource.FindRebuildTenantsAsync._tenantHighWater.PolledTenantsand drive onevectorized poll to fetch fresh ceilings (batched — one poll round-trip
per shard, not per tenant).
(shard, tenant), build a tenant-scoped agent(
asyncShard with { Name = asyncShard.Name.ForTenant(tenantId) }) andcatch it up to that tenant's ceiling.
Mirrors the
rebuildProjectionForTenantceiling-lookup pattern thatalready existed for per-tenant rebuilds.
Single-tenant stores and non-partitioned multi-tenant stores keep the
byte-for-byte global path. The dispatch is gated on
_tenantHighWater != null && Database is ICrossTenantRebuildSource— noother behavior changes.
Version
2.8.0 → 2.8.2(skipping 2.8.1 to avoid clashing with any localprerelease lines).
Marten-side regression
JasperFx/marten#4673 ships
the failing reproduction (currently
Skip'd to keep CI from hanging).After this lands on NuGet and Marten bumps
Directory.Packages.propsto2.8.2, the Marten regression test gets unskipped.
Verification
dotnet build src/JasperFx.Events/JasperFx.Events.csproj -f net9.0dotnet test src/EventStoreTests -f net9.0dotnet test src/EventTests -f net9.0🤖 Generated with Claude Code