Repository navigation
fix(mongo): keep the client and register metrics when MongoDB is down at startup - #4405
NitinKumar004 wants to merge 3 commits into
Conversation
… at startup Connect returned before creating the database handle and before registering the app_mongo_stats histogram whenever the first ping failed (or the config was invalid), and nothing retried. Every later operation and HealthCheck then dereferenced a nil *mongo.Database and panicked. The hostname/database metric labels and health details were also always empty because c.uri/c.database were never set. Register the histogram first, set the labels as soon as the URI parses, and keep the driver client when the ping fails: the driver connects lazily and reconnects on its own, so the app recovers once MongoDB is reachable. With an invalid config every GoFr method returns a "not connected" error instead of panicking, and HealthCheck reports DOWN with the reason in details.error. UpdateByID and UpdateMany no longer dereference a nil result when the driver returns an error. Fixes gofr-dev#4392
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
UpdateMany discards valid modification counts when the driver returns a result alongside an error.
Review effort: Balanced
Findings: 1
Open (1)
What changed in this PR
Improves MongoDB startup-failure handling so GoFr can recover from outages without restarting.
Changes:
- Registers metrics early and retains the driver client after a failed ping.
- Guards wrapped operations and adds health-check error details.
- Adds failure-path tests and documents recovery behavior.
| File | Description |
|---|---|
| pkg/gofr/datasource/mongo/mongo.go | Adds recovery handling, guards, and populated metric labels. |
| pkg/gofr/datasource/mongo/mongo_test.go | Tests invalid configuration and unreachable-server behavior. |
| docs/datasources/mongodb/page.md | Documents startup failures and embedded-driver limitations. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Umang01-hash
left a comment
There was a problem hiding this comment.
Reviewed end-to-end and verified independently against the PR head — approving.
Proven live against a real mongo:7: with the container paused at startup, InsertOne returns an error (no panic) and health is DOWN with host/database/error details; after unpausing, operations recover in the same process without a restart and health turns UP, with app_mongo_stats carrying hostname/database. The headline auto-recovery claim holds.
What I checked myself (not from the description):
- Driver semantics (v1.17.10):
mongo.Connectis lazy/non-blocking and reconnects via SDAM, so keeping the client after a failed ping genuinely recovers. All 14 ops guard viadb(); the UpdateByID/UpdateMany nil-result-on-error derefs are fixed with no sibling left unguarded. - Observability: histogram registered before every early return;
done()deferred before the guard so metrics + spans record even while down; labels bounded; no credential leak (host-onlyc.uri); name/label keys consistent. - Tests are fail-on-revert (re-ran the ping-failure early-return mutation myself → suite goes red), table-driven asserting error identity + Health contents.
- Gates: gofmt/vet/build clean,
-racepasses, golangci-lint 0 new issues, coverage 86.2%→93.8%, no breaking API change, docs updated.
One follow-up worth doing (not blocking): the auto-recovery path (down → up → op succeeds) isn't unit-tested — only the necessary condition (Database stays non-nil) is asserted. I confirmed it via a real-mongo E2E, but an mtest/toxiproxy regression test would keep the headline behavior from silently breaking later.
Nits below are optional.
| } | ||
| } | ||
|
|
||
| func TestClient_NotConnected(t *testing.T) { |
There was a problem hiding this comment.
The advertised auto-recovery (server down at Connect → later reachable → op succeeds in the same process) isn't unit-tested here — only the necessary condition (Database stays non-nil after a failed ping) is asserted. I verified the full recovery against a real mongo:7, but a regression test (mtest or a toxiproxy-gated server) would guard the headline behavior in CI. Not blocking.
There was a problem hiding this comment.
Agreed, that's the gap. The unit tests only check what recovery depends on: the client is kept, Database is non-nil, and operations return errors instead of panicking. The actual down-then-up recovery isn't covered. mtest's mock deployment can't simulate a server coming back, so a real test needs a toxiproxy- or container-backed server. I'd like to do that as a separate change rather than grow this PR. Thanks for running the pause/unpause check against a real mongo:7.
| // client, which does not happen when the config is invalid. GoFr's wrapped methods return errNotConnected in | ||
| // that case, but driver methods promoted from this field (Collection, Client, RunCommand, ...) bypass that | ||
| // guard and must not be called while it is nil. | ||
| *mongo.Database |
There was a problem hiding this comment.
Nicely documented. Just confirming the sharp edge is understood: with an invalid config this stays nil and promoted methods (Collection/RunCommand/Client) bypass db() and panic. Not reachable via the wrapped API, so fine as-is.
There was a problem hiding this comment.
Yes, that's intended. With an invalid config Database stays nil, and only the wrapped methods guard it through db(). The godoc on the field says to use the wrapped API, and the docs page notes that the embedded driver handles aren't usable until the client is configured.
…it with an error On a write error mongo-driver still returns an UpdateResult for UpdateMany (processWriteError maps WriteCommandError to rrMany), so a write-concern timeout reports how many documents were modified. The nil-result guard returned 0 for every error and dropped that count. Return the count whenever a result exists, and 0 only when the driver returns none, which is the case for UpdateByID on a write error.
Umang01-hash
left a comment
There was a problem hiding this comment.
Verified the fix end-to-end with a real MongoDB (Docker): down at startup → no panic, bounded failure, health DOWN with the dial error in details; then bringing Mongo up → the same client recovers on the first retry with no restart and health flips UP. Exactly the intended behavior, and a clear improvement over the previous nil-panic.
One operational note, not blocking: with Mongo unreachable and no REQUEST_TIMEOUT set, a handler op blocks on the driver's server-selection timeout (~30s) — I saw 50 no-deadline ops hold 50 goroutines until cancel. It's fully mitigated by REQUEST_TIMEOUT (the driver honors ctx-cancel instantly), but might be worth a line in the docs change, or exposing the driver's ServerSelectionTimeout. Your call.

What
When MongoDB was unreachable at startup, or the config was invalid,
Connectreturned before creating the database handle and before registering theapp_mongo_statshistogram. Every later operation andHealthCheckthen dereferenced a nil*mongo.Databaseand panicked, and nothing ever retried. Thehostname/databasemetric labels and the health details were also always empty, becausec.uri/c.databasewere never assigned.Changes
app_mongo_statsis registered at the start ofConnect, before any step that can fail.c.uriandc.databaseare set as soon as the URI is parsed, so both are populated.c.uriholds the host only, never the full URI, which can contain credentials.CreateCollection,DropandStartSession. Tracing, the query log and the histogram are still recorded, as in the ArangoDB fix (fix(arangodb): register metrics and return not-connected errors when Connect fails #4373).DOWNwith the reason indetails.error, both when not connected and when the ping fails.UpdateByIDandUpdateManyno longer dereference a nil result when the driver returns an error, for example a server-selection timeout while MongoDB is down.docs/datasources/mongodb/page.mddescribes the startup-failure behaviour.Notes
Disconnect, which leaked its SDAM (topology monitoring) goroutines. Keeping the client removes that leak.Clientstill embeds*mongo.Database, which is public API and is kept. With an invalid config that field isnil, and driver methods promoted from it (Collection,Client,RunCommand, ...) bypass the new guard. This is documented on the field and in the docs. With a valid config and the server down, the field is now non-nil, so only the invalid-config case remains.Testing
TestClient_Connect(table): invalid config, and server unreachable (127.0.0.1:1). It replacesTest_NewMongoClientandTest_NewMongoClientError.TestClient_NotConnected: every op plusStartSessionreturns the not-connected error.TestClient_ServerUnreachable: every op returns the driver error, and health is DOWN with host/database/error details.TestClient_HealthCheck_NotConnected, plus an assertion ondetails.errorin the ping-failure path.c.uri/c.databaseassignments;UpdateByID;details.error.development, andgo test -racepasses.mongo:7container, with the app started while MongoDB was down:/insertreturned a 500, with no panic.host,databaseanderrordetails./insertand/countsucceeded and health turned UP.app_mongo_statscarriedhostname="127.0.0.1"anddatabase="e2e".Fixes #4392