Skip to content

Commit fee783c

Browse files
authored
feat(NODE-4756)!: ok 1 with write concern failure event changes (#3525)
1 parent c357acf commit fee783c

File tree

5 files changed

+294
-9
lines changed

5 files changed

+294
-9
lines changed

src/cmap/connection.ts

+10-3
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 an 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
}

src/sdam/server.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -500,7 +500,8 @@ function makeOperationHandler(
500500
): Callback {
501501
const session = options?.session;
502502
return function handleOperationResult(error, result) {
503-
if (result != null) {
503+
// We should not swallow an error if it is present.
504+
if (error == null && result != null) {
504505
return callback(undefined, result);
505506
}
506507

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

+47-5
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
/* eslint-disable @typescript-eslint/no-non-null-assertion */
2+
import { once } from 'node:events';
3+
24
import { expect } from 'chai';
35
import * as sinon from 'sinon';
46

@@ -10,6 +12,7 @@ import {
1012
MongoWriteConcernError,
1113
Server
1214
} from '../../mongodb';
15+
import { sleep } from '../../tools/utils';
1316

1417
describe('Retryable Writes Spec Prose', () => {
1518
describe('1. Test that retryable writes raise an exception when using the MMAPv1 storage engine.', () => {
@@ -230,11 +233,6 @@ describe('Retryable Writes Spec Prose', () => {
230233
});
231234

232235
/**
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-
*
238236
* This test MUST be implemented by any driver that implements the Command Monitoring specification,
239237
* only run against replica sets as mongos does not propagate the NoWritesPerformed label to the drivers.
240238
* Additionally, this test requires drivers to set a fail point after an insertOne operation but before the subsequent retry.
@@ -299,5 +297,49 @@ describe('Retryable Writes Spec Prose', () => {
299297
expect(insertResult).to.have.property('code', 91);
300298
}
301299
);
300+
301+
// This is an extra test that is a complimentary test to prose test #3. We basically want to
302+
// test that in the case of a write concern error with ok: 1 in the response, that
303+
// a command succeeded event is emitted but that the driver still treats it as a failure
304+
// and retries. So for the success, we check the error code if exists, and since the retry
305+
// must succeed, we fail if any command failed event occurs on insert.
306+
it(
307+
'emits a command succeeded event for write concern errors with ok: 1',
308+
{ requires: { topology: 'replicaset', mongodb: '>=4.2.9' } },
309+
async () => {
310+
// Generate a write concern error to assert that we get a command
311+
// suceeded event but the operation will retry because it was an
312+
// actual write concern error.
313+
await client.db('admin').command({
314+
configureFailPoint: 'failCommand',
315+
mode: { times: 1 },
316+
data: {
317+
writeConcernError: {
318+
code: 91,
319+
errorLabels: ['RetryableWriteError']
320+
},
321+
failCommands: ['insert']
322+
}
323+
});
324+
325+
const willBeCommandSucceeded = once(client, 'commandSucceeded').catch(error => error);
326+
const willBeCommandFailed = Promise.race([
327+
once(client, 'commandFailed'),
328+
sleep(1000).then(() => Promise.reject(new Error('timeout')))
329+
]).catch(error => error);
330+
331+
const insertResult = await collection.insertOne({ _id: 1 }).catch(error => error);
332+
333+
const [commandSucceeded] = await willBeCommandSucceeded;
334+
expect(commandSucceeded.commandName).to.equal('insert');
335+
expect(commandSucceeded.reply).to.have.nested.property('writeConcernError.code', 91);
336+
const noCommandFailedEvent = await willBeCommandFailed;
337+
expect(
338+
noCommandFailedEvent.message,
339+
'expected timeout, since no failure event should emit'
340+
).to.equal('timeout');
341+
expect(insertResult).to.deep.equal({ acknowledged: true, insertedId: 1 });
342+
}
343+
);
302344
});
303345
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
{
2+
"description": "writeConcernError",
3+
"schemaVersion": "1.13",
4+
"runOnRequirements": [
5+
{
6+
"minServerVersion": "4.1.0",
7+
"topologies": [
8+
"replicaset"
9+
],
10+
"serverless": "forbid"
11+
}
12+
],
13+
"createEntities": [
14+
{
15+
"client": {
16+
"id": "client",
17+
"observeEvents": [
18+
"commandStartedEvent",
19+
"commandSucceededEvent",
20+
"commandFailedEvent"
21+
]
22+
}
23+
},
24+
{
25+
"database": {
26+
"id": "database",
27+
"client": "client",
28+
"databaseName": "command-monitoring-tests"
29+
}
30+
},
31+
{
32+
"collection": {
33+
"id": "collection",
34+
"database": "database",
35+
"collectionName": "test"
36+
}
37+
}
38+
],
39+
"initialData": [
40+
{
41+
"collectionName": "test",
42+
"databaseName": "command-monitoring-tests",
43+
"documents": [
44+
{
45+
"_id": 1,
46+
"x": 11
47+
}
48+
]
49+
}
50+
],
51+
"tests": [
52+
{
53+
"description": "A retryable write with write concern errors publishes success event",
54+
"operations": [
55+
{
56+
"name": "failPoint",
57+
"object": "testRunner",
58+
"arguments": {
59+
"client": "client",
60+
"failPoint": {
61+
"configureFailPoint": "failCommand",
62+
"mode": {
63+
"times": 1
64+
},
65+
"data": {
66+
"failCommands": [
67+
"insert"
68+
],
69+
"writeConcernError": {
70+
"code": 91,
71+
"errorLabels": [
72+
"RetryableWriteError"
73+
]
74+
}
75+
}
76+
}
77+
}
78+
},
79+
{
80+
"name": "insertOne",
81+
"object": "collection",
82+
"arguments": {
83+
"document": {
84+
"_id": 2,
85+
"x": 22
86+
}
87+
}
88+
}
89+
],
90+
"expectEvents": [
91+
{
92+
"client": "client",
93+
"events": [
94+
{
95+
"commandStartedEvent": {
96+
"command": {
97+
"insert": "test",
98+
"documents": [
99+
{
100+
"_id": 2,
101+
"x": 22
102+
}
103+
],
104+
"ordered": true
105+
},
106+
"commandName": "insert",
107+
"databaseName": "command-monitoring-tests"
108+
}
109+
},
110+
{
111+
"commandSucceededEvent": {
112+
"reply": {
113+
"ok": 1,
114+
"n": 1,
115+
"writeConcernError": {
116+
"code": 91,
117+
"errorLabels": [
118+
"RetryableWriteError"
119+
]
120+
}
121+
},
122+
"commandName": "insert"
123+
}
124+
},
125+
{
126+
"commandStartedEvent": {
127+
"command": {
128+
"insert": "test",
129+
"documents": [
130+
{
131+
"_id": 2,
132+
"x": 22
133+
}
134+
],
135+
"ordered": true
136+
},
137+
"commandName": "insert",
138+
"databaseName": "command-monitoring-tests"
139+
}
140+
},
141+
{
142+
"commandSucceededEvent": {
143+
"reply": {
144+
"ok": 1,
145+
"n": 1
146+
},
147+
"commandName": "insert"
148+
}
149+
}
150+
]
151+
}
152+
]
153+
}
154+
]
155+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
description: "writeConcernError"
2+
schemaVersion: "1.13"
3+
runOnRequirements:
4+
-
5+
minServerVersion: 4.1.0
6+
topologies:
7+
- replicaset
8+
serverless: "forbid"
9+
10+
createEntities:
11+
- client:
12+
id: &client client
13+
observeEvents:
14+
- commandStartedEvent
15+
- commandSucceededEvent
16+
- commandFailedEvent
17+
- database:
18+
id: &database database
19+
client: *client
20+
databaseName: &databaseName command-monitoring-tests
21+
- collection:
22+
id: &collection collection
23+
database: *database
24+
collectionName: &collectionName test
25+
26+
initialData:
27+
- collectionName: *collectionName
28+
databaseName: *databaseName
29+
documents:
30+
- { _id: 1, x: 11 }
31+
32+
tests:
33+
- description: "A retryable write with write concern errors publishes success event"
34+
operations:
35+
- name: failPoint
36+
object: testRunner
37+
arguments:
38+
client: *client
39+
failPoint:
40+
configureFailPoint: failCommand
41+
mode: { times: 1 }
42+
data:
43+
failCommands: [ insert ]
44+
writeConcernError:
45+
code: 91 # ShutdownInProgress
46+
errorLabels: [RetryableWriteError]
47+
- name: insertOne
48+
object: *collection
49+
arguments:
50+
document: { _id: 2, x: 22 }
51+
expectEvents:
52+
- client: *client
53+
events:
54+
- commandStartedEvent:
55+
command:
56+
insert: *collectionName
57+
documents:
58+
- { _id: 2, x: 22 }
59+
ordered: true
60+
commandName: insert
61+
databaseName: *databaseName
62+
- commandSucceededEvent:
63+
reply:
64+
ok: 1
65+
n: 1
66+
writeConcernError: { code: 91, errorLabels: [ "RetryableWriteError" ] }
67+
commandName: insert
68+
- commandStartedEvent:
69+
command:
70+
insert: *collectionName
71+
documents:
72+
- { _id: 2, x: 22 }
73+
ordered: true
74+
commandName: insert
75+
databaseName: *databaseName
76+
- commandSucceededEvent:
77+
reply:
78+
ok: 1
79+
n: 1
80+
commandName: insert

0 commit comments

Comments
 (0)