Skip to content

Commit cbe7533

Browse files
authored
feat(NODE-4650): handle handshake errors with SDAM (#3426)
1 parent 6b1cf88 commit cbe7533

File tree

5 files changed

+197
-23
lines changed

5 files changed

+197
-23
lines changed

src/cmap/connection_pool.ts

+10-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,13 @@ import {
1616
CONNECTION_POOL_READY,
1717
CONNECTION_READY
1818
} from '../constants';
19-
import { MongoError, MongoInvalidArgumentError, MongoRuntimeError } from '../error';
19+
import {
20+
MongoError,
21+
MongoInvalidArgumentError,
22+
MongoNetworkError,
23+
MongoRuntimeError,
24+
MongoServerError
25+
} from '../error';
2026
import { Logger } from '../logger';
2127
import { CancellationToken, TypedEventEmitter } from '../mongo_types';
2228
import type { Server } from '../sdam/server';
@@ -601,6 +607,9 @@ export class ConnectionPool extends TypedEventEmitter<ConnectionPoolEvents> {
601607
ConnectionPool.CONNECTION_CLOSED,
602608
new ConnectionClosedEvent(this, { id: connectOptions.id, serviceId: undefined }, 'error')
603609
);
610+
if (err instanceof MongoNetworkError || err instanceof MongoServerError) {
611+
err.connectionGeneration = connectOptions.generation;
612+
}
604613
callback(err ?? new MongoRuntimeError('Connection creation failed without error'));
605614
return;
606615
}

src/error.ts

