Skip to content

Commit dff7736

Browse files
committed
feat(NODE-6338): implement client bulk write error handling
1 parent 9f63397 commit dff7736

File tree

8 files changed

+250
-62
lines changed

8 files changed

+250
-62
lines changed

src/cmap/wire_protocol/responses.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -354,4 +354,8 @@ export class ClientBulkWriteCursorResponse extends CursorResponse {
354354
get deletedCount() {
355355
return this.get('nDeleted', BSONType.int, true);
356356
}
357+
358+
get writeConcernError() {
359+
return this.get('writeConcernError', BSONType.object, false);
360+
}
357361
}

src/error.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -643,6 +643,33 @@ export class MongoClientBulkWriteCursorError extends MongoRuntimeError {
643643
}
644644
}
645645

646+
/**
647+
* An error indicating that an error occurred when generating a bulk write update.
648+
*
649+
* @public
650+
* @category Error
651+
*/
652+
export class MongoClientBulkWriteUpdateError extends MongoRuntimeError {
653+
/**
654+
* **Do not use this constructor!**
655+
*
656+
* Meant for internal use only.
657+
*
658+
* @remarks
659+
* This class is only meant to be constructed within the driver. This constructor is
660+
* not subject to semantic versioning compatibility guarantees and may change at any time.
661+
*
662+
* @public
663+
**/
664+
constructor(message: string) {
665+
super(message);
666+
}
667+
668+
override get name(): string {
669+
return 'MongoClientBulkWriteUpdateError';
670+
}
671+
}
672+
646673
/**
647674
* An error indicating that an error occurred on the client when executing a client bulk write.
648675
*

src/operations/client_bulk_write/command_builder.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { BSON, type Document } from '../../bson';
22
import { DocumentSequence } from '../../cmap/commands';
3+
import { MongoClientBulkWriteUpdateError } from '../../error';
34
import { type PkFactory } from '../../mongo_client';
45
import type { Filter, OptionalId, UpdateFilter, WithoutId } from '../../mongo_types';
56
import { DEFAULT_PK_FACTORY } from '../../utils';
@@ -343,6 +344,22 @@ export const buildUpdateManyOperation = (
343344
return createUpdateOperation(model, index, true);
344345
};
345346

347+
/**
348+
* Validate the update document.
349+
* @param update - The update document.
350+
*/
351+
function validateUpdate(update: Document) {
352+
const keys = Object.keys(update);
353+
if (keys.length === 0) {
354+
throw new MongoClientBulkWriteUpdateError('Client bulk write update models may not be empty.');
355+
}
356+
if (!keys[0].startsWith('$')) {
357+
throw new MongoClientBulkWriteUpdateError(
358+
'Client bulk write update models must only contain atomic modifiers (start with $).'
359+
);
360+
}
361+
}
362+
346363
/**
347364
* Creates a delete operation based on the parameters.
348365
*/
@@ -351,6 +368,22 @@ function createUpdateOperation(
351368
index: number,
352369
multi: boolean
353370
): ClientUpdateOperation {
371+
// Update documents provided in UpdateOne and UpdateMany write models are
372+
// required only to contain atomic modifiers (i.e. keys that start with "$").
373+
// Drivers MUST throw an error if an update document is empty or if the
374+
// document's first key does not start with "$".
375+
if (Array.isArray(model.update)) {
376+
if (model.update.length === 0) {
377+
throw new MongoClientBulkWriteUpdateError(
378+
'Client bulk write update model pipelines may not be empty.'
379+
);
380+
}
381+
for (const update of model.update) {
382+
validateUpdate(update);
383+
}
384+
} else {
385+
validateUpdate(model.update);
386+
}
354387
const document: ClientUpdateOperation = {
355388
update: index,
356389
multi: multi,
@@ -393,6 +426,16 @@ export const buildReplaceOneOperation = (
393426
model: ClientReplaceOneModel,
394427
index: number
395428
): ClientReplaceOneOperation => {
429+
const keys = Object.keys(model.replacement);
430+
if (keys.length === 0) {
431+
throw new MongoClientBulkWriteUpdateError('Client bulk write replace models may not be empty.');
432+
}
433+
if (keys[0].startsWith('$')) {
434+
throw new MongoClientBulkWriteUpdateError(
435+
'Client bulk write replace models must not contain atomic modifiers (start with $).'
436+
);
437+
}
438+
396439
const document: ClientReplaceOneOperation = {
397440
update: index,
398441
multi: false,

src/operations/client_bulk_write/common.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { type Document } from '../../bson';
2+
import { type ErrorDescription, type MongoRuntimeError, MongoServerError } from '../../error';
23
import type { Filter, OptionalId, UpdateFilter, WithoutId } from '../../mongo_types';
34
import type { CollationOptions, CommandOperationOptions } from '../../operations/command';
45
import type { Hint } from '../../operations/operation';
@@ -181,6 +182,55 @@ export interface ClientBulkWriteResult {
181182
deleteResults?: Map<number, ClientDeleteResult>;
182183
}
183184

185+
export interface ClientBulkWriteError {
186+
code: number;
187+
message: string;
188+
}
189+
190+
/**
191+
* An error indicating that an error occurred when executing the bulk write.
192+
*
193+
* @public
194+
* @category Error
195+
*/
196+
export class MongoClientBulkWriteError extends MongoServerError {
197+
/**
198+
* A top-level error that occurred when attempting to communicate with the server or execute
199+
* the bulk write. This value may not be populated if the exception was thrown due to errors
200+
* occurring on individual writes.
201+
*/
202+
error?: MongoRuntimeError;
203+
/**
204+
* Write concern errors that occurred while executing the bulk write. This list may have
205+
* multiple items if more than one server command was required to execute the bulk write.
206+
*/
207+
writeConcernErrors: Document[];
208+
/**
209+
* Errors that occurred during the execution of individual write operations. This map will
210+
* contain at most one entry if the bulk write was ordered.
211+
*/
212+
writeErrors: Map<number, ClientBulkWriteError>;
213+
/**
214+
* The results of any successful operations that were performed before the error was
215+
* encountered.
216+
*/
217+
partialResult?: ClientBulkWriteResult;
218+
219+
/**
220+
* Initialize the client bulk write error.
221+
* @param message - The error message.
222+
*/
223+
constructor(message: ErrorDescription) {
224+
super(message);
225+
this.writeConcernErrors = [];
226+
this.writeErrors = new Map();
227+
}
228+
229+
override get name(): string {
230+
return 'MongoClientBulkWriteError';
231+
}
232+
}
233+
184234
/** @public */
185235
export interface ClientInsertOneResult {
186236
/**

src/operations/client_bulk_write/executor.ts

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { type Document } from 'bson';
22

33
import { ClientBulkWriteCursor } from '../../cursor/client_bulk_write_cursor';
4-
import { MongoClientBulkWriteExecutionError } from '../../error';
4+
import { MongoClientBulkWriteExecutionError, MongoWriteConcernError } from '../../error';
55
import { type MongoClient } from '../../mongo_client';
66
import { WriteConcern } from '../../write_concern';
77
import { executeOperation } from '../execute_operation';
@@ -10,7 +10,8 @@ import { type ClientBulkWriteCommand, ClientBulkWriteCommandBuilder } from './co
1010
import {
1111
type AnyClientBulkWriteModel,
1212
type ClientBulkWriteOptions,
13-
type ClientBulkWriteResult
13+
type ClientBulkWriteResult,
14+
MongoClientBulkWriteError
1415
} from './common';
1516
import { ClientBulkWriteResultsMerger } from './results_merger';
1617

@@ -34,9 +35,13 @@ export class ClientBulkWriteExecutor {
3435
operations: AnyClientBulkWriteModel[],
3536
options?: ClientBulkWriteOptions
3637
) {
38+
if (operations.length === 0) {
39+
throw new MongoClientBulkWriteExecutionError('No client bulk write models were provided.');
40+
}
41+
3742
this.client = client;
3843
this.operations = operations;
39-
this.options = { ...options };
44+
this.options = { ordered: true, ...options };
4045

4146
// If no write concern was provided, we inherit one from the client.
4247
if (!this.options.writeConcern) {
@@ -96,12 +101,46 @@ async function executeAcknowledged(
96101
let currentBatchOffset = 0;
97102
for (const command of commands) {
98103
const cursor = new ClientBulkWriteCursor(client, command, options);
99-
const docs = await cursor.toArray();
104+
let docs = [];
105+
let writeConcernErrorResult;
106+
try {
107+
docs = await cursor.toArray();
108+
} catch (error) {
109+
// Write concern errors are recorded in the writeConcernErrors field on MongoClientBulkWriteError.
110+
// When a write concern error is encountered, it should not terminate execution of the bulk write
111+
// for either ordered or unordered bulk writes. However, drivers MUST throw an exception at the end
112+
// of execution if any write concern errors were observed.
113+
if (error instanceof MongoWriteConcernError) {
114+
const result = error.result;
115+
writeConcernErrorResult = {
116+
insertedCount: result.nInserted,
117+
upsertedCount: result.nUpserted,
118+
matchedCount: result.nMatched,
119+
modifiedCount: result.nModified,
120+
deletedCount: result.nDeleted,
121+
writeConcernError: result.writeConcernError
122+
};
123+
docs = result.cursor.firstBatch;
124+
} else {
125+
throw error;
126+
}
127+
}
128+
// Note if we have a write concern error there will be no cursor response present.
129+
const response = writeConcernErrorResult ?? cursor.response;
100130
const operations = command.ops.documents;
101-
resultsMerger.merge(currentBatchOffset, operations, cursor.response, docs);
131+
resultsMerger.merge(currentBatchOffset, operations, response, docs);
102132
// Set the new batch index so we can back back to the index in the original models.
103133
currentBatchOffset += operations.length;
104134
}
135+
136+
if (resultsMerger.writeConcernErrors.length > 0) {
137+
const error = new MongoClientBulkWriteError({
138+
message: 'Mongo client bulk write encountered write concern errors during execution.'
139+
});
140+
error.writeConcernErrors = resultsMerger.writeConcernErrors;
141+
error.partialResult = resultsMerger.result;
142+
throw error;
143+
}
105144
return resultsMerger.result;
106145
}
107146

0 commit comments

Comments
 (0)