Skip to content

Commit 15ca7a0

Browse files
harmvclaudematthewdale
authored
GODRIVER-4096 Fix panic in Collection.insert on out-of-order write errors (#2557)
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: Matt Dale <9760375+matthewdale@users.noreply.github.com>
1 parent 325f1ac commit 15ca7a0

3 files changed

Lines changed: 205 additions & 10 deletions

File tree

internal/integration/collection_test.go

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,96 @@ func TestCollection(t *testing.T) {
257257
})
258258
}
259259
})
260+
// The server typically returns write errors for an unordered insert in
261+
// ascending "index" order. However, in rare cases, the server may
262+
// return write errors indexes not in ascending order. Use a mock
263+
// deployment to create server responses where the write error indexes
264+
// are not in ascending order.
265+
//
266+
// See GODRIVER-4096 for more background.
267+
mt.RunOpts("out-of-order write errors", mtest.NewOptions().ClientType(mtest.Mock), func(mt *mtest.T) {
268+
testCases := []struct {
269+
name string
270+
docs []any
271+
response bson.D
272+
wantErrs int
273+
wantInserts []any
274+
}{
275+
{
276+
// Before the fix, this response caused a panic. Note the
277+
// "writeErrors.index" values are not in ascending order.
278+
name: "all documents fail",
279+
docs: []any{
280+
bson.D{{"_id", int32(0)}},
281+
bson.D{{"_id", int32(1)}},
282+
},
283+
response: bson.D{
284+
{"ok", 1},
285+
{"n", 0},
286+
{"writeErrors", bson.A{
287+
bson.D{{"index", 1}, {"code", errorDuplicateKey}, {"errmsg", "duplicate key error"}},
288+
bson.D{{"index", 0}, {"code", errorDuplicateKey}, {"errmsg", "duplicate key error"}},
289+
}},
290+
},
291+
wantErrs: 2,
292+
wantInserts: []any{},
293+
},
294+
{
295+
// Before the fix, this removed the wrong IDs from the
296+
// result without panicking. Note the "writeErrors.index"
297+
// values are not in ascending order.
298+
name: "some documents fail",
299+
docs: []any{
300+
bson.D{{"_id", int32(0)}},
301+
bson.D{{"_id", int32(1)}},
302+
bson.D{{"_id", int32(2)}},
303+
bson.D{{"_id", int32(3)}},
304+
bson.D{{"_id", int32(4)}},
305+
},
306+
response: bson.D{
307+
{"ok", 1},
308+
{"n", 3},
309+
{"writeErrors", bson.A{
310+
bson.D{{"index", 3}, {"code", errorDuplicateKey}, {"errmsg", "duplicate key error"}},
311+
bson.D{{"index", 1}, {"code", errorDuplicateKey}, {"errmsg", "duplicate key error"}},
312+
}},
313+
},
314+
wantErrs: 2,
315+
wantInserts: []any{int32(0), int32(2), int32(4)},
316+
},
317+
}
318+
319+
for _, tc := range testCases {
320+
mt.Run(tc.name, func(mt *mtest.T) {
321+
mt.AddMockResponses(tc.response)
322+
323+
res, err := mt.Coll.InsertMany(
324+
context.Background(),
325+
tc.docs,
326+
options.InsertMany().SetOrdered(false),
327+
)
328+
329+
var bwe mongo.BulkWriteException
330+
require.True(
331+
mt,
332+
errors.As(err, &bwe),
333+
"expected error to be a mongo.BulkWriteException, got %#v",
334+
err,
335+
)
336+
assert.Len(
337+
mt,
338+
bwe.WriteErrors,
339+
tc.wantErrs,
340+
"expected %v write errors, got %v",
341+
tc.wantErrs,
342+
len(bwe.WriteErrors),
343+
)
344+
345+
require.NotNil(mt, res, "expected a non-nil result")
346+
assert.Equal(mt, tc.wantInserts, res.InsertedIDs, "expected inserted IDs to match")
347+
})
348+
}
349+
})
260350
mt.Run("writeError index", func(mt *mtest.T) {
261351
mt.Parallel()
262352

mongo/collection.go

Lines changed: 40 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -367,19 +367,49 @@ func (coll *Collection) insert(
367367
return result, err
368368
}
369369

370-
// remove the ids that had writeErrors from result
371-
for i, we := range wce.WriteErrors {
372-
// i indexes have been removed before the current error, so the index is we.Index-i
373-
idIndex := int(we.Index) - i
374-
// if the insert is ordered, nothing after the error was inserted
375-
if args.Ordered == nil || *args.Ordered {
376-
result = result[:idIndex]
377-
break
370+
ordered := args.Ordered == nil || *args.Ordered
371+
result = keepInsertedIDs(result, wce.WriteErrors, ordered)
372+
373+
return result, err
374+
}
375+
376+
// keepInsertedIDs returns the subset of result whose corresponding documents
377+
// were not rejected by the server. Each WriteError.Index refers to the
378+
// positional index of the document in the original insert request, which is
379+
// also the index space of result.
380+
//
381+
// writeErrors is not required to be sorted by Index: the server does not
382+
// guarantee that write errors for an unordered bulk write are returned in
383+
// ascending Index order, and assuming otherwise previously caused
384+
// out-of-range slice panics (GODRIVER-4096). Neither result nor writeErrors
385+
// is modified.
386+
func keepInsertedIDs(result []any, writeErrors driver.WriteErrors, ordered bool) []any {
387+
// If there are no write errors, then every document was inserted.
388+
if len(writeErrors) == 0 {
389+
return result
390+
}
391+
392+
failed := make(map[int]bool, len(writeErrors))
393+
for _, writeError := range writeErrors {
394+
failed[int(writeError.Index)] = true
395+
}
396+
397+
kept := result[:0:0]
398+
for idx, id := range result {
399+
if failed[idx] {
400+
// The server stops an ordered insert at the first document that
401+
// fails, so nothing at or after that index was inserted.
402+
if ordered {
403+
break
404+
}
405+
406+
continue
378407
}
379-
result = append(result[:idIndex], result[idIndex+1:]...)
408+
409+
kept = append(kept, id)
380410
}
381411

382-
return result, err
412+
return kept
383413
}
384414

385415
// InsertOne executes an insert command to insert a single document into the collection.

mongo/collection_test.go

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"go.mongodb.org/mongo-driver/v2/mongo/readpref"
2222
"go.mongodb.org/mongo-driver/v2/mongo/writeconcern"
2323
"go.mongodb.org/mongo-driver/v2/x/bsonx/bsoncore"
24+
"go.mongodb.org/mongo-driver/v2/x/mongo/driver"
2425
"go.mongodb.org/mongo-driver/v2/x/mongo/driver/topology"
2526
)
2627

@@ -307,3 +308,77 @@ func TestNewFindArgsFromFindOneArgs(t *testing.T) {
307308
})
308309
}
309310
}
311+
312+
func TestKeepInsertedIDs(t *testing.T) {
313+
newResult := func(n int) []any {
314+
result := make([]any, n)
315+
for i := range result {
316+
result[i] = i
317+
}
318+
return result
319+
}
320+
321+
tests := []struct {
322+
name string
323+
result []any
324+
writeErrors driver.WriteErrors
325+
ordered bool
326+
want []any
327+
}{
328+
{
329+
name: "no write errors",
330+
result: newResult(3),
331+
ordered: false,
332+
want: newResult(3),
333+
},
334+
{
335+
name: "unordered, ascending write errors",
336+
result: newResult(5),
337+
writeErrors: driver.WriteErrors{{Index: 1}, {Index: 3}},
338+
ordered: false,
339+
want: []any{0, 2, 4},
340+
},
341+
{
342+
// The server does not guarantee ascending Index order for an
343+
// unordered bulk write. Before the fix, this silently removed
344+
// the wrong document.
345+
name: "unordered, out-of-order write errors",
346+
result: newResult(5),
347+
writeErrors: driver.WriteErrors{{Index: 3}, {Index: 1}},
348+
ordered: false,
349+
want: []any{0, 2, 4},
350+
},
351+
{
352+
// Regression test for GODRIVER-4096: before the fix, this
353+
// input caused "slice bounds out of range [:-1]".
354+
name: "unordered, all documents fail, out of order",
355+
result: newResult(2),
356+
writeErrors: driver.WriteErrors{{Index: 1}, {Index: 0}},
357+
ordered: false,
358+
want: []any{},
359+
},
360+
{
361+
name: "ordered, single write error",
362+
result: newResult(5),
363+
writeErrors: driver.WriteErrors{{Index: 2}},
364+
ordered: true,
365+
want: []any{0, 1},
366+
},
367+
{
368+
// Sanity check for the ordered path: stop at the earliest
369+
// failed index, not at whichever error is listed first.
370+
name: "ordered, out-of-order write errors",
371+
result: newResult(2),
372+
writeErrors: driver.WriteErrors{{Index: 1}, {Index: 0}},
373+
ordered: true,
374+
want: []any{},
375+
},
376+
}
377+
378+
for _, test := range tests {
379+
t.Run(test.name, func(t *testing.T) {
380+
got := keepInsertedIDs(test.result, test.writeErrors, test.ordered)
381+
require.Equal(t, test.want, got)
382+
})
383+
}
384+
}

0 commit comments

Comments
 (0)