-
Type:
Bug
-
Resolution: Unresolved
-
Priority:
Major - P3
-
None
-
Affects Version/s: None
-
Component/s: CRUD
-
None
-
None
-
Go Drivers
-
None
-
None
-
None
-
None
-
None
-
None
Detailed steps to reproduce the problem?
The CRUD Bulk Write spec requires:
When a top-level error is encountered and individual results and/or errors have already been observed, drivers MUST embed the top-level error within a BulkWriteException as the error field to retain this information.
clientBulkWrite.execute does not do this for command errors ({ ok: 0 }). mongo/client_bulk_write.go:98-107 matches err against mongo.CommandError. But err comes straight from driver.Operation.Execute, which returns driver.Error. The conversion to mongo.CommandError happens later in wrapErrors at mongo/client.go:1080. The branch therefore never fires for a top-level { ok: 0 } reply.
failures:
1. Top-level error silently discarded when write errors are observed. With batches.writeErrors / writeConcernErrors non-empty, execute returns an exception at L126 and drops err entirely. The exception's WriteError stays nil, and the top-level failure is not reported.
2. Partial results are discarded when only results are observed. With successful writes but no write errors, no exception is built at all, as L108 tests only the error slices, so execute falls through to return err at L128. The caller gets a bare CommandError and the observed PartialResult is lost.
3. killCursors response is also ignored. mongo/client_bulk_write.go:501 defers cursor.Close(ctx), discarding Close's error. Prose test 9 asserts only that killCursors is sent.
Tests generated by LLM:
func TestGODRIVER_TopLevelErrorIsDropped(t *testing.T) { bigDoc := bson.D{{Key: "a", Value: strings.Repeat("x", 12_000_000)}} cursorReply := func(entries ...bson.D) bson.E { return bson.E{Key: "cursor", Value: bson.D{ {Key: "id", Value: int64(0)}, {Key: "ns", Value: "admin.$cmd.bulkWrite"}, {Key: "firstBatch", Value: func() bson.A { a := bson.A{} for _, e := range entries { a = append(a, e) } return a }()}, }} } // First batch: one write fails with a duplicate key error. writeErrBatch := bson.D{ {Key: "ok", Value: 1}, {Key: "nErrors", Value: 1}, cursorReply(bson.D{ {Key: "ok", Value: 0}, {Key: "idx", Value: int32(0)}, {Key: "code", Value: int32(11000)}, {Key: "errmsg", Value: "duplicate key"}, {Key: "n", Value: int32(0)}, }), } // First batch: every write succeeds. okBatch := bson.D{ {Key: "ok", Value: 1}, {Key: "nErrors", Value: 0}, {Key: "nInserted", Value: 3}, cursorReply( bson.D{{Key: "ok", Value: 1}, {Key: "idx", Value: int32(0)}, {Key: "n", Value: int32(1)}}, bson.D{{Key: "ok", Value: 1}, {Key: "idx", Value: int32(1)}, {Key: "n", Value: int32(1)}}, bson.D{{Key: "ok", Value: 1}, {Key: "idx", Value: int32(2)}, {Key: "n", Value: int32(1)}}, ), } // Second batch: a top-level command error. topLevelErr := bson.D{ {Key: "ok", Value: 0}, {Key: "code", Value: 189}, {Key: "errmsg", Value: "PrimarySteppedDown"}, } run := func(t *testing.T, replies ...bson.D) ClientBulkWriteException { t.Helper() client, err := newClient() require.NoError(t, err) client.deployment = drivertest.NewMockDeployment(replies...) ordered := false bw := &clientBulkWrite{client: client, ordered: &ordered} bw.result.Acknowledged = true // Client.BulkWrite sets this. for i := 0; i < 4; i++ { bw.writePairs = append(bw.writePairs, clientBulkWritePair{ "db.coll", &ClientInsertOneModel{Document: bigDoc}, }) } var bwe ClientBulkWriteException err = wrapErrors(bw.execute(context.Background())) require.True(t, errors.As(err, &bwe), "expected a ClientBulkWriteException, got %T: %v", err, err) return bwe } // Gap 1: write errors were observed, so the top-level error MUST be // embedded. On master it is discarded entirely. t.Run("top-level error after write errors", func(t *testing.T) { bwe := run(t, writeErrBatch, topLevelErr) assert.Len(t, bwe.WriteErrors, 1, "expected the observed write error") require.NotNil(t, bwe.WriteError, "top-level error was dropped") assert.Equal(t, 189, bwe.WriteError.Code) assert.NotEmpty(t, bwe.WriteError.Raw, "expected the raw server reply") }) // Gap 2: results were observed, so the top-level error MUST be embedded // and the partial result retained. On master no exception is built at all. t.Run("top-level error after results", func(t *testing.T) { bwe := run(t, okBatch, topLevelErr) require.NotNil(t, bwe.WriteError, "top-level error was dropped") assert.Equal(t, 189, bwe.WriteError.Code) require.NotNil(t, bwe.PartialResult, "partial result was dropped") assert.Equal(t, int64(3), bwe.PartialResult.InsertedCount) }) } func TestGODRIVER_KillCursorsResultIgnored(t *testing.T) { // TODO(GODRIVER-3328): FailPoints are not reliable on sharded topologies. mtOpts := mtest.NewOptions().MinServerVersion("8.0").ClientType(mtest.Pinned). Topologies(mtest.Single, mtest.ReplicaSet, mtest.LoadBalanced) mt := mtest.New(t, mtOpts) mt.Run("killCursors failure is not surfaced", func(mt *mtest.T) { const failpointMsg = "Failing command via 'failCommand' failpoint" var failedCmds []string monitor := &event.CommandMonitor{ Failed: func(_ context.Context, e *event.CommandFailedEvent) { failedCmds = append(failedCmds, e.CommandName) }, } 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) // Fail the getMore, producing a top-level error while the cursor is // unexhausted, and then fail the killCursors sent to clean it up. mt.SetFailPoint(failpoint.FailPoint{ ConfigureFailPoint: "failCommand", Mode: failpoint.Mode{Times: 2}, Data: failpoint.Data{ FailCommands: []string{"getMore", "killCursors"}, 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) // Two upserts with _ids large enough that the results cursor requires // a getMore, as in prose test 9. upsert := true var models []mongo.ClientBulkWrite for _, c := range []string{"a", "b"} { models = append(models, mongo.ClientBulkWrite{ Database: "db", Collection: "coll", Model: &mongo.ClientUpdateOneModel{ Filter: bson.D{{"_id", strings.Repeat(c, hello.MaxBsonObjectSize/2)}}, Update: bson.D{{"$set", bson.D{{"x", 1}}}}, Upsert: &upsert, }, }) } _, err = mt.Client.BulkWrite(context.Background(), models, options.ClientBulkWrite().SetVerboseResults(true)) require.Error(mt, err, "expected a BulkWrite error") // Precondition: both commands actually failed on the server. assert.Equal(mt, []string{"getMore", "killCursors"}, failedCmds, "expected getMore and killCursors to fail") // Both failures carry the same failpoint message, so a compliant // driver would report it twice. Today only the getMore failure is // reported and the killCursors result is silently dropped. assert.Equal(mt, 2, strings.Count(err.Error(), failpointMsg), "killCursors result was ignored; got: %v", err) }) }
Definition of done: what must be done to consider the task complete?
- Top-level error is reported as ClientBulkWriteException.WriteError when results or write errors were observed.
- A clean top-level failure still returns mongo.CommandError.
The exact Go version used, with patch level:
$ go version
The exact version of the Go driver used:
$ go list -m go.mongodb.org/mongo-driver
v2.0+
Describe how MongoDB is set up. Local vs Hosted, version, topology, load balanced, etc.
The operating system and version (e.g. Windows 7, OSX 10.8, ...)
Security Vulnerabilities
If you’ve identified a security vulnerability in a driver or any other MongoDB project, please report it according to the instructions here