Skip to content

Measure and surface per-server database connection budgets (GH-3397)#3422

Merged
jeremydmiller merged 1 commit into
mainfrom
connection-budget-3397
Jul 14, 2026
Merged

Measure and surface per-server database connection budgets (GH-3397)#3422
jeremydmiller merged 1 commit into
mainfrom
connection-budget-3397

Conversation

@jeremydmiller

Copy link
Copy Markdown
Member

Phase 1 of #3397measure and surface only. Wolverine reports database connection pressure; it does not yet react to it. Targets 6.19.0.

Why per-server

Connections are a resource of the database server, not of a logical database. That distinction is invisible when an app talks to one database, and load-bearing in the deployment #3397 comes from: ~512 tenant databases spread over a handful of Postgres clusters, every node drawing on the same finite pool. The reporter's actual failure was a schema-apply job that couldn't get a connection.

So the budget is keyed by server, and — critically — the probe is deduped by server: one query per server per metrics sweep, however many tenant databases the node happens to own. A node owning 200 databases across 2 servers issues 2 probes per sweep, not 200. Probing per database would multiply the very pressure the number exists to reveal. There's a test that pins this.

What's here

DatabaseServerId Keys the budget by (engine, host, port). DatabaseDescriptor.ServerName is host-only, so two clusters co-hosted on one box would otherwise collide onto one budget. SQL Server leaves Port null — its Data Source already carries the port or named instance.
IConnectionBudgetProbe Optional store capability. Implemented for PostgreSQL (sum(numbackends)) and SQL Server (sys.dm_exec_connections). Providers without a cheap server-wide count don't implement it and drop out.
ConnectionBudgets Fluent per-server config on DurabilitySettings.
ConnectionBudgetSnapshot Published per server per sweep via a new default no-op IWolverineObserver.ConnectionBudget — existing observers (incl. CritterWatch) are unaffected.
OTel wolverine-database-connection-count / -budget, tagged by server.
opts.Durability.ConnectionBudgets
    .ForServer("pg-shard-a", 5432, maxConnections: 400)
    .ForServer("pg-shard-b", 5432, maxConnections: 200);

The one design decision worth arguing about

The max side is explicit configuration, not the probed max_connections. Behind a pooler (pgBouncer et al.) the server's limit describes what the pooler may open, not what the application is entitled to take — charting utilization against it would be reassuring and wrong. A probed limit is a clearly-labelled fallback when nothing is declared, and Unknown is reported honestly when neither exists. SQL Server without VIEW SERVER STATE degrades to a single warning per server, not a failed metrics sweep.

The used count is deliberately server-wide and includes other applications — that's the point (shared resource), but it's documented so nobody reads it as "connections Wolverine is using." The pooler caveat gets its own section in the docs, since numbackends behind a transaction-pooling pgBouncer means something quite different from what most people will assume.

Activation

