Skip to content

fix(NODE-6589): background task does not prune idle connections when minPoolSize=0 #4569

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/cmap/connection_pool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -691,7 +691,7 @@ export class ConnectionPool extends TypedEventEmitter<ConnectionPoolEvents> {

private ensureMinPoolSize() {
const minPoolSize = this.options.minPoolSize;
if (this.poolState !== PoolState.ready || minPoolSize === 0) {
if (this.poolState !== PoolState.ready) {
return;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
import { once } from 'node:events';

import { expect } from 'chai';

import { type ConnectionPoolCreatedEvent, type Db, type MongoClient } from '../../mongodb';
import * as sinon from 'sinon';

import {
type ConnectionPoolCreatedEvent,
type Db,
type MongoClient,
type Server
} from '../../mongodb';
import { clearFailPoint, configureFailPoint, sleep } from '../../tools/utils';

describe('Connection Pool', function () {
Expand Down Expand Up @@ -150,4 +156,51 @@ describe('Connection Pool', function () {
});
});
});

describe(
'background task cleans up connections when minPoolSize=0',
{ requires: { topology: 'single' } },
function () {
let server: Server;
let ensureMinPoolSizeSpy: sinon.SinonSpy;

beforeEach(async function () {
client = this.configuration.newClient(
{},
{
maxConnecting: 10,
minPoolSize: 0,
maxIdleTimeMS: 100
}
);

await client.connect();

await Promise.all(
Array.from({ length: 10 }).map(() => {
return client.db('foo').collection('bar').insertOne({ a: 1 });
})
);

server = Array.from(client.topology.s.servers.entries())[0][1];
expect(
server.pool.availableConnectionCount,
'pool was not filled with connections'
).to.be.greaterThan(0);

ensureMinPoolSizeSpy = sinon.spy(server.pool, 'ensureMinPoolSize');
});

it(
'prunes idle connections when minPoolSize=0',
{ requires: { topology: 'single' } },
async function () {
await sleep(500);
expect(server.pool.availableConnectionCount).to.equal(0);

expect(ensureMinPoolSizeSpy).to.have.been.called;
}
);
}
);
});
2 changes: 1 addition & 1 deletion test/integration/crud/insert.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1825,7 +1825,7 @@ describe('crud - insert', function () {
.toArray();
const doc = docs.pop();
expect(doc.a._bsontype).to.equal('Long');
client.close();
await client.close();
}
});

Expand Down
7 changes: 3 additions & 4 deletions test/unit/assorted/max_staleness.spec.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,12 @@ describe('Max Staleness (spec)', function () {
});

const specTests = collectStalenessTests(maxStalenessDir);
Object.keys(specTests).forEach(specTestName => {
for (const [specTestName, test] of Object.entries(specTests)) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was debugging these tests for a leak and refactored this as a drive-by improvement.

describe(specTestName, () => {
specTests[specTestName].forEach(testData => {
for (const testData of test)
it(testData.description, async function () {
return executeServerSelectionTest(testData);
});
});
});
});
}
});
2 changes: 2 additions & 0 deletions test/unit/assorted/server_selection_latency_window_utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,4 +145,6 @@ export async function runServerSelectionLatencyWindowTest(test: ServerSelectionL
const observedFrequencies = calculateObservedFrequencies(selectedServers);

compareResultsToExpected(test.outcome, observedFrequencies);

await topology.close();
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is one of the causes of leaks in unit tests - gotta close the topology.

Before the change in this PR, the pool did not run the background thread by default. With this change, the background thread does run by default, which keeps the event loop open unless the pool's minPoolSize timer is explicitly cleared (which happens in pool.close()).

This isn't an issue for in real scenarios because the server class always closes its pool.

}
4 changes: 3 additions & 1 deletion test/unit/assorted/server_selection_spec_helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ async function executeServerSelectionTest(testDefinition) {
const readPreference = readPreferenceFromDefinition(testDefinition.read_preference);
selector = ServerSelectors.readPreferenceServerSelector(readPreference);
} catch (e) {
if (testDefinition.error) return;
if (testDefinition.error) return topology.close();
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above.

throw e;
}
} else {
Expand Down Expand Up @@ -179,6 +179,8 @@ async function executeServerSelectionTest(testDefinition) {
// this is another expected error case
if (expectedServers.length === 0 && err instanceof MongoServerSelectionError) return;
throw err;
} finally {
topology.close();
}
}

Expand Down
15 changes: 10 additions & 5 deletions test/unit/cmap/connection_pool.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const { TimeoutContext } = require('../../mongodb');
describe('Connection Pool', function () {
let timeoutContext;
let mockMongod;
let pool;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above.

const stubServer = {
topology: {
client: {
Expand Down Expand Up @@ -50,6 +51,8 @@ describe('Connection Pool', function () {
timeoutContext = TimeoutContext.create({ waitQueueTimeoutMS: 0, serverSelectionTimeoutMS: 0 });
});

afterEach(() => pool?.close());

it('should destroy connections which have been closed', async function () {
mockMongod.setMessageHandler(request => {
const doc = request.document;
Expand All @@ -61,7 +64,7 @@ describe('Connection Pool', function () {
}
});

const pool = new ConnectionPool(stubServer, {
pool = new ConnectionPool(stubServer, {
maxPoolSize: 1,
hostAddress: mockMongod.hostAddress()
});
Expand All @@ -81,6 +84,8 @@ describe('Connection Pool', function () {
expect(events).to.have.length(1);
const closeEvent = events[0];
expect(closeEvent).have.property('reason').equal('error');

conn.destroy();
});

it('should propagate socket timeouts to connections', async function () {
Expand All @@ -93,7 +98,7 @@ describe('Connection Pool', function () {
}
});

const pool = new ConnectionPool(stubServer, {
pool = new ConnectionPool(stubServer, {
maxPoolSize: 1,
socketTimeoutMS: 200,
hostAddress: mockMongod.hostAddress()
Expand All @@ -117,7 +122,7 @@ describe('Connection Pool', function () {
}
});

const pool = new ConnectionPool(stubServer, {
pool = new ConnectionPool(stubServer, {
maxPoolSize: 1,
waitQueueTimeoutMS: 200,
hostAddress: mockMongod.hostAddress()
Expand Down Expand Up @@ -157,7 +162,7 @@ describe('Connection Pool', function () {
});

it('should respect the minPoolSizeCheckFrequencyMS option', function () {
const pool = new ConnectionPool(stubServer, {
pool = new ConnectionPool(stubServer, {
minPoolSize: 2,
minPoolSizeCheckFrequencyMS: 42,
hostAddress: mockMongod.hostAddress()
Expand Down Expand Up @@ -193,7 +198,7 @@ describe('Connection Pool', function () {
});

it('should default minPoolSizeCheckFrequencyMS to 100ms', function () {
const pool = new ConnectionPool(stubServer, {
pool = new ConnectionPool(stubServer, {
minPoolSize: 2,
hostAddress: mockMongod.hostAddress()
});
Expand Down