Skip to content

Commit aff557b

Browse files
authored
GODRIVER-2078 Add a Client stress test that simulates traffic overload and recovery. (#732)
1 parent 0a5054e commit aff557b

File tree

3 files changed

+114
-14
lines changed

3 files changed

+114
-14
lines changed

internal/testutil/config.go

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -318,15 +318,6 @@ func GetDBName(cs connstring.ConnString) string {
318318
return fmt.Sprintf("mongo-go-driver-%d", os.Getpid())
319319
}
320320

321-
// Integration should be called at the beginning of integration
322-
// tests to ensure that they are skipped if integration testing is
323-
// turned off.
324-
func Integration(t *testing.T) {
325-
if testing.Short() {
326-
t.Skip("skipping integration test in short mode")
327-
}
328-
}
329-
330321
// compareVersions compares two version number strings (i.e. positive integers separated by
331322
// periods). Comparisons are done to the lesser precision of the two versions. For example, 3.2 is
332323
// considered equal to 3.2.11, whereas 3.2.0 is considered less than 3.2.11.

mongo/integration/client_test.go

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
"go.mongodb.org/mongo-driver/bson"
1919
"go.mongodb.org/mongo-driver/bson/bsoncodec"
2020
"go.mongodb.org/mongo-driver/bson/bsonrw"
21+
"go.mongodb.org/mongo-driver/bson/primitive"
2122
"go.mongodb.org/mongo-driver/internal"
2223
"go.mongodb.org/mongo-driver/internal/testutil"
2324
"go.mongodb.org/mongo-driver/internal/testutil/assert"
@@ -28,6 +29,7 @@ import (
2829
"go.mongodb.org/mongo-driver/x/bsonx/bsoncore"
2930
"go.mongodb.org/mongo-driver/x/mongo/driver"
3031
"go.mongodb.org/mongo-driver/x/mongo/driver/wiremessage"
32+
"golang.org/x/sync/errgroup"
3133
)
3234

3335
var noClientOpts = mtest.NewOptions().CreateClient(false)
@@ -472,3 +474,109 @@ type proxyMessage struct {
472474
sent wiremessage.WireMessage
473475
received wiremessage.WireMessage
474476
}
477+
478+
func TestClientStress(t *testing.T) {
479+
// TODO: Enable with GODRIVER-2038.
480+
t.Skip("TODO: Enable with GODRIVER-2038")
481+
482+
if testing.Short() {
483+
t.Skip("skipping integration test in short mode")
484+
}
485+
486+
mtOpts := mtest.NewOptions().CreateClient(false)
487+
mt := mtest.New(t, mtOpts)
488+
489+
// Test that a Client can recover from a massive traffic spike after the traffic spike is over.
490+
mt.Run("Client recovers from traffic spike", func(mt *mtest.T) {
491+
oid := primitive.NewObjectID()
492+
doc := bson.D{{Key: "_id", Value: oid}, {Key: "key", Value: "value"}}
493+
_, err := mt.Coll.InsertOne(context.Background(), doc)
494+
assert.Nil(mt, err, "InsertOne error: %v", err)
495+
496+
// findOne calls FindOne("_id": oid) on the given collection and with the given timeout. It
497+
// returns any errors.
498+
findOne := func(coll *mongo.Collection, timeout time.Duration) error {
499+
ctx, cancel := context.WithTimeout(context.Background(), timeout)
500+
defer cancel()
501+
var res map[string]interface{}
502+
return coll.FindOne(ctx, bson.D{{Key: "_id", Value: oid}}).Decode(&res)
503+
}
504+
505+
// findOneFor calls FindOne on the given collection and with the given timeout in a loop for
506+
// the given duration and returns any errors returned by FindOne.
507+
findOneFor := func(coll *mongo.Collection, timeout time.Duration, d time.Duration) []error {
508+
errs := make([]error, 0)
509+
start := time.Now()
510+
for time.Since(start) <= d {
511+
err := findOne(coll, timeout)
512+
if err != nil {
513+
errs = append(errs, err)
514+
}
515+
}
516+
return errs
517+
}
518+
519+
// Calculate the maximum observed round-trip time by measuring the duration of some FindOne
520+
// operations and picking the max.
521+
var maxRTT time.Duration
522+
for i := 0; i < 50; i++ {
523+
start := time.Now()
524+
err := findOne(mt.Coll, 10*time.Second)
525+
assert.Nil(t, err, "FindOne error: %v", err)
526+
duration := time.Since(start)
527+
if duration > maxRTT {
528+
maxRTT = duration
529+
}
530+
}
531+
assert.True(mt, maxRTT > 0, "RTT must be greater than 0")
532+
533+
// Run tests with various "maxPoolSize" values, including 1-connection pools and unlimited
534+
// size pools, to test how the client handles traffic spikes using different connection pool
535+
// configurations.
536+
maxPoolSizes := []uint64{0, 1, 10, 100}
537+
for _, maxPoolSize := range maxPoolSizes {
538+
maxPoolSizeOpt := mtest.NewOptions().ClientOptions(options.Client().SetMaxPoolSize(maxPoolSize))
539+
mt.RunOpts(fmt.Sprintf("maxPoolSize %d", maxPoolSize), maxPoolSizeOpt, func(mt *mtest.T) {
540+
doc := bson.D{{Key: "_id", Value: oid}, {Key: "key", Value: "value"}}
541+
_, err := mt.Coll.InsertOne(context.Background(), doc)
542+
assert.Nil(mt, err, "InsertOne error: %v", err)
543+
544+
// Set the timeout to be 10x the maximum observed RTT. Use a minimum 10ms timeout to
545+
// prevent spurious test failures due to extremely low timeouts.
546+
timeout := maxRTT * 10
547+
if timeout < 10*time.Millisecond {
548+
timeout = 10 * time.Millisecond
549+
}
550+
t.Logf("Max RTT %v; using a timeout of %v", maxRTT, timeout)
551+
552+
// Simulate normal traffic by running one FindOne loop for 1 second and assert that there
553+
// are no errors.
554+
errs := findOneFor(mt.Coll, timeout, 1*time.Second)
555+
assert.True(mt, len(errs) == 0, "expected no errors, but got %d (%v)", len(errs), errs)
556+
557+
// Simulate an extreme traffic spike by running 1,000 FindOne loops in parallel for 10
558+
// seconds and expect at least some errors to occur.
559+
g := new(errgroup.Group)
560+
for i := 0; i < 1000; i++ {
561+
g.Go(func() error {
562+
errs := findOneFor(mt.Coll, timeout, 10*time.Second)
563+
if len(errs) == 0 {
564+
return nil
565+
}
566+
return errs[len(errs)-1]
567+
})
568+
}
569+
err = g.Wait()
570+
assert.NotNil(mt, err, "expected at least one error, got nil")
571+
572+
// Simulate normal traffic again for 1 second. Ignore any errors to allow any outstanding
573+
// connection errors to stop.
574+
_ = findOneFor(mt.Coll, timeout, 1*time.Second)
575+
576+
// Simulate normal traffic again for 1 second and assert that there are no errors.
577+
errs = findOneFor(mt.Coll, timeout, 1*time.Second)
578+
assert.True(mt, len(errs) == 0, "expected no errors, but got %d (%v)", len(errs), errs)
579+
})
580+
}
581+
})
582+
}

mongo/integration/mtest/mongotest.go

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"fmt"
1212
"strings"
1313
"sync"
14+
"sync/atomic"
1415
"testing"
1516

1617
"go.mongodb.org/mongo-driver/bson"
@@ -103,7 +104,7 @@ type T struct {
103104
dataLake *bool
104105
ssl *bool
105106
collCreateOpts bson.D
106-
connsCheckedOut int // net number of connections checked out during test execution
107+
connsCheckedOut int64 // net number of connections checked out during test execution
107108
requireAPIVersion *bool
108109

109110
// options copied to sub-tests
@@ -231,7 +232,7 @@ func (t *T) RunOpts(name string, opts *Options, callback func(*T)) {
231232
// store number of sessions and connections checked out here but assert that they're equal to 0 after
232233
// cleaning up test resources to make sure resources are always cleared
233234
sessions := sub.Client.NumberSessionsInProgress()
234-
conns := sub.connsCheckedOut
235+
conns := sub.NumberConnectionsCheckedOut()
235236

236237
if sub.clientType != Mock {
237238
sub.ClearFailPoints()
@@ -369,7 +370,7 @@ func (t *T) GetProxiedMessages() []*ProxyMessage {
369370

370371
// NumberConnectionsCheckedOut returns the number of connections checked out from the test Client.
371372
func (t *T) NumberConnectionsCheckedOut() int {
372-
return t.connsCheckedOut
373+
return int(atomic.LoadInt64(&t.connsCheckedOut))
373374
}
374375

375376
// ClearEvents clears the existing command monitoring events.
@@ -594,9 +595,9 @@ func (t *T) createTestClient() {
594595

595596
switch evt.Type {
596597
case event.GetSucceeded:
597-
t.connsCheckedOut++
598+
atomic.AddInt64(&t.connsCheckedOut, 1)
598599
case event.ConnectionReturned:
599-
t.connsCheckedOut--
600+
atomic.AddInt64(&t.connsCheckedOut, -1)
600601
}
601602
},
602603
})

0 commit comments

Comments
 (0)