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

Commit a1d5b22

Browse files
committed
feat(server-session-pool): implement session pool per spect
NODE-1088
1 parent 9d63f5a commit a1d5b22

File tree

3 files changed

+228
-2
lines changed

3 files changed

+228
-2
lines changed

lib/sessions.js

+107
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
'use strict';
2+
3+
const Binary = require('mongodb-core').BSON.Binary,
4+
uuidV4 = require('./utils').uuidV4;
5+
6+
/**
7+
*
8+
*/
9+
class ClientSession {
10+
constructor(topology, options) {
11+
if (topology == null) {
12+
throw new Error('ClientSession requires a topology');
13+
}
14+
15+
this.topology = topology;
16+
this.options = options || {};
17+
this.hasEnded = false;
18+
this._serverSession = undefined; // TBD
19+
}
20+
21+
/**
22+
*
23+
*/
24+
endSession(callback) {
25+
if (this.hasEnded) {
26+
return callback(null, null);
27+
}
28+
29+
this.topology.command('admin.$cmd', { endSessions: 1, ids: [this.id] }, err => {
30+
this.hasEnded = true;
31+
32+
if (err) return callback(err, null);
33+
callback(null, null);
34+
});
35+
}
36+
}
37+
38+
/**
39+
*
40+
*/
41+
class ServerSession {
42+
constructor() {
43+
this.id = { id: new Binary(uuidV4(), Binary.SUBTYPE_UUID) };
44+
this.lastUse = Date.now();
45+
}
46+
47+
/**
48+
*
49+
* @param {*} sessionTimeoutMinutes
50+
*/
51+
hasTimedOut(sessionTimeoutMinutes) {
52+
const idleTimeMinutes = Math.round(
53+
(((Date.now() - this.lastUse) % 86400000) % 3600000) / 60000
54+
);
55+
56+
return idleTimeMinutes > sessionTimeoutMinutes;
57+
}
58+
}
59+
60+
/**
61+
*
62+
*/
63+
class ServerSessionPool {
64+
constructor(topology) {
65+
this.topology = topology;
66+
this.sessions = [];
67+
}
68+
69+
/**
70+
* @returns {ServerSession}
71+
*/
72+
dequeue() {
73+
const sessionTimeoutMinutes = this.topology.logicalSessionTimeoutMinutes;
74+
while (this.sessions.length) {
75+
const session = this.sessions.shift();
76+
if (!session.hasTimedOut(sessionTimeoutMinutes)) {
77+
return session;
78+
}
79+
}
80+
81+
return new ServerSession();
82+
}
83+
84+
/**
85+
*
86+
* @param {*} session
87+
*/
88+
enqueue(session) {
89+
const sessionTimeoutMinutes = this.topology.logicalSessionTimeoutMinutes;
90+
while (this.sessions.length) {
91+
const session = this.sessions[this.sessions.length - 1];
92+
if (session.hasTimedOut(sessionTimeoutMinutes)) {
93+
this.sessions.pop();
94+
} else {
95+
break;
96+
}
97+
}
98+
99+
this.sessions.push(session);
100+
}
101+
}
102+
103+
module.exports = {
104+
ClientSession: ClientSession,
105+
ServerSession: ServerSession,
106+
ServerSessionPool: ServerSessionPool
107+
};

lib/utils.js

+12-2
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
'use strict';
22

