Skip to content

Commit 778a2a1

Browse files
fix(NODE-7459): explicitly call setKeepAlive and setNoDelay on socket (#4879)
Co-authored-by: Pavel Safronov <pavel.safronov@gmail.com>
1 parent 0634c9b commit 778a2a1

File tree

2 files changed

+126
-1
lines changed

2 files changed

+126
-1
lines changed

src/cmap/connect.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,15 @@ export async function prepareHandshakeDocument(
281281
return handshakeDoc;
282282
}
283283

284+
/**
285+
* @internal
286+
* Default TCP keepAlive initial delay in milliseconds.
287+
* Set to half the Azure load balancer idle timeout (240s) to ensure
288+
* probes fire well before cloud LBs (Azure, AWS PrivateLink/NLB)
289+
* drop idle connections.
290+
*/
291+
export const DEFAULT_KEEP_ALIVE_INITIAL_DELAY_MS = 120_000;
292+
284293
/** @public */
285294
export const LEGAL_TLS_SOCKET_OPTIONS = [
286295
'allowPartialTrustChain',
@@ -324,7 +333,7 @@ function parseConnectOptions(options: ConnectionOptions): SocketConnectOpts {
324333
(result as Document)[name] = options[name];
325334
}
326335
}
327-
result.keepAliveInitialDelay ??= 120000;
336+
result.keepAliveInitialDelay ??= DEFAULT_KEEP_ALIVE_INITIAL_DELAY_MS;
328337
result.keepAlive = true;
329338
result.noDelay = options.noDelay ?? true;
330339

@@ -370,6 +379,9 @@ export async function makeSocket(options: MakeConnectionOptions): Promise<Stream
370379
const useTLS = options.tls ?? false;
371380
const connectTimeoutMS = options.connectTimeoutMS ?? 30000;
372381
const existingSocket = options.existingSocket;
382+
const keepAliveInitialDelay =
383+
options.keepAliveInitialDelay ?? DEFAULT_KEEP_ALIVE_INITIAL_DELAY_MS;
384+
const noDelay = options.noDelay ?? true;
373385

374386
let socket: Stream;
375387

@@ -396,6 +408,12 @@ export async function makeSocket(options: MakeConnectionOptions): Promise<Stream
396408
socket = net.createConnection(parseConnectOptions(options));
397409
}
398410

411+
// Explicit setKeepAlive/setNoDelay are required because tls.connect() silently
412+
// ignores these constructor options due to a Node.js bug.
413+
// See: https://github.com/nodejs/node/issues/62003
414+
// TODO(NODE-7474): remove this fix once the underlying Node.js issue is resolved.
415+
socket.setKeepAlive(true, keepAliveInitialDelay);
416+
socket.setNoDelay(noDelay);
399417
socket.setTimeout(connectTimeoutMS);
400418

401419
let cancellationHandler: ((err: Error) => void) | null = null;

test/unit/cmap/connect.test.ts

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,23 @@
11
import { expect } from 'chai';
2+
import * as fs from 'fs';
3+
import * as net from 'net';
4+
import * as path from 'path';
25
import * as process from 'process';
6+
import * as sinon from 'sinon';
7+
import * as tls from 'tls';
38

