Repository navigation
Start per-tenant subscription agents whose starting position seeds their progress (UseTenantPartitionedEvents) - #5700
Conversation
…eir progress
Under UseTenantPartitionedEvents the daemon runs one agent per tenant, and a
subscription configured with SubscribeFromPresent(), SubscribeFromSequence(),
SubscribeFromTime() or SubscribeAsInlineToAsync() has the daemon seed that
agent's progression row before it runs, through
IEventStore.RewindSubscriptionProgressAsync with the tenant agent's own
identity, "{Name}:{ShardKey}:{tenantId}" (JasperFx#487 / wolverine#3280).
Marten resolved that argument against the store-global shards only and threw
"Unknown subscription name", so no tenant agent of such a subscription ever
started: StartAllAsync logged the failure for every tenant, and
StartAgentAsync(ShardName), which is how Wolverine-managed distribution starts
each tenant's agent, threw ShardStartException.
RewindSubscriptionProgressAsync now resolves a tenant agent's identity to its
registered base shard (only under UseTenantPartitionedEvents, and only when no
store-global name or identity matches), rewinds that one tenant's row, and
deletes only that tenant's dead letters for that shard. Store-global names and
identities behave exactly as before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Heads-up from the JasperFx side: JasperFx/jasperfx#1025 (per-tenant Order:
Behaviour change to call out in Marten's release notes: on a store using 🤖 Generated with Claude Code |
JasperFx 2.91.0 (#1025) rewinds a subscription per tenant under UseTenantPartitionedEvents, through RewindSubscriptionProgressAsync with each tenant shard's own identity, which Marten accepts since #5700. The daemon logic is pinned in JasperFx against a substituted store, and rewind_subscription_per_tenant_pin (#5713) pins a per-tenant rewind of a projection to the beginning. Nothing in Marten pinned the rest against a live store. Three facts, with a raw ISubscription and two tenants at heights 5 and 3: - Rewinding every tenant to the beginning restarts each tenant's own agent, never a store-global one, and each replays only its own tenant's events. - Rewinding one tenant to sequence floor 2 replays only that tenant's events above it, deletes only that tenant's dead letters at or above it, and leaves the other tenant's agent instance running with its progression and dead letters untouched. - Rewinding every tenant to a sequence number is refused, and changes no agent, no progression row and no dead letter. All three fail on 8065f44, the master commit before #5700 merged (JasperFx 2.90.1), and pass on master. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem
Under
Events.UseTenantPartitionedEvents, a subscription whose starting position seeds its progression rowbefore it runs never gets a running agent, for any tenant. The affected positions are:
SubscribeFromPresent()SubscribeFromSequence(...)SubscribeFromTime(...)SubscribeAsInlineToAsync()Both ways of starting the agents fail:
StartAllAsync()logsError trying to start agent {Name}:All:{tenant}for every tenant, and leaves no agentrunning.
StartAgentAsync(ShardName)throwsShardStartException. This is how Wolverine-managed distribution starts eachtenant's agent:
EventStoreAgentsfans out one agent per tenant whenDistributesAgentsPerTenantis true, andEventSubscriptionAgentstarts each one by its shard name.The cause in both cases is the same exception:
Root cause
The daemon starts each tenant's agent under its own identity,
{Name}:{ShardKey}:{tenantId}. When the startingposition asks for the row to be seeded first, it calls
IEventStore.RewindSubscriptionProgressAsync(database, agent.Name.Identity, ...). That is the #487 /wolverine#3280 design, and JasperFx pins it in
PerTenantStartAgentAsyncTests.primes_the_tenant_ceiling_before_determining_the_starting_position, but only against asubstitute store.
DocumentStore.RewindSubscriptionProgressAsyncresolves its argument againstOptions.Projections.AllShards(),which carries only the store-global identities. A tenant identity matches nothing, so the call throws.
Fix
The fix is in
RewindSubscriptionProgressAsync. When no store-global name or identity matches, and the store usesUseTenantPartitionedEvents, it parses the argument withShardName.TryParse. If the result carries a tenant andits base shard is registered, it:
ProjectionName,ShardName,TenantId,EventSequence >= floor). Every other tenant draws its own sequence, so the floor means nothing to them.Store-global names and identities behave exactly as before. Exact matches still win, which mirrors the daemon's own
StartAgentAsync.Tests
New file:
TenantPartitionedEventsTests/Daemon/per_tenant_subscription_starting_position.cs, which uses its ownPartitionedStoreContextstore with aSubscribeFromPresent()subscription and two tenants at heights 3 and 5.start_all_starts_a_subscribe_from_present_agent_for_every_tenantchecks that:a_tenant_agent_started_by_its_shard_name_seeds_from_that_tenants_heightcovers the Wolverine path.rewinding_a_tenant_shard_moves_only_that_tenants_row_and_dead_letters, a theory at floors 0 and 3, callsRewindSubscriptionProgressAsyncwith a tenant identity. Only that tenant's row and dead letters move.Red (master
1e130e2, JasperFx 2.90.1): 4 of 4 fail.Green: 4 of 4 pass.
Neighbouring tests, net10.0, against Postgres 17:
TenantPartitionedEventsTests.Daemon: 32 of 32 pass.TenantPartitionedEventsTests, the whole project: 256 passed, 2 skipped, and 1 failure inBug_4596_partition_race_and_MT002_rebuild.concurrent_AddMartenManagedTenantsAsync_for_same_tenant_is_idempotent(a
23505onpg_type, a catalog race in concurrent tenant registration). It passes 3 of 3 alone and does nottouch this code.
DaemonTests.Subscriptionsplusconverting_projection_from_inline_to_async: 23 of 23 pass.Related
rewinds a tenant's shards through this same call with tenant identities, so it needs this fix to work against
Marten.
rewind_subscription_per_tenant_pin.per_tenant_RewindSubscriptionAsync_throws_NotSupportedException(marten#4637 pinned it "so a future impl flips it").
plus the JasperFx change.
RewindSubscription,DistributesAgentsPerTenant,Unknown subscription name,SubscribeFromPresent tenant/partitionedand related terms; nothing matches.🤖 Generated with Claude Code