Skip to content

Change MemoryStore to function like an LRU to prevent memory leaks #128

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,10 @@ This is the secret used to sign the session ID cookie.

The session store instance, defaults to a new `MemoryStore` instance.

##### cacheLimit

The limit on the size of the default `MemoryStore` instance, defaults to 1000 objects.

##### unset

Control the result of unsetting `req.session` (through `delete`, setting to `null`,
Expand Down
2 changes: 1 addition & 1 deletion index.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ function session(options){
var options = options || {}
// name - previously "options.key"
, name = options.name || options.key || 'connect.sid'
, store = options.store || new MemoryStore
, store = options.store || new MemoryStore(options)
, cookie = options.cookie || {}
, trustProxy = options.proxy
, storeReady = true
Expand Down
61 changes: 51 additions & 10 deletions session/memory.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,13 @@ module.exports = MemoryStore
* @public
*/

function MemoryStore() {
function MemoryStore(options) {
Store.call(this)
this.cacheLimit = options && options.cacheLimit || 1000
this.sessions = Object.create(null)
this.first = null
this.last = null
this.l = 0
}

/**
Expand All @@ -62,7 +66,7 @@ MemoryStore.prototype.all = function all(callback) {
var session = getSession.call(this, sessionId)

if (session) {
sessions[sessionId] = session;
sessions[sessionId] = session
}
}

Expand All @@ -78,6 +82,9 @@ MemoryStore.prototype.all = function all(callback) {

MemoryStore.prototype.clear = function clear(callback) {
this.sessions = Object.create(null)
this.last = null
this.first = null
this.l = 0
callback && defer(callback)
}

Expand All @@ -89,7 +96,24 @@ MemoryStore.prototype.clear = function clear(callback) {
*/

MemoryStore.prototype.destroy = function destroy(sessionId, callback) {
var sess = this.sessions[sessionId]
delete this.sessions[sessionId]

if(typeof sess !== 'undefined'){
if(sess.next !== null) sess.next.prev = sess.prev
if(sess.prev !== null) sess.prev.next = sess.next

if(this.last !== null && this.last.id === sessionId){
this.last = this.last.next
}

if(this.first !== null && this.first.id === sessionId){
this.first = this.first.prev
}

this.l--
}

callback && defer(callback)
}

Expand Down Expand Up @@ -122,14 +146,31 @@ MemoryStore.prototype.get = function get(sessionId, callback) {
*/

MemoryStore.prototype.length = function length(callback) {
this.all(function (err, sessions) {
if (err) return callback(err)
callback(null, Object.keys(sessions).length)
})
callback(null, this.l)
}

MemoryStore.prototype.set = function set(sessionId, session, callback) {
this.sessions[sessionId] = JSON.stringify(session)

var firstSession = this.first

if (this.l >= this.cacheLimit) {
this.destroy(this.last.id)
}

if (typeof this.sessions[sessionId] === 'undefined'){
this.l++
}

this.first = this.sessions[sessionId] = {id: sessionId, data: JSON.stringify(session), prev: firstSession, next: null}

if(firstSession !== null){
firstSession.next = this.first
}

if(this.last === null) {
this.last = this.first
}

callback && defer(callback)
}

Expand All @@ -148,7 +189,7 @@ MemoryStore.prototype.touch = function touch(sessionId, session, callback) {
if (currentSession) {
// update expiration
currentSession.cookie = session.cookie
this.sessions[sessionId] = JSON.stringify(currentSession)
this.sessions[sessionId].data = JSON.stringify(currentSession)
}

callback && defer(callback)
Expand All @@ -167,15 +208,15 @@ function getSession(sessionId) {
}

// parse
sess = JSON.parse(sess)
sess = JSON.parse(sess.data)

var expires = typeof sess.cookie.expires === 'string'
? new Date(sess.cookie.expires)
: sess.cookie.expires

// destroy expired session
if (expires && expires <= Date.now()) {
delete this.sessions[sessionId]
this.destroy(sessionId)
return
}

Expand Down
62 changes: 62 additions & 0 deletions test/session.js
Original file line number Diff line number Diff line change
Expand Up @@ -1878,6 +1878,68 @@ describe('session()', function(){
})
})

describe('memoryStore', function(done){

it('should only contain 10 items', function(done){
var store = new session.MemoryStore({cacheLimit: 10})

for (var i = 0; i < 15; i++){
store.set(i, {cookie: { expires: new Date((new Date()).valueOf() + 60*10*1000) }})
}

store.length(function (err, length) {
if (err) return done(err)
assert.equal(length, 10)
done()
})
})

it('delete the first item', function(){
var store = new session.MemoryStore({cacheLimit: 10})

for (var i = 0; i < 15; i++){
store.set(i, {cookie: { expires: new Date((new Date()).valueOf() + 60*10*1000) }})
}

store.destroy(14)

store.length(function (err, length) {
if (err) return done(err)
assert.equal(length, 9)
done
})
})

it('delete the last item', function(){
var store = new session.MemoryStore({cacheLimit: 10})

for (var i = 0; i < 10; i++){
store.set(i, {cookie: { expires: new Date((new Date()).valueOf() + 60*10*1000) }})
}

store.destroy(0)
store.destroy(1)

store.length(function (err, length) {
if (err) return done(err)
assert.equal(length, 8)
done
})

for (var i = 10; i < 12; i++){
store.set(i, {cookie: { expires: new Date((new Date()).valueOf() + 60*10*1000) }})
}

store.length(function (err, length) {
if (err) return done(err)
assert.equal(length, 10)
done
})
})


})

function cookie(res) {
var setCookie = res.headers['set-cookie'];
return (setCookie && setCookie[0]) || undefined;
Expand Down