From 80b9296ff010ea82e85ff24a9140ebcfe56ba394 Mon Sep 17 00:00:00 2001 From: Jeff Erickson <16201464+jee7s@users.noreply.github.com> Date: Tue, 7 Apr 2026 22:47:40 -0500 Subject: [PATCH 1/9] fix: reduce session store cleanup frequency to once per hour connect-session-knex defaults to running a DELETE on expired sessions every 60 seconds. Set clearInterval to 3600000ms to reduce DB churn. Co-Authored-By: Claude Sonnet 4.6 --- server/master.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/server/master.js b/server/master.js index 0679777ef8..0d845cb94e 100644 --- a/server/master.js +++ b/server/master.js @@ -81,7 +81,8 @@ module.exports = async () => { resave: false, saveUninitialized: false, store: new KnexSessionStore({ - knex: WIKI.models.knex + knex: WIKI.models.knex, + clearInterval: 3600000 }) })) app.use(WIKI.auth.passport.initialize()) From b0af4e6687ee4e149646c42b90502777d9d4ba05 Mon Sep 17 00:00:00 2001 From: Jeff Erickson <16201464+jee7s@users.noreply.github.com> Date: Tue, 7 Apr 2026 22:47:54 -0500 Subject: [PATCH 2/9] fix: change session store cleanup interval to once per day Co-Authored-By: Claude Sonnet 4.6 --- server/master.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/master.js b/server/master.js index 0d845cb94e..c90ff347c1 100644 --- a/server/master.js +++ b/server/master.js @@ -82,7 +82,7 @@ module.exports = async () => { saveUninitialized: false, store: new KnexSessionStore({ knex: WIKI.models.knex, - clearInterval: 3600000 + clearInterval: 86400000 }) })) app.use(WIKI.auth.passport.initialize()) From 336228752d1a2075391ea640b2fc865710cf9565 Mon Sep 17 00:00:00 2001 From: Jeff Erickson <16201464+jee7s@users.noreply.github.com> Date: Tue, 7 Apr 2026 22:49:48 -0500 Subject: [PATCH 3/9] feat: add db.min and db.sessionCleanupInterval config options Expose DB connection pool minimum and session store cleanup interval as configurable options under the db: config block. Defaults are min=2 and sessionCleanupInterval=86400000 (1 day). Co-Authored-By: Claude Sonnet 4.6 --- config.sample.yml | 6 ++++++ server/app/data.yml | 2 ++ server/core/db.js | 1 + server/master.js | 2 +- 4 files changed, 10 insertions(+), 1 deletion(-) diff --git a/config.sample.yml b/config.sample.yml index 47edd8d28f..ff60390cab 100644 --- a/config.sample.yml +++ b/config.sample.yml @@ -31,6 +31,12 @@ db: db: wiki ssl: false + # Optional - Minimum number of DB connections in the pool (default: 2) + # min: 2 + + # Optional - How often (in ms) to clean up expired sessions from the DB (default: 86400000 = 1 day) + # sessionCleanupInterval: 86400000 + # Optional - PostgreSQL / MySQL / MariaDB only: # -> Uncomment lines you need below and set `auto` to false # -> Full list of accepted options: https://nodejs.org/api/tls.html#tls_tls_createsecurecontext_options diff --git a/server/app/data.yml b/server/app/data.yml index 0cd628a6c9..e119364738 100644 --- a/server/app/data.yml +++ b/server/app/data.yml @@ -18,6 +18,8 @@ defaults: storage: ./db.sqlite sslOptions: auto: true + min: 2 + sessionCleanupInterval: 86400000 ssl: enabled: false pool: diff --git a/server/core/db.js b/server/core/db.js index f6a9d7080d..f4e53ad3e7 100644 --- a/server/core/db.js +++ b/server/core/db.js @@ -140,6 +140,7 @@ module.exports = { connection: dbConfig, pool: { ...WIKI.config.pool, + min: WIKI.config.db.min, async afterCreate(conn, done) { // -> Set Connection App Name switch (WIKI.config.db.type) { diff --git a/server/master.js b/server/master.js index c90ff347c1..09ae9fc8f6 100644 --- a/server/master.js +++ b/server/master.js @@ -82,7 +82,7 @@ module.exports = async () => { saveUninitialized: false, store: new KnexSessionStore({ knex: WIKI.models.knex, - clearInterval: 86400000 + clearInterval: WIKI.config.db.sessionCleanupInterval }) })) app.use(WIKI.auth.passport.initialize()) From 0571c5c5d9ce1f572280ae55e3b2e3806ce91a46 Mon Sep 17 00:00:00 2001 From: Jeff Erickson <16201464+jee7s@users.noreply.github.com> Date: Tue, 7 Apr 2026 22:50:20 -0500 Subject: [PATCH 4/9] fix: restore default sessionCleanupInterval to 60000ms Keeps the data.yml default at the original connect-session-knex value. Users can override to a longer interval (e.g. 86400000) in config.yml. Co-Authored-By: Claude Sonnet 4.6 --- server/app/data.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/app/data.yml b/server/app/data.yml index e119364738..c334003386 100644 --- a/server/app/data.yml +++ b/server/app/data.yml @@ -19,7 +19,7 @@ defaults: sslOptions: auto: true min: 2 - sessionCleanupInterval: 86400000 + sessionCleanupInterval: 60000 ssl: enabled: false pool: From 7f9a53047028d6eda8587d0ec6957906d0540552 Mon Sep 17 00:00:00 2001 From: Jeff Erickson <16201464+jee7s@users.noreply.github.com> Date: Tue, 7 Apr 2026 23:03:59 -0500 Subject: [PATCH 5/9] feat: expose DB_POOL_MIN and DB_SESSION_CLEANUP_INTERVAL env vars in container config Co-Authored-By: Claude Sonnet 4.6 --- dev/build/config.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dev/build/config.yml b/dev/build/config.yml index d9e58761a1..66335f07a3 100644 --- a/dev/build/config.yml +++ b/dev/build/config.yml @@ -9,6 +9,8 @@ db: db: $(DB_NAME) storage: $(DB_FILEPATH) ssl: $(DB_SSL) + min: $(DB_POOL_MIN:2) + sessionCleanupInterval: $(DB_SESSION_CLEANUP_INTERVAL:60000) ssl: enabled: $(SSL_ACTIVE) port: 3443 From 774793cd677c387c4c373780106f4d7a8a7f90a7 Mon Sep 17 00:00:00 2001 From: Jeff Erickson <16201464+jee7s@users.noreply.github.com> Date: Tue, 7 Apr 2026 23:05:28 -0500 Subject: [PATCH 6/9] fix: move pool min from db: to pool: config section Co-Authored-By: Claude Sonnet 4.6 --- config.sample.yml | 3 --- dev/build/config.yml | 3 ++- server/app/data.yml | 3 +-- server/core/db.js | 1 - 4 files changed, 3 insertions(+), 7 deletions(-) diff --git a/config.sample.yml b/config.sample.yml index ff60390cab..0efe345fd8 100644 --- a/config.sample.yml +++ b/config.sample.yml @@ -31,9 +31,6 @@ db: db: wiki ssl: false - # Optional - Minimum number of DB connections in the pool (default: 2) - # min: 2 - # Optional - How often (in ms) to clean up expired sessions from the DB (default: 86400000 = 1 day) # sessionCleanupInterval: 86400000 diff --git a/dev/build/config.yml b/dev/build/config.yml index 66335f07a3..f3920b98ea 100644 --- a/dev/build/config.yml +++ b/dev/build/config.yml @@ -9,8 +9,9 @@ db: db: $(DB_NAME) storage: $(DB_FILEPATH) ssl: $(DB_SSL) - min: $(DB_POOL_MIN:2) sessionCleanupInterval: $(DB_SESSION_CLEANUP_INTERVAL:60000) +pool: + min: $(DB_POOL_MIN:2) ssl: enabled: $(SSL_ACTIVE) port: 3443 diff --git a/server/app/data.yml b/server/app/data.yml index c334003386..9ee86fe52d 100644 --- a/server/app/data.yml +++ b/server/app/data.yml @@ -18,12 +18,11 @@ defaults: storage: ./db.sqlite sslOptions: auto: true - min: 2 sessionCleanupInterval: 60000 ssl: enabled: false pool: - min: 1 + min: 2 bindIP: 0.0.0.0 logLevel: info logFormat: default diff --git a/server/core/db.js b/server/core/db.js index f4e53ad3e7..f6a9d7080d 100644 --- a/server/core/db.js +++ b/server/core/db.js @@ -140,7 +140,6 @@ module.exports = { connection: dbConfig, pool: { ...WIKI.config.pool, - min: WIKI.config.db.min, async afterCreate(conn, done) { // -> Set Connection App Name switch (WIKI.config.db.type) { From 8ac3ee9bf6183e676d6b28f5338abb059e9384a1 Mon Sep 17 00:00:00 2001 From: Jeff Erickson <16201464+jee7s@users.noreply.github.com> Date: Wed, 8 Apr 2026 00:47:49 -0500 Subject: [PATCH 7/9] feat: add configurable /healthz DB check interval Caches the DB health check result and re-queries at most once per interval (default 4 hours) to avoid keeping the connection alive. Configurable via db.healthCheckInterval in config.yml or DB_HEALTH_CHECK_INTERVAL env var in containers. Co-Authored-By: Claude Sonnet 4.6 --- config.sample.yml | 3 +++ dev/build/config.yml | 1 + server/app/data.yml | 1 + server/controllers/common.js | 19 ++++++++++++++----- 4 files changed, 19 insertions(+), 5 deletions(-) diff --git a/config.sample.yml b/config.sample.yml index 0efe345fd8..517655d3fd 100644 --- a/config.sample.yml +++ b/config.sample.yml @@ -34,6 +34,9 @@ db: # Optional - How often (in ms) to clean up expired sessions from the DB (default: 86400000 = 1 day) # sessionCleanupInterval: 86400000 + # Optional - How often (in ms) the /healthz endpoint checks the DB (default: 14400000 = 4 hours) + # healthCheckInterval: 14400000 + # Optional - PostgreSQL / MySQL / MariaDB only: # -> Uncomment lines you need below and set `auto` to false # -> Full list of accepted options: https://nodejs.org/api/tls.html#tls_tls_createsecurecontext_options diff --git a/dev/build/config.yml b/dev/build/config.yml index f3920b98ea..02cc9c2e1f 100644 --- a/dev/build/config.yml +++ b/dev/build/config.yml @@ -10,6 +10,7 @@ db: storage: $(DB_FILEPATH) ssl: $(DB_SSL) sessionCleanupInterval: $(DB_SESSION_CLEANUP_INTERVAL:60000) + healthCheckInterval: $(DB_HEALTH_CHECK_INTERVAL:14400000) pool: min: $(DB_POOL_MIN:2) ssl: diff --git a/server/app/data.yml b/server/app/data.yml index 9ee86fe52d..afb171f851 100644 --- a/server/app/data.yml +++ b/server/app/data.yml @@ -19,6 +19,7 @@ defaults: sslOptions: auto: true sessionCleanupInterval: 60000 + healthCheckInterval: 14400000 ssl: enabled: false pool: diff --git a/server/controllers/common.js b/server/controllers/common.js index dec6fa3aa7..5ea1dca22c 100644 --- a/server/controllers/common.js +++ b/server/controllers/common.js @@ -25,12 +25,21 @@ router.get('/robots.txt', (req, res, next) => { /** * Health Endpoint */ -router.get('/healthz', (req, res, next) => { - if (WIKI.models.knex.client.pool.numFree() < 1 && WIKI.models.knex.client.pool.numUsed() < 1) { - res.status(503).json({ ok: false }).end() - } else { - res.status(200).json({ ok: true }).end() +let healthLastCheck = 0 +let healthLastOk = false + +router.get('/healthz', async (req, res, next) => { + const now = Date.now() + if (now - healthLastCheck > WIKI.config.db.healthCheckInterval) { + try { + await WIKI.models.knex.raw('SELECT 1') + healthLastOk = true + } catch (err) { + healthLastOk = false + } + healthLastCheck = now } + res.status(healthLastOk ? 200 : 503).json({ ok: healthLastOk }).end() }) /** From 9b385736ec51f66c6dd7c0dd90923541e0ea4aa1 Mon Sep 17 00:00:00 2001 From: Jeff Erickson <16201464+jee7s@users.noreply.github.com> Date: Wed, 8 Apr 2026 08:30:35 -0500 Subject: [PATCH 8/9] perf: reduce periodic DB queries to allow connection idling - Move /healthz before Passport middleware so healthchecks don't trigger deserializeUser - Cache deserialized users in memory (default 4h, configurable via auth.userCacheTTL / AUTH_USER_CACHE_TTL) - Increase analytics cache TTL default to 1 day (configurable via analytics.cacheTTL / ANALYTICS_CACHE_TTL) Co-Authored-By: Claude Sonnet 4.6 --- config.sample.yml | 16 ++++++++++++++++ dev/build/config.yml | 4 ++++ server/app/data.yml | 3 +++ server/core/auth.js | 11 +++++++++++ server/master.js | 7 ++++++- server/models/analytics.js | 2 +- 6 files changed, 41 insertions(+), 2 deletions(-) diff --git a/config.sample.yml b/config.sample.yml index 517655d3fd..579a64495e 100644 --- a/config.sample.yml +++ b/config.sample.yml @@ -37,6 +37,22 @@ db: # Optional - How often (in ms) the /healthz endpoint checks the DB (default: 14400000 = 4 hours) # healthCheckInterval: 14400000 +# --------------------------------------------------------------------- +# Analytics +# --------------------------------------------------------------------- + +# analytics: + # Optional - How long (in seconds) to cache analytics config (default: 86400 = 1 day) + # cacheTTL: 86400 + +# --------------------------------------------------------------------- +# Authentication +# --------------------------------------------------------------------- + +# auth: + # Optional - How long (in seconds) to cache deserialized users (default: 14400 = 4 hours, 0 to disable) + # userCacheTTL: 14400 + # Optional - PostgreSQL / MySQL / MariaDB only: # -> Uncomment lines you need below and set `auto` to false # -> Full list of accepted options: https://nodejs.org/api/tls.html#tls_tls_createsecurecontext_options diff --git a/dev/build/config.yml b/dev/build/config.yml index 02cc9c2e1f..392a236839 100644 --- a/dev/build/config.yml +++ b/dev/build/config.yml @@ -22,3 +22,7 @@ ssl: logLevel: $(LOG_LEVEL:info) logFormat: $(LOG_FORMAT:default) ha: $(HA_ACTIVE) +analytics: + cacheTTL: $(ANALYTICS_CACHE_TTL:86400) +auth: + userCacheTTL: $(AUTH_USER_CACHE_TTL:14400) diff --git a/server/app/data.yml b/server/app/data.yml index afb171f851..495518ed0a 100644 --- a/server/app/data.yml +++ b/server/app/data.yml @@ -63,6 +63,8 @@ defaults: iconset: 'md' darkMode: false tocPosition: 'left' + analytics: + cacheTTL: 86400 auth: autoLogin: false enforce2FA: false @@ -71,6 +73,7 @@ defaults: audience: 'urn:wiki.js' tokenExpiration: '30m' tokenRenewal: '14d' + userCacheTTL: 14400 editShortcuts: editFab: true editMenuBar: false diff --git a/server/core/auth.js b/server/core/auth.js index fb30c970c2..1da18ae5d6 100644 --- a/server/core/auth.js +++ b/server/core/auth.js @@ -21,6 +21,7 @@ module.exports = { groups: {}, validApiKeys: [], revocationList: require('./cache').init(), + userCache: require('./cache').init(), /** * Initialize the authentication module @@ -34,10 +35,20 @@ module.exports = { passport.deserializeUser(async (id, done) => { try { + const ttl = WIKI.config.auth.userCacheTTL + if (ttl > 0) { + const cached = this.userCache.get(id) + if (cached !== undefined) { + return done(null, cached) + } + } const user = await WIKI.models.users.query().findById(id).withGraphFetched('groups').modifyGraph('groups', builder => { builder.select('groups.id', 'permissions') }) if (user) { + if (ttl > 0) { + this.userCache.set(id, user, ttl) + } done(null, user) } else { done(new Error(WIKI.lang.t('auth:errors:usernotfound')), null) diff --git a/server/master.js b/server/master.js index 09ae9fc8f6..f54f241fbc 100644 --- a/server/master.js +++ b/server/master.js @@ -71,6 +71,12 @@ module.exports = async () => { app.use('/', ctrl.ssl) + // ---------------------------------------- + // Health Endpoint (before Passport to avoid DB queries on healthcheck) + // ---------------------------------------- + + app.use('/', ctrl.common) + // ---------------------------------------- // Passport Authentication // ---------------------------------------- @@ -165,7 +171,6 @@ module.exports = async () => { app.use('/', ctrl.auth) app.use('/', ctrl.upload) - app.use('/', ctrl.common) // ---------------------------------------- // Error handling diff --git a/server/models/analytics.js b/server/models/analytics.js index 17a3f6ab54..6895d7a407 100644 --- a/server/models/analytics.js +++ b/server/models/analytics.js @@ -127,7 +127,7 @@ module.exports = class Analytics extends Model { analyticsCode.bodyEnd += code.bodyEnd } - await WIKI.cache.set('analytics', analyticsCode, 300) + await WIKI.cache.set('analytics', analyticsCode, WIKI.config.analytics.cacheTTL) return analyticsCode } catch (err) { From 3bd07c5d93d67a934f008bed2ce22a3332a39385 Mon Sep 17 00:00:00 2001 From: Jeff Erickson <16201464+jee7s@users.noreply.github.com> Date: Wed, 8 Apr 2026 10:19:09 -0500 Subject: [PATCH 9/9] fix: register /healthz directly before Passport to avoid i18n middleware dependency Moving all of ctrl.common early caused req.i18n to be undefined for page routes since localization middleware hadn't run yet. Now only /healthz is registered early; ctrl.common remains in its original position. Co-Authored-By: Claude Sonnet 4.6 --- server/controllers/common.js | 19 ------------------- server/master.js | 17 ++++++++++++++++- 2 files changed, 16 insertions(+), 20 deletions(-) diff --git a/server/controllers/common.js b/server/controllers/common.js index 5ea1dca22c..12811ee54c 100644 --- a/server/controllers/common.js +++ b/server/controllers/common.js @@ -22,25 +22,6 @@ router.get('/robots.txt', (req, res, next) => { } }) -/** - * Health Endpoint - */ -let healthLastCheck = 0 -let healthLastOk = false - -router.get('/healthz', async (req, res, next) => { - const now = Date.now() - if (now - healthLastCheck > WIKI.config.db.healthCheckInterval) { - try { - await WIKI.models.knex.raw('SELECT 1') - healthLastOk = true - } catch (err) { - healthLastOk = false - } - healthLastCheck = now - } - res.status(healthLastOk ? 200 : 503).json({ ok: healthLastOk }).end() -}) /** * Administration diff --git a/server/master.js b/server/master.js index f54f241fbc..23a6fdcb6f 100644 --- a/server/master.js +++ b/server/master.js @@ -75,7 +75,21 @@ module.exports = async () => { // Health Endpoint (before Passport to avoid DB queries on healthcheck) // ---------------------------------------- - app.use('/', ctrl.common) + let healthLastCheck = 0 + let healthLastOk = false + app.get('/healthz', async (req, res) => { + const now = Date.now() + if (now - healthLastCheck > WIKI.config.db.healthCheckInterval) { + try { + await WIKI.models.knex.raw('SELECT 1') + healthLastOk = true + } catch (err) { + healthLastOk = false + } + healthLastCheck = now + } + res.status(healthLastOk ? 200 : 503).json({ ok: healthLastOk }).end() + }) // ---------------------------------------- // Passport Authentication @@ -171,6 +185,7 @@ module.exports = async () => { app.use('/', ctrl.auth) app.use('/', ctrl.upload) + app.use('/', ctrl.common) // ---------------------------------------- // Error handling