This repository has been archived by the owner on Feb 4, 2022. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 106
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(server-session-pool): implement session pool per spect
NODE-1088
- Loading branch information
Showing
3 changed files
with
228 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} | ||
}); | ||
}); | ||
}); |