Skip to content

Start per-tenant subscription agents whose starting position seeds their progress (UseTenantPartitionedEvents) - #5700

Merged
jeremydmiller merged 1 commit into
JasperFx:masterfrom
lahma:fix/rewind-tenant-identity
Oct 19, 2026
Merged

jeremydmiller merged 1 commit into
JasperFx:masterfrom
lahma:fix/rewind-tenant-identity

Conversation

@lahma

@lahma lahma commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

Problem

Under Events.UseTenantPartitionedEvents, a subscription whose starting position seeds its progression row
before 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() logs Error trying to start agent {Name}:All:{tenant} for every tenant, and leaves no agent
    running.
  • StartAgentAsync(ShardName) throws ShardStartException. This is how Wolverine-managed distribution starts each
    tenant's agent: EventStoreAgents fans out one agent per tenant when DistributesAgentsPerTenant is true, and
    EventSubscriptionAgent starts each one by its shard name.

The cause in both cases is the same exception:

ArgumentOutOfRangeException: Unknown subscription name 'TenantProbe:All:t_56cf11e0b59a'. Available options are TenantProbe (Parameter 'subscriptionName')

Root cause

The daemon starts each tenant's agent under its own identity, {Name}:{ShardKey}:{tenantId}. When the starting
position 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 a
substitute store.

DocumentStore.RewindSubscriptionProgressAsync resolves its argument against Options.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 uses
UseTenantPartitionedEvents, it parses the argument with ShardName.TryParse. If the result carries a tenant and
its base shard is registered, it:

  • rewinds that one row, deleting it at floor 0 and upserting it otherwise, exactly as for a store-global identity;
  • deletes only that tenant's dead letters for that shard (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 own
PartitionedStoreContext store with a SubscribeFromPresent() subscription and two tenants at heights 3 and 5.

  • start_all_starts_a_subscribe_from_present_agent_for_every_tenant checks that:
    • no start error is logged;
    • both tenant agents are running;
    • each row sits at its own tenant's height, and only events appended afterwards are delivered.
  • a_tenant_agent_started_by_its_shard_name_seeds_from_that_tenants_height covers the Wolverine path.
  • rewinding_a_tenant_shard_moves_only_that_tenants_row_and_dead_letters, a theory at floors 0 and 3, calls
    RewindSubscriptionProgressAsync with a tenant identity. Only that tenant's row and dead letters move.

Red (master 1e130e2, JasperFx 2.90.1): 4 of 4 fail.

start_all_starts_a_subscribe_from_present_agent_for_every_tenant
  logger.Errors should be empty but had
  ["Error trying to start agent TenantProbe:All:t_56cf11e0b59a -> ArgumentOutOfRangeException: Unknown subscription name 'TenantProbe:All:t_56cf11e0b59a'. Available options are TenantProbe (Parameter 'subscriptionName')", "Error trying to start agent TenantProbe:All:t_fc65a452fa55 -> ArgumentOutOfRangeException: ..."]
a_tenant_agent_started_by_its_shard_name_seeds_from_that_tenants_height
  JasperFx.Events.Daemon.ShardStartException : Failure while trying to start 'TenantProbe:All:t_14b1a5c07e89'
  ---- System.ArgumentOutOfRangeException : Unknown subscription name 'TenantProbe:All:t_14b1a5c07e89'. Available options are TenantProbe
rewinding_a_tenant_shard_moves_only_that_tenants_row_and_dead_letters(floor: 0)
rewinding_a_tenant_shard_moves_only_that_tenants_row_and_dead_letters(floor: 3)
  System.ArgumentOutOfRangeException : Unknown subscription name 'TenantProbe:All:t_863c90be7a93'. Available options are TenantProbe

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 in
    Bug_4596_partition_race_and_MT002_rebuild.concurrent_AddMartenManagedTenantsAsync_for_same_tenant_is_idempotent
    (a 23505 on pg_type, a catalog race in concurrent tenant registration). It passes 3 of 3 alone and does not
    touch this code.
  • DaemonTests.Subscriptions plus converting_projection_from_inline_to_async: 23 of 23 pass.

Related

  • This PR is independent of any JasperFx change, and is green on the JasperFx 2.90.1 that master consumes.
  • It is also the store half of per-tenant subscription rewind: Rewind subscriptions per tenant under per-tenant event partitioning jasperfx#1025. That PR
    rewinds a tenant's shards through this same call with tenant identities, so it needs this fix to work against
    Marten.
  • Order: this PR, then the JasperFx PR, then a Marten bump to the JasperFx release that carries it.
    • The bump should flip rewind_subscription_per_tenant_pin.per_tenant_RewindSubscriptionAsync_throws_NotSupportedException
      (marten#4637 pinned it "so a future impl flips it").
    • It should also add an end-to-end rewind test, which is ready: three tests, red on master and green with this PR
      plus the JasperFx change.
  • Searched open and closed issues and PRs in both repos for RewindSubscription, DistributesAgentsPerTenant,
    Unknown subscription name, SubscribeFromPresent tenant/partitioned and related terms; nothing matches.

🤖 Generated with Claude Code

…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>
@jeremydmiller

Copy link
Copy Markdown
Member

Heads-up from the JasperFx side: JasperFx/jasperfx#1025 (per-tenant RewindSubscriptionAsync under UseTenantPartitionedEvents) is approved and will ship in the next JasperFx release. It calls RewindSubscriptionProgressAsync with tenant shard identities, which is exactly what this PR teaches Marten to accept.

Order:

  1. Merge this PR. It's independent and green on JasperFx 2.90.1. Without it, a per-tenant rewind fails with "Unknown subscription name" instead of today's NotSupportedException. It throws either way, so there's no regression whichever ships first.
  2. Bump Marten to the JasperFx release that carries Support for tracking database calls #1025. In that bump:
    • flip marten#4637's per_tenant_RewindSubscriptionAsync_throws_NotSupportedException pin to "is supported";
    • add the end-to-end rewind tests described in Support for tracking database calls #1025: rewind every tenant to the beginning, the sequence-floor refusal, and rewind one tenant only.

Behaviour change to call out in Marten's release notes: on a store using UseTenantPartitionedEvents, the tenant-less RewindSubscriptionAsync(name, token, sequenceFloor) with a non-zero sequenceFloor now throws ArgumentOutOfRangeException, because a sequence number only identifies a position in one tenant's events. Rewinding to 0 or to a timestamp still works, and now fans out to each tenant agent the daemon runs. Before this, the call succeeded but applied one tenant's sequence to every tenant, and deleted other tenants' dead letters at or above it.

🤖 Generated with Claude Code

@jeremydmiller
jeremydmiller merged commit b3ed36c into JasperFx:master Oct 19, 2026
32 checks passed
jeremydmiller pushed a commit that referenced this pull request Oct 23, 2026
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants