Skip to content

Commit 611be8d

Browse files
committed
fix: only mark server session dirty if the client session is alive
There are race conditions where a `ClientSession` ends before the error handling code for an operation marks the session as dirty. This error handling code needs to check if the session has not ended before marking the server session dirty. NODE-2545
1 parent 7403e31 commit 611be8d

File tree

3 files changed

+56
-6
lines changed

3 files changed

+56
-6
lines changed

lib/core/sdam/server.js

+4-2
Original file line numberDiff line numberDiff line change
@@ -463,11 +463,13 @@ function markServerUnknown(server, error) {
463463
}
464464

465465
function makeOperationHandler(server, options, callback) {
466+
const session = options && options.session;
467+
466468
return function handleOperationResult(err, result) {
467469
if (err) {
468470
if (err instanceof MongoNetworkError) {
469-
if (options && options.session) {
470-
options.session.serverSession.isDirty = true;
471+
if (session && !session.hasEnded) {
472+
session.serverSession.isDirty = true;
471473
}
472474

473475
if (!isNetworkTimeoutError(err)) {

lib/core/sessions.js

+4-4
Original file line numberDiff line numberDiff line change
@@ -123,14 +123,14 @@ class ClientSession extends EventEmitter {
123123
this.abortTransaction(); // pass in callback?
124124
}
125125

126-
// mark the session as ended, and emit a signal
127-
this.hasEnded = true;
128-
this.emit('ended', this);
129-
130126
// release the server session back to the pool
131127
this.sessionPool.release(this.serverSession);
132128
this.serverSession = null;
133129

130+
// mark the session as ended, and emit a signal
131+
this.hasEnded = true;
132+
this.emit('ended', this);
133+
134134
// spec indicates that we should ignore all errors for `endSessions`
135135
if (typeof callback === 'function') callback(null, null);
136136
}

test/unit/core/sessions.test.js

+48
Original file line numberDiff line numberDiff line change
@@ -214,4 +214,52 @@ describe('Sessions', function() {
214214
}
215215
});
216216
});
217+
218+
context('error handling', function() {
219+
let mockServer;
220+
221+
afterEach(() => mock.cleanup());
222+
beforeEach(() => {
223+
return mock.createServer().then(server => (mockServer = server));
224+
});
225+
226+
it('should not mark session as dirty on network error if already ended', function(done) {
227+
mockServer.setMessageHandler(request => {
228+
const doc = request.document;
229+
if (doc.ismaster) {
230+
request.reply(
231+
Object.assign({}, mock.DEFAULT_ISMASTER, { logicalSessionTimeoutMinutes: 10 })
232+
);
233+
} else if (doc.ping) {
234+
request.reply({ ok: 1 });
235+
} else if (doc.endSessions) {
236+
request.reply({ ok: 1 });
237+
} else if (doc.insert) {
238+
request.connection.destroy();
239+
}
240+
});
241+
242+
const client = this.configuration.newClient(`mongodb://${mockServer.uri()}/test`, {
243+
useUnifiedTopology: true
244+
});
245+
246+
client.connect(err => {
247+
expect(err).to.not.exist;
248+
this.defer(() => client.close());
249+
250+
const session = client.startSession();
251+
const collection = client.db('test').collection('foo');
252+
253+
client.db('admin').command({ ping: 1 }, { session }, err => {
254+
expect(err).to.not.exist;
255+
256+
process.nextTick(() => session.endSession());
257+
collection.insertOne({ a: 42 }, { session }, err => {
258+
expect(err).to.exist;
259+
done();
260+
});
261+
});
262+
});
263+
});
264+
});
217265
});

0 commit comments

Comments
 (0)