Spaces:
Sleeping
Sleeping
| """Atomic JSON persistence for messaging session state.""" | |
| from __future__ import annotations | |
| import contextlib | |
| import json | |
| import os | |
| import tempfile | |
| import threading | |
| from collections.abc import Callable | |
| from dataclasses import dataclass | |
| from typing import Any | |
| from loguru import logger | |
| class _PendingWrite: | |
| generation: int | |
| snapshot: dict[str, Any] | |
| class DebouncedJsonPersistence: | |
| """Thread-safe debounced JSON writer with atomic replace semantics.""" | |
| def __init__( | |
| self, | |
| storage_path: str, | |
| *, | |
| snapshot: Callable[[], dict[str, Any]], | |
| on_dirty: Callable[[bool], None], | |
| debounce_secs: float = 0.5, | |
| ) -> None: | |
| self.storage_path = storage_path | |
| self._snapshot = snapshot | |
| self._on_dirty = on_dirty | |
| self._debounce_secs = debounce_secs | |
| self._save_timer: threading.Timer | None = None | |
| self._timer_lock = threading.Lock() | |
| self._writer_lock = threading.Lock() | |
| self._save_generation = 0 | |
| def load_json(self) -> dict[str, Any]: | |
| if not os.path.exists(self.storage_path): | |
| return {} | |
| with open(self.storage_path, encoding="utf-8") as file: | |
| data = json.load(file) | |
| return data if isinstance(data, dict) else {} | |
| def schedule_save(self) -> None: | |
| self._on_dirty(True) | |
| with self._timer_lock: | |
| if self._save_timer is not None: | |
| self._save_timer.cancel() | |
| self._save_generation += 1 | |
| generation = self._save_generation | |
| timer = threading.Timer( | |
| self._debounce_secs, | |
| self._save_from_timer, | |
| args=(generation,), | |
| ) | |
| timer.daemon = True | |
| self._save_timer = timer | |
| timer.start() | |
| def flush(self) -> None: | |
| self._on_dirty(True) | |
| pending = self._snapshot_for_write() | |
| if pending is None: | |
| return | |
| self._write_pending(pending) | |
| def _save_from_timer(self, generation: int) -> None: | |
| try: | |
| pending = self._snapshot_for_write(expected_generation=generation) | |
| if pending is None: | |
| return | |
| self._write_pending(pending) | |
| except Exception as e: | |
| self._on_dirty(True) | |
| logger.error( | |
| "Failed to save sessions: exc_type={}", | |
| type(e).__name__, | |
| ) | |
| def _write_pending(self, pending: _PendingWrite) -> None: | |
| try: | |
| written = self._write_if_current(pending) | |
| except Exception: | |
| self._on_dirty(True) | |
| raise | |
| if written: | |
| self._mark_clean_if_current(pending.generation) | |
| def _write_if_current(self, pending: _PendingWrite) -> bool: | |
| """Serialize writers and reject a snapshot superseded before replace.""" | |
| with self._writer_lock: | |
| with self._timer_lock: | |
| if pending.generation != self._save_generation: | |
| return False | |
| self._write_file(pending.snapshot) | |
| return True | |
| def _snapshot_for_write( | |
| self, *, expected_generation: int | None = None | |
| ) -> _PendingWrite | None: | |
| generation = self._claim_timer(expected_generation) | |
| if generation is None: | |
| return None | |
| snapshot = self._snapshot() | |
| return _PendingWrite(generation=generation, snapshot=snapshot) | |
| def _claim_timer(self, expected_generation: int | None) -> int | None: | |
| with self._timer_lock: | |
| if expected_generation is not None and ( | |
| expected_generation != self._save_generation or self._save_timer is None | |
| ): | |
| return None | |
| if self._save_timer is not None: | |
| self._save_timer.cancel() | |
| self._save_timer = None | |
| return self._save_generation | |
| def _mark_clean_if_current(self, generation: int) -> None: | |
| with self._timer_lock: | |
| is_current = ( | |
| self._save_timer is None and generation == self._save_generation | |
| ) | |
| if is_current: | |
| self._on_dirty(False) | |
| def write_data(self, data: dict[str, Any]) -> None: | |
| """Write authoritative state after invalidating older pending snapshots.""" | |
| self._on_dirty(True) | |
| with self._timer_lock: | |
| if self._save_timer is not None: | |
| self._save_timer.cancel() | |
| self._save_timer = None | |
| self._save_generation += 1 | |
| pending = _PendingWrite( | |
| generation=self._save_generation, | |
| snapshot=data, | |
| ) | |
| self._write_pending(pending) | |
| def _write_file(self, data: dict[str, Any]) -> None: | |
| abs_target = os.path.abspath(self.storage_path) | |
| dir_name = os.path.dirname(abs_target) or "." | |
| fd, tmp_path = tempfile.mkstemp( | |
| dir=dir_name, | |
| prefix=".sessions.", | |
| suffix=".tmp.json", | |
| ) | |
| try: | |
| with os.fdopen(fd, "w", encoding="utf-8") as file: | |
| json.dump(data, file, indent=2) | |
| file.flush() | |
| os.fsync(file.fileno()) | |
| os.replace(tmp_path, abs_target) | |
| except BaseException: | |
| with contextlib.suppress(OSError): | |
| os.unlink(tmp_path) | |
| raise | |