Skip to content

Commit 3f8765a

Browse files
authored
fix(NODE-3688): make handshake errors retryable (#3165)
1 parent ef99c27 commit 3f8765a

File tree

112 files changed

+2370
-47
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

112 files changed

+2370
-47
lines changed

src/cmap/connect.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,14 @@ import { LEGACY_HELLO_COMMAND } from '../constants';
1010
import {
1111
AnyError,
1212
MongoCompatibilityError,
13+
MongoError,
14+
MongoErrorLabel,
1315
MongoInvalidArgumentError,
1416
MongoNetworkError,
1517
MongoNetworkTimeoutError,
1618
MongoRuntimeError,
17-
MongoServerError
19+
MongoServerError,
20+
needsRetryableWriteLabel
1821
} from '../error';
1922
import { Callback, ClientMetadata, HostAddress, makeClientMetadata, ns } from '../utils';
2023
import { AuthContext, AuthProvider } from './auth/auth_provider';
@@ -182,7 +185,15 @@ function performInitialHandshake(
182185
);
183186
}
184187
provider.auth(authContext, err => {
185-
if (err) return callback(err);
188+
if (err) {
189+
if (err instanceof MongoError) {
190+
err.addErrorLabel(MongoErrorLabel.HandshakeError);
191+
if (needsRetryableWriteLabel(err, response.maxWireVersion)) {
192+
err.addErrorLabel(MongoErrorLabel.RetryableWriteError);
193+
}
194+
}
195+
return callback(err);
196+
}
186197
callback(undefined, conn);
187198
});
188199

src/error.ts

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,8 @@ export const MongoErrorLabel = Object.freeze({
8888
RetryableWriteError: 'RetryableWriteError',
8989
TransientTransactionError: 'TransientTransactionError',
9090
UnknownTransactionCommitResult: 'UnknownTransactionCommitResult',
91-
ResumableChangeStreamError: 'ResumableChangeStreamError'
91+
ResumableChangeStreamError: 'ResumableChangeStreamError',
92+
HandshakeError: 'HandshakeError'
9293
} as const);
9394

9495
/** @public */
@@ -744,21 +745,22 @@ const RETRYABLE_WRITE_ERROR_CODES = new Set<number>([
744745
]);
745746

746747
export function needsRetryableWriteLabel(error: Error, maxWireVersion: number): boolean {
747-
if (maxWireVersion >= 9) {
748-
// 4.4+ servers attach their own retryable write error
749-
return false;
750-
}
751748
// pre-4.4 server, then the driver adds an error label for every valid case
752749
// execute operation will only inspect the label, code/message logic is handled here
753-
754750
if (error instanceof MongoNetworkError) {
755751
return true;
756752
}
757753

758-
if (error instanceof MongoError && error.hasErrorLabel(MongoErrorLabel.RetryableWriteError)) {
759-
// Before 4.4 the error label can be one way of identifying retry
760-
// so we can return true if we have the label, but fall back to code checking below
761-
return true;
754+
if (error instanceof MongoError) {
755+
if (
756+
(maxWireVersion >= 9 || error.hasErrorLabel(MongoErrorLabel.RetryableWriteError)) &&
757+
!error.hasErrorLabel(MongoErrorLabel.HandshakeError)
758+
) {
759+
// If we already have the error label no need to add it again. 4.4+ servers add the label.
760+
// In the case where we have a handshake error, need to fall down to the logic checking
761+
// the codes.
762+
return false;
763+
}
762764
}
763765

764766
if (error instanceof MongoWriteConcernError) {
@@ -782,6 +784,10 @@ export function needsRetryableWriteLabel(error: Error, maxWireVersion: number):
782784
return false;
783785
}
784786

787+
export function isRetryableWriteError(error: MongoError): boolean {
788+
return error.hasErrorLabel(MongoErrorLabel.RetryableWriteError);
789+
}
790+
785791
/** Determines whether an error is something the driver should attempt to retry */
786792
export function isRetryableReadError(error: MongoError): boolean {
787793
const hasRetryableErrorCode =

src/operations/execute_operation.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import type { Document } from '../bson';
22
import {
33
isRetryableReadError,
4+
isRetryableWriteError,
45
MongoCompatibilityError,
56
MONGODB_ERROR_CODES,
67
MongoError,
7-
MongoErrorLabel,
88
MongoExpiredSessionError,
99
MongoNetworkError,
1010
MongoRuntimeError,
@@ -195,7 +195,7 @@ function executeWithServerSelection<TResult>(
195195
);
196196
}
197197

198-
if (isWriteOperation && !originalError.hasErrorLabel(MongoErrorLabel.RetryableWriteError)) {
198+
if (isWriteOperation && !isRetryableWriteError(originalError)) {
199199
return callback(originalError);
200200
}
201201

test/integration/retryable-reads/retryable_reads.spec.test.js

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
'use strict';
2-
1+
const path = require('path');
32
const { TestRunnerContext, generateTopologyTests } = require('../../tools/spec-runner');
43
const { loadSpecTests } = require('../../spec');
4+
const { runUnifiedSuite } = require('../../tools/unified-spec-runner/runner');
55

6-
describe('Retryable Reads', function () {
6+
describe('Retryable Reads (legacy)', function () {
77
const testContext = new TestRunnerContext();
8-
const testSuites = loadSpecTests('retryable-reads');
8+
const testSuites = loadSpecTests(path.join('retryable-reads', 'legacy'));
99

1010
after(() => testContext.teardown());
1111
before(function () {
@@ -28,3 +28,7 @@ describe('Retryable Reads', function () {
2828
);
2929
});
3030
});
31+
32+
describe('Retryable Reads (unified)', function () {
33+
runUnifiedSuite(loadSpecTests(path.join('retryable-reads', 'unified')));
34+
});

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import { expect } from 'chai';
2+
import * as path from 'path';
23

34
import type { Collection, Db, MongoClient } from '../../../src';
45
import { loadSpecTests } from '../../spec';
56
import { legacyRunOnToRunOnRequirement } from '../../tools/spec-runner';
7+
import { runUnifiedSuite } from '../../tools/unified-spec-runner/runner';
68
import { isAnyRequirementSatisfied } from '../../tools/unified-spec-runner/unified-utils';
79

810
interface RetryableWriteTestContext {
@@ -196,3 +198,7 @@ async function turnOffFailPoint(client, name) {
196198
mode: 'off'
197199
});
198200
}
201+
202+
describe('Retryable Writes (unified)', function () {
203+
runUnifiedSuite(loadSpecTests(path.join('retryable-writes', 'unified')));
204+
});

test/spec/retryable-reads/README.rst

Lines changed: 83 additions & 14 deletions

0 commit comments

Comments
 (0)