Skip to content

Per-database event-load throttle (#494 Phase A, epic #486 WS2)#495

Merged
jeremydmiller merged 1 commit into
mainfrom
feat/494-event-load-throttle
Jul 7, 2026
Merged

Per-database event-load throttle (#494 Phase A, epic #486 WS2)#495
jeremydmiller merged 1 commit into
mainfrom
feat/494-event-load-throttle

Conversation

@jeremydmiller

Copy link
Copy Markdown
Member

Phase A of #494 (epic #486 WS2 — connection conservation).

What

Every subscription agent's IEventLoader.LoadAsync opens its own store session, so a daemon running (projections × tenants) agents under per-tenant partitioning drives the pool's high-water mark toward the agent count. All agent loaders a daemon builds now share one SemaphoreSlim sized by DaemonSettings.MaxConcurrentEventLoadsPerDatabase (default 4; ≤0 disables), applied as a ThrottledEventLoader decorator in buildAgentForShard. The composite replay loader path is deliberately NOT throttled — one serial loader per rebuild, already bounded by the per-database rebuild cap.

Verification

  • Deterministic TCS-gated concurrency tests (bound honored, slot released on failure, cancelled waiter consumes no slot) + daemon wiring tests. Full EventTests 444/444 green on net9.0 + net10.0.
  • daemonload (marten ScaleTesting; 100 tenants × 2 projections = 200 agents, continuous append): event-loader sessions observed ≤ 4 via pg_stat_activity last-query attribution; 100/100 tenants caught up, throughput unchanged.

Honest finding → WS3

The throttle alone does not collapse total pool size in an all-tenants-active steady state: live sampling shows 29 of ~35 open sessions last executed COMMIT — they're per-agent projection-batch write sessions, which are unbounded (the racy cold-start burst of 200 first-batch commits explains run-to-run peaks of ~35 vs ~99). That's the WS3 write governor's territory, now with hard evidence; measurement details on #494. Where Phase A matters most on its own: cold-start/catch-up storms, which would otherwise be 200 concurrent loads.

🤖 Generated with Claude Code

…on (#494 Phase A)

Every subscription agent's IEventLoader.LoadAsync opens its own store
session, so a daemon running (projections x tenants) agents under per-tenant
event partitioning drives the connection pool's high-water mark toward the
agent count even though measurement (marten ScaleTesting daemonload, 200
agents) shows peak BUSY connections of ~5.

All agent loaders a daemon builds now share one SemaphoreSlim sized by
DaemonSettings.MaxConcurrentEventLoadsPerDatabase (default 4; zero or
negative disables), applied as a ThrottledEventLoader decorator in
buildAgentForShard. The composite replay loader path is deliberately NOT
throttled - it is one serial loader per rebuild, already bounded by the
per-database rebuild cap.

Verified against the daemonload scenario (100 tenants x 2 projections = 200
agents, continuous append): event-loader sessions observed at <= 4 via
pg_stat_activity last-query attribution, 100/100 tenants caught up, append
throughput unchanged. Total pool size is NOT collapsed by this alone in an
all-tenants-active workload - 29 of ~35 steady-state sessions are projection
batch COMMIT sessions, which is the WS3 write-governor's territory (evidence
recorded on #494).

Co-Authored-By: Claude Fable 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.

1 participant