Skip to content

Commit 643a875

Browse files
authoredSep 17, 2024··
feat(NODE-6060): set fire-and-forget protocol when writeConcern is w: 0 (#4219)
1 parent 20396e1 commit 643a875

File tree

5 files changed

+177
-13
lines changed

5 files changed

+177
-13
lines changed
 

‎src/cmap/commands.ts

+18-8
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,8 @@ export class OpQueryRequest {
7474
awaitData: boolean;
7575
exhaust: boolean;
7676
partial: boolean;
77+
/** moreToCome is an OP_MSG only concept */
78+
moreToCome = false;
7779

7880
constructor(
7981
public databaseName: string,
@@ -407,13 +409,21 @@ const OPTS_EXHAUST_ALLOWED = 1 << 16;
407409

408410
/** @internal */
409411
export interface OpMsgOptions {
410-
requestId: number;
411-
serializeFunctions: boolean;
412-
ignoreUndefined: boolean;
413-
checkKeys: boolean;
414-
maxBsonSize: number;
415-
moreToCome: boolean;
416-
exhaustAllowed: boolean;
412+
socketTimeoutMS?: number;
413+
session?: ClientSession;
414+
numberToSkip?: number;
415+
numberToReturn?: number;
416+
returnFieldSelector?: Document;
417+
pre32Limit?: number;
418+
serializeFunctions?: boolean;
419+
ignoreUndefined?: boolean;
420+
maxBsonSize?: number;
421+
checkKeys?: boolean;
422+
secondaryOk?: boolean;
423+
424+
requestId?: number;
425+
moreToCome?: boolean;
426+
exhaustAllowed?: boolean;
417427
readPreference: ReadPreference;
418428
}
419429

@@ -465,7 +475,7 @@ export class OpMsgRequest {
465475

466476
// flags
467477
this.checksumPresent = false;
468-
this.moreToCome = options.moreToCome || false;
478+
this.moreToCome = options.moreToCome ?? command.writeConcern?.w === 0;
469479
this.exhaustAllowed =
470480
typeof options.exhaustAllowed === 'boolean' ? options.exhaustAllowed : false;
471481
}

‎src/cmap/connection.ts

+6-2
Original file line numberDiff line numberDiff line change
@@ -439,7 +439,7 @@ export class Connection extends TypedEventEmitter<ConnectionEvents> {
439439
zlibCompressionLevel: this.description.zlibCompressionLevel
440440
});
441441

442-
if (options.noResponse) {
442+
if (options.noResponse || message.moreToCome) {
443443
yield MongoDBResponse.empty;
444444
return;
445445
}
@@ -527,7 +527,11 @@ export class Connection extends TypedEventEmitter<ConnectionEvents> {
527527
new CommandSucceededEvent(
528528
this,
529529
message,
530-
options.noResponse ? undefined : (object ??= document.toObject(bsonOptions)),
530+
options.noResponse
531+
? undefined
532+
: message.moreToCome
533+
? { ok: 1 }
534+
: (object ??= document.toObject(bsonOptions)),
531535
started,
532536
this.description.serverConnectionId
533537
)

‎src/write_concern.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,10 @@ interface CommandWriteConcernOptions {
5858
* @see https://www.mongodb.com/docs/manual/reference/write-concern/
5959
*/
6060
export class WriteConcern {
61-
/** Request acknowledgment that the write operation has propagated to a specified number of mongod instances or to mongod instances with specified tags. */
61+
/**
62+
* Request acknowledgment that the write operation has propagated to a specified number of mongod instances or to mongod instances with specified tags.
63+
* If w is 0 and is set on a write operation, the server will not send a response.
64+
*/
6265
readonly w?: W;
6366
/** Request acknowledgment that the write operation has been written to the on-disk journal */
6467
readonly journal?: boolean;

‎test/integration/read-write-concern/write_concern.test.ts

+138-2
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
import { expect } from 'chai';
22
import { on, once } from 'events';
3+
import { gte } from 'semver';
4+
import * as sinon from 'sinon';
35

46
import {
57
type Collection,
68
type CommandStartedEvent,
9+
type CommandSucceededEvent,
710
type Db,
811
LEGACY_HELLO_COMMAND,
9-
MongoClient
12+
MongoClient,
13+
OpMsgRequest
1014
} from '../../mongodb';
1115
import * as mock from '../../tools/mongodb-mock/index';
1216
import { filterForCommands } from '../shared';
@@ -93,7 +97,7 @@ describe('Write Concern', function () {
9397
});
9498

9599
afterEach(async function () {
96-
await db.dropDatabase();
100+
await db.dropDatabase({ writeConcern: { w: 'majority' } });
97101
await client.close();
98102
});
99103

@@ -168,4 +172,136 @@ describe('Write Concern', function () {
168172
});
169173
});
170174
});
175+
176+
describe('fire-and-forget protocol', function () {
177+
context('when writeConcern = 0 and OP_MSG is used', function () {
178+
const writeOperations: { name: string; command: any; expectedReturnVal: any }[] = [
179+
{
180+
name: 'insertOne',
181+
command: client => client.db('test').collection('test').insertOne({ a: 1 }),
182+
expectedReturnVal: { acknowledged: false }
183+
},
184+
{
185+
name: 'insertMany',
186+
command: client =>
187+
client
188+
.db('test')
189+
.collection('test')
190+
.insertMany([{ a: 1 }, { b: 2 }]),
191+
expectedReturnVal: { acknowledged: false }
192+
},
193+
{
194+
name: 'updateOne',
195+
command: client =>
196+
client
197+
.db('test')
198+
.collection('test')
199+
.updateOne({ i: 128 }, { $set: { c: 2 } }),
200+
expectedReturnVal: { acknowledged: false }
201+
},
202+
{
203+
name: 'updateMany',
204+
command: client =>
205+
client
206+
.db('test')
207+
.collection('test')
208+
.updateMany({ name: 'foobar' }, { $set: { name: 'fizzbuzz' } }),
209+
expectedReturnVal: { acknowledged: false }
210+
},
211+
{
212+
name: 'deleteOne',
213+
command: client => client.db('test').collection('test').deleteOne({ a: 1 }),
214+
expectedReturnVal: { acknowledged: false }
215+
},
216+
{
217+
name: 'deleteMany',
218+
command: client => client.db('test').collection('test').deleteMany({ name: 'foobar' }),
219+
expectedReturnVal: { acknowledged: false }
220+
},
221+
{
222+
name: 'replaceOne',
223+
command: client => client.db('test').collection('test').replaceOne({ a: 1 }, { b: 2 }),
224+
expectedReturnVal: { acknowledged: false }
225+
},
226+
{
227+
name: 'removeUser',
228+
command: client => client.db('test').removeUser('albert'),
229+
expectedReturnVal: true
230+
},
231+
{
232+
name: 'findAndModify',
233+
command: client =>
234+
client
235+
.db('test')
236+
.collection('test')
237+
.findOneAndUpdate({}, { $setOnInsert: { a: 1 } }, { upsert: true }),
238+
expectedReturnVal: null
239+
},
240+
{
241+
name: 'dropDatabase',
242+
command: client => client.db('test').dropDatabase(),
243+
expectedReturnVal: true
244+
},
245+
{
246+
name: 'dropCollection',
247+
command: client => client.db('test').dropCollection('test'),
248+
expectedReturnVal: true
249+
},
250+
{
251+
name: 'dropIndexes',
252+
command: client => client.db('test').collection('test').dropIndex('a'),
253+
expectedReturnVal: { ok: 1 }
254+
},
255+
{
256+
name: 'createIndexes',
257+
command: client => client.db('test').collection('test').createIndex({ a: 1 }),
258+
expectedReturnVal: 'a_1'
259+
},
260+
{
261+
name: 'createCollection',
262+
command: client => client.db('test').createCollection('test'),
263+
expectedReturnVal: {}
264+
}
265+
];
266+
267+
for (const op of writeOperations) {
268+
context(`when the write operation ${op.name} is run`, function () {
269+
let client;
270+
let spy;
271+
272+
beforeEach(async function () {
273+
if (gte('3.6.0', this.configuration.version)) {
274+
this.currentTest.skipReason = 'Test requires OP_MSG, needs to be on MongoDB 3.6+';
275+
this.skip();
276+
}
277+
spy = sinon.spy(OpMsgRequest.prototype, 'toBin');
278+
client = this.configuration.newClient({ monitorCommands: true, w: 0 });
279+
await client.connect();
280+
});
281+
282+
afterEach(function () {
283+
sinon.restore();
284+
client.close();
285+
});
286+
287+
it('the request should have moreToCome bit set', async function () {
288+
await op.command(client);
289+
expect(spy.returnValues[spy.returnValues.length - 1][0][16]).to.equal(2);
290+
});
291+
292+
it('the return value of the command should be nullish', async function () {
293+
const result = await op.command(client);
294+
expect(result).to.containSubset(op.expectedReturnVal);
295+
});
296+
297+
it('commandSucceededEvent should have reply with only {ok: 1}', async function () {
298+
const events: CommandSucceededEvent[] = [];
299+
client.on('commandSucceeded', event => events.push(event));
300+
await op.command(client);
301+
expect(events[0]).to.containSubset({ reply: { ok: 1 } });
302+
});
303+
});
304+
}
305+
});
306+
});
171307
});

‎test/unit/commands.test.ts

+11
Original file line numberDiff line numberDiff line change
@@ -109,3 +109,14 @@ describe('class OpCompressedRequest', () => {
109109
}
110110
});
111111
});
112+
113+
describe('OpMsgRequest', () => {
114+
describe('#constructor', () => {
115+
context('when writeConcern = 0', () => {
116+
it('moreToCome is set to true', async () => {
117+
const request = new OpMsgRequest('db', { a: 1, writeConcern: { w: 0 } }, {});
118+
expect(request.moreToCome).to.be.true;
119+
});
120+
});
121+
});
122+
});

0 commit comments

Comments
 (0)
Please sign in to comment.