PMM-14807 Enable the global connection pool by default - #1348
Open
ademidoff wants to merge 7 commits into
Open
Conversation
The exporter created a fresh mongo.Client for every scrape and disconnected
it immediately afterwards, so each scrape cost the monitored mongod a TCP
connect, a TLS handshake where configured, and a SCRAM authentication --
work paid for by the database, not by the exporter.
--mongodb.global-conn-pool already avoided that, but it defaulted to off and
was not reachable through pmm-admin, so nobody got the benefit. Default it to
on; --no-mongodb.global-conn-pool restores per-scrape connections.
Measured on a single-node replica set with SCRAM auth, 200 scrapes with
diagnosticdata, dbstats, collstats, indexstats, top, currentop, replset
status and config enabled:
pool off pool on
connections created 605 4
SCRAM-SHA-256 handshakes 408 6
mongod CPU per scrape 11.2 ms 4.8 ms
metric names exposed 5277 5277
Also stop pinning a disconnected client in the cache. Ping failures were
returned to the caller with the client left in place, which is right for a
transient error -- the driver reconnects the pool on its own, and tearing it
down would add churn while MongoDB is already struggling -- but a client that
returns ErrClientDisconnected never recovers, and every later scrape would
fail with it. Drop that one so the next scrape builds a new client.
ademidoff
requested review from
4nte and
JiriCtvrtka
and
a balanced review from Copilot
and removed request for
a team
August 27, 2026 07:50
There was a problem hiding this comment.
Pull request overview
Enables MongoDB connection pooling by default while allowing operators to restore per-scrape connections.
Changes:
- Defaults
--mongodb.global-conn-poolto enabled. - Replaces permanently disconnected cached clients on the next scrape.
- Adds flag and connection-cache tests and updates documentation/versioning.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
VERSION |
Bumps version to v0.54.0. |
REFERENCE.md |
Documents the new default and opt-out flag. |
main.go |
Enables global connection pooling by default. |
main_test.go |
Tests default, explicit, and negated flag behavior. |
exporter/exporter.go |
Handles disconnected cached clients. |
exporter/exporter_test.go |
Tests disconnected and transient-error cache behavior. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
connect pings, so on an unreachable server context.Background() let that ping run for the whole server-selection timeout (5s by default) while holding clientMu. A scrape with a shorter budget then overran it and Prometheus got nothing at all, rather than mongodb_up 0 -- the same failure mode the disconnected-client handling above deliberately avoids. The comment claiming the scrape context would close the shared pool was wrong. Topology.Connect takes no context and mongo.Connect does not retain the one it is given, so the pooled client outlives the scrape that created it either way. Verified: a client still serves pings and queries after the context that created it is cancelled. Both invariants are now covered by tests.
JiriCtvrtka
reviewed
Aug 27, 2026
JiriCtvrtka
reviewed
Aug 27, 2026
Contributor
|
I compared my solution with yours and Claude says your one is in better shape. So lets proceed with yours. |
Bounding the initial connect by the scrape context did not fully land: waiting for the mutex guarding the cached client was not bounded by anything. New() connects in the background holding that lock, and with the pool now on by default it takes this path, so a scrape arriving during startup blocked until server selection gave up -- measured at 5.0s against an unreachable server with a 1s budget. Concurrent scrapes serialised the same way while the cache was empty. Guard the client with a capacity-1 channel instead, so taking it selects on ctx.Done() and a scrape gives up with its own budget: 5.0s -> 1.1s. Bound the background connect too. It ran on context.Background(), so a server that never answered held the lock for the process lifetime -- forever when Opts.ConnectTimeoutMS is 0, since connect then passes 0 to SetServerSelectionTimeout and the driver creates no timer at all. Also disconnect the client that background connect creates when the global pool is off. Nothing else ever owned it, so it leaked a topology, its monitoring goroutines and a connection per exporter, times N under --split-cluster. Measured on a live mongod: connections.current +2 after startup before, +0 now. This makes the "caller disconnects it" comment on that branch true.
Two problems with guarding the cached client by a lock held across network I/O, both introduced by making the pool the default. The Ping ran under an exclusive lock, so concurrent scrapes of one target -- PMM scrapes each exporter at three resolutions with different collect[] sets -- had to queue behind each other, letting one slow scrape push the next past its budget and report mongodb_up 0 for a healthy client. The lock now guards the client pointer only and is never held across a command. Bounding the connect by the scrape context fixed one failure and created another: a scrape budget shorter than one connect cancelled every attempt, so the pool stayed empty and every scrape kept paying for a connect that could never finish. The connect now gets its own budget, taken from ConnectTimeoutMS rather than from the caller, concurrent attempts collapse onto it, and each caller gives up on its own deadline while it carries on in the background. That also removes the hardcoded startup bound, which truncated the connect at 30s for an operator who had asked for more via --mongodb.connect-timeout-ms. The ConnectTimeoutMS=0 case is still handled: connect would pass 0 to SetServerSelectionTimeout, which the driver reads as no timeout at all, so the budget falls back to defaultConnectTimeout. Both tests fail against the previous shape. Also state the flipped default in the --help text, since kong does not print defaults for bool flags.
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.
PMM-14807
Addresses #1346.
Problem
The exporter created a fresh
mongo.Clientfor every scrape and disconnected it immediately afterwards. Each scrape therefore cost the monitoredmongoda TCP connect, a TLS handshake where configured, a SCRAM authentication and topology discovery — work paid for by the database, not by the exporter.--mongodb.global-conn-poolalready avoided this, but it defaulted tofalse, so nobody got the benefit by default.Changes
main.go—--mongodb.global-conn-poolnow defaults totrue.--no-mongodb.global-conn-poolrestores the previous per-scrape behaviour. (tagalignrealigned the surrounding struct tag block as a side effect of the addeddefault:"true".)exporter/exporter.go— stop pinning a disconnected client in the cache. Ping failures were returned to the caller with the client left in place. That is correct for a transient error — the driver reconnects the pool on its own, and tearing it down would only add churn while MongoDB is already struggling — but a client that returnsErrClientDisconnectednever recovers, and every later scrape would keep failing with it. That one is dropped so the next scrape builds a new client.I deliberately did not reconnect inside the failing scrape. An earlier version did, and it cost the
mongodb_up 0datapoint during an outage: the in-scrape reconnect attempt ran past the Prometheus scrape timeout and returned no metrics at all, which makes "database down" indistinguishable from "exporter down".Measurements
Single-node replica set, SCRAM-SHA-256 auth, 200 scrapes with
diagnosticdata,dbstats,collstats,indexstats,top,currentop,replicasetstatusandreplicasetconfigenabled. A third arm with no exporter running measured backgroundmongodCPU, which is subtracted below. Three independent runs.mongodCPU per scrapeConnections were attributed to the exporter via
mongod'sclient metadatalog lines (id 51800) filtered byappName, and corroborated by theserverStatus().connections.totalCreateddelta.The connection and handshake counts are stable to within a fraction of a percent across runs. The CPU figures are noisier — the reduction came out at 48%, 58% and 45% — because the test host was running other containers, so treat "roughly halves" as the claim rather than any single figure.
At PMM's 15s scrape interval this is roughly 17,400 connections and 11,700 SCRAM handshakes per target per day replaced by a handful that persist. The CPU figure is a floor: this
mongodwas on localhost with no TLS, and production adds network RTT plus a TLS handshake per connection, both of which pooling also eliminates.Scrape budgets while MongoDB is unreachable
The review surfaced a second class of problem that none of the above covers: a scrape can be made to overrun its budget, so Prometheus receives nothing at all instead of
mongodb_up 0— which makes "database down" indistinguishable from "exporter down". Two causes, both fixed here, measured against a black-holed address (192.0.2.1, so TCP connect hangs rather than refuses) with a 1s scrape budget and the default 5s server-selection timeout:These matter specifically because of this PR: with the pool off there is no shared client and no lock, so scrapes of one target could not affect each other. Enabling the pool by default introduces that coupling.
getClientnow holds the lock for the client pointer only, never across a command:Pingruns outside the lock. Under an exclusive lock, concurrent scrapes of one target — PMM scrapes each exporter at three resolutions with differentcollect[]sets — queued behind each other, so one slow scrape could push the next past its budget and reportmongodb_up 0for a perfectly healthy client.ConnectTimeoutMS, rather than the caller's. Bounding it by the scrape context fixed the overrun but created the opposite failure: a scrape budget shorter than one connect cancelled every attempt, so the pool stayed permanently empty and every scrape kept paying for a connect that could never finish. Concurrent attempts now collapse onto one connect, and each caller gives up on its own deadline while it carries on in the background.ConnectTimeoutMS == 0is still handled:connectwould hand the driver a server-selection timeout of 0, which it reads as no timeout at all (topology.go:548only arms a timer when the value is positive), so the budget falls back to a 30s default.Both behaviours are covered by tests that fail against the previous shape.
Startup client leak without the pool
New()'s background connect discarded its client without disconnecting it, leaking a topology, its monitoring goroutines and a connection per exporter — times N under--split-cluster. This only affects--no-mongodb.global-conn-poolusers and predates the PR, but the branch it lives in is one this PR rewrites. On a live mongod, with no scrape issued,connections.currentafter startup: +2 before, +0 after.Behaviour verified
mongodmid-run and restarted it. Both before and after this change the exporter reportsmongodb_up 0while it is down and recovers to1on the first scrape after restart./metrics,/scrape?target=and/scrapeallall serve both targets withmongodb_up 1and no errors in the exporter log.go test ./...produces an identical set of failures on this branch and onmain(12 tests, all requiring the full Docker cluster, which was not running locally).golangci-lint run --new-from-rev=mainis clean.All of the above was re-measured after the scrape-context fix, and the churn and recovery results are unchanged: the changed line runs at most once per process, and only when the client cache is empty.
New tests:
main_test.gocovers the flag default and its negation;exporter/exporter_test.gocovers both halves of the cache contract. Both new exporter tests fail against the pre-change code and pass after it.Notes for reviewers
--discovering-mode. The driver never reaps idle connections (maxIdleTimeMSdefaults to 0), so this is peak concurrency becoming steady state. It is bounded and small at these scales, but settingSetMaxConnIdleTimewould make it explicit — happy to add it here or leave it as a follow-up.VERSIONis bumpedv0.53.0→v0.54.0in a separate commit. Minor rather than patch, since this flips a user-visible default. It was previously equal to the latest tag, so the next release would have collided.1.26.2→1.26.7in a separate commit, for the vulnerability fixes in the intervening patch releases. Onlygo.modandtools/go.modneeded it — all four workflows resolve the toolchain throughgo-version-file: go.mod, and nothing inDockerfileor.goreleaser.ymlpins a Go version.go mod tidyon both modules produced no dependency churn.--collector.pbmremains unpooled:exporter/pbm_collector.gocallssdk.NewClientperCollect. That is PMM-13998 and is left alone here, but it is now the dominant remaining churn source when PBM metrics are enabled.pmm-admin add mongodb— is handled separately. With this default flip, PMM users get the benefit without needing that flag at all; the PMM work is only needed to let users turn pooling off.CHANGELOGis untouched; this repo only updates it at release time.