// Copyright (C) MongoDB, Inc. 2017-present. // // Licensed under the Apache License, Version 2.0 (the "License"); you may // not use this file except in compliance with the License. You may obtain // a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 package integration import ( "bytes" "context" "errors" "os" "strings" "testing" "go.mongodb.org/mongo-driver/v2/bson" "go.mongodb.org/mongo-driver/v2/event" "go.mongodb.org/mongo-driver/v2/internal/assert" "go.mongodb.org/mongo-driver/v2/internal/failpoint" "go.mongodb.org/mongo-driver/v2/internal/integration/mtest" "go.mongodb.org/mongo-driver/v2/internal/require" "go.mongodb.org/mongo-driver/v2/mongo" "go.mongodb.org/mongo-driver/v2/mongo/options" "go.mongodb.org/mongo-driver/v2/mongo/writeconcern" "go.mongodb.org/mongo-driver/v2/x/bsonx/bsoncore" "go.mongodb.org/mongo-driver/v2/x/mongo/driver" ) func TestWriteErrorsWithLabels(t *testing.T) { clientOpts := options.Client().SetRetryWrites(false).SetWriteConcern(mtest.MajorityWc). SetReadConcern(mtest.MajorityRc) mtOpts := mtest.NewOptions().ClientOptions(clientOpts).MinServerVersion("4.0").Topologies(mtest.ReplicaSet). CreateClient(false) mt := mtest.New(t, mtOpts) label := "ExampleError" mt.Run("InsertMany errors with label", func(mt *mtest.T) { mt.SetFailPoint(failpoint.FailPoint{ ConfigureFailPoint: "failCommand", Mode: failpoint.Mode{ Times: 1, }, Data: failpoint.Data{ FailCommands: []string{"insert"}, WriteConcernError: &failpoint.WriteConcernError{ Code: 100, ErrorLabels: &[]string{label}, }, }, }) _, err := mt.Coll.InsertMany(context.Background(), []any{ bson.D{ {"a", 1}, }, bson.D{ {"a", 2}, }, }) assert.NotNil(mt, err, "expected non-nil error, got nil") we, ok := err.(mongo.BulkWriteException) assert.True(mt, ok, "expected mongo.BulkWriteException, got %T", err) assert.True(mt, we.HasErrorLabel(label), "expected error to have label: %v", label) }) mt.Run("WriteException with label", func(mt *mtest.T) { mt.SetFailPoint(failpoint.FailPoint{ ConfigureFailPoint: "failCommand", Mode: failpoint.Mode{ Times: 1, }, Data: failpoint.Data{ FailCommands: []string{"delete"}, WriteConcernError: &failpoint.WriteConcernError{ Code: 100, ErrorLabels: &[]string{label}, }, }, }) _, err := mt.Coll.DeleteMany(context.Background(), bson.D{{"a", 1}}) assert.NotNil(mt, err, "expected non-nil error, got nil") we, ok := err.(mongo.WriteException) assert.True(mt, ok, "expected mongo.WriteException, got %T", err) assert.True(mt, we.HasErrorLabel(label), "expected error to have label: %v", label) }) mt.Run("BulkWriteException with label", func(mt *mtest.T) { mt.SetFailPoint(failpoint.FailPoint{ ConfigureFailPoint: "failCommand", Mode: failpoint.Mode{ Times: 1, }, Data: failpoint.Data{ FailCommands: []string{"delete"}, WriteConcernError: &failpoint.WriteConcernError{ Code: 100, ErrorLabels: &[]string{label}, }, }, }) models := []mongo.WriteModel{ &mongo.InsertOneModel{bson.D{{"a", 2}}}, &mongo.DeleteOneModel{bson.D{{"a", 2}}, nil, nil}, } _, err := mt.Coll.BulkWrite(context.Background(), models) assert.NotNil(mt, err, "expected non-nil error, got nil") we, ok := err.(mongo.BulkWriteException) assert.True(mt, ok, "expected mongo.BulkWriteException, got %T", err) assert.True(mt, we.HasErrorLabel(label), "expected error to have label: %v", label) }) } func TestWriteErrorsDetails(t *testing.T) { clientOpts := options.Client(). SetRetryWrites(false). SetWriteConcern(mtest.MajorityWc). SetReadConcern(mtest.MajorityRc) mtOpts := mtest.NewOptions(). ClientOptions(clientOpts). MinServerVersion("5.0"). Topologies(mtest.ReplicaSet, mtest.Single). CreateClient(false) mt := mtest.New(t, mtOpts) mt.Run("JSON Schema validation", func(mt *mtest.T) { // Create a JSON Schema validator document that requires properties "a" and "b". Use it in // the collection creation options so that collections created for subtests have the JSON // Schema validator applied. validator := bson.M{ "$jsonSchema": bson.M{ "bsonType": "object", "required": []string{"a", "b"}, "properties": bson.M{ "a": bson.M{"bsonType": "string"}, "b": bson.M{"bsonType": "int"}, }, }, } cco := options.CreateCollection().SetValidator(validator) validatorOpts := mtest.NewOptions().CollectionCreateOptions(cco) cases := []struct { desc string operation func(*mongo.Collection) error expectBulkError bool expectedCommandName string }{ { desc: "InsertOne schema validation errors should include Details", operation: func(coll *mongo.Collection) error { // Try to insert a document that doesn't contain the required properties. _, err := coll.InsertOne(context.Background(), bson.D{{"nope", 1}}) return err }, expectBulkError: false, expectedCommandName: "insert", }, { desc: "InsertMany schema validation errors should include Details", operation: func(coll *mongo.Collection) error { // Try to insert a document that doesn't contain the required properties. _, err := coll.InsertMany(context.Background(), []any{bson.D{{"nope", 1}}}) return err }, expectBulkError: true, expectedCommandName: "insert", }, { desc: "UpdateOne schema validation errors should include Details", operation: func(coll *mongo.Collection) error { // Try to set "a" to be an int, which violates the string type requirement. _, err := coll.UpdateOne( context.Background(), bson.D{}, bson.D{{"$set", bson.D{{"a", 1}}}}) return err }, expectBulkError: false, expectedCommandName: "update", }, { desc: "UpdateMany schema validation errors should include Details", operation: func(coll *mongo.Collection) error { // Try to set "a" to be an int in all documents in the collection, which violates // the string type requirement. _, err := coll.UpdateMany( context.Background(), bson.D{}, bson.D{{"$set", bson.D{{"a", 1}}}}) return err }, expectBulkError: false, expectedCommandName: "update", }, } for _, tc := range cases { mt.RunOpts(tc.desc, validatorOpts, func(mt *mtest.T) { // Insert two valid documents so that the Update* tests can try to update them. { _, err := mt.Coll.InsertMany( context.Background(), []any{ bson.D{{"a", "str1"}, {"b", 1}}, bson.D{{"a", "str2"}, {"b", 2}}, }) assert.Nil(mt, err, "unexpected error inserting valid documents: %s", err) } err := tc.operation(mt.Coll) assert.NotNil(mt, err, "expected an error from calling the operation") sErr := err.(mongo.ServerError) assert.True( mt, sErr.HasErrorCode(121), "expected mongo.ServerError to have error code 121 (DocumentValidationFailure)") var details bson.Raw if tc.expectBulkError { bwe, ok := err.(mongo.BulkWriteException) assert.True( mt, ok, "expected error to be type mongo.BulkWriteException, got type %T (error %q)", err, err) // Assert that there is one WriteError and that the Details field is populated. assert.Equal( mt, 1, len(bwe.WriteErrors), "expected exactly 1 write error, but got %d write errors (error %q)", len(bwe.WriteErrors), err) details = bwe.WriteErrors[0].Details } else { we, ok := err.(mongo.WriteException) assert.True( mt, ok, "expected error to be type mongo.WriteException, got type %T (error %q)", err, err) // Assert that there is one WriteError and that the Details field is populated. assert.Equal( mt, 1, len(we.WriteErrors), "expected exactly 1 write error, but got %d write errors (error %q)", len(we.WriteErrors), err) details = we.WriteErrors[0].Details } assert.True( mt, len(details) > 0, "expected WriteError.Details to be populated, but is empty") // Assert that the most recent CommandSucceededEvent was triggered by the expected // operation and contains the resulting write errors and that // "writeErrors[0].errInfo" is the same as "WriteException.WriteErrors[0].Details". evts := mt.GetAllSucceededEvents() assert.True( mt, len(evts) >= 2, "expected there to be at least 2 CommandSucceededEvent recorded") evt := evts[len(evts)-1] assert.Equal( mt, tc.expectedCommandName, evt.CommandName, "expected the last CommandSucceededEvent to be for %q, was %q", tc.expectedCommandName, evt.CommandName) errInfo, ok := evt.Reply.Lookup("writeErrors", "0", "errInfo").DocumentOK() assert.True( mt, ok, "expected evt.Reply to contain writeErrors[0].errInfo but doesn't (evt.Reply = %v)", evt.Reply) assert.Equal(mt, details, errInfo, "want %v, got %v", details, errInfo) }) } }) } func TestHintErrors(t *testing.T) { mtOpts := mtest.NewOptions().MaxServerVersion("3.2").CreateClient(false) mt := mtest.New(t, mtOpts) expected := errors.New("the 'hint' command parameter requires a minimum server wire version of 5") mt.Run("UpdateMany", func(mt *mtest.T) { _, got := mt.Coll.UpdateMany(context.Background(), bson.D{{"a", 1}}, bson.D{{"$inc", bson.D{{"a", 1}}}}, options.UpdateMany().SetHint("_id_")) assert.NotNil(mt, got, "expected non-nil error, got nil") assert.Equal(mt, got, expected, "expected: %v got: %v", expected, got) }) mt.Run("ReplaceOne", func(mt *mtest.T) { _, got := mt.Coll.ReplaceOne(context.Background(), bson.D{{"a", 1}}, bson.D{{"a", 2}}, options.Replace().SetHint("_id_")) assert.NotNil(mt, got, "expected non-nil error, got nil") assert.Equal(mt, got, expected, "expected: %v got: %v", expected, got) }) mt.Run("BulkWrite", func(mt *mtest.T) { models := []mongo.WriteModel{ &mongo.InsertOneModel{bson.D{{"_id", 2}}}, &mongo.ReplaceOneModel{Filter: bson.D{{"_id", 2}}, Replacement: bson.D{{"a", 2}}, Hint: "_id_"}, } _, got := mt.Coll.BulkWrite(context.Background(), models) assert.NotNil(mt, got, "expected non-nil error, got nil") assert.Equal(mt, got, expected, "expected: %v got: %v", expected, got) }) } func TestWriteConcernError(t *testing.T) { mt := mtest.New(t, noClientOpts) errInfoOpts := mtest.NewOptions().MinServerVersion("4.0").Topologies(mtest.ReplicaSet) mt.RunOpts("errInfo is propagated", errInfoOpts, func(mt *mtest.T) { wcDoc := bsoncore.BuildDocumentFromElements(nil, bsoncore.AppendInt32Element(nil, "w", 2), bsoncore.AppendInt32Element(nil, "wtimeout", 0), bsoncore.AppendStringElement(nil, "provenance", "clientSupplied"), ) errInfoDoc := bsoncore.BuildDocumentFromElements(nil, bsoncore.AppendDocumentElement(nil, "writeConcern", wcDoc), ) fp := failpoint.FailPoint{ ConfigureFailPoint: "failCommand", Mode: failpoint.Mode{ Times: 1, }, Data: failpoint.Data{ FailCommands: []string{"insert"}, WriteConcernError: &failpoint.WriteConcernError{ Code: 100, Name: "UnsatisfiableWriteConcern", Errmsg: "Not enough data-bearing nodes", ErrInfo: errInfoDoc, }, }, } mt.SetFailPoint(fp) _, err := mt.Coll.InsertOne(context.Background(), bson.D{{"x", 1}}) assert.NotNil(mt, err, "expected InsertOne error, got nil") writeException, ok := err.(mongo.WriteException) assert.True(mt, ok, "expected WriteException, got error %v of type %T", err, err) wcError := writeException.WriteConcernError assert.NotNil(mt, wcError, "expected write-concern error, got %v", err) assert.True(mt, bytes.Equal(wcError.Details, errInfoDoc), "expected errInfo document %v, got %v", bson.Raw(errInfoDoc), wcError.Details) }) } func TestErrorsCodeNamePropagated(t *testing.T) { // Ensure the codeName field is propagated for both command and write concern errors. mtOpts := mtest.NewOptions(). Topologies(mtest.ReplicaSet). CreateClient(false) mt := mtest.New(t, mtOpts) mt.RunOpts("command error", mtest.NewOptions().MinServerVersion("3.4"), func(mt *mtest.T) { // codeName is propagated in an ok:0 error. cmd := bson.D{ {"insert", mt.Coll.Name()}, {"documents", []bson.D{}}, } err := mt.DB.RunCommand(context.Background(), cmd).Err() assert.NotNil(mt, err, "expected RunCommand error, got nil") ce, ok := err.(mongo.CommandError) assert.True(mt, ok, "expected error of type %T, got %v of type %T", mongo.CommandError{}, err, err) expectedCodeName := "InvalidLength" assert.Equal(mt, expectedCodeName, ce.Name, "expected error code name %q, got %q", expectedCodeName, ce.Name) }) wcCollOpts := options.Collection(). SetWriteConcern(impossibleWc) wcMtOpts := mtest.NewOptions(). CollectionOptions(wcCollOpts) mt.RunOpts("write concern error", wcMtOpts, func(mt *mtest.T) { // codeName is propagated for write concern errors. _, err := mt.Coll.InsertOne(context.Background(), bson.D{}) assert.NotNil(mt, err, "expected InsertOne error, got nil") we, ok := err.(mongo.WriteException) assert.True(mt, ok, "expected error of type %T, got %v of type %T", mongo.WriteException{}, err, err) wce := we.WriteConcernError assert.NotNil(mt, wce, "expected write concern error, got %v", we) var expectedCodeName string if codeNameVal, err := mt.GetSucceededEvent().Reply.LookupErr("writeConcernError", "codeName"); err == nil { expectedCodeName = codeNameVal.StringValue() } assert.Equal(mt, expectedCodeName, wce.Name, "expected code name %q, got %q", expectedCodeName, wce.Name) }) } func TestClientBulkWriteProse(t *testing.T) { mtOpts := mtest.NewOptions().MinServerVersion("8.0").ClientType(mtest.Pinned) mt := mtest.New(t, mtOpts) mt.Run("3. MongoClient.bulkWrite batch splits a writeModels input with greater than maxWriteBatchSize operations", func(mt *mtest.T) { var opsCnt []int monitor := &event.CommandMonitor{ Started: func(_ context.Context, e *event.CommandStartedEvent) { if e.CommandName == "bulkWrite" { var c struct { Ops []bson.D } err := bson.Unmarshal(e.Command, &c) require.NoError(mt, err) opsCnt = append(opsCnt, len(c.Ops)) } }, } mt.ResetClient(options.Client().SetMonitor(monitor)) var hello struct { MaxWriteBatchSize int } err := mt.DB.RunCommand(context.Background(), bson.D{{"hello", 1}}).Decode(&hello) require.NoError(mt, err, "Hello error: %v", err) var writes []mongo.ClientBulkWrite num := hello.MaxWriteBatchSize + 1 for i := 0; i < num; i++ { writes = append(writes, mongo.ClientBulkWrite{ Database: "db", Collection: "coll", Model: &mongo.ClientInsertOneModel{ Document: bson.D{{"a", "b"}}, }, }) } result, err := mt.Client.BulkWrite(context.Background(), writes) require.NoError(mt, err, "BulkWrite error: %v", err) assert.Equal(mt, num, int(result.InsertedCount), "expected InsertedCount: %d, got %d", num, result.InsertedCount) require.Len(mt, opsCnt, 2, "expected %d bulkWrite commands, got: %d", 2, len(opsCnt)) assert.Equal(mt, num-1, opsCnt[0], "expected %d firstEvent.command.ops, got: %d", num-1, opsCnt[0]) assert.Equal(mt, 1, opsCnt[1], "expected %d secondEvent.command.ops, got: %d", 1, opsCnt[1]) }) mt.Run("4. MongoClient.bulkWrite batch splits when an ops payload exceeds maxMessageSizeBytes", func(mt *mtest.T) { var opsCnt []int monitor := &event.CommandMonitor{ Started: func(_ context.Context, e *event.CommandStartedEvent) { if e.CommandName == "bulkWrite" { var c struct { Ops []bson.D } err := bson.Unmarshal(e.Command, &c) require.NoError(mt, err) opsCnt = append(opsCnt, len(c.Ops)) } }, } mt.ResetClient(options.Client().SetMonitor(monitor)) var hello struct { MaxBsonObjectSize int MaxMessageSizeBytes int } err := mt.DB.RunCommand(context.Background(), bson.D{{"hello", 1}}).Decode(&hello) require.NoError(mt, err, "Hello error: %v", err) var writes []mongo.ClientBulkWrite num := hello.MaxMessageSizeBytes/hello.MaxBsonObjectSize + 1 for i := 0; i < num; i++ { writes = append(writes, mongo.ClientBulkWrite{ Database: "db", Collection: "coll", Model: &mongo.ClientInsertOneModel{ Document: bson.D{{"a", strings.Repeat("b", hello.MaxBsonObjectSize-500)}}, }, }) } result, err := mt.Client.BulkWrite(context.Background(), writes) require.NoError(mt, err, "BulkWrite error: %v", err) assert.Equal(mt, num, int(result.InsertedCount), "expected InsertedCount: %d, got: %d", num, result.InsertedCount) require.Len(mt, opsCnt, 2, "expected %d bulkWrite commands, got: %d", 2, len(opsCnt)) assert.Equal(mt, num-1, opsCnt[0], "expected %d firstEvent.command.ops, got: %d", num-1, opsCnt[0]) assert.Equal(mt, 1, opsCnt[1], "expected %d secondEvent.command.ops, got: %d", 1, opsCnt[1]) }) // TODO(GODRIVER-3328): FailPoints are not currently reliable on sharded // topologies. Allow running on sharded topologies once that is fixed. noShardedOpts := mtest.NewOptions().Topologies(mtest.Single, mtest.ReplicaSet, mtest.LoadBalanced) mt.RunOpts("5. MongoClient.bulkWrite collects WriteConcernErrors across batches", noShardedOpts, func(mt *mtest.T) { var eventCnt int monitor := &event.CommandMonitor{ Started: func(_ context.Context, e *event.CommandStartedEvent) { if e.CommandName == "bulkWrite" { eventCnt++ } }, } mt.ResetClient(options.Client().SetRetryWrites(false).SetMonitor(monitor)) var hello struct { MaxWriteBatchSize int } err := mt.DB.RunCommand(context.Background(), bson.D{{"hello", 1}}).Decode(&hello) require.NoError(mt, err, "Hello error: %v", err) mt.SetFailPoint(failpoint.FailPoint{ ConfigureFailPoint: "failCommand", Mode: failpoint.Mode{ Times: 2, }, Data: failpoint.Data{ FailCommands: []string{"bulkWrite"}, WriteConcernError: &failpoint.WriteConcernError{ Code: 91, Errmsg: "Replication is being shut down", }, }, }) var writes []mongo.ClientBulkWrite num := hello.MaxWriteBatchSize + 1 for i := 0; i < num; i++ { writes = append(writes, mongo.ClientBulkWrite{ Database: "db", Collection: "coll", Model: &mongo.ClientInsertOneModel{ Document: bson.D{{"a", "b"}}, }, }) } _, err = mt.Client.BulkWrite(context.Background(), writes) require.Error(mt, err, "expected a BulkWrite error") bwe, ok := err.(mongo.ClientBulkWriteException) require.True(mt, ok, "expected a BulkWriteException, got %T: %v", err, err) assert.Len(mt, bwe.WriteConcernErrors, 2, "expected %d writeConcernErrors, got: %d", 2, len(bwe.WriteConcernErrors)) require.NotNil(mt, bwe.PartialResult) assert.Equal(mt, num, int(bwe.PartialResult.InsertedCount), "expected InsertedCount: %d, got: %d", num, bwe.PartialResult.InsertedCount) require.Equal(mt, 2, eventCnt, "expected %d bulkWrite commands, got: %d", 2, eventCnt) }) mt.Run("6. MongoClient.bulkWrite handles individual WriteErrors across batches", func(mt *mtest.T) { var eventCnt int monitor := &event.CommandMonitor{ Started: func(_ context.Context, e *event.CommandStartedEvent) { if e.CommandName == "bulkWrite" { eventCnt++ } }, } mt.ResetClient(options.Client()) var hello struct { MaxWriteBatchSize int } err := mt.DB.RunCommand(context.Background(), bson.D{{"hello", 1}}).Decode(&hello) require.NoError(mt, err, "Hello error: %v", err) coll := mt.CreateCollection(mtest.Collection{DB: "db", Name: "coll"}, false) err = coll.Drop(context.Background()) require.NoError(mt, err, "Drop error: %v", err) _, err = coll.InsertOne(context.Background(), bson.D{{"_id", 1}}) require.NoError(mt, err, "InsertOne error: %v", err) var writes []mongo.ClientBulkWrite numModels := hello.MaxWriteBatchSize + 1 for i := 0; i < numModels; i++ { writes = append(writes, mongo.ClientBulkWrite{ Database: "db", Collection: "coll", Model: &mongo.ClientInsertOneModel{ Document: bson.D{{"_id", 1}}, }, }) } mt.Run("unordered", func(mt *mtest.T) { eventCnt = 0 mt.ResetClient(options.Client().SetMonitor(monitor)) _, err := mt.Client.BulkWrite(context.Background(), writes, options.ClientBulkWrite().SetOrdered(false)) require.Error(mt, err, "expected a BulkWrite error") bwe, ok := err.(mongo.ClientBulkWriteException) require.True(mt, ok, "expected a BulkWriteException, got %T: %v", err, err) assert.Len(mt, bwe.WriteErrors, numModels, "expected %d writeErrors, got %d", numModels, len(bwe.WriteErrors)) require.Equal(mt, 2, eventCnt, "expected %d bulkWrite commands, got: %d", 2, eventCnt) }) mt.Run("ordered", func(mt *mtest.T) { eventCnt = 0 mt.ResetClient(options.Client().SetMonitor(monitor)) _, err := mt.Client.BulkWrite(context.Background(), writes, options.ClientBulkWrite().SetOrdered(true)) require.Error(mt, err, "expected a BulkWrite error") bwe, ok := err.(mongo.ClientBulkWriteException) require.True(mt, ok, "expected a BulkWriteException, got %T: %v", err, err) assert.Len(mt, bwe.WriteErrors, 1, "expected %d writeErrors, got: %d", 1, len(bwe.WriteErrors)) require.Equal(mt, 1, eventCnt, "expected %d bulkWrite commands, got: %d", 1, eventCnt) }) }) mt.Run("7. MongoClient.bulkWrite handles a cursor requiring a getMore", func(mt *mtest.T) { var getMoreCalled int monitor := &event.CommandMonitor{ Started: func(_ context.Context, e *event.CommandStartedEvent) { if e.CommandName == "getMore" { getMoreCalled++ } }, } mt.ResetClient(options.Client().SetMonitor(monitor)) var hello struct { MaxBsonObjectSize int } err := mt.DB.RunCommand(context.Background(), bson.D{{"hello", 1}}).Decode(&hello) require.NoError(mt, err, "Hello error: %v", err) coll := mt.CreateCollection(mtest.Collection{DB: "db", Name: "coll"}, false) err = coll.Drop(context.Background()) require.NoError(mt, err, "Drop error: %v", err) upsert := true models := []mongo.ClientBulkWrite{ { Database: "db", Collection: "coll", Model: &mongo.ClientUpdateOneModel{ Filter: bson.D{{"_id", strings.Repeat("a", hello.MaxBsonObjectSize/2)}}, Update: bson.D{{"$set", bson.D{{"x", 1}}}}, Upsert: &upsert, }, }, { Database: "db", Collection: "coll", Model: &mongo.ClientUpdateOneModel{ Filter: bson.D{{"_id", strings.Repeat("b", hello.MaxBsonObjectSize/2)}}, Update: bson.D{{"$set", bson.D{{"x", 1}}}}, Upsert: &upsert, }, }, } result, err := mt.Client.BulkWrite(context.Background(), models, options.ClientBulkWrite().SetVerboseResults(true)) require.NoError(mt, err, "BulkWrite error: %v", err) assert.Equal(mt, int64(2), result.UpsertedCount, "expected InsertedCount: %d, got: %d", 2, result.UpsertedCount) assert.Len(mt, result.UpdateResults, 2, "expected %d UpdateResults, got: %d", 2, len(result.UpdateResults)) assert.Equal(mt, 1, getMoreCalled, "expected %d getMore call, got: %d", 1, getMoreCalled) }) mt.RunOpts("8. MongoClient.bulkWrite handles a cursor requiring getMore within a transaction", mtest.NewOptions().MinServerVersion("8.0").ClientType(mtest.Pinned). Topologies(mtest.ReplicaSet, mtest.Sharded, mtest.LoadBalanced, mtest.ShardedReplicaSet), func(mt *mtest.T) { var getMoreCalled int monitor := &event.CommandMonitor{ Started: func(_ context.Context, e *event.CommandStartedEvent) { if e.CommandName == "getMore" { getMoreCalled++ } }, } mt.ResetClient(options.Client().SetMonitor(monitor)) var hello struct { MaxBsonObjectSize int } err := mt.DB.RunCommand(context.Background(), bson.D{{"hello", 1}}).Decode(&hello) require.NoError(mt, err, "Hello error: %v", err) coll := mt.CreateCollection(mtest.Collection{DB: "db", Name: "coll"}, false) err = coll.Drop(context.Background()) require.NoError(mt, err, "Drop error: %v", err) session, err := mt.Client.StartSession() require.NoError(mt, err, "StartSession error: %v", err) defer session.EndSession(context.Background()) upsert := true models := []mongo.ClientBulkWrite{ { Database: "db", Collection: "coll", Model: &mongo.ClientUpdateOneModel{ Filter: bson.D{{"_id", strings.Repeat("a", hello.MaxBsonObjectSize/2)}}, Update: bson.D{{"$set", bson.D{{"x", 1}}}}, Upsert: &upsert, }, }, { Database: "db", Collection: "coll", Model: &mongo.ClientUpdateOneModel{ Filter: bson.D{{"_id", strings.Repeat("b", hello.MaxBsonObjectSize/2)}}, Update: bson.D{{"$set", bson.D{{"x", 1}}}}, Upsert: &upsert, }, }, } result, err := session.WithTransaction(context.Background(), func(ctx context.Context) (any, error) { return mt.Client.BulkWrite(ctx, models, options.ClientBulkWrite().SetVerboseResults(true)) }) require.NoError(mt, err, "BulkWrite error: %v", err) cbwResult, ok := result.(*mongo.ClientBulkWriteResult) require.True(mt, ok, "expected a ClientBulkWriteResult, got %T", result) assert.Equal(mt, int64(2), cbwResult.UpsertedCount, "expected InsertedCount: %d, got: %d", 2, cbwResult.UpsertedCount) assert.Len(mt, cbwResult.UpdateResults, 2, "expected %d UpdateResults, got: %d", 2, len(cbwResult.UpdateResults)) assert.Equal(mt, 1, getMoreCalled, "expected %d getMore call, got: %d", 1, getMoreCalled) }) // TODO(GODRIVER-3328): FailPoints are not currently reliable on sharded // topologies. Allow running on sharded topologies once that is fixed. mt.RunOpts("9. MongoClient.bulkWrite handles a getMore error", noShardedOpts, func(mt *mtest.T) { var getMoreCalled int var killCursorsCalled int monitor := &event.CommandMonitor{ Started: func(_ context.Context, e *event.CommandStartedEvent) { switch e.CommandName { case "getMore": getMoreCalled++ case "killCursors": killCursorsCalled++ } }, } mt.ResetClient(options.Client().SetMonitor(monitor)) var hello struct { MaxBsonObjectSize int } err := mt.DB.RunCommand(context.Background(), bson.D{{"hello", 1}}).Decode(&hello) require.NoError(mt, err, "Hello error: %v", err) mt.SetFailPoint(failpoint.FailPoint{ ConfigureFailPoint: "failCommand", Mode: failpoint.Mode{ Times: 1, }, Data: failpoint.Data{ FailCommands: []string{"getMore"}, ErrorCode: 8, }, }) coll := mt.CreateCollection(mtest.Collection{DB: "db", Name: "coll"}, false) err = coll.Drop(context.Background()) require.NoError(mt, err, "Drop error: %v", err) upsert := true models := []mongo.ClientBulkWrite{ { Database: "db", Collection: "coll", Model: &mongo.ClientUpdateOneModel{ Filter: bson.D{{"_id", strings.Repeat("a", hello.MaxBsonObjectSize/2)}}, Update: bson.D{{"$set", bson.D{{"x", 1}}}}, Upsert: &upsert, }, }, { Database: "db", Collection: "coll", Model: &mongo.ClientUpdateOneModel{ Filter: bson.D{{"_id", strings.Repeat("b", hello.MaxBsonObjectSize/2)}}, Update: bson.D{{"$set", bson.D{{"x", 1}}}}, Upsert: &upsert, }, }, } _, err = mt.Client.BulkWrite(context.Background(), models, options.ClientBulkWrite().SetVerboseResults(true)) assert.Error(mt, err, "expected a BulkWrite error") bwe, ok := err.(mongo.ClientBulkWriteException) require.True(mt, ok, "expected a BulkWriteException, got %T: %v", err, err) require.NotNil(mt, bwe.WriteError) assert.Equal(mt, 8, bwe.WriteError.Code, "expected top level error code: %d, got; %d", 8, bwe.WriteError.Code) require.NotNil(mt, bwe.PartialResult) assert.Equal(mt, int64(2), bwe.PartialResult.UpsertedCount, "expected UpsertedCount: %d, got: %d", 2, bwe.PartialResult.UpsertedCount) assert.Len(mt, bwe.PartialResult.UpdateResults, 1, "expected %d UpdateResults, got: %d", 1, len(bwe.PartialResult.UpdateResults)) assert.Equal(mt, 1, getMoreCalled, "expected %d getMore call, got: %d", 1, getMoreCalled) assert.Equal(mt, 1, killCursorsCalled, "expected %d killCursors call, got: %d", 1, killCursorsCalled) }) mt.Run("11. MongoClient.bulkWrite batch splits when the addition of a new namespace exceeds the maximum message size", func(mt *mtest.T) { type cmd struct { Ops []bson.D NsInfo []struct { Ns string } } var bwCmd []cmd monitor := &event.CommandMonitor{ Started: func(_ context.Context, e *event.CommandStartedEvent) { if e.CommandName == "bulkWrite" { var c cmd err := bson.Unmarshal(e.Command, &c) require.NoError(mt, err, "Unmarshal error: %v", err) bwCmd = append(bwCmd, c) } }, } mt.ResetClient(options.Client()) var hello struct { MaxBsonObjectSize int MaxMessageSizeBytes int } err := mt.DB.RunCommand(context.Background(), bson.D{{"hello", 1}}).Decode(&hello) require.NoError(mt, err, "Hello error: %v", err) newWrites := func() (int, []mongo.ClientBulkWrite) { maxBsonObjectSize := hello.MaxBsonObjectSize opsBytes := hello.MaxMessageSizeBytes - 1122 num := opsBytes / maxBsonObjectSize var writes []mongo.ClientBulkWrite for i := 0; i < num; i++ { writes = append(writes, mongo.ClientBulkWrite{ Database: "db", Collection: "coll", Model: &mongo.ClientInsertOneModel{ Document: bson.D{{"a", strings.Repeat("b", maxBsonObjectSize-57)}}, }, }) } if remainderBytes := opsBytes % maxBsonObjectSize; remainderBytes > 217 { num++ writes = append(writes, mongo.ClientBulkWrite{ Database: "db", Collection: "coll", Model: &mongo.ClientInsertOneModel{ Document: bson.D{{"a", strings.Repeat("b", remainderBytes-57)}}, }, }) } return num, writes } mt.Run("Case 1: No batch-splitting required", func(mt *mtest.T) { bwCmd = bwCmd[:0] mt.ResetClient(options.Client().SetMonitor(monitor)) num, writes := newWrites() writes = append(writes, mongo.ClientBulkWrite{ Database: "db", Collection: "coll", Model: &mongo.ClientInsertOneModel{ Document: bson.D{{"a", "b"}}, }, }) result, err := mt.Client.BulkWrite(context.Background(), writes) require.NoError(mt, err, "BulkWrite error: %v", err) assert.Equal(mt, num+1, int(result.InsertedCount), "expected insertedCound: %d, got: %d", num+1, result.InsertedCount) require.Len(mt, bwCmd, 1, "expected %d bulkWrite call, got: %d", 1, len(bwCmd)) assert.Len(mt, bwCmd[0].Ops, num+1, "expected %d ops, got: %d", num+1, len(bwCmd[0].Ops)) require.Len(mt, bwCmd[0].NsInfo, 1, "expected %d nsInfo, got: %d", 1, len(bwCmd[0].NsInfo)) assert.Equal(mt, "db.coll", bwCmd[0].NsInfo[0].Ns, "expected namespace: %s, got: %s", "db.coll", bwCmd[0].NsInfo[0].Ns) }) mt.Run("Case 2: Batch-splitting required", func(mt *mtest.T) { bwCmd = bwCmd[:0] mt.ResetClient(options.Client().SetMonitor(monitor)) coll := strings.Repeat("c", 200) num, writes := newWrites() writes = append(writes, mongo.ClientBulkWrite{ Database: "db", Collection: coll, Model: &mongo.ClientInsertOneModel{ Document: bson.D{{"a", "b"}}, }, }) result, err := mt.Client.BulkWrite(context.Background(), writes) require.NoError(mt, err, "BulkWrite error: %v", err) assert.Equal(mt, num+1, int(result.InsertedCount), "expected insertedCound: %d, got: %d", num+1, result.InsertedCount) require.Len(mt, bwCmd, 2, "expected %d bulkWrite calls, got: %d", 2, len(bwCmd)) assert.Len(mt, bwCmd[0].Ops, num, "expected %d ops, got: %d", num, len(bwCmd[0].Ops)) require.Len(mt, bwCmd[0].NsInfo, 1, "expected %d nsInfo, got: %d", 1, len(bwCmd[0].NsInfo)) assert.Equal(mt, "db.coll", bwCmd[0].NsInfo[0].Ns, "expected namespace: %s, got: %s", "db.coll", bwCmd[0].NsInfo[0].Ns) assert.Len(mt, bwCmd[1].Ops, 1, "expected %d ops, got: %d", 1, len(bwCmd[1].Ops)) require.Len(mt, bwCmd[1].NsInfo, 1, "expected %d nsInfo, got: %d", 1, len(bwCmd[1].NsInfo)) assert.Equal(mt, "db."+coll, bwCmd[1].NsInfo[0].Ns, "expected namespace: %s, got: %s", "db."+coll, bwCmd[1].NsInfo[0].Ns) }) }) mt.Run("12. MongoClient.bulkWrite returns an error if no operations can be added to ops", func(mt *mtest.T) { mt.ResetClient(options.Client()) var hello struct { MaxMessageSizeBytes int } err := mt.DB.RunCommand(context.Background(), bson.D{{"hello", 1}}).Decode(&hello) require.NoError(mt, err, "Hello error: %v", err) mt.Run("Case 1: document too large", func(mt *mtest.T) { writes := []mongo.ClientBulkWrite{{ Database: "db", Collection: "coll", Model: &mongo.ClientInsertOneModel{ Document: bson.D{{"a", strings.Repeat("b", hello.MaxMessageSizeBytes)}}, }, }} _, err := mt.Client.BulkWrite(context.Background(), writes) require.EqualError(mt, err, driver.ErrDocumentTooLarge.Error()) var cbwe mongo.ClientBulkWriteException if errors.As(err, &cbwe) { assert.Nil(mt, cbwe.PartialResult, "expected nil PartialResult in ClientBulkWriteException") } }) mt.Run("Case 2: namespace too large", func(mt *mtest.T) { writes := []mongo.ClientBulkWrite{{ Database: "db", Collection: strings.Repeat("c", hello.MaxMessageSizeBytes), Model: &mongo.ClientInsertOneModel{ Document: bson.D{{"a", "b"}}, }, }} _, err := mt.Client.BulkWrite(context.Background(), writes) require.EqualError(mt, err, driver.ErrDocumentTooLarge.Error()) var cbwe mongo.ClientBulkWriteException if errors.As(err, &cbwe) { assert.Nil(mt, cbwe.PartialResult, "expected nil PartialResult in ClientBulkWriteException") } }) }) mt.Run("13. MongoClient.bulkWrite returns an error if auto-encryption is configured", func(mt *mtest.T) { if !mtest.IsCSFLEEnabled() { mt.Skip("CSFLE is not enabled") } if os.Getenv("DOCKER_RUNNING") != "" { mt.Skip("skipping test in docker environment") } autoEncryptionOpts := options.AutoEncryption(). SetKeyVaultNamespace("db.coll"). SetKmsProviders(map[string]map[string]any{ "aws": { "accessKeyId": "foo", "secretAccessKey": "bar", }, }) mt.ResetClient(options.Client().SetAutoEncryptionOptions(autoEncryptionOpts)) writes := []mongo.ClientBulkWrite{{ Database: "db", Collection: "coll", Model: &mongo.ClientInsertOneModel{ Document: bson.D{{"a", "b"}}, }, }} _, err := mt.Client.BulkWrite(context.Background(), writes) require.ErrorContains(mt, err, "bulkWrite does not currently support automatic encryption") var cbwe mongo.ClientBulkWriteException if errors.As(err, &cbwe) { assert.Nil(mt, cbwe.PartialResult, "expected nil PartialResult in ClientBulkWriteException") } }) mt.Run("15. MongoClient.bulkWrite with unacknowledged write concern uses w:0 for all batches", func(mt *mtest.T) { type cmd struct { Ops []bson.D WriteConcern struct { W any } } var bwCmd []cmd monitor := &event.CommandMonitor{ Started: func(_ context.Context, e *event.CommandStartedEvent) { if e.CommandName == "bulkWrite" { var c cmd err := bson.Unmarshal(e.Command, &c) require.NoError(mt, err, "Unmarshal error: %v", err) bwCmd = append(bwCmd, c) } }, } mt.ResetClient(options.Client().SetMonitor(monitor)) var hello struct { MaxBsonObjectSize int MaxMessageSizeBytes int } err := mt.DB.RunCommand(context.Background(), bson.D{{"hello", 1}}).Decode(&hello) require.NoError(mt, err, "Hello error: %v", err) coll := mt.CreateCollection(mtest.Collection{DB: "db", Name: "coll"}, false) err = coll.Drop(context.Background()) require.NoError(mt, err, "Drop error: %v", err) num := hello.MaxMessageSizeBytes/hello.MaxBsonObjectSize + 1 var writes []mongo.ClientBulkWrite for i := 0; i < num; i++ { writes = append(writes, mongo.ClientBulkWrite{ Database: "db", Collection: "coll", Model: &mongo.ClientInsertOneModel{ Document: bson.D{{"a", strings.Repeat("b", hello.MaxBsonObjectSize-500)}}, }, }) } result, err := mt.Client.BulkWrite(context.Background(), writes, options.ClientBulkWrite().SetOrdered(false).SetWriteConcern(writeconcern.Unacknowledged())) require.NoError(mt, err, "BulkWrite error: %v", err) assert.False(mt, result.Acknowledged) require.Len(mt, bwCmd, 2, "expected %d bulkWrite calls, got: %d", 2, len(bwCmd)) assert.Len(mt, bwCmd[0].Ops, num-1, "expected %d ops, got: %d", num-1, len(bwCmd[0].Ops)) assert.Equal(mt, int32(0), bwCmd[0].WriteConcern.W, "expected writeConcern: %d, got: %v", 0, bwCmd[0].WriteConcern.W) assert.Len(mt, bwCmd[1].Ops, 1, "expected %d ops, got: %d", 1, len(bwCmd[1].Ops)) assert.Equal(mt, int32(0), bwCmd[1].WriteConcern.W, "expected writeConcern: %d, got: %v", 0, bwCmd[1].WriteConcern.W) n, err := coll.CountDocuments(context.Background(), bson.D{}) require.NoError(mt, err, "CountDocuments error: %v", err) assert.Equal(mt, num, int(n), "expected %d documents, got: %d", num, n) }) }