Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(NODE-6409): new errors for unacknowledged bulk writes #4276

Merged
merged 2 commits into from
Oct 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
nbbeeken marked this conversation as resolved.
Show resolved Hide resolved
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"