Skip to content

Stamp the owning database onto every published ShardState#501

Merged
jeremydmiller merged 1 commit into
mainfrom
feature/678-database-identifier
Jul 9, 2026
Merged

Stamp the owning database onto every published ShardState#501
jeremydmiller merged 1 commit into
mainfrom
feature/678-database-identifier

Conversation

@jeremydmiller

Copy link
Copy Markdown
Member

Problem

An event store backed by more than one database — database-per-tenant, sharded tenancy — runs one daemon, and one ShardStateTracker, per database. Every one of them publishes the same shard names (Trip:All) and the same HighWaterMark.

ShardState carries no database identity, so a consumer keying on shard name alone collapses all of them into a single entry and reports whichever database published last.

This surfaced as JasperFx/CritterWatch#678: a database-per-tenant service showed a high water mark of 0 and every projection as "awaiting its first event", while its per-tenant daemons were actively running and advancing. Wolverine's EventStoreAgents subscribes the same observer to each database's daemon, so the monitoring side had no way to tell one tenant database's Trip:All from another's.

Change

Three additions, all in JasperFx.Events:

  • ShardState.DatabaseIdentifier — the IEventDatabase.Identifier of the database the state came from.
  • ShardStateTracker.DatabaseIdentifierPublishAsync stamps it onto any state that doesn't already carry one.
  • JasperFxAsyncDaemon sets Tracker.DatabaseIdentifier ??= Database.Identifier in both constructors.

JasperFxAsyncDaemon already holds the IEventDatabase and takes its Tracker from it, so the stamp lands in exactly one place and rides both the subscription agents' progress and the high water agent's marks. Marten and Polecat inherit it with no change of their own.

Compatibility

Additive and binary-safe: two nullable properties on classes, no positional-record parameters touched. A publisher that predates this leaves the field null, and a state that already names its database is left alone (??=), so a republishing consumer can't have its attribution overwritten.

Verification

  • 4 new unit tests in ShardStateTrackerTests (default null, stamped on both shard states and high-water marks, pre-set value preserved, null tracker leaves null).
  • Full EventTests suite green: 481/481 on net9.0 and net10.0.
  • Verified end-to-end from CritterWatch against a locally-packed 2.18.2-cw678, driving a real two-tenant-database Marten store (MultiTenantedDatabasesWithMasterDatabaseTable): the observed shard states now report tenant-a / tenant-b with correctly attributed high-water marks, where previously they collapsed to a single mark of 0.

An event store backed by more than one database — database-per-tenant, sharded
tenancy — runs one daemon, and one ShardStateTracker, per database. Every one of
them publishes the same shard names (Trip:All) and the same HighWaterMark, so a
consumer keying on shard name alone collapses them into a single entry and reports
whichever database published last.

JasperFxAsyncDaemon already holds the IEventDatabase and takes its Tracker from it,
so the stamp lands in one place and rides both the subscription agents' progress and
the high water agent's marks. Marten and Polecat inherit it with no change.

Additive and binary-safe: two nullable properties on classes, and PublishAsync leaves
a state that already names its database alone.

Reported as JasperFx/CritterWatch#678 — a database-per-tenant service showed a high
water mark of 0 and every projection as "awaiting its first event" while its
per-tenant daemons were actively running.
@jeremydmiller jeremydmiller merged commit e5a6174 into main Jul 9, 2026
1 check passed
@jeremydmiller jeremydmiller deleted the feature/678-database-identifier branch July 9, 2026 20:00
jeremydmiller added a commit that referenced this pull request Jul 9, 2026
…fier, #501 / CritterWatch#678)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HHNVFwbKb6fkn4d9G6j3v3
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.

1 participant