Skip to content

Marten.ScaleTesting: rebuild-load governor sweep + load-test-informed defaults (#4884, epic jasperfx#486 WS6/WS3)#4896

Merged
jeremydmiller merged 4 commits into
masterfrom
feat/4884-rebuild-load-tuning
Jul 7, 2026
Merged

Marten.ScaleTesting: rebuild-load governor sweep + load-test-informed defaults (#4884, epic jasperfx#486 WS6/WS3)#4896
jeremydmiller merged 4 commits into
masterfrom
feat/4884-rebuild-load-tuning

Conversation

@jeremydmiller

Copy link
Copy Markdown
Member

Addresses #4884 (epic #486 WS6/WS3 closeout). Stacked on #4894#4890#4887 → master; review/merge in that order. This PR's own diff is the top commit.

What's here — governor tuning by evidence

rebuildload load-tests N projections × many partitioned tenants rebuilding concurrently and sweeps the three governor knobs so their shipped defaults can be confirmed — or a change recommended — without ever mutating a shipped default (measurement tool only, per the epic's WS3 constraint).

For each configuration in the cross-product sweep it:

  • applies the inner governors (MaxConcurrentEventLoadsPerDatabase, MaxConcurrentBatchWritesPerDatabase) + optional EnableExtendedProgressionTracking,
  • rebuilds every projection with the outer fan-out throttled to the swept cap via SemaphoreSlim(cap) — the exact shape of ProjectionHost.RebuildProjectionsWithCapAsync (Address Dictionary query generation #463), whose shipped default is max(1, MaxPoolSize / 8),
  • samples pg_stat_activity + mt_event_progression lock-waits throughout (reusing the Phase E: Per-batch measurement instrumentation in Marten.ScaleTesting harness #4684 ConnectionSampler / ProgressionLockSampler),

then prints a per-config comparison + an advisory recommendation, and documents the derivation in docs/events/projections/rebuilding.md.

Measured (local reduced scale — 20 tenants × 8 projections × 1200 events, MaxPoolSize=100 ⇒ default cap 12)

cap ext elapsed peak conns max progression waiters
2 off 6.0s 3 0
4 off 5.6s 5 0
8 off 5.3s 9 0
12* off 5.2s 9 0
12* on 5.0s 9 0

* = shipped default cap for this pool

Findings (local scale, indicative):

  • Two-layer model validated: peak connections ≈ min(cap, projections) + 1 — the outer cap is the dominant connection driver; inner slice workers didn't add connection pressure at this per-tenant volume. Worst peak 9 ≪ pool 100 → the MaxPoolSize/8 fraction keeps headroom.
  • Diminishing returns past cap≈4: wall-clock 6.0→5.2s flattening; raising the cap trades pool headroom for little speedup.
  • Zero mt_event_progression waiters at every cap — the concurrent-rebuild cap creates no progression-row contention at this scale.
  • EnableExtendedProgressionTracking: no measurable rebuild overhead (within noise).

Recommendation: CONFIRM the shipped max(1, MaxPoolSize/8) rebuild cap and the load/write governor default of 4. No defaults changed. Documented in rebuilding.md with the explicit caveat to re-run at production pool + event volume before deviating.

Remaining

🤖 Generated with Claude Code

https://claude.ai/code/session_01XNfeNz73Q3EdiTgSeDbo96

jeremydmiller and others added 3 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
…4884, epic #486 WS6/WS3)

rebuildload subcommand load-tests N projections x many partitioned
tenants rebuilding concurrently and sweeps the three governor knobs
(MaxConcurrentRebuildsPerDatabase cap, MaxConcurrentEventLoads/
BatchWritesPerDatabase) + EnableExtendedProgressionTracking, measuring
wall-clock, peak/idle connections and mt_event_progression contention
per configuration via the existing ConnectionSampler + ProgressionLock
Sampler. Outer cap enforced by SemaphoreSlim(cap) mirroring
ProjectionHost.RebuildProjectionsWithCapAsync (#463). Prints a
comparison table + advisory recommendation; never changes a shipped
default (measurement only).

Local-scale finding (20 tenants x 8 projections x 1200 events, pool 100
=> default cap 12): peak conns ~= min(cap, projections)+1 (outer cap is
the dominant connection driver, validating the two-layer model); zero
progression waiters at every cap; wall-clock flattens past cap=4;
extended progression tracking within noise. Recommendation: CONFIRM the
max(1, MaxPoolSize/8) cap + governor default of 4. Derivation documented
in rebuilding.md.

--databases > 1 (sharded rebuild sweep) noted as a follow-up; the
governor tuning evidence lives on a single database.

Addresses #4884

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/4884-rebuild-load-tuning branch from 3ef4046 to 3b61a06 Compare July 7, 2026 19:36
Base automatically changed from feat/4883-multinode-daemonload to master July 7, 2026 21:28
…oad-tuning

# Conflicts:
#	src/Marten.ScaleTesting/README.md
@jeremydmiller
jeremydmiller merged commit d3f0a2e into master Jul 7, 2026
9 checks passed
@jeremydmiller
jeremydmiller deleted the feat/4884-rebuild-load-tuning branch July 7, 2026 21:38
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