@@ -17,21 +17,14 @@ package exporter
1717
1818import (
1919 "context"
20- "fmt"
2120 "strings"
2221 "testing"
2322 "time"
2423
25- pbmconnect "github.com/percona/percona-backup-mongodb/pbm/connect"
26- "github.com/prometheus/client_golang/prometheus"
2724 "github.com/prometheus/client_golang/prometheus/testutil"
2825 "github.com/prometheus/common/promslog"
2926 "github.com/stretchr/testify/assert"
3027 "github.com/stretchr/testify/require"
31- "go.mongodb.org/mongo-driver/bson"
32- "go.mongodb.org/mongo-driver/mongo"
33- "go.mongodb.org/mongo-driver/mongo/options"
34- "go.mongodb.org/mongo-driver/mongo/readpref"
3528
3629 "github.com/percona/mongodb_exporter/internal/tu"
3730)
@@ -71,163 +64,3 @@ func TestPBMCollector(t *testing.T) {
7164 assert .Equal (t , expectedLength , count , "PBM metrics are missing" )
7265 })
7366}
74-
75- // TestPBMCollectorConnectionLeak verifies that the PBM collector does not leak
76- // MongoDB connections across multiple scrape cycles.
77- //
78- //nolint:paralleltest
79- func TestPBMCollectorConnectionLeak (t * testing.T ) {
80- ctx , cancel := context .WithTimeout (context .Background (), 60 * time .Second )
81- defer cancel ()
82-
83- port , err := tu .PortForContainer ("mongo-2-1" )
84- require .NoError (t , err )
85-
86- mongoURI := fmt .Sprintf (
87- "mongodb://admin:[email protected] :%s/?connectTimeoutMS=500&serverSelectionTimeoutMS=500" ,
88- port ,
89- ) //nolint:gosec
90-
91- client , err := mongo .Connect (ctx , options .Client ().ApplyURI (mongoURI ))
92- require .NoError (t , err )
93- t .Cleanup (func () {
94- client .Disconnect (ctx ) //nolint:errcheck
95- })
96- err = client .Ping (ctx , nil )
97- require .NoError (t , err )
98-
99- // Allow connections to stabilize
100- time .Sleep (100 * time .Millisecond )
101-
102- initialConns := getServerConnectionCount (ctx , t , client )
103- t .Logf ("Initial connections: %d" , initialConns )
104-
105- c := newPbmCollector (ctx , client , mongoURI , promslog .New (& promslog.Config {}))
106-
107- // Run multiple collection cycles
108- const scrapeCount = 10
109- for i := 0 ; i < scrapeCount ; i ++ {
110- ch := make (chan prometheus.Metric , 100 )
111- c .Collect (ch )
112- close (ch )
113- for range ch {
114- }
115- }
116-
117- // Allow time for cleanup
118- time .Sleep (1 * time .Second )
119-
120- finalConns := getServerConnectionCount (ctx , t , client )
121- connGrowth := finalConns - initialConns
122-
123- t .Logf ("Final connections: %d (growth=%d over %d scrapes)" , finalConns , connGrowth , scrapeCount )
124-
125- // With a leak: expect ~3-4 connections per scrape (one per cluster member)
126- // Without leak: should be near zero growth
127- assert .Less (t , connGrowth , int64 (scrapeCount ),
128- "Connection leak detected: server connections grew by %d over %d scrapes" , connGrowth , scrapeCount )
129- }
130-
131- // TestPBMSDKConnectionLeakOnPingFailure tests the PBM SDK's connect.MongoConnect
132- // for connection leaks when Ping fails after Connect succeeds.
133- //
134- // The bug in PBM SDK's connect.MongoConnectWithOpts():
135- // - mongo.Connect() succeeds (establishes connections)
136- // - Ping() fails (e.g., unreachable server, wrong replica set)
137- // - Connection is NOT disconnected -> connections leak
138- //
139- // This test uses an unsatisfiable ReadPreference to trigger Ping failure
140- // after the driver has established connections.
141- //
142- //nolint:paralleltest
143- func TestPBMSDKConnectionLeakOnPingFailure (t * testing.T ) {
144- ctx , cancel := context .WithTimeout (context .Background (), 60 * time .Second )
145- defer cancel ()
146-
147- port , err := tu .PortForContainer ("mongo-2-1" )
148- require .NoError (t , err )
149-
150- // Create a monitoring client to check server connection count
151- monitorURI := fmt .
Sprintf (
"mongodb://admin:[email protected] :%s/" ,
port )
//nolint:gosec 152- monitorClient , err := mongo .Connect (ctx , options .Client ().ApplyURI (monitorURI ))
153- require .NoError (t , err )
154- t .Cleanup (func () {
155- monitorClient .Disconnect (ctx ) //nolint:errcheck
156- })
157- err = monitorClient .Ping (ctx , nil )
158- require .NoError (t , err )
159-
160- // Allow connections to stabilize
161- time .Sleep (100 * time .Millisecond )
162-
163- initialConns := getServerConnectionCount (ctx , t , monitorClient )
164- t .Logf ("Initial connections: %d" , initialConns )
165-
166- const iterations = 5
167-
168- for i := 0 ; i < iterations ; i ++ {
169- // Use a ReadPreference that cannot be satisfied to trigger the leak:
170- // 1. mongo.Connect() succeeds and establishes connections
171- // 2. Ping() fails because no server matches the tag selector
172- // 3. Without proper cleanup, connections are leaked
173- _ , err := pbmconnect .MongoConnect (ctx ,
174- monitorURI ,
175- pbmconnect .AppName ("leak-test" ),
176- func (opts * options.ClientOptions ) error {
177- opts .SetReadPreference (readpref .Nearest (readpref .WithTags ("dc" , "nonexistent" )))
178- opts .SetServerSelectionTimeout (300 * time .Millisecond )
179- return nil
180- },
181- )
182- require .Error (t , err , "MongoConnect should fail due to unsatisfiable ReadPreference" )
183- t .Logf ("Iteration %d: MongoConnect failed as expected" , i )
184- }
185-
186- // Allow time for any cleanup
187- time .Sleep (500 * time .Millisecond )
188-
189- finalConns := getServerConnectionCount (ctx , t , monitorClient )
190- connGrowth := finalConns - initialConns
191-
192- t .Logf ("Final connections: %d (growth=%d over %d iterations)" , finalConns , connGrowth , iterations )
193-
194- // Each leaked connection attempt should leave connections open
195- // If there's a leak, we'd see growth proportional to iterations
196- // Without leak, growth should be zero or minimal
197- leakThreshold := int64 (iterations * 2 ) // Allow some buffer
198-
199- if connGrowth >= leakThreshold {
200- t .Logf ("LEAK DETECTED: %d connections leaked over %d iterations" , connGrowth , iterations )
201- t .Logf ("This confirms the bug in PBM SDK's connect.MongoConnectWithOpts()" )
202- }
203-
204- // This test documents the leak. Once the PBM SDK is fixed, flip this assertion:
205- // assert.Less(t, connGrowth, leakThreshold, "Connection leak in PBM SDK")
206- assert .GreaterOrEqual (t , connGrowth , leakThreshold ,
207- "Expected connection leak when Ping fails. Growth was %d, expected at least %d. " +
208- "If this fails, the PBM SDK may have been fixed!" ,
209- connGrowth , leakThreshold )
210- }
211-
212- // getServerConnectionCount returns the current number of connections to the MongoDB server.
213- func getServerConnectionCount (ctx context.Context , t * testing.T , client * mongo.Client ) int64 {
214- t .Helper ()
215-
216- var result bson.M
217- err := client .Database ("admin" ).RunCommand (ctx , bson.D {{Key : "serverStatus" , Value : 1 }}).Decode (& result )
218- require .NoError (t , err )
219-
220- connections , ok := result ["connections" ].(bson.M )
221- require .True (t , ok , "serverStatus should contain connections field" )
222-
223- current , ok := connections ["current" ].(int32 )
224- if ok {
225- return int64 (current )
226- }
227-
228- // Try int64 in case MongoDB returns it differently
229- current64 , ok := connections ["current" ].(int64 )
230- require .True (t , ok , "connections.current should be numeric" )
231-
232- return current64
233- }
0 commit comments