Skip to content

Commit c775a4a

Browse files
committed
feat: expand use of error labels for retryable writes
The work here encompasses changes to the retryable writes logic of the driver, such that the `RetryableWriteError` label becomes the primary means of determining whether an operation will be retried. 4.4+ servers will attach this label server-side, so this change allows us to gracefully remove client-side checking of retryable write errors. NODE-2379
1 parent 7bbad9c commit c775a4a

12 files changed

+86
-19
lines changed

lib/core/error.js

+32-1
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,10 @@ class MongoWriteConcernError extends MongoError {
168168
super(message);
169169
this.name = 'MongoWriteConcernError';
170170

171+
if (result && Array.isArray(result.errorLabels)) {
172+
this[kErrorLabels] = new Set(result.errorLabels);
173+
}
174+
171175
if (result != null) {
172176
this.result = makeWriteConcernResultObject(result);
173177
}
@@ -189,6 +193,32 @@ const RETRYABLE_ERROR_CODES = new Set([
189193
13436 // NotMasterOrSecondary
190194
]);
191195

196+
const RETRYABLE_WRITE_ERROR_CODES = new Set([
197+
11600, // InterruptedAtShutdown
198+
11602, // InterruptedDueToReplStateChange
199+
10107, // NotMaster
200+
13435, // NotMasterNoSlaveOk
201+
13436, // NotMasterOrSecondary
202+
189, // PrimarySteppedDown
203+
91, // ShutdownInProgress
204+
7, // HostNotFound
205+
6, // HostUnreachable
206+
89, // NetworkTimeout
207+
9001, // SocketException
208+
262 // ExceededTimeLimit
209+
]);
210+
211+
function isRetryableWriteError(error) {
212+
if (error instanceof MongoWriteConcernError) {
213+
return (
214+
RETRYABLE_WRITE_ERROR_CODES.has(error.code) ||
215+
RETRYABLE_WRITE_ERROR_CODES.has(error.result.code)
216+
);
217+
}
218+
219+
return RETRYABLE_WRITE_ERROR_CODES.has(error.code);
220+
}
221+
192222
/**
193223
* Determines whether an error is something the driver should attempt to retry
194224
*
@@ -284,5 +314,6 @@ module.exports = {
284314
isRetryableError,
285315
isSDAMUnrecoverableError,
286316
isNodeShuttingDownError,
287-
isNetworkTimeoutError
317+
isNetworkTimeoutError,
318+
isRetryableWriteError
288319
};

lib/core/sdam/server.js

+23-2
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,10 @@ const collationNotSupported = require('../utils').collationNotSupported;
1414
const debugOptions = require('../connection/utils').debugOptions;
1515
const isSDAMUnrecoverableError = require('../error').isSDAMUnrecoverableError;
1616
const isNetworkTimeoutError = require('../error').isNetworkTimeoutError;
17+
const isRetryableWriteError = require('../error').isRetryableWriteError;
1718
const makeStateMachine = require('../utils').makeStateMachine;
1819
const common = require('./common');
20+
const ServerType = common.ServerType;
1921

2022
// Used for filtering out fields for logging
2123
const DEBUG_FIELDS = [
@@ -404,6 +406,14 @@ Object.defineProperty(Server.prototype, 'clusterTime', {
404406
}
405407
});
406408

409+
function supportsRetryableWrites(server) {
410+
return (
411+
server.description.maxWireVersion >= 6 &&
412+
server.description.logicalSessionTimeoutMinutes &&
413+
server.description.type !== ServerType.Standalone
414+
);
415+
}
416+
407417
function calculateRoundTripTime(oldRtt, duration) {
408418
const alpha = 0.2;
409419
return alpha * duration + (1 - alpha) * oldRtt;
@@ -449,11 +459,22 @@ function makeOperationHandler(server, options, callback) {
449459
options.session.serverSession.isDirty = true;
450460
}
451461

462+
if (supportsRetryableWrites(server)) {
463+
err.addErrorLabel('RetryableWriteError');
464+
}
465+
452466
if (!isNetworkTimeoutError(err)) {
453467
server.emit('error', err);
454468
}
455-
} else if (isSDAMUnrecoverableError(err)) {
456-
server.emit('error', err);
469+
} else {
470+
// if pre-4.4 server, then add error label if its a retryable write error
471+
if (server.description.maxWireVersion < 9 && isRetryableWriteError(err)) {
472+
err.addErrorLabel('RetryableWriteError');
473+
}
474+
475+
if (isSDAMUnrecoverableError(err)) {
476+
server.emit('error', err);
477+
}
457478
}
458479
}
459480

lib/core/sdam/topology.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -680,7 +680,7 @@ class Topology extends EventEmitter {
680680

681681
const cb = (err, result) => {
682682
if (!err) return callback(null, result);
683-
if (!isRetryableError(err)) {
683+
if (!err.hasErrorLabel('RetryableWriteError')) {
684684
return callback(err);
685685
}
686686

@@ -944,7 +944,7 @@ function executeWriteOperation(args, options, callback) {
944944

945945
const handler = (err, result) => {
946946
if (!err) return callback(null, result);
947-
if (!isRetryableError(err)) {
947+
if (!err.hasErrorLabel('RetryableWriteError')) {
948948
err = getMMAPError(err);
949949
return callback(err);
950950
}

test/functional/retryable_writes.test.js

+5-1
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,11 @@ function executeScenarioSetup(scenario, test, config, ctx) {
7171
throw err;
7272
}
7373
})
74-
.then(() => (scenario.data ? ctx.collection.insertMany(scenario.data) : {}))
74+
.then(() =>
75+
Array.isArray(scenario.data) && scenario.data.length
76+
? ctx.collection.insertMany(scenario.data)
77+
: {}
78+
)
7579
.then(() => (test.failPoint ? ctx.db.executeDbAdminCommand(test.failPoint) : {}));
7680
}
7781

test/spec/retryable-writes/bulkWrite-errorLabels.json

+4-4
Original file line numberDiff line numberDiff line change
@@ -166,13 +166,13 @@
166166
},
167167
"collection": {
168168
"data": [
169-
{
170-
"_id": 1,
171-
"x": 11
172-
},
173169
{
174170
"_id": 2,
175171
"x": 22
172+
},
173+
{
174+
"_id": 3,
175+
"x": 33
176176
}
177177
]
178178
}

test/spec/retryable-writes/bulkWrite-errorLabels.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -73,5 +73,5 @@ tests:
7373
errorLabelsOmit: ["RetryableWriteError"]
7474
collection:
7575
data:
76-
- { _id: 1, x: 11 }
7776
- { _id: 2, x: 22 }
77+
- { _id: 3, x: 33 }

test/spec/retryable-writes/bulkWrite-serverErrors.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -252,7 +252,7 @@
252252
"data": [
253253
{
254254
"_id": 2,
255-
"x": 23
255+
"x": 22
256256
},
257257
{
258258
"_id": 3,

test/spec/retryable-writes/bulkWrite-serverErrors.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -124,5 +124,5 @@ tests:
124124
errorLabelsContain: ["RetryableWriteError"]
125125
collection:
126126
data:
127-
- { _id: 2, x: 23 }
127+
- { _id: 2, x: 22 }
128128
- { _id: 3, x: 33 }

test/spec/retryable-writes/findOneAndReplace-errorLabels.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
},
2929
"data": {
3030
"failCommands": [
31-
"findOneAndModify"
31+
"findAndModify"
3232
],
3333
"errorCode": 112,
3434
"errorLabels": [
@@ -77,7 +77,7 @@
7777
},
7878
"data": {
7979
"failCommands": [
80-
"findOneAndModify"
80+
"findAndModify"
8181
],
8282
"errorCode": 11600,
8383
"errorLabels": []

test/spec/retryable-writes/findOneAndReplace-errorLabels.yml

+2-2
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ tests:
1212
configureFailPoint: failCommand
1313
mode: { times: 1 }
1414
data:
15-
failCommands: ["findOneAndModify"]
15+
failCommands: ["findAndModify"]
1616
errorCode: 112 # WriteConflict, not a retryable error code
1717
errorLabels: ["RetryableWriteError"] # Override server behavior: send RetryableWriteError label with non-retryable error code
1818
operation:
@@ -33,7 +33,7 @@ tests:
3333
configureFailPoint: failCommand
3434
mode: { times: 1 }
3535
data:
36-
failCommands: ["findOneAndModify"]
36+
failCommands: ["findAndModify"]
3737
errorCode: 11600 # InterruptedAtShutdown, normally a retryable error code
3838
errorLabels: [] # Override server behavior: do not send RetryableWriteError label with retryable code
3939
operation:

test/spec/retryable-writes/insertOne-serverErrors.json

+10-1
Original file line numberDiff line numberDiff line change
@@ -1046,7 +1046,16 @@
10461046
]
10471047
},
10481048
"collection": {
1049-
"data": []
1049+
"data": [
1050+
{
1051+
"_id": 1,
1052+
"x": 11
1053+
},
1054+
{
1055+
"_id": 2,
1056+
"x": 22
1057+
}
1058+
]
10501059
}
10511060
}
10521061
}

test/spec/retryable-writes/insertOne-serverErrors.yml

+3-1
Original file line numberDiff line numberDiff line change
@@ -481,4 +481,6 @@ tests:
481481
result:
482482
errorLabelsContain: ["RetryableWriteError"]
483483
collection:
484-
data: []
484+
data:
485+
- { _id: 1, x: 11 }
486+
- { _id: 2, x: 22 }

0 commit comments

Comments
 (0)