49
import {
510
CancellationToken,
611
type ClientMetadata,
712
connect,
813
type Connection,
914
type ConnectionOptions,
15+
DEFAULT_KEEP_ALIVE_INITIAL_DELAY_MS,
1016
HostAddress,
1117
isHello,
1218
LEGACY_HELLO_COMMAND,
1319
makeClientMetadata,
20+
makeSocket,
1421
MongoClientAuthProviders,
1522
MongoCredentials,
1623
MongoNetworkError,
@@ -448,4 +455,104 @@ describe('Connect Tests', function () {
448455
});
449456
});
450457
});
458+
459+
describe('makeSocket', function () {
460+
let tlsServer: tls.Server;
461+
let tlsPort: number;
462+
let setKeepAliveSpy: sinon.SinonSpy;
463+
let setNoDelaySpy: sinon.SinonSpy;
464+
465+
const serverPem = fs.readFileSync(
466+
path.join(__dirname, '../../integration/auth/ssl/server.pem')
467+
);
468+
469+
before(function (done) {
470+
// @SECLEVEL=0 allows the legacy test certificate (signed with SHA-1/1024-bit RSA)
471+
// to be accepted by OpenSSL 3.x, which rejects at the default security level.
472+
tlsServer = tls.createServer(
473+
{ key: serverPem, cert: serverPem, ciphers: 'DEFAULT:@SECLEVEL=0' },
474+
() => {
475+
/* empty */
476+
}
477+
);
478+
tlsServer.listen(0, '127.0.0.1', () => {
479+
tlsPort = (tlsServer.address() as net.AddressInfo).port;
480+
done();
481+
});
482+
});
483+
484+
after(function () {
485+
tlsServer?.close();
486+
});
487+
488+
beforeEach(function () {
489+
setKeepAliveSpy = sinon.spy(net.Socket.prototype, 'setKeepAlive');
490+
setNoDelaySpy = sinon.spy(net.Socket.prototype, 'setNoDelay');
491+
});
492+
493+
afterEach(function () {
494+
sinon.restore();
495+
});
496+
497+
context('when tls is enabled', function () {
498+
it('calls setKeepAlive with default keepAliveInitialDelay', async function () {
499+
const socket = await makeSocket({
500+
hostAddress: new HostAddress(`127.0.0.1:${tlsPort}`),
501+
tls: true,
502+
rejectUnauthorized: false,
503+
ciphers: 'DEFAULT:@SECLEVEL=0'
504+
} as ConnectionOptions);
505+
socket.destroy();
506+
507+
expect(setKeepAliveSpy).to.have.been.calledWith(true, DEFAULT_KEEP_ALIVE_INITIAL_DELAY_MS);
508+
});
509+
510+
it('calls setKeepAlive with custom keepAliveInitialDelay', async function () {
511+
const socket = await makeSocket({
512+
hostAddress: new HostAddress(`127.0.0.1:${tlsPort}`),
513+
tls: true,
514+
rejectUnauthorized: false,
515+
ciphers: 'DEFAULT:@SECLEVEL=0',
516+
keepAliveInitialDelay: 5000
517+
} as ConnectionOptions);
518+
socket.destroy();
519+
520+
expect(setKeepAliveSpy).to.have.been.calledWith(true, 5000);
521+
});
522+
523+
it('calls setNoDelay with true by default', async function () {
524+
const socket = await makeSocket({
525+
hostAddress: new HostAddress(`127.0.0.1:${tlsPort}`),
526+
tls: true,
527+
rejectUnauthorized: false,
528+
ciphers: 'DEFAULT:@SECLEVEL=0'
529+
} as ConnectionOptions);
530+
socket.destroy();
531+
532+
expect(setNoDelaySpy).to.have.been.calledWith(true);
533+
});
534+
});
535+
536+
context('when tls is disabled', function () {
537+
it('calls setKeepAlive with default keepAliveInitialDelay', async function () {
538+
const socket = await makeSocket({
539+
hostAddress: new HostAddress(`127.0.0.1:${tlsPort}`),
540+
tls: false
541+
} as ConnectionOptions);
542+
socket.destroy();
543+
544+
expect(setKeepAliveSpy).to.have.been.calledWith(true, DEFAULT_KEEP_ALIVE_INITIAL_DELAY_MS);
545+
});
546+
547+
it('calls setNoDelay with true by default', async function () {
548+
const socket = await makeSocket({
549+
hostAddress: new HostAddress(`127.0.0.1:${tlsPort}`),
550+
tls: false
551+
} as ConnectionOptions);
552+
socket.destroy();
553+
554+
expect(setNoDelaySpy).to.have.been.calledWith(true);
555+
});
556+
});
557+
});
451558
});

0 commit comments

Comments
 (0)