fix: add periodic cleanup of expired/stale sessions from database#7448
Conversation
Review Summary by QodoAdd periodic cleanup of expired/stale sessions from database
WalkthroughsDescription• Adds periodic cleanup of expired/stale sessions from database • Removes sessions with expired cookies or no meaningful data • Prevents unbounded session storage growth in database • Cleanup runs hourly with initial run 5 seconds after startup Diagramflowchart LR
A["SessionStore initialized"] --> B["startCleanup called"]
B --> C["Initial cleanup after 5s"]
B --> D["Periodic cleanup every 1 hour"]
C --> E["Find all sessionstorage keys"]
D --> E
E --> F["Check each session"]
F --> G["Remove if expired"]
F --> H["Remove if stale and empty"]
G --> I["Log removed count"]
H --> I
File Changes1. src/node/db/SessionStore.ts
|
Code Review by Qodo
1. sessionCleanup not documented
|
|
Persistent review updated to latest commit dd40743 |
|
Maybe some people want stale sessions so this should be under settings? Because this PR could be disruptive (shouldn't be could) I want approval from another maintainer before merging. |
The session is only cleared after the user has interacted with it, then left it inactive for some time—so when they return, it may appear as though they are no longer the original author, correct? I think it is correct to make it as an option but enable it as default. So if there are usecases where you need the sessions to be persisted forever it does not cause any issues. |
I think it's this from settings.json but I'll confirm before merging: I do have a concern here that if we release this patch it might lock the CPU thread of Node to do clear up batches, it's not ideal but it's a one time suffer and probably needs to happen... |
|
Good point. I'll add a |
SessionStore now runs a periodic cleanup (every hour, plus once on startup) that removes: - Sessions with expired cookies (expires date in the past) - Sessions with no expiry that contain no data beyond the default cookie (the empty sessions that accumulate indefinitely per ether#5010) Without this, sessions accumulated forever in the database because: 1. Sessions with no maxAge never got an expiry date 2. On server restart, in-memory expiration timeouts were lost 3. There was no mechanism to clean up sessions that were never accessed again Fixes ether#5010 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Use a local variable for the SessionStore instance to avoid type narrowing issues with the module-level Store|null variable. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Replace setInterval with chained setTimeout to prevent overlapping cleanup runs on large databases - Store and clear startup timeout in shutdown() to prevent leaks - Add .unref() on all timers so they don't delay process exit - Fix misleading docstring — cleanup removes empty no-expiry sessions, not sessions older than STALE_SESSION_MAX_AGE_MS (removed unused const) - Add 5 regression tests: expired sessions removed, empty sessions removed, sessions with data preserved, valid sessions preserved, shutdown cancels timer Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Session cleanup is now gated behind cookie.sessionCleanup (default true). Admins who want to keep stale sessions can set this to false in settings.json. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
/review |
d4b8633 to
9cfe63d
Compare
|
Persistent review updated to latest commit 9cfe63d |
| /* | ||
| * Whether to periodically clean up expired and stale sessions from the | ||
| * database. Set to false to disable. Default: true. | ||
| */ | ||
| "sessionCleanup": true |
There was a problem hiding this comment.
1. sessioncleanup not documented 📘 Rule violation ⚙ Maintainability
A new configuration option cookie.sessionCleanup is introduced, but no corresponding documentation update under doc/ is provided to describe the setting and its impact. This can cause operators to miss the new behavior and how to control it.
Agent Prompt
## Issue description
A new config option (`cookie.sessionCleanup`) was added without updating `doc/` documentation to explain it.
## Issue Context
Operators rely on `doc/` (including Docker configuration docs) to understand available settings and defaults.
## Fix Focus Areas
- settings.json.template[468-472]
- doc/docker.md[186-193]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| _scheduleCleanup(delay: number) { | ||
| this._cleanupTimer = setTimeout(async () => { | ||
| try { | ||
| await this._cleanup(); | ||
| } catch (err) { | ||
| logger.error('Session cleanup error:', err); | ||
| } | ||
| // Schedule the next run only after this one completes. | ||
| this._scheduleCleanup(CLEANUP_INTERVAL_MS); | ||
| }, delay); | ||
| // Don't prevent Node.js from exiting. | ||
| if (this._cleanupTimer.unref) this._cleanupTimer.unref(); | ||
| } | ||
|
|
||
| shutdown() { | ||
| for (const {timeout} of this._expirations.values()) clearTimeout(timeout); | ||
| if (this._cleanupTimer) { | ||
| clearTimeout(this._cleanupTimer); | ||
| this._cleanupTimer = null; | ||
| } | ||
| } |
There was a problem hiding this comment.
2. Cleanup survives shutdown race 🐞 Bug ☼ Reliability
If shutdown() happens while the async cleanup callback is running, the callback will still schedule the next cleanup run, leaving a “zombie” periodic cleanup loop running after restart/shutdown. This can cause unexpected DB activity after sessionStore is nulled out and can overlap with the new store’s cleanup after restartServer().
Agent Prompt
## Issue description
`SessionStore._scheduleCleanup()` always reschedules itself after `_cleanup()` completes. If `shutdown()` is called while `_cleanup()` is in progress (the timer already fired), the callback will still reschedule another timer, so cleanup continues even after the store is shut down and dereferenced.
## Issue Context
This can happen during `restartServer()` because `closeServer()` calls `sessionStore.shutdown()` while the process continues running and then replaces the store.
## Fix Focus Areas
- src/node/db/SessionStore.ts[40-68]
- src/node/hooks/express.ts[32-65]
## Suggested fix
- Add a boolean like `_cleanupStopped` (or reuse `_cleanupRunning` + a new stop flag) that is set to `true` in `shutdown()`.
- In the timer callback, check the stop flag before calling `_scheduleCleanup()` again.
- Make `startCleanup()` idempotent (if a timer is already scheduled and not stopped, do nothing) to avoid multiple loops.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| async _cleanup() { | ||
| const keys = await DB.findKeys('sessionstorage:*', null); | ||
| if (!keys || keys.length === 0) return; | ||
| const now = Date.now(); | ||
| let removed = 0; | ||
| for (const key of keys) { | ||
| const sess = await DB.get(key); | ||
| if (!sess) { | ||
| await DB.remove(key); | ||
| removed++; | ||
| continue; | ||
| } | ||
| const expires = sess.cookie?.expires; | ||
| if (expires) { | ||
| // Session has an expiry — remove if expired. | ||
| if (new Date(expires).getTime() <= now) { | ||
| await DB.remove(key); | ||
| removed++; | ||
| } | ||
| } else { | ||
| // Session has no expiry and no user data beyond the cookie — remove as empty/stale. | ||
| const hasData = Object.keys(sess).some((k) => k !== 'cookie'); | ||
| if (!hasData) { | ||
| await DB.remove(key); | ||
| removed++; | ||
| } | ||
| } |
There was a problem hiding this comment.
3. Stale cache deletes valid sessions 🐞 Bug ≡ Correctness
_cleanup() deletes sessions based on the expires value returned by DB.get(), which can be stale in multi-instance deployments if ueberdb client-side caching returns an outdated record. This can incorrectly delete still-valid sessions whose expiration was extended by another instance.
Agent Prompt
## Issue description
`SessionStore._cleanup()` removes sessions when `cookie.expires <= now` based on `DB.get()` results. In multi-instance deployments, `DB.get()` can return stale cached data, so an expiration that was extended by another instance might still look expired and be deleted.
## Issue Context
The code already documents this exact risk for expiration-based deletion logic (ueberdb client-side caching). Cleanup is another deletion path that should be at least as conservative.
## Fix Focus Areas
- src/node/db/SessionStore.ts[78-105]
- src/node/db/SessionStore.ts[125-132]
## Suggested fix options (pick one)
1) **Grace period**: only delete if expired for some buffer (e.g., `expiresTime <= now - GRACE_MS`), reducing the chance that a short-lived stale cache causes deletion.
2) **Re-check before delete**: when an entry appears expired, perform a second read intended to bypass cache (if supported) or otherwise re-validate before removal.
3) **Config guidance / enforcement**: if the project supports clustered deployments, document and/or enforce disabling client-side DB caching for session records when `sessionCleanup` is enabled.
(1) is implementable purely within this PR’s code surface and is the safest default without requiring DB-layer changes.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
Review Summary by QodoAdd periodic cleanup of expired/stale sessions from database
WalkthroughsDescription• Adds periodic cleanup of expired/stale sessions from database - Removes sessions with expired cookies - Removes empty sessions with no expiry (bug #5010) - Uses chained setTimeout to prevent overlapping runs • Implements configurable session cleanup via cookie.sessionCleanup setting • Adds 5 regression tests for cleanup functionality • Ensures timers use .unref() and are properly cancelled on shutdown Diagramflowchart LR
A["SessionStore startup"] -->|"startCleanup()"| B["Schedule cleanup"]
B -->|"every 1 hour"| C["_cleanup() runs"]
C -->|"find expired"| D["Remove expired sessions"]
C -->|"find empty"| E["Remove empty sessions"]
D --> F["Log removed count"]
E --> F
F -->|"reschedule"| B
G["shutdown()"] -->|"clearTimeout()"| H["Cancel cleanup timer"]
File Changes1. src/node/db/SessionStore.ts
|
Code Review by Qodo
1. sessionCleanup not documented
|
Summary
Adds periodic session cleanup to
SessionStorethat removes expired and stale sessions from the database, preventing unbounded growth.Root Cause
Session storage grew indefinitely (16M+ records reported) because:
_expires: null) accumulated forever with no cleanupFix
SessionStore._cleanup()runs periodically (chainedsetTimeout, notsetInterval, to prevent overlapping runs) which:All timers use
.unref()and are properly cancelled inshutdown().Test plan
SessionStore.ts: expired removed, empty removed, data preserved, valid preserved, shutdown cancels timerFixes #5010
🤖 Generated with Claude Code