Skip to content

Commit d4e4438

Browse files
refactor(NODE-7323): more closely align SDAM error handling with spec pseudocode (#4803)
Co-authored-by: Pavel Safronov <pavel.safronov@mongodb.com>
1 parent bfb7160 commit d4e4438

File tree

3 files changed

+52
-59
lines changed

3 files changed

+52
-59
lines changed

src/error.ts

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1528,13 +1528,7 @@ export function isNodeShuttingDownError(err: MongoError): boolean {
15281528
*
15291529
* @see https://github.com/mongodb/specifications/blob/master/source/server-discovery-and-monitoring/server-discovery-and-monitoring.md#not-writable-primary-and-node-is-recovering
15301530
*/
1531-
export function isSDAMUnrecoverableError(error: MongoError): boolean {
1532-
// NOTE: null check is here for a strictly pre-CMAP world, a timeout or
1533-
// close event are considered unrecoverable
1534-
if (error instanceof MongoParseError || error == null) {
1535-
return true;
1536-
}
1537-
1531+
export function isStateChangeError(error: MongoError): boolean {
15381532
return isRecoveringError(error) || isNotWritablePrimaryError(error);
15391533
}
15401534

src/sdam/server.ts

Lines changed: 43 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,13 @@ import {
2222
import {
2323
type AnyError,
2424
isNodeShuttingDownError,
25-
isSDAMUnrecoverableError,
25+
isStateChangeError,
2626
MONGODB_ERROR_CODES,
2727
MongoError,
2828
MongoErrorLabel,
2929
MongoNetworkError,
3030
MongoNetworkTimeoutError,
31+
MongoParseError,
3132
MongoRuntimeError,
3233
MongoServerClosedError,
3334
type MongoServerError,
@@ -391,9 +392,7 @@ export class Server extends TypedEventEmitter<ServerEvents> {
391392
return;
392393
}
393394

394-
const isStaleError =
395-
error.connectionGeneration && error.connectionGeneration < this.pool.generation;
396-
if (isStaleError) {
395+
if (isStaleError(this, error)) {
397396
return;
398397
}
399398

@@ -402,32 +401,40 @@ export class Server extends TypedEventEmitter<ServerEvents> {
402401
const isNetworkTimeoutBeforeHandshakeError =
403402
error instanceof MongoNetworkError && error.beforeHandshake;
404403
const isAuthHandshakeError = error.hasErrorLabel(MongoErrorLabel.HandshakeError);
405-
if (isNetworkNonTimeoutError || isNetworkTimeoutBeforeHandshakeError || isAuthHandshakeError) {
406-
// In load balanced mode we never mark the server as unknown and always
407-
// clear for the specific service id.
404+
405+
// Perhaps questionable and divergent from the spec, but considering MongoParseErrors like state change errors was legacy behavior.
406+
if (isStateChangeError(error) || error instanceof MongoParseError) {
407+
const shouldClearPool = isNodeShuttingDownError(error);
408+
409+
// from the SDAM spec: The driver MUST synchronize clearing the pool with updating the topology.
410+
// In load balanced mode: there is no monitoring, so there is no topology to update. We simply clear the pool.
411+
// For other topologies: the `ResetPool` label instructs the topology to clear the server's pool in `updateServer()`.
412+
if (!this.loadBalanced) {
413+
if (shouldClearPool) {
414+
error.addErrorLabel(MongoErrorLabel.ResetPool);
415+
}
416+
markServerUnknown(this, error);
417+
process.nextTick(() => this.requestCheck());
418+
return;
419+
}
420+
421+
if (connection && shouldClearPool) {
422+
this.pool.clear({ serviceId: connection.serviceId });
423+
}
424+
} else if (
425+
isNetworkNonTimeoutError ||
426+
isNetworkTimeoutBeforeHandshakeError ||
427+
isAuthHandshakeError
428+
) {
429+
// from the SDAM spec: The driver MUST synchronize clearing the pool with updating the topology.
430+
// In load balanced mode: there is no monitoring, so there is no topology to update. We simply clear the pool.
431+
// For other topologies: the `ResetPool` label instructs the topology to clear the server's pool in `updateServer()`.
408432
if (!this.loadBalanced) {
409433
error.addErrorLabel(MongoErrorLabel.ResetPool);
410434
markServerUnknown(this, error);
411435
} else if (connection) {
412436
this.pool.clear({ serviceId: connection.serviceId });
413437
}
414-
} else {
415-
if (isSDAMUnrecoverableError(error)) {
416-
if (shouldHandleStateChangeError(this, error)) {
417-
const shouldClearPool = isNodeShuttingDownError(error);
418-
if (this.loadBalanced && connection && shouldClearPool) {
419-
this.pool.clear({ serviceId: connection.serviceId });
420-
}
421-
422-
if (!this.loadBalanced) {
423-
if (shouldClearPool) {
424-
error.addErrorLabel(MongoErrorLabel.ResetPool);
425-
}
426-
markServerUnknown(this, error);
427-
process.nextTick(() => this.requestCheck());
428-
}
429-
}
430-
}
431438
}
432439
}
433440

@@ -560,12 +567,6 @@ function connectionIsStale(pool: ConnectionPool, connection: Connection) {
560567
return connection.generation !== pool.generation;
561568
}
562569

563-
function shouldHandleStateChangeError(server: Server, err: MongoError) {
564-
const etv = err.topologyVersion;
565-
const stv = server.description.topologyVersion;
566-
return compareTopologyVersion(stv, etv) < 0;
567-
}
568-
569570
function inActiveTransaction(session: ClientSession | undefined, cmd: Document) {
570571
return session && session.inTransaction() && !isTransactionCommand(cmd);
571572
}
@@ -575,3 +576,15 @@ function inActiveTransaction(session: ClientSession | undefined, cmd: Document)
575576
function isRetryableWritesEnabled(topology: Topology) {
576577
return topology.s.options.retryWrites !== false;
577578
}
579+
580+
function isStaleError(server: Server, error: MongoError): boolean {
581+
const currentGeneration = server.pool.generation;
582+
const generation = error.connectionGeneration;
583+
584+
if (generation && generation < currentGeneration) {
585+
return true;
586+
}
587+
588+
const currentTopologyVersion = server.description.topologyVersion;
589+
return compareTopologyVersion(currentTopologyVersion, error.topologyVersion) >= 0;
590+
}

test/unit/error.test.ts

Lines changed: 8 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import * as importsFromErrorSrc from '../../src/error';
1010
import {
1111
isResumableError,
1212
isRetryableReadError,
13-
isSDAMUnrecoverableError,
13+
isStateChangeError,
1414
LEGACY_NOT_PRIMARY_OR_SECONDARY_ERROR_MESSAGE,
1515
LEGACY_NOT_WRITABLE_PRIMARY_ERROR_MESSAGE,
1616
MONGODB_ERROR_CODES,
@@ -26,7 +26,6 @@ import {
2626
MongoNetworkError,
2727
MongoNetworkTimeoutError,
2828
MongoOperationTimeoutError,
29-
MongoParseError,
3029
MongoRuntimeError,
3130
MongoServerError,
3231
MongoSystemError,
@@ -211,26 +210,13 @@ describe('MongoErrors', () => {
211210
});
212211
});
213212

214-
describe('#isSDAMUnrecoverableError', function () {
215-
context('when the error is a MongoParseError', function () {
216-
it('returns true', function () {
217-
const error = new MongoParseError('');
218-
expect(isSDAMUnrecoverableError(error)).to.be.true;
219-
});
220-
});
221-
222-
context('when the error is null', function () {
223-
it('returns true', function () {
224-
expect(isSDAMUnrecoverableError(null)).to.be.true;
225-
});
226-
});
227-
213+
describe('#isStateChangeError', function () {
228214
context('when the error has a "node is recovering" error code', function () {
229215
it('returns true', function () {
230216
const error = new MongoError('');
231217
// Code for NotPrimaryOrSecondary
232218
error.code = 13436;
233-
expect(isSDAMUnrecoverableError(error)).to.be.true;
219+
expect(isStateChangeError(error)).to.be.true;
234220
});
235221
});
236222

@@ -239,7 +225,7 @@ describe('MongoErrors', () => {
239225
const error = new MongoError('');
240226
// Code for NotWritablePrimary
241227
error.code = 10107;
242-
expect(isSDAMUnrecoverableError(error)).to.be.true;
228+
expect(isStateChangeError(error)).to.be.true;
243229
});
244230
});
245231

@@ -250,7 +236,7 @@ describe('MongoErrors', () => {
250236
// If the response includes an error code, it MUST be solely used to determine if error is a "node is recovering" or "not writable primary" error.
251237
const error = new MongoError(NODE_IS_RECOVERING_ERROR_MESSAGE.source);
252238
error.code = 555;
253-
expect(isSDAMUnrecoverableError(error)).to.be.false;
239+
expect(isStateChangeError(error)).to.be.false;
254240
});
255241
}
256242
);
@@ -262,7 +248,7 @@ describe('MongoErrors', () => {
262248
const error = new MongoError(
263249
`this is ${LEGACY_NOT_WRITABLE_PRIMARY_ERROR_MESSAGE.source}.`
264250
);
265-
expect(isSDAMUnrecoverableError(error)).to.be.true;
251+
expect(isStateChangeError(error)).to.be.true;
266252
});
267253
}
268254
);
@@ -272,7 +258,7 @@ describe('MongoErrors', () => {
272258
function () {
273259
it('returns true', function () {
274260
const error = new MongoError(`the ${NODE_IS_RECOVERING_ERROR_MESSAGE} from an error`);
275-
expect(isSDAMUnrecoverableError(error)).to.be.true;
261+
expect(isStateChangeError(error)).to.be.true;
276262
});
277263
}
278264
);
@@ -284,7 +270,7 @@ describe('MongoErrors', () => {
284270
const error = new MongoError(
285271
`this is ${LEGACY_NOT_PRIMARY_OR_SECONDARY_ERROR_MESSAGE}, so we have a problem `
286272
);
287-
expect(isSDAMUnrecoverableError(error)).to.be.true;
273+
expect(isStateChangeError(error)).to.be.true;
288274
});
289275
}
290276
);

0 commit comments

Comments
 (0)