Skip to content

Commit 5f34ad0

Browse files
authored
feat(NODE-3255): add minPoolSizeCheckIntervalMS option to connection pool (#3429)
1 parent 6aeff81 commit 5f34ad0

File tree

4 files changed

+117
-21
lines changed

4 files changed

+117
-21
lines changed

src/cmap/connection_pool.ts

+11-2
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,8 @@ export interface ConnectionPoolOptions extends Omit<ConnectionOptions, 'id' | 'g
9090
waitQueueTimeoutMS: number;
9191
/** If we are in load balancer mode. */
9292
loadBalanced: boolean;
93+
/** @internal */
94+
minPoolSizeCheckFrequencyMS?: number;
9395
}
9496

9597
/** @internal */
@@ -234,6 +236,7 @@ export class ConnectionPool extends TypedEventEmitter<ConnectionPoolEvents> {
234236
maxConnecting: options.maxConnecting ?? 2,
235237
maxIdleTimeMS: options.maxIdleTimeMS ?? 0,
236238
waitQueueTimeoutMS: options.waitQueueTimeoutMS ?? 0,
239+
minPoolSizeCheckFrequencyMS: options.minPoolSizeCheckFrequencyMS ?? 100,
237240
autoEncrypter: options.autoEncrypter,
238241
metadata: options.metadata
239242
});
@@ -683,12 +686,18 @@ export class ConnectionPool extends TypedEventEmitter<ConnectionPoolEvents> {
683686
}
684687
if (this[kPoolState] === PoolState.ready) {
685688
clearTimeout(this[kMinPoolSizeTimer]);
686-
this[kMinPoolSizeTimer] = setTimeout(() => this.ensureMinPoolSize(), 10);
689+
this[kMinPoolSizeTimer] = setTimeout(
690+
() => this.ensureMinPoolSize(),
691+
this.options.minPoolSizeCheckFrequencyMS
692+
);
687693
}
688694
});
689695
} else {
690696
clearTimeout(this[kMinPoolSizeTimer]);
691-
this[kMinPoolSizeTimer] = setTimeout(() => this.ensureMinPoolSize(), 100);
697+
this[kMinPoolSizeTimer] = setTimeout(
698+
() => this.ensureMinPoolSize(),
699+
this.options.minPoolSizeCheckFrequencyMS
700+
);
692701
}
693702
}
694703

test/integration/server-selection/server_selection.prose.operation_count.test.ts

+13-17
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import { expect } from 'chai';
2-
import { setTimeout } from 'timers';
3-
import { promisify } from 'util';
2+
import { on } from 'events';
43

54
import { CommandStartedEvent } from '../../../src';
65
import { Collection } from '../../../src/collection';
76
import { MongoClient } from '../../../src/mongo_client';
7+
import { sleep } from '../../tools/utils';
88

99
const failPoint = {
1010
configureFailPoint: 'failCommand',
@@ -25,21 +25,14 @@ async function runTaskGroup(collection: Collection, count: 10 | 100 | 1000) {
2525
}
2626
}
2727

