Skip to content

Commit b14ba21

Browse files
fix(NODE-7482): explicitly call setKeepAlive and setNoDelay on socket (#4900)
1 parent 237c9ab commit b14ba21

File tree

2 files changed

+130
-2
lines changed

2 files changed

+130
-2
lines changed

src/cmap/connect.ts

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

282+
/**
283+
* @internal
284+
* Default TCP keepAlive initial delay in milliseconds.
285+
* Set to half the Azure load balancer idle timeout (240s) to ensure
286+
* probes fire well before cloud LBs (Azure, AWS PrivateLink/NLB)
287+
* drop idle connections.
288+
*/
289+
export const DEFAULT_KEEP_ALIVE_INITIAL_DELAY_MS = 120_000;
290+
282291
/** @public */
283292
export const LEGAL_TLS_SOCKET_OPTIONS = [
284293
'allowPartialTrustChain',
@@ -322,7 +331,7 @@ function parseConnectOptions(options: ConnectionOptions): SocketConnectOpts {
322331
(result as Document)[name] = options[name];
323332
}
324333
}
325-
result.keepAliveInitialDelay ??= 120000;
334+
result.keepAliveInitialDelay ??= DEFAULT_KEEP_ALIVE_INITIAL_DELAY_MS;
326335
result.keepAlive = true;
327336
result.noDelay = options.noDelay ?? true;
328337

@@ -368,6 +377,9 @@ export async function makeSocket(options: MakeConnectionOptions): Promise<Stream
368377
const useTLS = options.tls ?? false;
369378
const connectTimeoutMS = options.connectTimeoutMS ?? 30000;
370379
const existingSocket = options.existingSocket;
380+
const keepAliveInitialDelay =
381+
options.keepAliveInitialDelay ?? DEFAULT_KEEP_ALIVE_INITIAL_DELAY_MS;
382+
const noDelay = options.noDelay ?? true;
371383

372384
let socket: Stream;
373385

@@ -394,6 +406,12 @@ export async function makeSocket(options: MakeConnectionOptions): Promise<Stream
394406
socket = net.createConnection(parseConnectOptions(options));
395407
}
396408

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

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

test/unit/cmap/connect.test.ts

Lines changed: 111 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,18 @@
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 { MongoCredentials } from '../../../src/cmap/auth/mongo_credentials';
5-
import { connect, prepareHandshakeDocument } from '../../../src/cmap/connect';
10+
import {
11+
connect,
12+
DEFAULT_KEEP_ALIVE_INITIAL_DELAY_MS,
13+
makeSocket,
14+
prepareHandshakeDocument
15+
} from '../../../src/cmap/connect';
616
import { type Connection, type ConnectionOptions } from '../../../src/cmap/connection';
717
import {
818
type ClientMetadata,
@@ -434,4 +444,104 @@ describe('Connect Tests', function () {
434444
});
435445
});
436446
});
447+
448+
describe('makeSocket', function () {
449+
let tlsServer: tls.Server;
450+
let tlsPort: number;
451+
let setKeepAliveSpy: sinon.SinonSpy;
452+
let setNoDelaySpy: sinon.SinonSpy;
453+
454+
const serverPem = fs.readFileSync(
455+
path.join(__dirname, '../../integration/auth/ssl/server.pem')
456+
);
457+
458+
before(function (done) {
459+
// @SECLEVEL=0 allows the legacy test certificate (signed with SHA-1/1024-bit RSA)
460+
// to be accepted by OpenSSL 3.x, which rejects at the default security level.
461+
tlsServer = tls.createServer(
462+
{ key: serverPem, cert: serverPem, ciphers: 'DEFAULT:@SECLEVEL=0' },
463+
() => {
464+
/* empty */
465+
}
466+
);
467+
tlsServer.listen(0, '127.0.0.1', () => {
468+
tlsPort = (tlsServer.address() as net.AddressInfo).port;
469+
done();
470+
});
471+
});
472+
473+
after(function () {
474+
tlsServer?.close();
475+
});
476+
477+
beforeEach(function () {
478+
setKeepAliveSpy = sinon.spy(net.Socket.prototype, 'setKeepAlive');
479+
setNoDelaySpy = sinon.spy(net.Socket.prototype, 'setNoDelay');
480+
});
481+
482+
afterEach(function () {
483+
sinon.restore();
484+
});
485+
486+
context('when tls is enabled', function () {
487+
it('calls setKeepAlive with default keepAliveInitialDelay', async function () {
488+
const socket = await makeSocket({
489+
hostAddress: new HostAddress(`127.0.0.1:${tlsPort}`),
490+
tls: true,
491+
rejectUnauthorized: false,
492+
ciphers: 'DEFAULT:@SECLEVEL=0'
493+
} as ConnectionOptions);
494+
socket.destroy();
495+
496+
expect(setKeepAliveSpy).to.have.been.calledWith(true, DEFAULT_KEEP_ALIVE_INITIAL_DELAY_MS);
497+
});
498+
499+
it('calls setKeepAlive with custom keepAliveInitialDelay', async function () {
500+
const socket = await makeSocket({
501+
hostAddress: new HostAddress(`127.0.0.1:${tlsPort}`),
502+
tls: true,
503+
rejectUnauthorized: false,
504+
ciphers: 'DEFAULT:@SECLEVEL=0',
505+
keepAliveInitialDelay: 5000
506+
} as ConnectionOptions);
507+
socket.destroy();
508+
509+
expect(setKeepAliveSpy).to.have.been.calledWith(true, 5000);
510+
});
511+
512+
it('calls setNoDelay with true by default', async function () {
513+
const socket = await makeSocket({
514+
hostAddress: new HostAddress(`127.0.0.1:${tlsPort}`),
515+
tls: true,
516+
rejectUnauthorized: false,
517+
ciphers: 'DEFAULT:@SECLEVEL=0'
518+
} as ConnectionOptions);
519+
socket.destroy();
520+
521+
expect(setNoDelaySpy).to.have.been.calledWith(true);
522+
});
523+
});
524+
525+
context('when tls is disabled', function () {
526+
it('calls setKeepAlive with default keepAliveInitialDelay', async function () {
527+
const socket = await makeSocket({
528+
hostAddress: new HostAddress(`127.0.0.1:${tlsPort}`),
529+
tls: false
530+
} as ConnectionOptions);
531+
socket.destroy();
532+
533+
expect(setKeepAliveSpy).to.have.been.calledWith(true, DEFAULT_KEEP_ALIVE_INITIAL_DELAY_MS);
534+
});
535+
536+
it('calls setNoDelay with true by default', async function () {
537+
const socket = await makeSocket({
538+
hostAddress: new HostAddress(`127.0.0.1:${tlsPort}`),
539+
tls: false
540+
} as ConnectionOptions);
541+
socket.destroy();
542+
543+
expect(setNoDelaySpy).to.have.been.calledWith(true);
544+
});
545+
});
546+
});
437547
});

0 commit comments

Comments
 (0)