Skip to main content

Module session_store_cache

Module session_store_cache 

Source
Expand description

LRU-bounded cache of per-palace ChatSessionStore handles (issue #4639).

Why: the previous unbounded DashMap never released a chat_sessions.redb file descriptor, leaking one per palace for the daemon’s lifetime. What: see session_store_cache::SessionStoreCache. Test: session_store_cache::tests. LRU-bounded cache of per-palace ChatSessionStore handles (issue #4639).

Why: AppState::session_stores was a plain DashMap<String, Arc<ChatSessionStore>> with no remove, no TTL, and no cap — every palace the daemon ever touched leaked one chat_sessions.redb file descriptor for the process lifetime. A live daemon was measured holding 844 such handles (all 844 pointing at files already unlinked from disk) against an 8 192 fd ceiling, growing ~250-300/day. PalaceRegistry already solved exactly this failure class for kg/usearch/recall via an LRU (issue #463); this module applies the same shape to the one file type that registry never tracked.

What: SessionStoreCache — a parking_lot::Mutex<LruCache<..>> (opened unbounded, trimmed manually to SessionStoreCache::capacity) that opens a store on miss, promotes on hit, and evicts from the cold end once resident entries exceed the cap. Dropping the last Arc closes the redb Database and releases its fd; the next request reopens from disk transparently.

Two invariants make eviction safe, both enforced under the single cache mutex:

  1. Never evict a store a caller still holds. redb takes an exclusive flock, and ChatSessionStore::open has no snapshot fallback, so a second open of a file that is still open in this same process fails hard with Database already open. Cannot acquire lock. (reproduced directly while diagnosing #4639). The chat streaming handler holds an Arc across a whole SSE response (chat::handler), so unconditional LRU eviction would turn a leak into an outage. Eviction therefore skips any entry whose Arc::strong_count() > 1 and overshoots the cap rather than closing a store out from under its user.
  2. Exactly one store per palace. The open runs while the cache mutex is held, so two concurrent callers for the same id cannot both open the file (the DatabaseAlreadyOpen path PalaceRegistry guards with per-id mutexes). The critical section is pure blocking I/O with no .await, so parking_lot::Mutex is safe here.

Test: tests::open_handles_are_bounded_by_cap, session_store_fd_count_is_bounded_by_cap (in tests/session_store_fd_bound.rs — needs its own process to measure real fds; see that file’s header), tests::evicted_store_reopens_with_data_intact, tests::in_use_store_is_never_evicted, tests::concurrent_callers_share_one_store, tests::remove_drops_cached_handle.

Structs§

SessionStoreCache
LRU-bounded, thread-safe cache of open per-palace chat-session stores.

Constants§

DEFAULT_MAX_OPEN_SESSION_STORES
Default maximum number of chat_sessions.redb handles held open at once.
MAX_OPEN_SESSION_STORES_ENV
Environment variable overriding the resident chat-session-store cap.

Functions§

max_open_session_stores_from_env
Resolve the effective cap from the environment.