codeSentry / codesentry-backend /memory /session_store.py
YashashviAlva's picture
Initial commit for HF Spaces deploy
7b4f5dd
"""
In-memory session store.
No database required β€” all sessions are held in process memory
and automatically expire after a configurable TTL.
"""
from __future__ import annotations
import asyncio
import logging
import time
from collections import OrderedDict
from typing import Any, Dict, Optional
logger = logging.getLogger(__name__)
DEFAULT_TTL_SECONDS = 3600 # 1 hour
MAX_SESSIONS = 1000 # prevent unbounded growth
class SessionStore:
"""
Thread-safe (asyncio-safe) in-memory key-value session store.
Sessions expire after TTL seconds and are evicted on next access.
"""
def __init__(self, ttl: int = DEFAULT_TTL_SECONDS, max_sessions: int = MAX_SESSIONS) -> None:
self._store: OrderedDict[str, Dict[str, Any]] = OrderedDict()
self._ttl = ttl
self._max_sessions = max_sessions
self._lock = asyncio.Lock()
# ── Internal helpers ─────────────────────────────
def _is_expired(self, session: Dict[str, Any]) -> bool:
return time.monotonic() - session["_created_at"] > self._ttl
def _evict_expired(self) -> None:
expired = [sid for sid, s in self._store.items() if self._is_expired(s)]
for sid in expired:
del self._store[sid]
logger.debug("[Session] Evicted expired session %s", sid)
def _evict_oldest(self) -> None:
if self._store:
oldest_id, _ = next(iter(self._store.items()))
del self._store[oldest_id]
logger.debug("[Session] Evicted oldest session %s (capacity limit)", oldest_id)
# ── Public API ───────────────────────────────────
async def create(self, session_id: str, data: Optional[Dict] = None) -> Dict[str, Any]:
"""Create a new session, returning the initial session dict."""
async with self._lock:
self._evict_expired()
if len(self._store) >= self._max_sessions:
self._evict_oldest()
session: Dict[str, Any] = {
"_session_id": session_id,
"_created_at": time.monotonic(),
"_status": "pending",
**(data or {}),
}
self._store[session_id] = session
logger.info("[Session] Created session %s", session_id)
return session
async def get(self, session_id: str) -> Optional[Dict[str, Any]]:
"""Retrieve a session by ID, or None if not found / expired."""
async with self._lock:
session = self._store.get(session_id)
if session is None:
return None
if self._is_expired(session):
del self._store[session_id]
logger.debug("[Session] Session %s expired on get", session_id)
return None
# Move to end (LRU-style freshness)
self._store.move_to_end(session_id)
return session
async def update(self, session_id: str, updates: Dict[str, Any]) -> bool:
"""Update fields in an existing session. Returns False if session not found."""
async with self._lock:
session = self._store.get(session_id)
if session is None or self._is_expired(session):
return False
session.update(updates)
self._store.move_to_end(session_id)
return True
async def delete(self, session_id: str) -> bool:
"""Delete a session by ID. Returns True if it existed."""
async with self._lock:
existed = session_id in self._store
self._store.pop(session_id, None)
if existed:
logger.info("[Session] Deleted session %s", session_id)
return existed
async def set_status(self, session_id: str, status: str) -> None:
"""Convenience method to update only the session status."""
await self.update(session_id, {"_status": status})
async def list_sessions(self) -> list:
"""Return a list of non-expired session IDs."""
async with self._lock:
self._evict_expired()
return list(self._store.keys())
async def count(self) -> int:
"""Return the number of active (non-expired) sessions."""
async with self._lock:
self._evict_expired()
return len(self._store)
async def clear_all(self) -> int:
"""Wipe all sessions. Returns the count of sessions removed."""
async with self._lock:
count = len(self._store)
self._store.clear()
logger.info("[Session] Cleared all %d sessions", count)
return count
# ──────────────────────────────────────────────
# Singleton instance (shared across the app)
# ──────────────────────────────────────────────
_store: Optional[SessionStore] = None
def get_store() -> SessionStore:
"""Return the global singleton SessionStore, creating it if necessary."""
global _store
if _store is None:
_store = SessionStore()
return _store