28-
async function ensurePoolIsFull(client: MongoClient) {
28+
async function ensurePoolIsFull(client: MongoClient): Promise<boolean> {
2929
let connectionCount = 0;
30-
const onConnectionCreated = () => connectionCount++;
31-
client.on('connectionCreated', onConnectionCreated);
32-
33-
// 250ms should be plenty of time to fill the connection pool,
34-
// but just in case we'll loop a couple of times.
35-
for (let i = 0; connectionCount < POOL_SIZE * 2 && i < 10; ++i) {
36-
await promisify(setTimeout)(250);
37-
}
38-
39-
client.removeListener('connectionCreated', onConnectionCreated);
40-
41-
if (connectionCount !== POOL_SIZE * 2) {
42-
throw new Error('Connection pool did not fill up');
30+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
31+
for await (const _event of on(client, 'connectionCreated')) {
32+
connectionCount++;
33+
if (connectionCount === POOL_SIZE * 2) {
34+
break;
35+
}
4336
}
4437
}
4538

@@ -82,7 +75,10 @@ describe('operationCount-based Selection Within Latency Window - Prose Test', fu
8275
await client.connect();
8376

8477
// Step 4: Using CMAP events, ensure the client's connection pools for both mongoses have been saturated
85-
await poolIsFullPromise;
78+
const poolIsFull = Promise.race([poolIsFullPromise, sleep(30 * 1000)]);
79+
if (!poolIsFull) {
80+
throw new Error('Timed out waiting for connection pool to fill to minPoolSize');
81+
}
8682

8783
seeds = client.topology.s.seedlist.map(address => address.toString());
8884

test/tools/cmap_spec_runner.ts

+5-2
Original file line numberDiff line numberDiff line change
@@ -351,8 +351,11 @@ async function runCmapTest(test: CmapTest, threadContext: ThreadContext) {
351351
const poolOptions = test.poolOptions || {};
352352
expect(CMAP_POOL_OPTION_NAMES).to.include.members(Object.keys(poolOptions));
353353

354-
// TODO(NODE-3255): update condition to only remove option if set to -1
354+
let minPoolSizeCheckFrequencyMS;
355355
if (poolOptions.backgroundThreadIntervalMS) {
356+
if (poolOptions.backgroundThreadIntervalMS !== -1) {
357+
minPoolSizeCheckFrequencyMS = poolOptions.backgroundThreadIntervalMS;
358+
}
356359
delete poolOptions.backgroundThreadIntervalMS;
357360
}
358361

@@ -373,7 +376,7 @@ async function runCmapTest(test: CmapTest, threadContext: ThreadContext) {
373376
const mainThread = threadContext.getThread(MAIN_THREAD_KEY);
374377
mainThread.start();
375378

376-
threadContext.createPool({ ...poolOptions, metadata });
379+
threadContext.createPool({ ...poolOptions, metadata, minPoolSizeCheckFrequencyMS });
377380
// yield control back to the event loop so that the ConnectionPoolCreatedEvent
378381
// has a chance to be fired before any synchronously-emitted events from
379382
// the queued operations

test/unit/cmap/connection_pool.test.js

+88
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ const { expect } = require('chai');
99
const { setImmediate } = require('timers');
1010
const { ns, isHello } = require('../../../src/utils');
1111
const { LEGACY_HELLO_COMMAND } = require('../../../src/constants');
12+
const { createTimerSandbox } = require('../timer_sandbox');
1213

1314
describe('Connection Pool', function () {
1415
let server;
@@ -128,6 +129,93 @@ describe('Connection Pool', function () {
128129
});
129130
});
130131

132+
describe('minPoolSize population', function () {
133+
let clock, timerSandbox;
134+
beforeEach(() => {
135+
timerSandbox = createTimerSandbox();
136+
clock = sinon.useFakeTimers();
137+
});
138+
139+
afterEach(() => {
140+
if (clock) {
141+
timerSandbox.restore();
142+
clock.restore();
143+
clock = undefined;
144+
}
145+
});
146+
147+
it('should respect the minPoolSizeCheckFrequencyMS option', function () {
148+
const pool = new ConnectionPool(server, {
149+
minPoolSize: 2,
150+
minPoolSizeCheckFrequencyMS: 42,
151+
hostAddress: server.hostAddress()
152+
});
153+
const ensureSpy = sinon.spy(pool, 'ensureMinPoolSize');
154+
155+
// return a fake connection that won't get identified as perished
156+
const createConnStub = sinon
157+
.stub(pool, 'createConnection')
158+
.yields(null, { destroy: () => null, generation: 0 });
159+
160+
pool.ready();
161+
162+
// expect ensureMinPoolSize to execute immediately
163+
expect(ensureSpy).to.have.been.calledOnce;
164+
expect(createConnStub).to.have.been.calledOnce;
165+
166+
// check that the successful connection return schedules another run
167+
clock.tick(42);
168+
expect(ensureSpy).to.have.been.calledTwice;
169+
expect(createConnStub).to.have.been.calledTwice;
170+
171+
// check that the 2nd successful connection return schedules another run
172+
// but don't expect to get a new connection since we are at minPoolSize
173+
clock.tick(42);
174+
expect(ensureSpy).to.have.been.calledThrice;
175+
expect(createConnStub).to.have.been.calledTwice;
176+
177+
// check that the next scheduled check runs even after we're at minPoolSize
178+
clock.tick(42);
179+
expect(ensureSpy).to.have.callCount(4);
180+
expect(createConnStub).to.have.been.calledTwice;
181+
});
182+
183+
it('should default minPoolSizeCheckFrequencyMS to 100ms', function () {
184+
const pool = new ConnectionPool(server, {
185+
minPoolSize: 2,
186+
hostAddress: server.hostAddress()
187+
});
188+
const ensureSpy = sinon.spy(pool, 'ensureMinPoolSize');
189+
190+
// return a fake connection that won't get identified as perished
191+
const createConnStub = sinon
192+
.stub(pool, 'createConnection')
193+
.yields(null, { destroy: () => null, generation: 0 });
194+
195+
pool.ready();
196+
197+
// expect ensureMinPoolSize to execute immediately
198+
expect(ensureSpy).to.have.been.calledOnce;
199+
expect(createConnStub).to.have.been.calledOnce;
200+
201+
// check that the successful connection return schedules another run
202+
clock.tick(100);
203+
expect(ensureSpy).to.have.been.calledTwice;
204+
expect(createConnStub).to.have.been.calledTwice;
205+
206+
// check that the 2nd successful connection return schedules another run
207+
// but don't expect to get a new connection since we are at minPoolSize
208+
clock.tick(100);
209+
expect(ensureSpy).to.have.been.calledThrice;
210+
expect(createConnStub).to.have.been.calledTwice;
211+
212+
// check that the next scheduled check runs even after we're at minPoolSize
213+
clock.tick(100);
214+
expect(ensureSpy).to.have.callCount(4);
215+
expect(createConnStub).to.have.been.calledTwice;
216+
});
217+
});
218+
131219
describe('withConnection', function () {
132220
it('should manage a connection for a successful operation', function (done) {
133221
server.setMessageHandler(request => {

0 commit comments

Comments
 (0)