Skip to content

Commit 7f017b0

Browse files
committed
wip
1 parent 072beb0 commit 7f017b0

File tree

12 files changed

+58
-31
lines changed

12 files changed

+58
-31
lines changed

.eslintrc.json

+4
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,10 @@
131131
{
132132
"selector": "CallExpression[callee.name='clearTimeout']",
133133
"message": "clearTimeout must remove abort listener"
134+
},
135+
{
136+
"selector": "CallExpression[callee.property.name='removeAllListeners'][arguments.length=0]",
137+
"message": "removeAllListeners can remove error listeners leading to uncaught errors"
134138
}
135139
],
136140
"@typescript-eslint/no-unused-vars": "error",

src/cmap/connection_pool.ts

+2
Original file line numberDiff line numberDiff line change
@@ -374,6 +374,7 @@ export class ConnectionPool extends TypedEventEmitter<ConnectionPoolEvents> {
374374

375375
try {
376376
timeout?.throwIfExpired();
377+
timeout?.ref();
377378
return await (timeout ? Promise.race([promise, timeout]) : promise);
378379
} catch (error) {
379380
if (TimeoutError.is(error)) {
@@ -399,6 +400,7 @@ export class ConnectionPool extends TypedEventEmitter<ConnectionPoolEvents> {
399400
}
400401
throw error;
401402
} finally {
403+
timeout?.unref();
402404
abortListener?.[kDispose]();
403405
timeout?.clear();
404406
}

src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ export {
5353
MongoClientBulkWriteCursorError,
5454
MongoClientBulkWriteError,
5555
MongoClientBulkWriteExecutionError,
56+
MongoClientClosedError,
5657
MongoCompatibilityError,
5758
MongoCursorExhaustedError,
5859
MongoCursorInUseError,

src/mongo_client.ts

+10-3
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import { MONGO_CLIENT_EVENTS } from './constants';
2222
import { type AbstractCursor } from './cursor/abstract_cursor';
2323
import { Db, type DbOptions } from './db';
2424
import type { Encrypter } from './encrypter';
25-
import { MongoInvalidArgumentError } from './error';
25+
import { MongoClientClosedError, MongoInvalidArgumentError } from './error';
2626
import { MongoClientAuthProviders } from './mongo_client_auth_providers';
2727
import {
2828
type LogComponentSeveritiesClientOptions,
@@ -692,7 +692,6 @@ export class MongoClient extends TypedEventEmitter<MongoClientEvents> implements
692692
/* @internal */
693693
private async _close(force = false): Promise<void> {
694694
try {
695-
this.closeController.abort();
696695
// There's no way to set hasBeenClosed back to false
697696
Object.defineProperty(this.s, 'hasBeenClosed', {
698697
value: true,
@@ -701,6 +700,12 @@ export class MongoClient extends TypedEventEmitter<MongoClientEvents> implements
701700
writable: false
702701
});
703702

703+
if (this.options.maxPoolSize === 1) {
704+
// If maxPoolSize is 1 we won't be able to run anything
705+
// unless we interrupt whatever is using the one connection.
706+
this.closeController.abort(new MongoClientClosedError());
707+
}
708+
704709
const activeCursorCloses = Array.from(this.s.activeCursors, cursor => cursor.close());
705710
this.s.activeCursors.clear();
706711

@@ -749,7 +754,9 @@ export class MongoClient extends TypedEventEmitter<MongoClientEvents> implements
749754
await encrypter.close(this, force);
750755
}
751756
} finally {
752-
// ignore
757+
if (!this.closeController.signal.aborted) {
758+
this.closeController.abort(new MongoClientClosedError());
759+
}
753760
}
754761
}
755762

src/sdam/monitor.ts

+7-9
Original file line numberDiff line numberDiff line change
@@ -387,6 +387,7 @@ function checkServer(monitor: Monitor, callback: Callback<Document | null>) {
387387
const makeMonitoringConnection = async () => {
388388
const socket = await makeSocket(monitor.connectOptions, monitor.closeSignal);
389389
const connection = makeConnection(monitor.connectOptions, socket);
390+
connection.unref();
390391
// The start time is after socket creation but before the handshake
391392
start = now();
392393
try {
@@ -447,15 +448,11 @@ function monitorServer(monitor: Monitor) {
447448

448449
// if the check indicates streaming is supported, immediately reschedule monitoring
449450
if (useStreamingProtocol(monitor, hello?.topologyVersion)) {
450-
clearOnAbortTimeout(
451-
() => {
452-
if (!isInCloseState(monitor)) {
453-
monitor.monitorId?.wake();
454-
}
455-
},
456-
0,
457-
monitor.closeSignal
458-
);
451+
queueMicrotask(() => {
452+
if (!isInCloseState(monitor)) {
453+
monitor.monitorId?.wake();
454+
}
455+
});
459456
}
460457

461458
done();
@@ -554,6 +551,7 @@ export class RTTPinger {
554551
if (connection == null) {
555552
connect(this.monitor.connectOptions, this.closeSignal).then(
556553
connection => {
554+
connection.unref();
557555
this.measureAndReschedule(start, connection);
558556
},
559557
() => {

src/sdam/topology.ts

+2
Original file line numberDiff line numberDiff line change
@@ -621,6 +621,7 @@ export class Topology extends TypedEventEmitter<TopologyEvents> {
621621

622622
try {
623623
timeout?.throwIfExpired();
624+
timeout?.ref();
624625
const server = await (timeout ? Promise.race([serverPromise, timeout]) : serverPromise);
625626
if (options.timeoutContext?.csotEnabled() && server.description.minRoundTripTime !== 0) {
626627
options.timeoutContext.minRoundTripTime = server.description.minRoundTripTime;
@@ -661,6 +662,7 @@ export class Topology extends TypedEventEmitter<TopologyEvents> {
661662
// Other server selection error
662663
throw error;
663664
} finally {
665+
timeout?.unref();
664666
abortListener?.[kDispose]();
665667
if (options.timeoutContext?.clearServerSelectionTimeout) timeout?.clear();
666668
}

src/timeout.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ export function clearOnAbortTimeout(
3939
}, ms);
4040

4141
if ('unref' in id && typeof id.unref === 'function') {
42-
id.unref();
42+
// id.unref();
4343
}
4444

4545
const abortListener = addAbortListener(closeSignal, function clearId() {
@@ -116,7 +116,9 @@ export class Timeout extends Promise<never> {
116116
this.start = Math.trunc(performance.now());
117117

118118
if (rejection == null && this.duration > 0) {
119-
if (options.closeSignal == null) throw new Error('incorrect timer use detected!');
119+
if (options.closeSignal == null) {
120+
throw new Error('You must provide a close signal to timeoutContext');
121+
}
120122

121123
this.id = clearOnAbortTimeout(
122124
() => {

src/utils.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1571,7 +1571,7 @@ export function addAbortSignalToStream(
15711571

15721572
const abortListener = addAbortListener(signal, function () {
15731573
stream.off('close', abortListener[kDispose]).off('error', abortListener[kDispose]);
1574-
stream.destroy(this.reason);
1574+
if (!stream.destroyed) stream.destroy(this.reason);
15751575
});
15761576
// not nearly as complex as node's eos() but... do we need all that?? sobbing emoji.
15771577
stream.once('close', abortListener[kDispose]).once('error', abortListener[kDispose]);

test/integration/client-side-operations-timeout/client_side_operations_timeout.unit.test.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,8 @@ describe('CSOT spec unit tests', function () {
110110

111111
describe('Client side encryption', function () {
112112
describe('KMS requests', function () {
113-
const stateMachine = new StateMachine({} as any);
113+
const closeSignal = new AbortController().signal;
114+
const stateMachine = new StateMachine({} as any, undefined, closeSignal);
114115
const request = {
115116
addResponse: _response => {},
116117
status: {
@@ -137,7 +138,7 @@ describe('CSOT spec unit tests', function () {
137138
const timeoutContext = new CSOTTimeoutContext({
138139
timeoutMS: 500,
139140
serverSelectionTimeoutMS: 30000,
140-
closeSignal: new AbortController().signal
141+
closeSignal
141142
});
142143
const err = await stateMachine.kmsRequest(request, { timeoutContext }).catch(e => e);
143144
expect(err).to.be.instanceOf(MongoOperationTimeoutError);

test/integration/crud/misc_cursors.test.js

+10-3
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ const sinon = require('sinon');
1010
const { Writable } = require('stream');
1111
const { once, on } = require('events');
1212
const { setTimeout } = require('timers');
13-
const { ReadPreference } = require('../../mongodb');
13+
const { ReadPreference, MongoClientClosedError } = require('../../mongodb');
1414
const { ServerType } = require('../../mongodb');
1515
const { formatSort } = require('../../mongodb');
1616

@@ -1861,18 +1861,25 @@ describe('Cursor', function () {
18611861
// insert only 2 docs in capped coll of 3
18621862
await collection.insertMany([{ a: 1 }, { a: 1 }]);
18631863

1864-
const cursor = collection.find({}, { tailable: true, awaitData: true, maxAwaitTimeMS: 2000 });
1864+
const maxAwaitTimeMS = 5000;
1865+
1866+
const cursor = collection.find({}, { tailable: true, awaitData: true, maxAwaitTimeMS });
18651867

18661868
await cursor.next();
18671869
await cursor.next();
18681870
// will block for maxAwaitTimeMS (except we are closing the client)
18691871
const rejectedEarlyBecauseClientClosed = cursor.next().catch(error => error);
18701872

1873+
const start = performance.now();
18711874
await client.close();
1875+
const end = performance.now();
1876+
18721877
expect(cursor).to.have.property('closed', true);
18731878

1879+
expect(end - start, "close returns before cursor's await time").to.be.lessThan(maxAwaitTimeMS);
1880+
18741881
const error = await rejectedEarlyBecauseClientClosed;
1875-
expect(error).to.be.instanceOf(Error); // TODO: Whatever the MongoClient aborts with.
1882+
expect(error).to.be.instanceOf(MongoClientClosedError);
18761883
});
18771884

18781885
it('shouldAwaitData', {

test/integration/node-specific/abstract_cursor.test.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -416,7 +416,11 @@ describe('class AbstractCursor', function () {
416416
client.on('commandStarted', filterForCommands('killCursors', commands));
417417

418418
collection = client.db('abstract_cursor_integration').collection('test');
419-
internalContext = TimeoutContext.create({ timeoutMS: 1000, serverSelectionTimeoutMS: 2000 });
419+
internalContext = TimeoutContext.create({
420+
timeoutMS: 1000,
421+
serverSelectionTimeoutMS: 2000,
422+
closeSignal: new AbortController().signal
423+
});
420424

421425
context = new CursorTimeoutContext(internalContext, Symbol());
422426

test/integration/uri-options/uri.test.js

+9-10
Original file line numberDiff line numberDiff line change
@@ -112,21 +112,20 @@ describe('URI', function () {
112112
);
113113
});
114114

115-
it('should correctly translate uri options', {
116-
metadata: { requires: { topology: 'replicaset' } },
117-
test: function (done) {
115+
it(
116+
'should correctly translate uri options',
117+
{ requires: { topology: 'replicaset' } },
118+
async function () {
118119
const config = this.configuration;
119120
const uri = `mongodb://${config.host}:${config.port}/${config.db}?replicaSet=${config.replicasetName}`;
120121

121122
const client = this.configuration.newClient(uri);
122-
client.connect((err, client) => {
123-
expect(err).to.not.exist;
124-
expect(client).to.exist;
125-
expect(client.options.replicaSet).to.exist.and.equal(config.replicasetName);
126-
client.close(done);
127-
});
123+
await client.connect();
124+
expect(client).to.exist;
125+
expect(client.options.replicaSet).to.exist.and.equal(config.replicasetName);
126+
await client.close();
128127
}
129-
});
128+
);
130129

131130
it('should generate valid credentials with X509', {
132131
metadata: { requires: { topology: 'single' } },

0 commit comments

Comments
 (0)