Context
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
- Top-level error is reported as ClientBulkWriteException.WriteError when results or write errors were observed.
- A clean top-level failure still returns mongo.CommandError.
Pitfalls
Not investigated
- is depended on by
-
GODRIVER-3954 Implement the ServerError interface for ClientBulkWriteException
-
- Blocked
-
- related to
-
DRIVERS-3670 Add test coverage for top-level error handling in multi-batch bulkWrite
-
- In Review
-