Skip to content

Commit 6829f47

Browse files
committed
feat(sessions): MongoClient will now track sessions and release
As initially implemented, sessions must be explicitly ended by the user. This is likely to lead to cases where users forget to do this, and therefore leak sessions. Instead, MongoClient now tracks all sessions created on the client, and explicitly cleans them up upon client close. NODE-1106
1 parent 96052c8 commit 6829f47

File tree

5 files changed

+72
-5
lines changed

5 files changed

+72
-5
lines changed

lib/mongo_client.js

+17-2
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,8 @@ function MongoClient(url, options) {
194194
url: url,
195195
options: options || {},
196196
promiseLibrary: null,
197-
dbCache: {}
197+
dbCache: {},
198+
sessions: []
198199
};
199200

200201
// Get the promiseLibrary
@@ -308,6 +309,14 @@ MongoClient.prototype.close = function(force, callback) {
308309
// Remove listeners after emit
309310
self.removeAllListeners('close');
310311

312+
// If we have sessions, we want to send a single `endSessions` command for them,
313+
// and then individually clean them up. They will be removed from the internal state
314+
// when they emit their `ended` events.
315+
if (this.s.sessions.length) {
316+
this.topology.endSessions(this.s.sessions);
317+
this.s.sessions.forEach(session => session.endSession({ skipCommand: true }));
318+
}
319+
311320
// Callback after next event loop tick
312321
if (typeof callback === 'function')
313322
return process.nextTick(function() {
@@ -481,7 +490,13 @@ MongoClient.prototype.startSession = function(options) {
481490
throw new MongoError('Current topology does not support sessions');
482491
}
483492

484-
return this.topology.startSession(options);
493+
const session = this.topology.startSession(options);
494+
session.once('ended', () => {
495+
this.s.sessions = this.s.sessions.filter(s => s.equals(session));
496+
});
497+
498+
this.s.sessions.push(session);
499+
return session;
485500
};
486501

487502
var mergeOptions = function(target, source, flatten) {

lib/topologies/topology_base.js

+4
Original file line numberDiff line numberDiff line change
@@ -368,6 +368,10 @@ class TopologyBase extends EventEmitter {
368368
return this.s.coreTopology.getServer(options);
369369
}
370370

371+
endSessions(sessions, callback) {
372+
return this.s.coreTopology.endSessions(sessions, callback);
373+
}
374+
371375
/**
372376
* Unref all sockets
373377
* @method

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
"official"
1414
],
1515
"dependencies": {
16-
"mongodb-core": "mongodb-js/mongodb-core#8aaac8bcc7bdc02bf8bf4b03337a0af71ddc513e"
16+
"mongodb-core": "mongodb-js/mongodb-core#6510d7d3d82d84cc0811a853355deaa918b96941"
1717
},
1818
"devDependencies": {
1919
"betterbenchmarks": "^0.1.0",

test/functional/sessions_tests.js

+48
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
'use strict';
2+
const expect = require('chai').expect,
3+
mongo = require('../..'),
4+
setupDatabase = require('./shared').setupDatabase;
5+
6+
const ignoredCommands = ['ismaster'];
7+
const test = { commands: { started: [], succeeded: [] } };
8+
describe('Sessions', function() {
9+
before(function() {
10+
return setupDatabase(this.configuration);
11+
});
12+
13+
afterEach(() => test.listener.uninstrument());
14+
beforeEach(function() {
15+
test.commands = { started: [], succeeded: [] };
16+
test.listener = mongo.instrument(err => expect(err).to.be.null);
17+
test.listener.on('started', event => {
18+
if (ignoredCommands.indexOf(event.commandName) === -1) test.commands.started.push(event);
19+
});
20+
21+
test.listener.on('succeeded', event => {
22+
if (ignoredCommands.indexOf(event.commandName) === -1) test.commands.succeeded.push(event);
23+
});
24+
25+
test.client = this.configuration.newClient({ w: 1 }, { poolSize: 1, auto_reconnect: false });
26+
return test.client.connect();
27+
});
28+
29+
it('should send endSessions for multiple sessions', {
30+
metadata: { requires: { topology: ['single'] } },
31+
test: function(done) {
32+
var client = this.configuration.newClient({ w: 1 }, { poolSize: 1, auto_reconnect: false });
33+
client.connect((err, client) => {
34+
let sessions = [client.startSession(), client.startSession()].map(s => s.id);
35+
36+
client.close(err => {
37+
expect(err).to.not.exist;
38+
expect(test.commands.started).to.have.length(1);
39+
expect(test.commands.started[0].commandName).to.equal('endSessions');
40+
expect(test.commands.started[0].command.endSessions).to.include.deep.members(sessions);
41+
42+
expect(client.s.sessions).to.have.length(0);
43+
done();
44+
});
45+
});
46+
}
47+
});
48+
});

yarn.lock

+2-2
Original file line numberDiff line numberDiff line change
@@ -2115,9 +2115,9 @@ mongodb-core@mongodb-js/mongodb-core#3.0.0:
21152115
bson "~1.0.4"
21162116
require_optional "^1.0.1"
21172117

2118-
mongodb-core@mongodb-js/mongodb-core#8aaac8bcc7bdc02bf8bf4b03337a0af71ddc513e:
2118+
mongodb-core@mongodb-js/mongodb-core#6510d7d3d82d84cc0811a853355deaa918b96941:
21192119
version "3.0.0-beta2"
2120-
resolved "https://codeload.github.com/mongodb-js/mongodb-core/tar.gz/8aaac8bcc7bdc02bf8bf4b03337a0af71ddc513e"
2120+
resolved "https://codeload.github.com/mongodb-js/mongodb-core/tar.gz/6510d7d3d82d84cc0811a853355deaa918b96941"
21212121
dependencies:
21222122
bson "~1.0.4"
21232123
require_optional "^1.0.1"

0 commit comments

Comments
 (0)