Skip to content
This repository has been archived by the owner on Feb 4, 2022. It is now read-only.

Commit

Permalink
feat(server-session-pool): implement session pool per spect
Browse files Browse the repository at this point in the history
NODE-1088
  • Loading branch information
mbroadst committed Oct 6, 2017
1 parent 9d63f5a commit a1d5b22
Show file tree
Hide file tree
Showing 3 changed files with 228 additions and 2 deletions.
107 changes: 107 additions & 0 deletions lib/sessions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
'use strict';

const Binary = require('mongodb-core').BSON.Binary,
uuidV4 = require('./utils').uuidV4;

/**
*
*/
class ClientSession {
constructor(topology, options) {
if (topology == null) {
throw new Error('ClientSession requires a topology');
}

this.topology = topology;
this.options = options || {};
this.hasEnded = false;
this._serverSession = undefined; // TBD
}

/**
*
*/
endSession(callback) {
if (this.hasEnded) {
return callback(null, null);
}

this.topology.command('admin.$cmd', { endSessions: 1, ids: [this.id] }, err => {
this.hasEnded = true;

if (err) return callback(err, null);
callback(null, null);
});
}
}

/**
*
*/
class ServerSession {
constructor() {
this.id = { id: new Binary(uuidV4(), Binary.SUBTYPE_UUID) };
this.lastUse = Date.now();
}

/**
*
* @param {*} sessionTimeoutMinutes
*/
hasTimedOut(sessionTimeoutMinutes) {
const idleTimeMinutes = Math.round(
(((Date.now() - this.lastUse) % 86400000) % 3600000) / 60000
);

return idleTimeMinutes > sessionTimeoutMinutes;
}
}

/**
*
*/
class ServerSessionPool {
constructor(topology) {
this.topology = topology;
this.sessions = [];
}

/**
* @returns {ServerSession}
*/
dequeue() {
const sessionTimeoutMinutes = this.topology.logicalSessionTimeoutMinutes;
while (this.sessions.length) {
const session = this.sessions.shift();
if (!session.hasTimedOut(sessionTimeoutMinutes)) {
return session;
}
}

return new ServerSession();
}

/**
*
* @param {*} session
*/
enqueue(session) {
const sessionTimeoutMinutes = this.topology.logicalSessionTimeoutMinutes;
while (this.sessions.length) {
const session = this.sessions[this.sessions.length - 1];
if (session.hasTimedOut(sessionTimeoutMinutes)) {
this.sessions.pop();
} else {
break;
}
}

this.sessions.push(session);
}
}

module.exports = {
ClientSession: ClientSession,
ServerSession: ServerSession,
ServerSessionPool: ServerSessionPool
};
14 changes: 12 additions & 2 deletions lib/utils.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
'use strict';

const crypto = require('crypto');

/**
* Copy the values of all enumerable own properties from one or more
* source objects to a target object. It will return the target object.
*/
var assign = Object.assign
const assign = Object.assign
? Object.assign
: function assign(target) {
if (target === undefined || target === null) {
Expand All @@ -31,6 +33,14 @@ var assign = Object.assign
return to;
};

const uuidV4 = () => {
const result = crypto.randomBytes(16);
result[6] = (result[6] & 0x0f) | 0x40;
result[8] = (result[8] & 0x3f) | 0x80;
return result;
};

module.exports = {
assign: assign
assign: assign,
uuidV4: uuidV4
};
109 changes: 109 additions & 0 deletions test/tests/unit/sessions_tests.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
'use strict';

const Server = require('../../..').Server,
mock = require('../../mock'),
expect = require('chai').expect,
ServerSessionPool = require('../../../lib/sessions').ServerSessionPool,
ServerSession = require('../../../lib/sessions').ServerSession;

let test = {};
describe('Sessions', function() {
describe('ServerSessionPool', function() {
afterEach(() => {
test.client.destroy();
return mock.cleanup();
});

beforeEach(() => {
return mock
.createServer()
.then(server => {
test.server = server;
test.server.setMessageHandler(request => {
var doc = request.document;
if (doc.ismaster) {
request.reply(
Object.assign({}, mock.DEFAULT_ISMASTER, { logicalSessionTimeoutMinutes: 10 })
);
}
});
})
.then(() => {
test.client = new Server(test.server.address());

return new Promise((resolve, reject) => {
test.client.once('error', reject);
test.client.once('connect', resolve);
test.client.connect();
});
});
});

it('should create a new session if the pool is empty', {
metadata: { requires: { topology: 'single' } },

test: function(done) {
const pool = new ServerSessionPool(test.client);
expect(pool.sessions).to.have.length(0);
const session = pool.dequeue();
expect(session).to.exist;
expect(pool.sessions).to.have.length(0);
done();
}
});

it('should reuse sessions which have not timed out yet on dequeue', {
metadata: { requires: { topology: 'single' } },

test: function(done) {
const oldSession = new ServerSession();
const pool = new ServerSessionPool(test.client);
pool.sessions.push(oldSession);

const session = pool.dequeue();
expect(session).to.exist;
expect(session).to.eql(oldSession);

done();
}
});

it('should remove sessions which have timed out on dequeue, and return a fresh session', {
metadata: { requires: { topology: 'single' } },

test: function(done) {
const oldSession = new ServerSession();
oldSession.lastUse = new Date(Date.now() - 30 * 60 * 1000).getTime(); // add 30min

const pool = new ServerSessionPool(test.client);
pool.sessions.push(oldSession);

const session = pool.dequeue();
expect(session).to.exist;
expect(session).to.not.eql(oldSession);

done();
}
});

it('should remove sessions which have timed out on enqueue', {
metadata: { requires: { topology: 'single' } },

test: function(done) {
const newSession = new ServerSession();
const oldSessions = [new ServerSession(), new ServerSession()].map(session => {
session.lastUse = new Date(Date.now() - 30 * 60 * 1000).getTime(); // add 30min
return session;
});

const pool = new ServerSessionPool(test.client);
pool.sessions = pool.sessions.concat(oldSessions);

pool.enqueue(newSession);
expect(pool.sessions).to.have.length(1);
expect(pool.sessions[0]).to.eql(newSession);
done();
}
});
});
});

0 comments on commit a1d5b22

Please sign in to comment.