diff --git a/src/cmap/connect.ts b/src/cmap/connect.ts index 394b70689c..aa73e88e5f 100644 --- a/src/cmap/connect.ts +++ b/src/cmap/connect.ts @@ -289,6 +289,7 @@ export const LEGAL_TLS_SOCKET_OPTIONS = [ export const LEGAL_TCP_SOCKET_OPTIONS = [ 'autoSelectFamily', 'autoSelectFamilyAttemptTimeout', + 'keepAliveInitialDelay', 'family', 'hints', 'localAddress', @@ -306,6 +307,9 @@ function parseConnectOptions(options: ConnectionOptions): SocketConnectOpts { (result as Document)[name] = options[name]; } } + result.keepAliveInitialDelay ??= 120000; + result.keepAlive = true; + result.noDelay = options.noDelay ?? true; if (typeof hostAddress.socketPath === 'string') { result.path = hostAddress.socketPath; @@ -347,7 +351,6 @@ function parseSslOptions(options: MakeConnectionOptions): TLSConnectionOpts { export async function makeSocket(options: MakeConnectionOptions): Promise { const useTLS = options.tls ?? false; - const noDelay = options.noDelay ?? true; const connectTimeoutMS = options.connectTimeoutMS ?? 30000; const existingSocket = options.existingSocket; @@ -376,9 +379,7 @@ export async function makeSocket(options: MakeConnectionOptions): Promise void) | null = null; diff --git a/src/connection_string.ts b/src/connection_string.ts index f1b23e5ac7..09e4e280a3 100644 --- a/src/connection_string.ts +++ b/src/connection_string.ts @@ -1273,6 +1273,7 @@ export const OPTIONS = { requestCert: { type: 'any' }, rejectUnauthorized: { type: 'any' }, checkServerIdentity: { type: 'any' }, + keepAliveInitialDelay: { type: 'any' }, ALPNProtocols: { type: 'any' }, SNICallback: { type: 'any' }, session: { type: 'any' }, diff --git a/src/mongo_client.ts b/src/mongo_client.ts index 9fe8d6cd40..e77afc4026 100644 --- a/src/mongo_client.ts +++ b/src/mongo_client.ts @@ -115,7 +115,12 @@ export type SupportedTLSSocketOptions = Pick< /** @public */ export type SupportedSocketOptions = Pick< - TcpNetConnectOpts & { autoSelectFamily?: boolean; autoSelectFamilyAttemptTimeout?: number }, + TcpNetConnectOpts & { + autoSelectFamily?: boolean; + autoSelectFamilyAttemptTimeout?: number; + /** Node.JS socket option to set the time the first keepalive probe is sent on an idle socket. Defaults to 120000ms */ + keepAliveInitialDelay?: number; + }, (typeof LEGAL_TCP_SOCKET_OPTIONS)[number] >; diff --git a/test/integration/node-specific/mongo_client.test.ts b/test/integration/node-specific/mongo_client.test.ts index 4a97035d55..9a0cea014b 100644 --- a/test/integration/node-specific/mongo_client.test.ts +++ b/test/integration/node-specific/mongo_client.test.ts @@ -135,6 +135,206 @@ describe('class MongoClient', function () { expect(error).to.be.instanceOf(MongoServerSelectionError); }); + describe('#connect', function () { + context('when keepAliveInitialDelay is provided', function () { + context('when the value is 0', function () { + const options = { keepAliveInitialDelay: 0 }; + let client; + let spy; + + beforeEach(async function () { + spy = sinon.spy(net, 'createConnection'); + const uri = this.configuration.url(); + client = new MongoClient(uri, options); + await client.connect(); + }); + + afterEach(async function () { + await client?.close(); + spy.restore(); + }); + + it('passes through the option', { + metadata: { requires: { apiVersion: false } }, + test: function () { + expect(spy).to.have.been.calledWith( + sinon.match({ + keepAlive: true, + keepAliveInitialDelay: 0 + }) + ); + } + }); + }); + + context('when the value is positive', function () { + const options = { keepAliveInitialDelay: 100 }; + let client; + let spy; + + beforeEach(async function () { + spy = sinon.spy(net, 'createConnection'); + const uri = this.configuration.url(); + client = new MongoClient(uri, options); + await client.connect(); + }); + + afterEach(async function () { + await client?.close(); + spy.restore(); + }); + + it('passes through the option', { + metadata: { requires: { apiVersion: false } }, + test: function () { + expect(spy).to.have.been.calledWith( + sinon.match({ + keepAlive: true, + keepAliveInitialDelay: 100 + }) + ); + } + }); + }); + + context('when the value is negative', function () { + const options = { keepAliveInitialDelay: -100 }; + let client; + let spy; + + beforeEach(async function () { + spy = sinon.spy(net, 'createConnection'); + const uri = this.configuration.url(); + client = new MongoClient(uri, options); + await client.connect(); + }); + + afterEach(async function () { + await client?.close(); + spy.restore(); + }); + + it('the Node.js runtime sets the option to 0', { + metadata: { requires: { apiVersion: false } }, + test: function () { + expect(spy).to.have.been.calledWith( + sinon.match({ + keepAlive: true, + keepAliveInitialDelay: 0 + }) + ); + } + }); + }); + + context('when the value is mistyped', function () { + // Set server selection timeout to get the error quicker. + const options = { keepAliveInitialDelay: 'test', serverSelectionTimeoutMS: 1000 }; + let client; + let spy; + + beforeEach(async function () { + spy = sinon.spy(net, 'createConnection'); + const uri = this.configuration.url(); + client = new MongoClient(uri, options); + }); + + afterEach(async function () { + await client?.close(); + spy.restore(); + }); + + it('throws an error', { + metadata: { requires: { apiVersion: false } }, + test: async function () { + const error = await client.connect().catch(error => error); + expect(error.message).to.include( + 'property must be of type number. Received type string' + ); + } + }); + }); + }); + + context('when keepAliveInitialDelay is not provided', function () { + let client; + let spy; + + beforeEach(async function () { + spy = sinon.spy(net, 'createConnection'); + client = this.configuration.newClient(); + await client.connect(); + }); + + afterEach(async function () { + await client?.close(); + spy.restore(); + }); + + it('sets keepalive to 120000', function () { + expect(spy).to.have.been.calledWith( + sinon.match({ + keepAlive: true, + keepAliveInitialDelay: 120000 + }) + ); + }); + }); + + context('when noDelay is not provided', function () { + let client; + let spy; + + beforeEach(async function () { + spy = sinon.spy(net, 'createConnection'); + client = this.configuration.newClient(); + await client.connect(); + }); + + afterEach(async function () { + await client?.close(); + spy.restore(); + }); + + it('sets noDelay to true', function () { + expect(spy).to.have.been.calledWith( + sinon.match({ + noDelay: true + }) + ); + }); + }); + + context('when noDelay is provided', function () { + let client; + let spy; + + beforeEach(async function () { + const options = { noDelay: false }; + spy = sinon.spy(net, 'createConnection'); + const uri = this.configuration.url(); + client = new MongoClient(uri, options); + await client.connect(); + }); + + afterEach(async function () { + await client?.close(); + spy.restore(); + }); + + it('sets noDelay', { + metadata: { requires: { apiVersion: false } }, + test: function () { + expect(spy).to.have.been.calledWith( + sinon.match({ + noDelay: false + }) + ); + } + }); + }); + }); + it('Should correctly pass through appname', { metadata: { requires: { @@ -889,12 +1089,12 @@ describe('class MongoClient', function () { metadata: { requires: { topology: ['single'] } }, test: async function () { await client.connect(); - expect(netSpy).to.have.been.calledWith({ - autoSelectFamily: false, - autoSelectFamilyAttemptTimeout: 100, - host: 'localhost', - port: 27017 - }); + expect(netSpy).to.have.been.calledWith( + sinon.match({ + autoSelectFamily: false, + autoSelectFamilyAttemptTimeout: 100 + }) + ); } }); }); @@ -908,11 +1108,11 @@ describe('class MongoClient', function () { metadata: { requires: { topology: ['single'] } }, test: async function () { await client.connect(); - expect(netSpy).to.have.been.calledWith({ - autoSelectFamily: true, - host: 'localhost', - port: 27017 - }); + expect(netSpy).to.have.been.calledWith( + sinon.match({ + autoSelectFamily: true + }) + ); } }); }); diff --git a/test/manual/tls_support.test.ts b/test/manual/tls_support.test.ts index 5359708626..5448c0a02e 100644 --- a/test/manual/tls_support.test.ts +++ b/test/manual/tls_support.test.ts @@ -77,19 +77,17 @@ describe('TLS Support', function () { it('sets the default options', async function () { await client.connect(); - expect(tlsSpy).to.have.been.calledWith({ - autoSelectFamily: true, - host: 'localhost', - port: 27017, - servername: 'localhost', - ca: sinon.match.defined, - cert: sinon.match.defined, - key: sinon.match.defined - }); + expect(tlsSpy).to.have.been.calledWith( + sinon.match({ + ca: sinon.match.defined, + cert: sinon.match.defined, + key: sinon.match.defined + }) + ); }); }); - context('when auto family options are set', function () { + context('when auto select family options are set', function () { let tlsSpy; afterEach(function () { @@ -107,16 +105,15 @@ describe('TLS Support', function () { it('sets the provided options', async function () { await client.connect(); - expect(tlsSpy).to.have.been.calledWith({ - autoSelectFamily: false, - autoSelectFamilyAttemptTimeout: 100, - host: 'localhost', - port: 27017, - servername: 'localhost', - ca: sinon.match.defined, - cert: sinon.match.defined, - key: sinon.match.defined - }); + expect(tlsSpy).to.have.been.calledWith( + sinon.match({ + autoSelectFamily: false, + autoSelectFamilyAttemptTimeout: 100, + ca: sinon.match.defined, + cert: sinon.match.defined, + key: sinon.match.defined + }) + ); }); });