Skip to content
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

Session Touch Reduction Option #892

Closed
Closed
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
12 changes: 12 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ var defer = typeof setImmediate === 'function'
* @param {String} [options.name=connect.sid] Session ID cookie name
* @param {Boolean} [options.proxy]
* @param {Boolean} [options.resave] Resave unmodified sessions back to the store
* @param {Boolean} [options.touchAtMaxAgeRatio] `.touch()` session when `maxAge` has reached `.touchAtMaxAgeRatio` of the `originalMaxAge`
* @param {Boolean} [options.rolling] Enable/disable rolling session expiration
* @param {Boolean} [options.saveUninitialized] Save uninitialized sessions to the store
* @param {String|Array} [options.secret] Secret for signing session ID
Expand Down Expand Up @@ -105,6 +106,9 @@ function session(options) {
// get the resave session option
var resaveSession = opts.resave;

// get the max age touch ratio
var touchAtMaxAgeRatio = opts.touchAtMaxAgeRatio;

// get the rolling session option
var rollingSessions = Boolean(opts.rolling)

Expand All @@ -123,6 +127,10 @@ function session(options) {
resaveSession = true;
}

if (touchAtMaxAgeRatio && (touchAtMaxAgeRatio < 0 || touchAtMaxAgeRatio > 1)) {
throw new TypeError('touchAtMaxAgeRatio must be a number between 0 and 1');
}

if (saveUninitializedSession === undefined) {
deprecate('undefined saveUninitialized option; provide saveUninitialized option');
saveUninitializedSession = true;
Expand Down Expand Up @@ -457,6 +465,10 @@ function session(options) {
return false;
}

if (touchAtMaxAgeRatio && req.session.cookie.maxAge > touchAtMaxAgeRatio * req.session.cookie.originalMaxAge) {
return false;
}

return cookieId === req.sessionID && !shouldSave(req);
}

Expand Down
1 change: 1 addition & 0 deletions session/memory.js
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ MemoryStore.prototype.touch = function touch(sessionId, session, callback) {
var currentSession = getSession.call(this, sessionId)

if (currentSession) {
var maxAge = new Date(currentSession.cookie.expires).getTime() - Date.now()
// update expiration
currentSession.cookie = session.cookie
this.sessions[sessionId] = JSON.stringify(currentSession)
Expand Down
152 changes: 152 additions & 0 deletions test/session.js
Original file line number Diff line number Diff line change
Expand Up @@ -1101,6 +1101,158 @@ describe('session()', function(){
.end(cb)
})
})

it('should not call touch when > 75% of maxAge remains', function (done) {
var store = new session.MemoryStore()
var sid
var server = createServer({ cookie: { maxAge: min }, store: store, resave: false, touchAtMaxAgeRatio: 0.75 }, function (req, res) {
req.session.user = 'bob'
sid = req.session.id
res.end(req.session.id)
})

// First call will not call touch because it creates the session
// Second call should not call touch
// Touches are expensive as they cause writes on the session store
// On DynamoDB for example, touches cost 5x more than a read
request(server)
.get('/')
.expect(shouldSetCookie('connect.sid'))
.expect(200, function (err, res) {
if (err) return done(err)
var originalExpires = expires(res)
var sessCookie = cookie(res)

store.touch = function touch (sid, sess, callback) {
callback(new Error('boom!'))
}

setTimeout(function () {
request(server)
.get('/')
.set('Cookie', sessCookie)
.expect(200, function (err, res) {
store.get(sid, function (err, sess) {
if (err) return done(err)
// Take 26% of the maxAge off the session
var cookieExpiresReduced = new Date(sess.cookie.expires).getTime() - (min * 0.26)
sess.cookie.expires = new Date(cookieExpiresReduced).toISOString()
store.set(sid, sess, function (err) {
if (err) return done(err)
request(server)
.get('/')
.set('Cookie', sessCookie)
.expect(200, function (err, res) {
var newExpires = expires(res)

// Should not update the session via store.touch
assert.notStrictEqual(newExpires, originalExpires)

if (err) return done(err)
done()
})
})
})
})
}, (1000 - (Date.now() % 1000) + 200));
})
})

it('should call touch when touchAtMaxAgeRatio not set', function (done) {
var cb = after(2, done)
var store = new session.MemoryStore()
var sid
var server = createServer({ cookie: { maxAge: min }, store: store, resave: false }, function (req, res) {
req.session.user = 'bob'
sid = req.session.id
res.end(req.session.id)
})

// First call will not call touch because it creates the session
// Second call should not call touch
// Touches are expensive as they cause writes on the session store
// On DynamoDB for example, touches cost 5x more than a read
request(server)
.get('/')
.expect(shouldSetCookie('connect.sid'))
.expect(200, function (err, res) {
if (err) return done(err)
var sessCookie = cookie(res)

store.touch = function touch (sid, sess, callback) {
callback();
cb();
}

request(server)
.get('/')
.set('Cookie', sessCookie)
.expect(200, function (err, res) {
if (err) return cb(err)
cb();
});
})
})

it('should call touch when < 75% of maxAge remains', function (done) {
var store = new session.MemoryStore()
var sid
var server = createServer({ cookie: { maxAge: min }, store: store, resave: false, touchAtMaxAgeRatio: 0.75 }, function (req, res) {
sid = req.session.id
req.session.user = 'bob'
res.end(req.session.id)
})

// First call will not call touch because it creates the session
// Second call should not call touch
// Touches are expensive as they cause writes on the session store
// On DynamoDB for example, touches cost 5x more than a read
request(server)
.get('/')
.expect(shouldSetCookie('connect.sid'))
.expect(200, function (err, res) {
if (err) return done(err)
var originalExpires = expires(res)
var sessCookie = cookie(res)

store.get(sid, function (err, sess) {
if (err) return done(err)
// Take 26% of the maxAge off the session
var cookieExpiresReduced = new Date(sess.cookie.expires).getTime() - (min * 0.26)
sess.cookie.expires = new Date(cookieExpiresReduced).toISOString()
store.set(sid, sess, function (err) {
if (err) return done(err)
request(server)
.get('/')
.set('Cookie', sessCookie)
.expect(200, function (err, res) {

store.get(sid, function (err, sess) {
if (err) return done(err)
// Take 26% of the maxAge off the session
var cookieExpiresReduced = new Date(sess.cookie.expires).getTime() - (min * 0.26)
sess.cookie.expires = new Date(cookieExpiresReduced).toISOString()
store.set(sid, sess, function (err) {
if (err) return done(err)
request(server)
.get('/')
.set('Cookie', sessCookie)
.expect(200, function (err, res) {
var newExpires = expires(res)

// Should not update the session via store.touch
assert.notStrictEqual(newExpires, originalExpires)

if (err) return done(err)
done()
})
})
})
})
})
})
})
})
})
});

Expand Down