From cc5684f6b054a025492b6b5a168d8924f0f72b17 Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Mon, 14 Oct 2024 17:22:36 +0200 Subject: [PATCH] feat(NODE-6409): new errors for unacknowledged bulk writes --- src/operations/client_bulk_write/executor.ts | 15 ++++ test/integration/crud/crud.prose.test.ts | 86 ++++++++++++++++++- .../unacknowledged-client-bulkWrite.json | 8 +- .../unacknowledged-client-bulkWrite.yml | 4 +- .../crud/unified/client-bulkWrite-errors.json | 58 +++++++++++++ .../crud/unified/client-bulkWrite-errors.yml | 29 +++++++ 6 files changed, 194 insertions(+), 6 deletions(-) diff --git a/src/operations/client_bulk_write/executor.ts b/src/operations/client_bulk_write/executor.ts index 93acaac2160..7475fcdab20 100644 --- a/src/operations/client_bulk_write/executor.ts +++ b/src/operations/client_bulk_write/executor.ts @@ -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'; @@ -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' + ); + } + } } /** diff --git a/test/integration/crud/crud.prose.test.ts b/test/integration/crud/crud.prose.test.ts index de48cbbaca8..6722bf605c9 100644 --- a/test/integration/crud/crud.prose.test.ts +++ b/test/integration/crud/crud.prose.test.ts @@ -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'); @@ -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'); @@ -1079,4 +1080,85 @@ 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; + const 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(); + 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; + Array.from({ length: numModels }, () => { + models.push({ + 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(3); + } + }); + }); }); diff --git a/test/spec/command-logging-and-monitoring/monitoring/unacknowledged-client-bulkWrite.json b/test/spec/command-logging-and-monitoring/monitoring/unacknowledged-client-bulkWrite.json index 1099b6a1e9f..61bb00726c0 100644 --- a/test/spec/command-logging-and-monitoring/monitoring/unacknowledged-client-bulkWrite.json +++ b/test/spec/command-logging-and-monitoring/monitoring/unacknowledged-client-bulkWrite.json @@ -3,7 +3,8 @@ "schemaVersion": "1.7", "runOnRequirements": [ { - "minServerVersion": "8.0" + "minServerVersion": "8.0", + "serverless": "forbid" } ], "createEntities": [ @@ -90,7 +91,8 @@ } } } - ] + ], + "ordered": false }, "expectResult": { "insertedCount": { @@ -157,7 +159,7 @@ "command": { "bulkWrite": 1, "errorsOnly": true, - "ordered": true, + "ordered": false, "ops": [ { "insert": 0, diff --git a/test/spec/command-logging-and-monitoring/monitoring/unacknowledged-client-bulkWrite.yml b/test/spec/command-logging-and-monitoring/monitoring/unacknowledged-client-bulkWrite.yml index fcc6b7b3ec2..2d545259539 100644 --- a/test/spec/command-logging-and-monitoring/monitoring/unacknowledged-client-bulkWrite.yml +++ b/test/spec/command-logging-and-monitoring/monitoring/unacknowledged-client-bulkWrite.yml @@ -4,6 +4,7 @@ schemaVersion: "1.7" runOnRequirements: - minServerVersion: "8.0" + serverless: forbid createEntities: - client: @@ -49,6 +50,7 @@ tests: namespace: *namespace filter: { _id: 3 } update: { $set: { x: 333 } } + ordered: false expectResult: insertedCount: $$unsetOrMatches: 0 @@ -88,7 +90,7 @@ tests: command: bulkWrite: 1 errorsOnly: true - ordered: true + ordered: false ops: - insert: 0 document: { _id: 4, x: 44 } diff --git a/test/spec/crud/unified/client-bulkWrite-errors.json b/test/spec/crud/unified/client-bulkWrite-errors.json index 8cc45bb5f2d..015bd95c990 100644 --- a/test/spec/crud/unified/client-bulkWrite-errors.json +++ b/test/spec/crud/unified/client-bulkWrite-errors.json @@ -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" + } + } + ] } ] } diff --git a/test/spec/crud/unified/client-bulkWrite-errors.yml b/test/spec/crud/unified/client-bulkWrite-errors.yml index 6c513006cec..79c04961615 100644 --- a/test/spec/crud/unified/client-bulkWrite-errors.yml +++ b/test/spec/crud/unified/client-bulkWrite-errors.yml @@ -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"