fix: reap expired sessions with a periodic background sweep
The sessions table only grew — find_active ignored expired rows but nothing deleted them, so lapsed sessions accumulated indefinitely. - repo::session::delete_expired: indexed DELETE (sessions_expires_idx) returning the reaped count. - app::build spawns a detached hourly reaper (SESSION_GC_INTERVAL) that calls it, independent of crawler/analysis config. Test: delete_expired_removes_only_lapsed_sessions. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -277,6 +277,11 @@ impl DaemonReloader for Supervisors {
|
||||
}
|
||||
}
|
||||
|
||||
/// How often the background reaper sweeps expired sessions. Hourly is ample:
|
||||
/// the sweep is a single indexed DELETE and expired rows are already invisible
|
||||
/// to auth, so this is purely storage hygiene.
|
||||
const SESSION_GC_INTERVAL: std::time::Duration = std::time::Duration::from_secs(3600);
|
||||
|
||||
pub async fn build(config: Config) -> anyhow::Result<AppHandle> {
|
||||
let db = PgPoolOptions::new()
|
||||
.max_connections(config.db.max_connections)
|
||||
@@ -337,6 +342,27 @@ pub async fn build(config: Config) -> anyhow::Result<AppHandle> {
|
||||
tracing::info!("analysis worker disabled");
|
||||
}
|
||||
|
||||
// Periodic reaper for lapsed sessions. `find_active` already ignores
|
||||
// expired rows, so this only reclaims storage — without it the table grows
|
||||
// unbounded as sessions lapse. Detached and best-effort: a failed sweep is
|
||||
// logged and retried next tick. Runs regardless of crawler/analysis config
|
||||
// since sessions exist in every deployment.
|
||||
{
|
||||
let db = db.clone();
|
||||
tokio::spawn(async move {
|
||||
let mut ticker = tokio::time::interval(SESSION_GC_INTERVAL);
|
||||
ticker.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
|
||||
loop {
|
||||
ticker.tick().await;
|
||||
match repo::session::delete_expired(&db).await {
|
||||
Ok(0) => {}
|
||||
Ok(n) => tracing::info!(reaped = n, "session gc: removed expired sessions"),
|
||||
Err(e) => tracing::warn!(?e, "session gc sweep failed; retrying next tick"),
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let auth_limiter = Arc::new(AuthRateLimiter::new(config.auth.rate_limit));
|
||||
let state = AppState {
|
||||
db,
|
||||
|
||||
@@ -65,3 +65,15 @@ pub async fn delete_by_token_hash(pool: &PgPool, token_hash: &[u8]) -> AppResult
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Delete every session whose `expires_at` has passed, returning the number
|
||||
/// reaped. `find_active` already refuses expired sessions, so this only
|
||||
/// reclaims storage — running it on any schedule (or not at all) is safe.
|
||||
/// Backed by `sessions_expires_idx` (0002). Called by the periodic reaper in
|
||||
/// `app::build`.
|
||||
pub async fn delete_expired(pool: &PgPool) -> AppResult<u64> {
|
||||
let result = sqlx::query("DELETE FROM sessions WHERE expires_at <= now()")
|
||||
.execute(pool)
|
||||
.await?;
|
||||
Ok(result.rows_affected())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user