Merge pull request #17949 from overleaf/jpa-set-nx-xx

[web] stricter writes to redis when creating and updating sessions

GitOrigin-RevId: 79723e0d38884bf723c7a2ba32993e4daa2612a0
This commit is contained in:
Jakob Ackermann 2024-04-25 13:54:01 +01:00 committed by Copybot
parent cb97bb5170
commit 90a02ebc2f

View file

@ -2,14 +2,30 @@ const session = require('express-session')
const RedisStore = require('connect-redis')(session) const RedisStore = require('connect-redis')(session)
const metrics = require('@overleaf/metrics') const metrics = require('@overleaf/metrics')
const logger = require('@overleaf/logger') const logger = require('@overleaf/logger')
const Settings = require('@overleaf/settings')
const SessionManager = require('../Features/Authentication/SessionManager') const SessionManager = require('../Features/Authentication/SessionManager')
const MAX_SESSION_SIZE_THRESHOLD = 4096 const MAX_SESSION_SIZE_THRESHOLD = 4096
// Define a custom session store to record session metrics and log large // Define a custom session store to record session metrics and log large
// anonymous sessions for debugging purposes // anonymous sessions for debugging purposes
// Also make the SET calls more robust/consistent by adding flags
// - XX: ensure update in place, expect that the old session value is still in redis at that key
// - NX: ensure initial set, expect that there is no other session at that key already
class CustomSessionStore extends RedisStore { class CustomSessionStore extends RedisStore {
static largestSessionSize = 3 * 1024 // ignore sessions smaller than 3KB static largestSessionSize = 3 * 1024 // ignore sessions smaller than 3KB
#initialSetStore
#updateInPlaceStore
constructor({ client }) {
super({ client })
this.#initialSetStore = new RedisStore({
client: new CustomSetRedisClient(client, 'NX'),
})
this.#updateInPlaceStore = new RedisStore({
client: new CustomSetRedisClient(client, 'XX'),
})
}
static metric(method, sess) { static metric(method, sess) {
let type // type of session: 'logged-in', 'anonymous', or 'na' (not available) let type // type of session: 'logged-in', 'anonymous', or 'na' (not available)
@ -51,7 +67,14 @@ class CustomSessionStore extends RedisStore {
set(sid, sess, cb) { set(sid, sess, cb) {
CustomSessionStore.metric('set', sess) CustomSessionStore.metric('set', sess)
super.set(sid, sess, cb) const originalId = sess.req.signedCookies[Settings.cookieName]
if (sid === originalId || sid === sess.req.newSessionId) {
this.#updateInPlaceStore.set(sid, sess, cb)
} else {
// Multiple writes can get issued with the new sid. Keep track of it.
Object.defineProperty(sess.req, 'newSessionId', { value: sid })
this.#initialSetStore.set(sid, sess, cb)
}
} }
touch(sid, sess, cb) { touch(sid, sess, cb) {
@ -81,4 +104,24 @@ function redactSession(sess) {
) )
} }
class CustomSetRedisClient {
#client
#flag
constructor(client, flag) {
this.#client = client
this.#flag = flag
}
set(args, cb) {
args.push(this.#flag)
this.#client.set(args, (err, ok) => {
metrics.inc('session.store.set', 1, {
path: this.#flag,
status: err ? 'error' : ok ? 'success' : 'failure',
})
cb(err, ok)
})
}
}
module.exports = CustomSessionStore module.exports = CustomSessionStore