On automatically only for the statically-known-multiple-databases shape (Marten's MultiTenantedWithShardedDatabases) — where a per-server number tells you something a per-database count can't. Off for a single database. Declaring a budget is itself an opt-in, and ConnectionBudgets.Enabled forces it either way. Silent entirely when DurabilityMetricsEnabled is false.

Not in scope

The adaptive half (back off polling/concurrency under pressure) is deliberately not here. The knobs #3397 names — SlowPollingTime/FastPollingTime, the #486 concurrency governors — live in JasperFx.Events' DaemonSettings, not Wolverine, so the back-off hooks have to ship in a JasperFx release. And it wants #3376 resolved first, since owned-agent scoping changes the baseline any tuning would work against. Phase 1 is independent of both, and happens to be the instrument that answers the connection-breakdown question still open on #3376 — which is why it ships first.

Workstream P resolved as (b): SQL Server gets the probe plumbing and gauge parity now; activation follows Polecat's tenancy roadmap (it has no sharded-database tenancy today). No Polecat changes needed.

Testing

22 new tests. dotnet build wolverine.slnx -c Release is clean.

  • N databases on one server ⇒ 1 probe per pass (the definition-of-done assertion)
  • 4 databases on server A + 1 on server B ⇒ both probed on the same cadence
  • configured budget beats the server's own limit, which is then never even read
  • probed fallback is read exactly once per process, not once per pass
  • unknown budget reported honestly; utilization null
  • a failing probe publishes nothing and leaves the rest of the sweep running
  • the existing 5 persistence_metrics_sweeper tests still pass

Note: CoreTests.Persistence has 20 pre-existing ClaimCheck failures when the group is run together (verified identical on a clean main) — unrelated to this change.

🤖 Generated with Claude Code

Connections are a resource of the database server, not of a logical database.
That distinction is invisible when an application talks to one database, and
load-bearing in the sharded-tenancy deployment #3397 comes from: hundreds of
tenant databases spread over a handful of Postgres clusters, all drawing on the
same finite pool, with the failures showing up as diffuse things like a schema
apply job that cannot get a connection.

This is phase 1 only: measure and surface. Wolverine reports connection pressure;
it does not yet react to it. The adaptive back-off half needs hooks that live in
JasperFx.Events' DaemonSettings (the polling knobs and the #486 governors are not
Wolverine's), and it needs #3376 to resolve first since that changes the baseline
any tuning would work against.

- DatabaseServerId keys the budget by (engine, host, port). DatabaseDescriptor's
  ServerName is host-only, so two clusters co-hosted on one box would otherwise
  collide onto a single budget. SQL Server leaves Port null, since its Data Source
  already carries the port or named instance.
- IConnectionBudgetProbe is an optional store capability, implemented for
  PostgreSQL and SQL Server. Providers without a cheap server-wide connection
  count simply do not implement it and drop out of the sweep.
- The probe rides the existing metrics sweeper and is deduped by server: one query
  per server per pass, however many tenant databases the node owns. Probing per
  database would multiply the very pressure the number exists to reveal.
- The max side of the budget is explicit configuration, not the server's
  max_connections. Behind a pooler, the server's limit describes what the pooler
  may open, not what the application is entitled to take; charting utilization
  against it would be reassuring and wrong. A probed limit is the labelled fallback
  when nothing is declared, and "unknown" is reported honestly when neither exists
  (SQL Server without VIEW SERVER STATE degrades to one warning, not a failed sweep).
- Surfaced as OTel gauges tagged by server, and as IWolverineObserver.ConnectionBudget
  for CritterWatch. The observer member is a default no-op, so existing observers are
  unaffected.

Active by default only for the statically-known-multiple-databases shape (Marten's
MultiTenantedWithShardedDatabases), where a per-server number tells you something a
per-database count cannot. Declaring a budget is itself an opt-in.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@jeremydmiller
jeremydmiller merged commit de27d3d into main Jul 14, 2026
30 checks passed
mysticmind pushed a commit that referenced this pull request Jul 14, 2026
Closes the rest of #3423. The remaining 19 failures were not an ordering or fixture
problem at all: Wolverine was resolving the wrong application assembly.

When nothing pins it, WolverineOptions.establishApplicationAssembly falls back to
determineCallingAssembly(), a stack walk that takes the first non-System/Microsoft
frame outside Wolverine. Under xUnit that frame is not reliably the test assembly —
instrumenting a failing run showed ApplicationAssembly resolving to
"xunit.execution.dotnet". Conventional discovery then scans xUnit, finds no handlers,
and HandlerGraph.CanHandle returns false. The IndeterminateRoutesException the tests
reported was only the downstream symptom; the real failure was swallowed and logged at
WolverineRuntime.cs:315.

Which frame the walk lands on depends on what ran before, which is why these tests
passed alone and in the full assembly but failed as a filtered subset.

Pin ApplicationAssembly explicitly in DefaultApp and in the two ClaimCheck harnesses
that build their own hosts inside an async InitializeAsync. This is what Wolverine's
own docs prescribe for test harnesses (see the ApplicationAssembly /
RememberedApplicationAssembly doc comments) and it makes discovery deterministic
regardless of call stack.

Also fixes two races in the connection-budget tests added in #3422, both mine. Each
compared counters sampled at different instants while the sweeper kept advancing them
in the background: one waited on the snapshot count and then asserted on fetch counts
(the budget probe runs at the top of a pass, before the per-database walk, so the Nth
probe can land while a store has only been fetched N-1 times), and one asserted an
exact probes == snapshots equality across two samples. Both now assert the invariant
that holds at any instant. 25 consecutive runs green.

Persistence subset: 20 failures -> 0 (178/178, stable over 5 runs).
Full CoreTests assembly: 1953/1953.

Co-Authored-By: Claude Opus 4.8 (1M context) <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