Measure and surface per-server database connection budgets (GH-3397)#3422
Merged
Conversation
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>
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Phase 1 of #3397 — measure 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
DatabaseServerIdDatabaseDescriptor.ServerNameis host-only, so two clusters co-hosted on one box would otherwise collide onto one budget. SQL Server leavesPortnull — itsData Sourcealready carries the port or named instance.IConnectionBudgetProbesum(numbackends)) and SQL Server (sys.dm_exec_connections). Providers without a cheap server-wide count don't implement it and drop out.ConnectionBudgetsDurabilitySettings.ConnectionBudgetSnapshotIWolverineObserver.ConnectionBudget— existing observers (incl. CritterWatch) are unaffected.wolverine-database-connection-count/-budget, tagged byserver.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, andUnknownis reported honestly when neither exists. SQL Server withoutVIEW SERVER STATEdegrades 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
numbackendsbehind 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, andConnectionBudgets.Enabledforces it either way. Silent entirely whenDurabilityMetricsEnabledis 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 Releaseis clean.persistence_metrics_sweepertests still passNote:
CoreTests.Persistencehas 20 pre-existingClaimCheckfailures when the group is run together (verified identical on a cleanmain) — unrelated to this change.🤖 Generated with Claude Code