Skip to content

Commit aad1aa7

Browse files
committed
feat(NODE-4756): ok 1 with write concern failure event changes
1 parent 14427d1 commit aad1aa7

File tree

2 files changed

+59
-8
lines changed

2 files changed

+59
-8
lines changed

src/cmap/connection.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -416,7 +416,7 @@ export class Connection extends TypedEventEmitter<ConnectionEvents> {
416416

417417
if (operationDescription.command) {
418418
if (document.writeConcernError) {
419-
callback(new MongoWriteConcernError(document.writeConcernError, document));
419+
callback(new MongoWriteConcernError(document.writeConcernError, document), document);
420420
return;
421421
}
422422

@@ -680,7 +680,10 @@ function write(
680680

681681
operationDescription.started = now();
682682
operationDescription.cb = (err, reply) => {
683-
if (err) {
683+
// Command monitoring spec states that if ok is 1, then we must always emit
684+
// a command suceeded event, even if there's and error. Write concern errors
685+
// will have an ok: 1 in their reply.
686+
if (err && reply?.ok !== 1) {
684687
conn.emit(
685688
Connection.COMMAND_FAILED,
686689
new CommandFailedEvent(conn, command, err, operationDescription.started)
@@ -700,7 +703,11 @@ function write(
700703
}
701704

702705
if (typeof callback === 'function') {
703-
callback(err, reply);
706+
// Since we're passing through the reply with the write concern error now, we
707+
// need it not to be provided to the original callback in this case so
708+
// retryability does not get tricked into thinking the command actually
709+
// succeeded.
710+
callback(err, err instanceof MongoWriteConcernError ? undefined : reply);
704711
}
705712
};
706713
}

test/integration/retryable-writes/retryable_writes.spec.prose.test.ts

Lines changed: 49 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -230,11 +230,6 @@ describe('Retryable Writes Spec Prose', () => {
230230
});
231231

232232
/**
233-
* **NOTE:** Node emits a command failed event for writeConcern errors, making the commandSucceeded part of this test inconsistent see (DRIVERS-2468).
234-
* Second our event emitters are called synchronously but operations are asynchronous, we don't have a way to make sure a fail point is set before a retry
235-
* is attempted, if the server allowed us to specify an ordered list of fail points this would be possible, alas we can use sinon. Sinon will set an error
236-
* to be thrown on the first and second call to Server.command(), this should enter the retry logic for the second error thrown.
237-
*
238233
* This test MUST be implemented by any driver that implements the Command Monitoring specification,
239234
* only run against replica sets as mongos does not propagate the NoWritesPerformed label to the drivers.
240235
* Additionally, this test requires drivers to set a fail point after an insertOne operation but before the subsequent retry.
@@ -299,5 +294,54 @@ describe('Retryable Writes Spec Prose', () => {
299294
expect(insertResult).to.have.property('code', 91);
300295
}
301296
);
297+
298+
// This is an extra test that is a complimentary test to prose test #3. We basically want to
299+
// test that in the case of a write concern error with ok: 1 in the response, that
300+
// a command succeeded event is emitted but that the driver still treats it as a failure
301+
// and retries. So for the success, we check the error code if exists, and since the retry
302+
// must succeed, we fail if any command failed event occurs on insert.
303+
it(
304+
'emits a command succeeded event for write concern errors with ok: 1',
305+
{ requires: { topology: 'replicaset', mongodb: '>=4.2.9' } },
306+
async () => {
307+
const successListener = event => {
308+
if (event.commandName === 'insert' && event.reply.writeConcernError) {
309+
expect(event.reply.writeConcernError.code).to.equal(91);
310+
}
311+
};
312+
313+
const failureListener = event => {
314+
if (event.commandName === 'insert') {
315+
expect.fail('the insert must be retried after the first suceeded event');
316+
}
317+
};
318+
319+
// Generate a write concern error to assert that we get a command
320+
// suceeded event but the operation will retry because it was an
321+
// actual write concern error.
322+
client.db('admin').command({
323+
configureFailPoint: 'failCommand',
324+
mode: { times: 1 },
325+
data: {
326+
writeConcernError: {
327+
code: 91,
328+
errorLabels: ['RetryableWriteError']
329+
},
330+
failCommands: ['insert']
331+
}
332+
});
333+
334+
client.addListener('commandSucceeded', successListener);
335+
client.addListener('commandFailed', failureListener);
336+
337+
const insertResult = await collection.insertOne({ _id: 1 }).catch(error => error);
338+
339+
// sinon.restore();
340+
client.removeListener('commandSucceeded', successListener);
341+
client.removeListener('commandFailed', failureListener);
342+
343+
expect(insertResult).to.deep.equal({ acknowledged: true, insertedId: 1 });
344+
}
345+
);
302346
});
303347
});

0 commit comments

Comments
 (0)