Skip to content

Marten.ScaleTesting: multi-node native-HotCold daemonload + leadership failover (#4883, epic jasperfx#486 WS6)#4894

Merged
jeremydmiller merged 3 commits into
masterfrom
feat/4883-multinode-daemonload
Jul 7, 2026
Merged

Marten.ScaleTesting: multi-node native-HotCold daemonload + leadership failover (#4883, epic jasperfx#486 WS6)#4894
jeremydmiller merged 3 commits into
masterfrom
feat/4883-multinode-daemonload

Conversation

@jeremydmiller

Copy link
Copy Markdown
Member

Addresses #4883 (epic #486 WS6). Stacked on #4890 (feat/4882-sharded-daemonload) → #4887 (feat/4684-batch-instrumentation) → master. Review/merge in that order; this PR's own diff is the top commit.

What's here — native HotCold multi-node

Multi-node without cluster infra = multiple OS processes of this same binary:

  • daemonload-multinode (coordinator) provisions one tenant-partitioned store, launches --nodes child processes, appends continuously, samples pg_stat_activity grouped by per-node Application Name × database, optionally kills the leader, verifies per-tenant catch-up
  • daemonload-node (internal worker) — an IHost running AddAsyncDaemon(DaemonMode.HotCold) over the shared store on one DaemonLockId; idles until the coordinator closes its stdin (graceful) or kills it (failover). Signals readiness with a NODE_READY stdout handshake
  • MultiNodeStore centralizes the shared store config so coordinator and nodes contend on a byte-identical projection set + lock; NodeProcess wraps launch/handshake/stop/kill; NodeConnectionSampler does the per-node×database attribution

WS6 assertions the run makes

Under native HotCold + one tenant-partitioned database, MultiTenantedProjectionDistributor locks the database's whole per-tenant agent set (#491 fan-out) to ONE leader at a time. So: single-leader exclusivity, failover to a survivor when the leader is killed, per-tenant catch-up to every tenant's own ceiling afterward, governed per-node footprint (--max-connections-per-node).

Measured (local reduced scale)

daemonload-multinode --nodes 2 --tenants 8 --projections 2 --duration-seconds 30 --kill-leader-after-seconds 10 --wipe

Metric Value
Leader before kill scaletest-multinode-node0
Leader after failover scaletest-multinode-node1
Failover took over yes
Events appended / failures 5,860 / 0
node0 / node1 peak connections 10 / 9
Tenants caught up 8 / 8

The killed leader's per-tenant agents moved to the survivor and every tenant still reached its own ceiling — no stall.

Full-scale rerun for the epic record: --nodes 3 --tenants 100 --projections 2 --duration-seconds 120 --kill-leader-after-seconds 30 --max-connections-per-node 16.

Remaining (listed in issue #4883, documented in README)

  1. Cross-node simultaneous distribution — with a single tenant-partitioned DB, HotCold keeps one hot leader. Getting agents on different nodes at once needs the sharded multi-DB topology (per-database locks CAN land on different nodes). This wires cleanly onto Marten.ScaleTesting: daemonload over pooled sharded databases — gate connections at O(databases) (epic jasperfx#486 WS6) #4882's --databases and is the natural follow-up.
  2. Wolverine-managed mode (wolverine#3328 per-tenant fan-out + affinity, DaemonMode.ExternallyManaged) — the harness references only Marten.csproj, not Wolverine, so this mode needs either a Wolverine package addition here or a sibling harness.

🤖 Generated with Claude Code

https://claude.ai/code/session_01XNfeNz73Q3EdiTgSeDbo96

jeremydmiller and others added 2 commits July 7, 2026 14:36
--databases N pools the tenants across N scaletest_dl_shard_* databases
via MultiTenantedWithShardedDatabases (explicit round-robin placement,
one daemon per shard). ShardConnectionSampler groups pg_stat_activity by
datname so the --max-connections gate is enforced PER DATABASE, plus
per-tenant catch-up verification on every shard and a database-affine
placement check (per-tenant sequence in exactly the home shard).

Local-scale result (25 tenants x 2 shards x 2 projections, 60s):
shard peaks 16/11, all 25 tenants caught up, zero placement violations.
Single-database path (--databases 1, the default) unchanged.

Addresses #4882

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNfeNz73Q3EdiTgSeDbo96
daemonload-multinode coordinator launches N daemonload-node child
processes (multiple OS processes of this binary = multi-node without
cluster infra), each an IHost running AddAsyncDaemon(HotCold) over one
shared tenant-partitioned store on one DaemonLockId. Coordinator appends
under load, samples pg_stat_activity per node x database (NodeConnectionSampler),
optionally kills the leader (--kill-leader-after-seconds) and verifies a
survivor takes over, then confirms per-tenant catch-up to each tenant's
own ceiling. --max-connections-per-node gates each node's footprint.

MultiNodeStore centralizes the shared config so coordinator and nodes
can't drift. NodeProcess wraps launch/NODE_READY handshake/graceful
stdin-close stop/kill. Reuses DaemonLoadCommand catch-up probe.

Local-scale result (2 nodes, 8 tenants x 2 projections, kill leader at
10s of 30s): leader node0 killed -> node1 took over, 8/8 tenants caught
up, 0 append failures, per-node peak 9-10 connections.

Native HotCold single-DB = one hot leader (per-database lock). Remaining
WS6 multi-node work: cross-node simultaneous distribution over sharded
multi-DB topology + Wolverine-managed mode (wolverine#3328; harness has
no Wolverine dependency). Documented in README.

Addresses #4883

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNfeNz73Q3EdiTgSeDbo96
@jeremydmiller
jeremydmiller force-pushed the feat/4883-multinode-daemonload branch from b0967b9 to 1b7b0fd Compare July 7, 2026 19:36
@jeremydmiller
jeremydmiller force-pushed the feat/4882-sharded-daemonload branch from f8d8f7a to b76029b Compare July 7, 2026 19:36
Base automatically changed from feat/4882-sharded-daemonload to master July 7, 2026 20:10
…-daemonload

# Conflicts:
#	src/Marten.ScaleTesting/README.md
@jeremydmiller
jeremydmiller merged commit 52cdad4 into master Jul 7, 2026
9 checks passed
@jeremydmiller
jeremydmiller deleted the feat/4883-multinode-daemonload branch July 7, 2026 21:28
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