Skip to content

Commit 9976b86

Browse files
committed
feat(withSession): add top level helper for session lifetime
`withSession` allows users to ignore resource management while using sessions. A provided method/operation will be provided with an implicitly created session, which will be cleaned up for them automatically once the operation is complete. NODE-1418
1 parent f5a7227 commit 9976b86

File tree

3 files changed

+184
-1
lines changed

3 files changed

+184
-1
lines changed

lib/mongo_client.js

+42
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ const shallowClone = require('./utils').shallowClone;
1616
const authenticate = require('./authenticate');
1717
const ServerSessionPool = require('mongodb-core').Sessions.ServerSessionPool;
1818
const executeOperation = require('./utils').executeOperation;
19+
const isPromiseLike = require('./utils').isPromiseLike;
1920

2021
/**
2122
* @fileOverview The **MongoClient** class is a class that allows for making Connections to MongoDB.
@@ -504,6 +505,47 @@ MongoClient.prototype.startSession = function(options) {
504505
return this.topology.startSession(options, this.s.options);
505506
};
506507

508+
/**
509+
* Runs a given operation with an implicitly created session. The lifetime of the session
510+
* will be handled without the need for user interaction.
511+
*
512+
* NOTE: presently the operation MUST return a Promise (either explicit or implicity as an async function)
513+
*
514+
* @param {Function} operation An operation to execute with an implicitly created session. The signature of this MUST be `(session) => {}`
515+
* @param {Object} [options] Optional settings to be appled to implicitly created session
516+
* @return {Promise} returns Promise if no callback passed
517+
*/
518+
MongoClient.prototype.withSession = function(operation, options, callback) {
519+
if (typeof options === 'function') (callback = options), (options = undefined);
520+
const session = this.startSession(options);
521+
522+
const cleanupHandler = (err, result, opts) => {
523+
opts = Object.assign({ throw: true }, opts);
524+
session.endSession();
525+
526+
if (typeof callback === 'function') {
527+
return err ? callback(err, null) : callback(null, result);
528+
} else {
529+
if (err) {
530+
if (opts.throw) throw err;
531+
return Promise.reject(err);
532+
}
533+
return result;
534+
}
535+
};
536+
537+
try {
538+
const result = operation(session);
539+
const promise = isPromiseLike(result) ? result : Promise.resolve(result);
540+
541+
return promise
542+
.then(result => cleanupHandler(null, result))
543+
.catch(err => cleanupHandler(err, null, { throw: true }));
544+
} catch (err) {
545+
return cleanupHandler(err, null, { throw: false });
546+
}
547+
};
548+
507549
var mergeOptions = function(target, source, flatten) {
508550
for (var name in source) {
509551
if (source[name] && typeof source[name] === 'object' && flatten) {

lib/utils.js

+11
Original file line numberDiff line numberDiff line change
@@ -495,6 +495,16 @@ function applyWriteConcern(target, sources, options) {
495495
return target;
496496
}
497497

498+
/**
499+
* Checks if a given value is a Promise
500+
*
501+
* @param {*} maybePromise
502+
* @return true if the provided value is a Promise
503+
*/
504+
function isPromiseLike(maybePromise) {
505+
return maybePromise && typeof maybePromise.then === 'function';
506+
}
507+
498508
exports.filterOptions = filterOptions;
499509
exports.mergeOptions = mergeOptions;
500510
exports.translateOptions = translateOptions;
@@ -514,3 +524,4 @@ exports.mergeOptionsAndWriteConcern = mergeOptionsAndWriteConcern;
514524
exports.translateReadPreference = translateReadPreference;
515525
exports.executeOperation = executeOperation;
516526
exports.applyWriteConcern = applyWriteConcern;
527+
exports.isPromiseLike = isPromiseLike;

test/functional/sessions_tests.js

+131-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ describe('Sessions', function() {
2828

2929
it('should send endSessions for multiple sessions', {
3030
metadata: {
31-
requires: { topology: ['single'], mongodb: '>3.6.0-rc0' },
31+
requires: { topology: ['single'], mongodb: '>3.6.0' },
3232
// Skipping session leak tests b/c these are explicit sessions
3333
sessions: { skipLeakTests: true }
3434
},
@@ -49,4 +49,134 @@ describe('Sessions', function() {
4949
});
5050
}
5151
});
52+
53+
describe.only('withSession', {
54+
metadata: { requires: { mongodb: '>3.6.0' } },
55+
test: function() {
56+
[
57+
{
58+
description: 'should support operations that return promises',
59+
operation: client => session => {
60+
return client
61+
.db('test')
62+
.collection('foo')
63+
.find({}, { session })
64+
.toArray();
65+
}
66+
},
67+
{
68+
nodeVersion: '>=8.x',
69+
description: 'should support async operations',
70+
operation: client => session =>
71+
async function() {
72+
await client
73+
.db('test')
74+
.collection('foo')
75+
.find({}, { session })
76+
.toArray();
77+
}
78+
},
79+
{
80+
description: 'should support operations that return rejected promises',
81+
operation: (/* client */) => (/* session */) => {
82+
return Promise.reject(new Error('something awful'));
83+
}
84+
},
85+
{
86+
description: "should support operations that don't return promises",
87+
operation: (/* client */) => (/* session */) => {
88+
setTimeout(() => {});
89+
}
90+
},
91+
{
92+
description: 'should support operations that throw exceptions',
93+
operation: (/* client */) => (/* session */) => {
94+
throw new Error('something went wrong!');
95+
}
96+
},
97+
{
98+
description: 'should support operations that return promises with a callback',
99+
operation: client => session => {
100+
return client
101+
.db('test')
102+
.collection('foo')
103+
.find({}, { session })
104+
.toArray();
105+
},
106+
callback: resolve => (err, res) => {
107+
expect(err).to.not.exist;
108+
expect(res).to.exist;
109+
resolve();
110+
}
111+
},
112+
{
113+
description: 'should support operations that return rejected promises and a callback',
114+
operation: (/* client */) => (/* session */) => {
115+
return Promise.reject(new Error('something awful'));
116+
},
117+
callback: resolve => (err, res) => {
118+
expect(err).to.exist;
119+
expect(res).to.not.exist;
120+
resolve();
121+
}
122+
},
123+
{
124+
description: "should support operations that don't return promises with a callback",
125+
operation: (/* client */) => (/* session */) => {
126+
setTimeout(() => {});
127+
},
128+
callback: resolve => (err, res) => {
129+
expect(err).to.exist;
130+
expect(res).to.not.exist;
131+
resolve();
132+
}
133+
},
134+
{
135+
description: 'should support operations that throw exceptions with a callback',
136+
operation: (/* client */) => (/* session */) => {
137+
throw new Error('something went wrong!');
138+
},
139+
callback: resolve => (err, res) => {
140+
expect(err).to.exist;
141+
expect(res).to.not.exist;
142+
resolve();
143+
}
144+
}
145+
].forEach(testCase => {
146+
const metadata = {};
147+
if (testCase.nodeVersion) metadata.requires = { node: testCase.nodeVersion };
148+
it(testCase.description, {
149+
metadata: metadata,
150+
test: function() {
151+
const client = this.configuration.newClient(
152+
{ w: 1 },
153+
{ poolSize: 1, auto_reconnect: false }
154+
);
155+
156+
return client.connect().then(client => {
157+
let promise;
158+
if (testCase.callback) {
159+
promise = new Promise(resolve => {
160+
client.withSession(testCase.operation(client), {}, testCase.callback(resolve));
161+
});
162+
} else {
163+
promise = client.withSession(testCase.operation(client));
164+
}
165+
166+
return promise
167+
.catch(() => expect(client.topology.s.sessionPool.sessions).to.have.length(1))
168+
.then(() => expect(client.topology.s.sessionPool.sessions).to.have.length(1))
169+
.then(() => client.close())
170+
.then(() => {
171+
// verify that the `endSessions` command was sent
172+
const lastCommand = test.commands.started[test.commands.started.length - 1];
173+
expect(lastCommand.commandName).to.equal('endSessions');
174+
expect(client.topology.s.sessionPool.sessions).to.have.length(0);
175+
});
176+
});
177+
}
178+
});
179+
});
180+
}
181+
});
52182
});

0 commit comments

Comments
 (0)