+1
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ export class MongoError extends Error {
122122
*/
123123
code?: number | string;
124124
topologyVersion?: TopologyVersion;
125+
connectionGeneration?: number;
125126
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
126127
// @ts-ignore
127128
cause?: Error; // depending on the node version, this may or may not exist on the base

src/sdam/server.ts

+24-12
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import {
2929
MongoInvalidArgumentError,
3030
MongoNetworkError,
3131
MongoNetworkTimeoutError,
32+
MongoRuntimeError,
3233
MongoServerClosedError,
3334
MongoServerError,
3435
MongoUnexpectedServerResponseError,
@@ -348,8 +349,11 @@ export class Server extends TypedEventEmitter<ServerEvents> {
348349
(err, conn, cb) => {
349350
if (err || !conn) {
350351
this.s.operationCount -= 1;
352+
if (!err) {
353+
return cb(new MongoRuntimeError('Failed to create connection without error'));
354+
}
351355
if (!(err instanceof PoolClearedError)) {
352-
markServerUnknown(this, err);
356+
this.handleError(err);
353357
} else {
354358
err.addErrorLabel(MongoErrorLabel.RetryableWriteError);
355359
}
@@ -378,17 +382,25 @@ export class Server extends TypedEventEmitter<ServerEvents> {
378382
if (!(error instanceof MongoError)) {
379383
return;
380384
}
381-
if (error instanceof MongoNetworkError) {
382-
if (!(error instanceof MongoNetworkTimeoutError) || isNetworkErrorBeforeHandshake(error)) {
383-
// In load balanced mode we never mark the server as unknown and always
384-
// clear for the specific service id.
385-
386-
if (!this.loadBalanced) {
387-
error.addErrorLabel(MongoErrorLabel.ResetPool);
388-
markServerUnknown(this, error);
389-
} else if (connection) {
390-
this.s.pool.clear(connection.serviceId);
391-
}
385+
386+
const isStaleError =
387+
error.connectionGeneration && error.connectionGeneration < this.s.pool.generation;
388+
if (isStaleError) {
389+
return;
390+
}
391+
392+
const isNetworkNonTimeoutError =
393+
error instanceof MongoNetworkError && !(error instanceof MongoNetworkTimeoutError);
394+
const isNetworkTimeoutBeforeHandshakeError = isNetworkErrorBeforeHandshake(error);
395+
const isAuthHandshakeError = error.hasErrorLabel(MongoErrorLabel.HandshakeError);
396+
if (isNetworkNonTimeoutError || isNetworkTimeoutBeforeHandshakeError || isAuthHandshakeError) {
397+
// In load balanced mode we never mark the server as unknown and always
398+
// clear for the specific service id.
399+
if (!this.loadBalanced) {
400+
error.addErrorLabel(MongoErrorLabel.ResetPool);
401+
markServerUnknown(this, error);
402+
} else if (connection) {
403+
this.s.pool.clear(connection.serviceId);
392404
}
393405
} else {
394406
if (isSDAMUnrecoverableError(error)) {

test/integration/server-discovery-and-monitoring/server_discovery_and_monitoring.spec.test.ts

-10
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,7 @@ import { TestFilter } from '../../tools/unified-spec-runner/schema';
88
import { sleep } from '../../tools/utils';
99

1010
const filter: TestFilter = ({ description }) => {
11-
const isAuthEnabled = process.env.AUTH === 'auth';
1211
switch (description) {
13-
case 'Reset server and pool after AuthenticationFailure error':
14-
case 'Reset server and pool after misc command error':
15-
case 'Reset server and pool after network error during authentication':
16-
case 'Reset server and pool after network timeout error during authentication':
17-
case 'Reset server and pool after shutdown error during authentication':
18-
// These tests time out waiting for the PoolCleared event
19-
return isAuthEnabled
20-
? 'TODO(NODE-3135): handle auth errors, also see NODE-3891: fix tests broken when AUTH enabled'
21-
: false;
2212
case 'Network error on Monitor check':
2313
case 'Network timeout on Monitor check':
2414
return 'TODO(NODE-4608): Disallow parallel monitor checks';

test/unit/sdam/server.test.ts

+162
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
/* eslint-disable @typescript-eslint/no-non-null-assertion */
2+
import { expect } from 'chai';
3+
import { once } from 'events';
4+
import * as sinon from 'sinon';
5+
6+
import {
7+
Connection,
8+
MongoError,
9+
MongoErrorLabel,
10+
MongoNetworkError,
11+
MongoNetworkTimeoutError,
12+
ObjectId,
13+
ServerType,
14+
TopologyType
15+
} from '../../../src';
16+
import { Server } from '../../../src/sdam/server';
17+
import { ServerDescription } from '../../../src/sdam/server_description';
18+
import { Topology } from '../../../src/sdam/topology';
19+
import { sleep } from '../../tools/utils';
20+
21+
const handledErrors = [
22+
{
23+
description: 'any non-timeout network error',
24+
errorClass: MongoNetworkError,
25+
errorArgs: ['TestError']
26+
},
27+
{
28+
description: 'a network timeout error before handshake',
29+
errorClass: MongoNetworkTimeoutError,
30+
errorArgs: ['TestError', { beforeHandshake: true }]
31+
},
32+
{
33+
description: 'an auth handshake error',
34+
errorClass: MongoError,
35+
errorArgs: ['TestError'],
36+
errorLabel: MongoErrorLabel.HandshakeError
37+
}
38+
];
39+
40+
const unhandledErrors = [
41+
{
42+
description: 'a non-MongoError',
43+
errorClass: Error,
44+
errorArgs: ['TestError']
45+
},
46+
{
47+
description: 'a network timeout error after handshake',
48+
errorClass: MongoNetworkTimeoutError,
49+
errorArgs: ['TestError', { beforeHandshake: false }]
50+
},
51+
{
52+
description: 'a non-network non-handshake MongoError',
53+
errorClass: MongoError,
54+
errorArgs: ['TestError']
55+
}
56+
];
57+
58+
describe('Server', () => {
59+
describe('#handleError', () => {
60+
let server: Server, connection: Connection | undefined;
61+
beforeEach(() => {
62+
server = new Server(new Topology([], {} as any), new ServerDescription('a:1'), {} as any);
63+
});
64+
for (const loadBalanced of [true, false]) {
65+
const mode = loadBalanced ? 'loadBalanced' : 'non-loadBalanced';
66+
const contextSuffix = loadBalanced ? ' with connection provided' : '';
67+
context(`in ${mode} mode${contextSuffix}`, () => {
68+
beforeEach(() => {
69+
if (loadBalanced) {
70+
server.s.topology.description.type = TopologyType.LoadBalanced;
71+
connection = { serviceId: new ObjectId() } as Connection;
72+
server.s.pool.clear = sinon.stub();
73+
} else {
74+
connection = undefined;
75+
}
76+
});
77+
for (const { description, errorClass, errorArgs, errorLabel } of handledErrors) {
78+
const handledDescription = loadBalanced
79+
? `should reset the pool but not attach a ResetPool label to the error or mark the server unknown on ${description}`
80+
: `should attach a ResetPool label to the error and mark the server unknown on ${description}`;
81+
it(`${handledDescription}`, async () => {
82+
// @ts-expect-error because of varied number of args
83+
const error = new errorClass(...errorArgs);
84+
if (errorLabel) {
85+
error.addErrorLabel(errorLabel);
86+
}
87+
const newDescriptionEvent = Promise.race([
88+
once(server, Server.DESCRIPTION_RECEIVED),
89+
sleep(1000)
90+
]);
91+
server.handleError(error, connection);
92+
if (!loadBalanced) {
93+
expect(
94+
error.hasErrorLabel(MongoErrorLabel.ResetPool),
95+
'expected error to have a ResetPool label'
96+
).to.be.true;
97+
} else {
98+
expect(
99+
error.hasErrorLabel(MongoErrorLabel.ResetPool),
100+
'expected error NOT to have a ResetPool label'
101+
).to.be.false;
102+
}
103+
const newDescription = await newDescriptionEvent;
104+
if (!loadBalanced) {
105+
expect(newDescription).to.have.nested.property('[0].type', ServerType.Unknown);
106+
} else {
107+
expect(newDescription).to.be.undefined;
108+
expect(server.s.pool.clear).to.have.been.calledOnceWith(connection!.serviceId);
109+
}
110+
});
111+
112+
it(`should not attach a ResetPool label to the error or mark the server unknown on ${description} if it is stale`, async () => {
113+
// @ts-expect-error because of varied number of args
114+
const error = new errorClass(...errorArgs);
115+
if (errorLabel) {
116+
error.addErrorLabel(errorLabel);
117+
}
118+
119+
error.connectionGeneration = -1;
120+
expect(
121+
server.s.pool.generation,
122+
'expected test server to have a pool of generation 0'
123+
).to.equal(0); // sanity check
124+
125+
const newDescriptionEvent = Promise.race([
126+
once(server, Server.DESCRIPTION_RECEIVED),
127+
sleep(1000)
128+
]);
129+
server.handleError(error, connection);
130+
expect(
131+
error.hasErrorLabel(MongoErrorLabel.ResetPool),
132+
'expected error NOT to have a ResetPool label'
133+
).to.be.false;
134+
const newDescription = await newDescriptionEvent;
135+
expect(newDescription).to.be.undefined;
136+
});
137+
}
138+
139+
for (const { description, errorClass, errorArgs } of unhandledErrors) {
140+
it(`should not attach a ResetPool label to the error or mark the server unknown on ${description}`, async () => {
141+
// @ts-expect-error because of varied number of args
142+
const error = new errorClass(...errorArgs);
143+
144+
const newDescriptionEvent = Promise.race([
145+
once(server, Server.DESCRIPTION_RECEIVED),
146+
sleep(1000)
147+
]);
148+
server.handleError(error, connection);
149+
if (error instanceof MongoError) {
150+
expect(
151+
error.hasErrorLabel(MongoErrorLabel.ResetPool),
152+
'expected error NOT to have a ResetPool label'
153+
).to.be.false;
154+
}
155+
const newDescription = await newDescriptionEvent;
156+
expect(newDescription).to.be.undefined;
157+
});
158+
}
159+
});
160+
}
161+
});
162+
});

0 commit comments

Comments
 (0)