3+
const crypto = require('crypto');
4+
35
/**
46
* Copy the values of all enumerable own properties from one or more
57
* source objects to a target object. It will return the target object.
68
*/
7-
var assign = Object.assign
9+
const assign = Object.assign
810
? Object.assign
911
: function assign(target) {
1012
if (target === undefined || target === null) {
@@ -31,6 +33,14 @@ var assign = Object.assign
3133
return to;
3234
};
3335

36+
const uuidV4 = () => {
37+
const result = crypto.randomBytes(16);
38+
result[6] = (result[6] & 0x0f) | 0x40;
39+
result[8] = (result[8] & 0x3f) | 0x80;
40+
return result;
41+
};
42+
3443
module.exports = {
35-
assign: assign
44+
assign: assign,
45+
uuidV4: uuidV4
3646
};

test/tests/unit/sessions_tests.js

+109
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
'use strict';
2+
3+
const Server = require('../../..').Server,
4+
mock = require('../../mock'),
5+
expect = require('chai').expect,
6+
ServerSessionPool = require('../../../lib/sessions').ServerSessionPool,
7+
ServerSession = require('../../../lib/sessions').ServerSession;
8+
9+
let test = {};
10+
describe('Sessions', function() {
11+
describe('ServerSessionPool', function() {
12+
afterEach(() => {
13+
test.client.destroy();
14+
return mock.cleanup();
15+
});
16+
17+
beforeEach(() => {
18+
return mock
19+
.createServer()
20+
.then(server => {
21+
test.server = server;
22+
test.server.setMessageHandler(request => {
23+
var doc = request.document;
24+
if (doc.ismaster) {
25+
request.reply(
26+
Object.assign({}, mock.DEFAULT_ISMASTER, { logicalSessionTimeoutMinutes: 10 })
27+
);
28+
}
29+
});
30+
})
31+
.then(() => {
32+
test.client = new Server(test.server.address());
33+
34+
return new Promise((resolve, reject) => {
35+
test.client.once('error', reject);
36+
test.client.once('connect', resolve);
37+
test.client.connect();
38+
});
39+
});
40+
});
41+
42+
it('should create a new session if the pool is empty', {
43+
metadata: { requires: { topology: 'single' } },
44+
45+
test: function(done) {
46+
const pool = new ServerSessionPool(test.client);
47+
expect(pool.sessions).to.have.length(0);
48+
const session = pool.dequeue();
49+
expect(session).to.exist;
50+
expect(pool.sessions).to.have.length(0);
51+
done();
52+
}
53+
});
54+
55+
it('should reuse sessions which have not timed out yet on dequeue', {
56+
metadata: { requires: { topology: 'single' } },
57+
58+
test: function(done) {
59+
const oldSession = new ServerSession();
60+
const pool = new ServerSessionPool(test.client);
61+
pool.sessions.push(oldSession);
62+
63+
const session = pool.dequeue();
64+
expect(session).to.exist;
65+
expect(session).to.eql(oldSession);
66+
67+
done();
68+
}
69+
});
70+
71+
it('should remove sessions which have timed out on dequeue, and return a fresh session', {
72+
metadata: { requires: { topology: 'single' } },
73+
74+
test: function(done) {
75+
const oldSession = new ServerSession();
76+
oldSession.lastUse = new Date(Date.now() - 30 * 60 * 1000).getTime(); // add 30min
77+
78+
const pool = new ServerSessionPool(test.client);
79+
pool.sessions.push(oldSession);
80+
81+
const session = pool.dequeue();
82+
expect(session).to.exist;
83+
expect(session).to.not.eql(oldSession);
84+
85+
done();
86+
}
87+
});
88+
89+
it('should remove sessions which have timed out on enqueue', {
90+
metadata: { requires: { topology: 'single' } },
91+
92+
test: function(done) {
93+
const newSession = new ServerSession();
94+
const oldSessions = [new ServerSession(), new ServerSession()].map(session => {
95+
session.lastUse = new Date(Date.now() - 30 * 60 * 1000).getTime(); // add 30min
96+
return session;
97+
});
98+
99+
const pool = new ServerSessionPool(test.client);
100+
pool.sessions = pool.sessions.concat(oldSessions);
101+
102+
pool.enqueue(newSession);
103+
expect(pool.sessions).to.have.length(1);
104+
expect(pool.sessions[0]).to.eql(newSession);
105+
done();
106+
}
107+
});
108+
});
109+
});

0 commit comments

Comments
 (0)