Skip to content

Commit 8338bae

Browse files
authored
fix(NODE-4834): ensure that MessageStream is destroyed when connections are destroyed (#3482)
1 parent 9f945c4 commit 8338bae

13 files changed

+278
-127
lines changed

src/cmap/connect.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ function performInitialHandshake(
9696
) {
9797
const callback: Callback<Document> = function (err, ret) {
9898
if (err && conn) {
99-
conn.destroy();
99+
conn.destroy({ force: false });
100100
}
101101
_callback(err, ret);
102102
};

src/cmap/connection.ts

+16-37
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ export interface ConnectionOptions
149149
/** @public */
150150
export interface DestroyOptions {
151151
/** Force the destruction. */
152-
force?: boolean;
152+
force: boolean;
153153
}
154154

155155
/** @public */
@@ -170,8 +170,8 @@ export class Connection extends TypedEventEmitter<ConnectionEvents> {
170170
address: string;
171171
socketTimeoutMS: number;
172172
monitorCommands: boolean;
173+
/** Indicates that the connection (including underlying TCP socket) has been closed. */
173174
closed: boolean;
174-
destroyed: boolean;
175175
lastHelloMS?: number;
176176
serverApi?: ServerApi;
177177
helloOk?: boolean;
@@ -220,7 +220,6 @@ export class Connection extends TypedEventEmitter<ConnectionEvents> {
220220
this.monitorCommands = options.monitorCommands;
221221
this.serverApi = options.serverApi;
222222
this.closed = false;
223-
this.destroyed = false;
224223
this[kHello] = null;
225224
this[kClusterTime] = null;
226225

@@ -313,10 +312,7 @@ export class Connection extends TypedEventEmitter<ConnectionEvents> {
313312
if (this.closed) {
314313
return;
315314
}
316-
317-
this[kStream].destroy(error);
318-
319-
this.closed = true;
315+
this.destroy({ force: false });
320316

321317
for (const op of this[kQueue].values()) {
322318
op.cb(error);
@@ -330,8 +326,7 @@ export class Connection extends TypedEventEmitter<ConnectionEvents> {
330326
if (this.closed) {
331327
return;
332328
}
333-
334-
this.closed = true;
329+
this.destroy({ force: false });
335330

336331
const message = `connection ${this.id} to ${this.address} closed`;
337332
for (const op of this[kQueue].values()) {
@@ -348,9 +343,7 @@ export class Connection extends TypedEventEmitter<ConnectionEvents> {
348343
}
349344

350345
this[kDelayedTimeoutId] = setTimeout(() => {
351-
this[kStream].destroy();
352-
353-
this.closed = true;
346+
this.destroy({ force: false });
354347

355348
const message = `connection ${this.id} to ${this.address} timed out`;
356349
const beforeHandshake = this.hello == null;
@@ -459,41 +452,27 @@ export class Connection extends TypedEventEmitter<ConnectionEvents> {
459452
callback(undefined, message.documents[0]);
460453
}
461454

462-
destroy(options?: DestroyOptions, callback?: Callback): void {
463-
if (typeof options === 'function') {
464-
callback = options;
465-
options = { force: false };
466-
}
467-
455+
destroy(options: DestroyOptions, callback?: Callback): void {
468456
this.removeAllListeners(Connection.PINNED);
469457
this.removeAllListeners(Connection.UNPINNED);
470458

471-
options = Object.assign({ force: false }, options);
472-
if (this[kStream] == null || this.destroyed) {
473-
this.destroyed = true;
474-
if (typeof callback === 'function') {
475-
callback();
476-
}
477-
478-
return;
479-
}
459+
this[kMessageStream].destroy();
460+
this.closed = true;
480461

481462
if (options.force) {
482463
this[kStream].destroy();
483-
this.destroyed = true;
484-
if (typeof callback === 'function') {
485-
callback();
464+
if (callback) {
465+
return process.nextTick(callback);
486466
}
487-
488-
return;
489467
}
490468

491-
this[kStream].end(() => {
492-
this.destroyed = true;
493-
if (typeof callback === 'function') {
494-
callback();
469+
if (!this[kStream].writableEnded) {
470+
this[kStream].end(callback);
471+
} else {
472+
if (callback) {
473+
return process.nextTick(callback);
495474
}
496-
});
475+
}
497476
}
498477

499478
command(

src/cmap/connection_pool.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -515,7 +515,7 @@ export class ConnectionPool extends TypedEventEmitter<ConnectionPoolEvents> {
515515
ConnectionPool.CONNECTION_CLOSED,
516516
new ConnectionClosedEvent(this, conn, 'poolClosed')
517517
);
518-
conn.destroy(options, cb);
518+
conn.destroy({ force: !!options.force }, cb);
519519
},
520520
err => {
521521
this[kConnections].clear();
@@ -591,7 +591,7 @@ export class ConnectionPool extends TypedEventEmitter<ConnectionPoolEvents> {
591591
new ConnectionClosedEvent(this, connection, reason)
592592
);
593593
// destroy the connection
594-
process.nextTick(() => connection.destroy());
594+
process.nextTick(() => connection.destroy({ force: false }));
595595
}
596596

597597
private connectionIsStale(connection: Connection) {

src/sdam/server.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -244,7 +244,10 @@ export class Server extends TypedEventEmitter<ServerEvents> {
244244

245245
/** Destroy the server connection */
246246
destroy(options?: DestroyOptions, callback?: Callback): void {
247-
if (typeof options === 'function') (callback = options), (options = {});
247+
if (typeof options === 'function') {
248+
callback = options;
249+
options = { force: false };
250+
}
248251
options = Object.assign({}, { force: false }, options);
249252

250253
if (this.s.state === STATE_CLOSED) {

src/sdam/topology.ts

+4-13
Original file line numberDiff line numberDiff line change
@@ -484,26 +484,17 @@ export class Topology extends TypedEventEmitter<TopologyEvents> {
484484
}
485485

486486
/** Close this topology */
487-
close(callback: Callback): void;
488487
close(options: CloseOptions): void;
489488
close(options: CloseOptions, callback: Callback): void;
490-
close(options?: CloseOptions | Callback, callback?: Callback): void {
491-
if (typeof options === 'function') {
492-
callback = options;
493-
options = {};
494-
}
495-
496-
if (typeof options === 'boolean') {
497-
options = { force: options };
498-
}
499-
options = options ?? {};
489+
close(options?: CloseOptions, callback?: Callback): void {
490+
options = options ?? { force: false };
500491

501492
if (this.s.state === STATE_CLOSED || this.s.state === STATE_CLOSING) {
502493
return callback?.();
503494
}
504495

505496
const destroyedServers = Array.from(this.s.servers.values(), server => {
506-
return promisify(destroyServer)(server, this, options as CloseOptions);
497+
return promisify(destroyServer)(server, this, { force: !!options?.force });
507498
});
508499

509500
Promise.all(destroyedServers)
@@ -765,7 +756,7 @@ function destroyServer(
765756
options?: DestroyOptions,
766757
callback?: Callback
767758
) {
768-
options = options ?? {};
759+
options = options ?? { force: false };
769760
for (const event of LOCAL_SERVER_EVENTS) {
770761
server.removeAllListeners(event);
771762
}

test/integration/crud/misc_cursors.test.js

+20-49
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ const { ReadPreference } = require('../../../src/read_preference');
1313
const { ServerType } = require('../../../src/sdam/common');
1414
const { formatSort } = require('../../../src/sort');
1515
const { getSymbolFrom } = require('../../tools/utils');
16+
const { MongoExpiredSessionError } = require('../../../src/error');
1617

1718
describe('Cursor', function () {
1819
before(function () {
@@ -1905,61 +1906,31 @@ describe('Cursor', function () {
19051906
}
19061907
});
19071908

1908-
it('should close dead tailable cursors', {
1909-
metadata: {
1910-
os: '!win32' // NODE-2943: timeout on windows
1911-
},
1912-
1913-
test: function (done) {
1914-
// http://www.mongodb.org/display/DOCS/Tailable+Cursors
1915-
1916-
const configuration = this.configuration;
1917-
client.connect((err, client) => {
1918-
expect(err).to.not.exist;
1919-
this.defer(() => client.close());
1920-
1921-
const db = client.db(configuration.db);
1922-
const options = { capped: true, size: 10000000 };
1923-
db.createCollection(
1924-
'test_if_dead_tailable_cursors_close',
1925-
options,
1926-
function (err, collection) {
1927-
expect(err).to.not.exist;
1909+
it('closes cursors when client is closed even if it has not been exhausted', async function () {
1910+
await client
1911+
.db()
1912+
.dropCollection('test_cleanup_tailable')
1913+
.catch(() => null);
19281914

1929-
let closeCount = 0;
1930-
const docs = Array.from({ length: 100 }).map(() => ({ a: 1 }));
1931-
collection.insertMany(docs, { w: 'majority', wtimeoutMS: 5000 }, err => {
1932-
expect(err).to.not.exist;
1933-
1934-
const cursor = collection.find({}, { tailable: true, awaitData: true });
1935-
const stream = cursor.stream();
1915+
const collection = await client
1916+
.db()
1917+
.createCollection('test_cleanup_tailable', { capped: true, size: 1000, max: 3 });
19361918

1937-
stream.resume();
1938-
1939-
var validator = () => {
1940-
closeCount++;
1941-
if (closeCount === 2) {
1942-
done();
1943-
}
1944-
};
1919+
// insert only 2 docs in capped coll of 3
1920+
await collection.insertMany([{ a: 1 }, { a: 1 }]);
19451921

1946-
// we validate that the stream "ends" either cleanly or with an error
1947-
stream.on('end', validator);
1948-
stream.on('error', validator);
1922+
const cursor = collection.find({}, { tailable: true, awaitData: true, maxAwaitTimeMS: 2000 });
19491923

1950-
cursor.on('close', validator);
1924+
await cursor.next();
1925+
await cursor.next();
1926+
// will block for maxAwaitTimeMS (except we are closing the client)
1927+
const rejectedEarlyBecauseClientClosed = cursor.next().catch(error => error);
19511928

1952-
const docs = Array.from({ length: 100 }).map(() => ({ a: 1 }));
1953-
collection.insertMany(docs, err => {
1954-
expect(err).to.not.exist;
1929+
await client.close();
1930+
expect(cursor).to.have.property('killed', true);
19551931

1956-
setTimeout(() => client.close());
1957-
});
1958-
});
1959-
}
1960-
);
1961-
});
1962-
}
1932+
const error = await rejectedEarlyBecauseClientClosed;
1933+
expect(error).to.be.instanceOf(MongoExpiredSessionError);
19631934
});
19641935

19651936
it('shouldAwaitData', {

test/integration/node-specific/topology.test.js

+13-5
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,20 @@ describe('Topology', function () {
1010
const states = [];
1111
topology.on('stateChanged', (_, newState) => states.push(newState));
1212
topology.connect(err => {
13-
expect(err).to.not.exist;
14-
topology.close(err => {
13+
try {
1514
expect(err).to.not.exist;
16-
expect(topology.isDestroyed()).to.be.true;
17-
expect(states).to.eql(['connecting', 'connected', 'closing', 'closed']);
18-
done();
15+
} catch (error) {
16+
done(error);
17+
}
18+
topology.close({}, err => {
19+
try {
20+
expect(err).to.not.exist;
21+
expect(topology.isDestroyed()).to.be.true;
22+
expect(states).to.eql(['connecting', 'connected', 'closing', 'closed']);
23+
done();
24+
} catch (error) {
25+
done(error);
26+
}
1927
});
2028
});
2129
}

test/unit/assorted/polling_srv_records_for_mongos_discovery.prose.test.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ describe('Polling Srv Records for Mongos Discovery', () => {
9797

9898
afterEach(function (done) {
9999
if (context.topology) {
100-
context.topology.close(done);
100+
context.topology.close({}, done);
101101
} else {
102102
done();
103103
}

test/unit/assorted/server_selection_spec_helper.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ function executeServerSelectionTest(testDefinition, testDone) {
109109
});
110110

111111
function done(err) {
112-
topology.close(e => testDone(e || err));
112+
topology.close({}, e => testDone(e || err));
113113
}
114114

115115
topology.connect(err => {

0 commit comments

Comments
 (0)