| 246 | |
| 247 | |
| 248 | class SessionManager: |
| 249 | def __init__(self): |
| 250 | self._lock = threading.Lock() |
| 251 | self._sessions: Dict[str, SessionRuntime] = {} |
| 252 | self._stop = threading.Event() |
| 253 | self._janitor = threading.Thread( |
| 254 | target=self._run_janitor, daemon=True, name="session-janitor" |
| 255 | ) |
| 256 | self._janitor.start() |
| 257 | atexit.register(self.shutdown) |
| 258 | |
| 259 | def get(self, session_id: str) -> SessionRuntime: |
| 260 | if not session_id: |
| 261 | raise KeyError("session_id_REQUIRED") |
| 262 | with self._lock: |
| 263 | rt = self._sessions.get(session_id) |
| 264 | if rt: |
| 265 | return rt |
| 266 | # Evict LRU if needed |
| 267 | if len(self._sessions) >= MAX_SESSIONS: |
| 268 | stalest_sid, stalest_rt = None, None |
| 269 | for sid, srt in self._sessions.items(): |
| 270 | if stalest_rt is None or srt.last_used < stalest_rt.last_used: |
| 271 | stalest_sid, stalest_rt = sid, srt |
| 272 | if stalest_sid: |
| 273 | self._sessions.pop(stalest_sid, None) |
| 274 | try: |
| 275 | stalest_rt.close_sync() |
| 276 | except Exception: |
| 277 | pass |
| 278 | root = (SESSIONS_DIR / session_fs_key(session_id)).resolve() |
| 279 | rt = SessionRuntime(session_id=session_id, root=root) |
| 280 | rt.ensure_dirs() |
| 281 | self._sessions[session_id] = rt |
| 282 | return rt |
| 283 | |
| 284 | def touch(self, session_id: str) -> None: |
| 285 | with self._lock: |
| 286 | if session_id in self._sessions: |
| 287 | self._sessions[session_id].touch() |
| 288 | |
| 289 | def destroy(self, session_id: str) -> None: |
| 290 | with self._lock: |
| 291 | rt = self._sessions.pop(session_id, None) |
| 292 | if rt: |
| 293 | rt.close_sync() |
| 294 | |
| 295 | def _run_janitor(self) -> None: |
| 296 | while not self._stop.is_set(): |
| 297 | time.sleep(10) |
| 298 | now = time.time() |
| 299 | expired: list[tuple[str, SessionRuntime]] = [] |
| 300 | with self._lock: |
| 301 | for sid, rt in list(self._sessions.items()): |
| 302 | if now - rt.last_used > SESSION_TTL_SECONDS: |
| 303 | expired.append((sid, rt)) |
| 304 | self._sessions.pop(sid, None) |
| 305 | |