Skip to content

Commit

Permalink
feat(NODE-6409): new errors for unacknowledged bulk writes (#4276)
Browse files Browse the repository at this point in the history
  • Loading branch information
durran authored Oct 14, 2024
1 parent a473de9 commit 7fde8dd
Show file tree
Hide file tree
Showing 6 changed files with 198 additions and 6 deletions.
15 changes: 15 additions & 0 deletions src/operations/client_bulk_write/executor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { ClientBulkWriteCursor } from '../../cursor/client_bulk_write_cursor';
import {
MongoClientBulkWriteError,
MongoClientBulkWriteExecutionError,
MongoInvalidArgumentError,
MongoServerError
} from '../../error';
import { type MongoClient } from '../../mongo_client';
Expand Down Expand Up @@ -53,6 +54,20 @@ export class ClientBulkWriteExecutor {
if (!this.options.writeConcern) {
this.options.writeConcern = WriteConcern.fromOptions(this.client.options);
}

if (this.options.writeConcern?.w === 0) {
if (this.options.verboseResults) {
throw new MongoInvalidArgumentError(
'Cannot request unacknowledged write concern and verbose results'
);
}

if (this.options.ordered) {
throw new MongoInvalidArgumentError(
'Cannot request unacknowledged write concern and ordered writes'
);
}
}
}

/**
Expand Down
90 changes: 88 additions & 2 deletions test/integration/crud/crud.prose.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -738,7 +738,8 @@ describe('CRUD Prose Spec Tests', () => {
async test() {
const error = await client
.bulkWrite([{ name: 'insertOne', namespace: 'db.coll', document: document }], {
writeConcern: { w: 0 }
writeConcern: { w: 0 },
ordered: false
})
.catch(error => error);
expect(error.message).to.include('Client bulk write operation ops of length');
Expand All @@ -763,7 +764,7 @@ describe('CRUD Prose Spec Tests', () => {
const error = await client
.bulkWrite(
[{ name: 'replaceOne', namespace: 'db.coll', filter: {}, replacement: document }],
{ writeConcern: { w: 0 } }
{ writeConcern: { w: 0 }, ordered: false }
)
.catch(error => error);
expect(error.message).to.include('Client bulk write operation ops of length');
Expand Down Expand Up @@ -1079,4 +1080,89 @@ describe('CRUD Prose Spec Tests', () => {
expect(command).to.have.property('maxTimeMS', 2000);
});
});

describe('15. `MongoClient.bulkWrite` with unacknowledged write concern uses `w:0` for all batches', function () {
// This test must only be run on 8.0+ servers. This test must be skipped on Atlas Serverless.
// If testing with a sharded cluster, only connect to one mongos. This is intended to ensure the `countDocuments` operation
// uses the same connection as the `bulkWrite` to get the correct connection count. (See
// [DRIVERS-2921](https://jira.mongodb.org/browse/DRIVERS-2921)).
// Construct a `MongoClient` (referred to as `client`) with
// [command monitoring](../../command-logging-and-monitoring/command-logging-and-monitoring.md) enabled to observe
// CommandStartedEvents. Perform a `hello` command using `client` and record the `maxBsonObjectSize` and
// `maxMessageSizeBytes` values in the response.
// Construct a `MongoCollection` (referred to as `coll`) for the collection "db.coll". Drop `coll`.
// Use the `create` command to create "db.coll" to workaround [SERVER-95537](https://jira.mongodb.org/browse/SERVER-95537).
// Construct the following write model (referred to as `model`):
// InsertOne: {
// "namespace": "db.coll",
// "document": { "a": "b".repeat(maxBsonObjectSize - 500) }
// }
// Construct a list of write models (referred to as `models`) with `model` repeated
// `maxMessageSizeBytes / maxBsonObjectSize + 1` times.
// Call `client.bulkWrite` with `models`. Pass `BulkWriteOptions` with `ordered` set to `false` and `writeConcern` set to
// an unacknowledged write concern. Assert no error occurred. Assert the result indicates the write was unacknowledged.
// Assert that two CommandStartedEvents (referred to as `firstEvent` and `secondEvent`) were observed for the `bulkWrite`
// command. Assert that the length of `firstEvent.command.ops` is `maxMessageSizeBytes / maxBsonObjectSize`. Assert that
// the length of `secondEvent.command.ops` is 1. If the driver exposes `operationId`s in its CommandStartedEvents, assert
// that `firstEvent.operationId` is equal to `secondEvent.operationId`. Assert both commands include
// `writeConcern: {w: 0}`.
// To force completion of the `w:0` writes, execute `coll.countDocuments` and expect the returned count is
// `maxMessageSizeBytes / maxBsonObjectSize + 1`. This is intended to avoid incomplete writes interfering with other tests
// that may use this collection.
let client: MongoClient;
let maxBsonObjectSize;
let maxMessageSizeBytes;
let numModels;
let models: AnyClientBulkWriteModel[] = [];
const commands: CommandStartedEvent[] = [];

beforeEach(async function () {
const uri = this.configuration.url({
useMultipleMongoses: false
});
client = this.configuration.newClient(uri, { monitorCommands: true });
await client.connect();
await client
.db('db')
.collection('coll')
.drop()
.catch(() => null);
await client.db('db').createCollection('coll');
const hello = await client.db('admin').command({ hello: 1 });
maxBsonObjectSize = hello.maxBsonObjectSize;
maxMessageSizeBytes = hello.maxMessageSizeBytes;
numModels = Math.floor(maxMessageSizeBytes / maxBsonObjectSize) + 1;
models = Array.from({ length: numModels }, () => {
return {
name: 'insertOne',
namespace: 'db.coll',
document: {
a: 'b'.repeat(maxBsonObjectSize - 500)
}
};
});

client.on('commandStarted', filterForCommands('bulkWrite', commands));
commands.length = 0;
});

afterEach(async function () {
await client.close();
});

it('performs all writes unacknowledged', {
metadata: { requires: { mongodb: '>=8.0.0', serverless: 'forbid' } },
async test() {
const result = await client.bulkWrite(models, { ordered: false, writeConcern: { w: 0 } });
expect(result).to.deep.equal({ ok: 1 });
expect(commands.length).to.equal(2);
expect(commands[0].command.ops.length).to.equal(numModels - 1);
expect(commands[0].command.writeConcern.w).to.equal(0);
expect(commands[1].command.ops.length).to.equal(1);
expect(commands[1].command.writeConcern.w).to.equal(0);
const count = await client.db('db').collection('coll').countDocuments();
expect(count).to.equal(numModels);
}
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
"schemaVersion": "1.7",
"runOnRequirements": [
{
"minServerVersion": "8.0"
"minServerVersion": "8.0",
"serverless": "forbid"
}
],
"createEntities": [
Expand Down Expand Up @@ -90,7 +91,8 @@
}
}
}
]
],
"ordered": false
},
"expectResult": {
"insertedCount": {
Expand Down Expand Up @@ -157,7 +159,7 @@
"command": {
"bulkWrite": 1,
"errorsOnly": true,
"ordered": true,
"ordered": false,
"ops": [
{
"insert": 0,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ schemaVersion: "1.7"

runOnRequirements:
- minServerVersion: "8.0"
serverless: forbid

createEntities:
- client:
Expand Down Expand Up @@ -49,6 +50,7 @@ tests:
namespace: *namespace
filter: { _id: 3 }
update: { $set: { x: 333 } }
ordered: false
expectResult:
insertedCount:
$$unsetOrMatches: 0
Expand Down Expand Up @@ -88,7 +90,7 @@ tests:
command:
bulkWrite: 1
errorsOnly: true
ordered: true
ordered: false
ops:
- insert: 0
document: { _id: 4, x: 44 }
Expand Down
58 changes: 58 additions & 0 deletions test/spec/crud/unified/client-bulkWrite-errors.json
Original file line number Diff line number Diff line change
Expand Up @@ -450,6 +450,64 @@
}
}
]
},
{
"description": "Requesting unacknowledged write with verboseResults is a client-side error",
"operations": [
{
"name": "clientBulkWrite",
"object": "client0",
"arguments": {
"models": [
{
"insertOne": {
"namespace": "crud-tests.coll0",
"document": {
"_id": 10
}
}
}
],
"verboseResults": true,
"ordered": false,
"writeConcern": {
"w": 0
}
},
"expectError": {
"isClientError": true,
"errorContains": "Cannot request unacknowledged write concern and verbose results"
}
}
]
},
{
"description": "Requesting unacknowledged write with ordered is a client-side error",
"operations": [
{
"name": "clientBulkWrite",
"object": "client0",
"arguments": {
"models": [
{
"insertOne": {
"namespace": "crud-tests.coll0",
"document": {
"_id": 10
}
}
}
],
"writeConcern": {
"w": 0
}
},
"expectError": {
"isClientError": true,
"errorContains": "Cannot request unacknowledged write concern and ordered writes"
}
}
]
}
]
}
29 changes: 29 additions & 0 deletions test/spec/crud/unified/client-bulkWrite-errors.yml
Original file line number Diff line number Diff line change
Expand Up @@ -239,3 +239,32 @@ tests:
verboseResults: true
expectError:
isClientError: true
- description: "Requesting unacknowledged write with verboseResults is a client-side error"
operations:
- name: clientBulkWrite
object: *client0
arguments:
models:
- insertOne:
namespace: *namespace
document: { _id: 10 }
verboseResults: true
ordered: false
writeConcern: { w: 0 }
expectError:
isClientError: true
errorContains: "Cannot request unacknowledged write concern and verbose results"
- description: "Requesting unacknowledged write with ordered is a client-side error"
operations:
- name: clientBulkWrite
object: *client0
arguments:
models:
- insertOne:
namespace: *namespace
document: { _id: 10 }
# Omit `ordered` option. Defaults to true.
writeConcern: { w: 0 }
expectError:
isClientError: true
errorContains: "Cannot request unacknowledged write concern and ordered writes"

0 comments on commit 7fde8dd

Please sign in to comment.