import logging import os import time logger = logging.getLogger(__name__) _DEFAULT_TIMEOUT = 30 _CLEANUP_INTERVAL = 60 class AlexaSessionManager: def __init__(self, account=None): self._sessions: dict[str, dict] = {} self._timeout = getattr(account, "session_timeout_seconds", _DEFAULT_TIMEOUT) if account else _DEFAULT_TIMEOUT self._persistence_enabled = getattr(account, "session_persistence_enabled", False) if account else False self._last_cleanup = time.time() self._redis = None if self._persistence_enabled: self._init_redis() def _init_redis(self): redis_url = os.getenv("REDIS_URL", os.getenv("YUXI_REDIS_URL", "")) if not redis_url: logger.warning("Alexa session persistence enabled but no REDIS_URL configured") self._persistence_enabled = False return try: import redis.asyncio as aioredis self._redis = aioredis.from_url(redis_url, decode_responses=True) logger.info("Alexa session persistence: using Redis backend") except Exception: logger.warning("Alexa session persistence: Redis unavailable, falling back to in-memory") self._persistence_enabled = False def get(self, session_id: str) -> dict: self._cleanup_expired() session = self._sessions.get(session_id) if session: session["_last_access"] = time.time() return session or {} async def get_async(self, session_id: str) -> dict: if self._redis: try: import json data = await self._redis.get(f"alexa:session:{session_id}") if data: session = json.loads(data) session["_last_access"] = time.time() self._sessions[session_id] = session await self._redis.expire(f"alexa:session:{session_id}", self._timeout) return session except Exception: logger.debug("Redis session read failed, falling back to memory") return self.get(session_id) def set(self, session_id: str, attrs: dict): attrs["_created_at"] = attrs.get("_created_at", time.time()) attrs["_last_access"] = time.time() self._sessions[session_id] = attrs async def set_async(self, session_id: str, attrs: dict): attrs["_created_at"] = attrs.get("_created_at", time.time()) attrs["_last_access"] = time.time() self._sessions[session_id] = attrs if self._redis: try: import json await self._redis.setex( f"alexa:session:{session_id}", self._timeout, json.dumps(attrs, ensure_ascii=False, default=str), ) except Exception: logger.debug("Redis session write failed") def delete(self, session_id: str): self._sessions.pop(session_id, None) async def delete_async(self, session_id: str): self._sessions.pop(session_id, None) if self._redis: try: await self._redis.delete(f"alexa:session:{session_id}") except Exception: logger.debug("Redis session delete failed") def _cleanup_expired(self): now = time.time() if now - self._last_cleanup < _CLEANUP_INTERVAL: return self._last_cleanup = now expired = [ sid for sid, s in self._sessions.items() if now - s.get("_last_access", 0) > self._timeout ] for sid in expired: self._sessions.pop(sid, None) if expired: logger.debug("Alexa session: cleaned up %d expired sessions", len(expired)) async def close(self): self._sessions.clear() if self._redis: try: await self._redis.aclose() except Exception: pass self._redis = None