Download app/utils/guest_session_pool.py from bg5/zai: direct link, hf CLI and curl.
- Browser
- Download file 28.3 kB
-
https://huggingface.co/spaces/bg5/zai/resolve/main/app/utils/guest_session_pool.py
- Command line
-
hf download hf://spaces/bg5/zai/app/utils/guest_session_pool.py
-
curl -L -o guest_session_pool.py https://huggingface.co/spaces/bg5/zai/resolve/main/app/utils/guest_session_pool.py
28.3 kB
| #!/usr/bin/env python | |
| # -*- coding: utf-8 -*- | |
| """匿名访客会话池。""" | |
| import asyncio | |
| import time | |
| from dataclasses import dataclass, field | |
| from threading import Lock | |
| from typing import Dict, Any, List, Optional, Set | |
| import httpx | |
| import jwt | |
| from app.core.config import settings | |
| from app.core.http_client import SharedHttpClients | |
| from app.core.headers import build_dynamic_headers as _build_dynamic_headers | |
| from app.core.httpx_errors import normalize_httpx_exception, normalize_httpx_response | |
| from app.core.resin_compat import apply_resin_account_header | |
| from app.core.upstream_urls import build_upstream_url | |
| from app.utils.fe_version import get_latest_fe_version | |
| from app.utils.logger import logger, log_exception | |
| from app.utils.utlis import mask_token | |
| AUTH_URL = "/api/v1/auths/" | |
| CHATS_URL = "/api/v1/chats/" | |
| def _decode_token_payload(token: str) -> Dict[str, Any]: | |
| """无需校验签名,仅解密 JWT Payload 以获取元数据。""" | |
| try: | |
| return jwt.decode(token, options={"verify_signature": False}) | |
| except Exception: | |
| return {} | |
| class GuestSession: | |
| """单个匿名访客会话。""" | |
| token: str | |
| user_id: str | |
| username: str | |
| created_at: float = field(default_factory=time.time) | |
| active_requests: int = 0 | |
| valid: bool = True | |
| failure_count: int = 0 | |
| last_failure_time: float = 0.0 | |
| def age(self) -> float: | |
| """会话存活时间。""" | |
| return time.time() - self.created_at | |
| def snapshot(self) -> Dict[str, str]: | |
| """获取当前会话快照。""" | |
| return { | |
| "token": self.token, | |
| "user_id": self.user_id, | |
| "username": self.username, | |
| } | |
| class GuestSessionPool: | |
| """匿名访客会话池,支持最小负载获取与失败替换。""" | |
| def __init__( | |
| self, | |
| pool_size: int = 3, | |
| session_max_age: int = 480, | |
| maintenance_interval: int = 30, | |
| max_failures: int = 10, | |
| ): | |
| self.pool_size = max(1, pool_size) | |
| self.session_max_age = max(60, session_max_age) | |
| self.maintenance_interval = max(10, maintenance_interval) | |
| self.max_failures = max(1, max_failures) | |
| self._lock = Lock() | |
| self._sessions: Dict[str, GuestSession] = {} | |
| self._maintenance_task: Optional[asyncio.Task] = None | |
| self._capacity_lock = asyncio.Lock() | |
| self._create_rate_lock = asyncio.Lock() | |
| self._next_create_allowed_at = 0.0 | |
| self._create_min_interval = 1.0 | |
| self._http_clients = SharedHttpClients(follow_redirects=True) | |
| self._background_tasks: Set[asyncio.Task] = set() | |
| self._cleanup_parallelism = max(1, settings.GUEST_CLEANUP_PARALLELISM) | |
| def _is_retryable_create_error(exc: Exception) -> bool: | |
| """匿名会话创建阶段,哪些异常值得重试。""" | |
| return isinstance(exc, httpx.TransportError) | |
| def _track_background_task(self, coro) -> asyncio.Task: | |
| """跟踪后台任务,避免清理阻塞前台重试路径。""" | |
| task = asyncio.create_task(coro) | |
| self._background_tasks.add(task) | |
| def _on_done(done_task: asyncio.Task): | |
| self._background_tasks.discard(done_task) | |
| try: | |
| done_task.result() | |
| except asyncio.CancelledError: | |
| pass | |
| except Exception as exc: | |
| logger.warning( | |
| "⚠️ 匿名会话后台任务异常", | |
| exc_info=settings.DEBUG_LOGGING, | |
| ) | |
| task.add_done_callback(_on_done) | |
| return task | |
| async def _wait_background_tasks(self, cancel: bool = False): | |
| """等待当前已注册的后台任务结束。""" | |
| pending = list(self._background_tasks) | |
| if pending: | |
| if cancel: | |
| for task in pending: | |
| task.cancel() | |
| await asyncio.gather(*pending, return_exceptions=True) | |
| async def _delete_sessions_concurrently(self, sessions: List[GuestSession]): | |
| """并发清理多枚匿名会话,加快池维护速度。""" | |
| if not sessions: | |
| return | |
| semaphore = asyncio.Semaphore(self._cleanup_parallelism) | |
| async def _cleanup(session: GuestSession): | |
| async with semaphore: | |
| await self._delete_all_chats(session) | |
| await asyncio.gather(*(_cleanup(session) for session in sessions)) | |
| async def _wait_create_slot(self, reason: str) -> None: | |
| async with self._create_rate_lock: | |
| now = time.time() | |
| wait_seconds = max(0.0, self._next_create_allowed_at - now) | |
| if wait_seconds > 0: | |
| logger.debug( | |
| "[guest_session.create] throttled reason=%s wait_seconds=%.2f", | |
| reason, | |
| wait_seconds, | |
| ) | |
| await asyncio.sleep(wait_seconds) | |
| self._next_create_allowed_at = time.time() + self._create_min_interval | |
| async def _create_session(self, reason: str = "unspecified") -> GuestSession: | |
| """创建一个新的匿名访客会话。 | |
| 从上游获取真实 token,并解密其内部值作为 session 元数据。 | |
| """ | |
| max_retries = 3 | |
| for attempt in range(1, max_retries + 1): | |
| try: | |
| await self._wait_create_slot(reason) | |
| fe_version = await get_latest_fe_version() | |
| headers = _build_dynamic_headers(fe_version) | |
| client = self._http_clients.get_client() | |
| response = await client.get(build_upstream_url(AUTH_URL), headers=headers) | |
| if response.status_code != 200: | |
| raise RuntimeError( | |
| f"匿名会话创建失败: HTTP {response.status_code} {response.text[:200]}" | |
| ) | |
| data = response.json() | |
| token = str(data.get("token") or "").strip() | |
| if not token: | |
| raise RuntimeError(f"匿名会话创建失败: 未返回 token {data}") | |
| logger.debug( | |
| "[guest_session.create] reason=%s attempt=%s status=%s bytes=%s token=%s", | |
| reason, | |
| attempt, | |
| response.status_code, | |
| response.headers.get("content-length") or len(response.content or b""), | |
| mask_token(token), | |
| ) | |
| # 优化:通过解密 token 内部值获取准确的 user_id 和 email | |
| # Payload 结构通常为: {"id": "...", "email": "Guest-...@guest.com"} | |
| payload = _decode_token_payload(token) | |
| user_id = str( | |
| payload.get("id") | |
| or data.get("id") | |
| or data.get("user_id") | |
| or f"guest-{token[:12]}" | |
| ).strip() | |
| # 从 email 中提取用户名,例如 Guest-1773376198189 | |
| raw_email = ( | |
| payload.get("email") or data.get("name") or data.get("email") or "" | |
| ) | |
| username = str(raw_email).split("@")[0] or f"Guest-{user_id[:8]}" | |
| logger.debug( | |
| "获取匿名会话成功: user_id=%s, username=%s, reason=%s", user_id, username, reason | |
| ) | |
| return GuestSession( | |
| token=token, | |
| user_id=user_id, | |
| username=username, | |
| ) | |
| except Exception as exc: | |
| if self._is_retryable_create_error(exc): | |
| logger.warning( | |
| "⚠️ %s", | |
| normalize_httpx_exception( | |
| exc, | |
| method="GET", | |
| url=build_upstream_url(AUTH_URL), | |
| context="guest_session.create", | |
| attempt=attempt, | |
| ), | |
| ) | |
| if attempt < max_retries: | |
| await asyncio.sleep(min(2 ** (attempt - 1), 4)) | |
| continue | |
| else: | |
| log_exception(logger, "❌ 匿名会话创建异常") | |
| raise | |
| async def _delete_all_chats(self, session: GuestSession) -> bool: | |
| """删除匿名会话的全部对话,尽量释放并发占用。""" | |
| fe_version = await get_latest_fe_version() | |
| headers = _build_dynamic_headers(fe_version) | |
| headers.update( | |
| { | |
| "Authorization": f"Bearer {session.token}", | |
| "Accept": "application/json", | |
| "Content-Type": "application/json", | |
| } | |
| ) | |
| apply_resin_account_header(headers, session.token) | |
| try: | |
| client = self._http_clients.get_client() | |
| chats_url = build_upstream_url(CHATS_URL) | |
| response = await client.delete(chats_url, headers=headers) | |
| if response.status_code == 200: | |
| logger.debug("已清理匿名会话聊天记录: %s", session.user_id) | |
| return True | |
| logger.warning( | |
| "⚠️ 清理匿名会话聊天记录失败: %s", | |
| normalize_httpx_response( | |
| response.status_code, | |
| response.text, | |
| content_type=response.headers.get("content-type"), | |
| method="DELETE", | |
| url=chats_url, | |
| context=f"guest_session.cleanup:{session.user_id}", | |
| ), | |
| ) | |
| except Exception as exc: | |
| logger.warning( | |
| "⚠️ 清理匿名会话聊天记录异常: %s", | |
| normalize_httpx_exception( | |
| exc, | |
| method="DELETE", | |
| url=build_upstream_url(CHATS_URL), | |
| context=f"guest_session.cleanup:{session.user_id}", | |
| ), | |
| exc_info=settings.DEBUG_LOGGING, | |
| ) | |
| return False | |
| def _list_valid_sessions( | |
| self, | |
| exclude_user_ids: Optional[Set[str]] = None, | |
| ) -> List[GuestSession]: | |
| """获取有效匿名会话列表。""" | |
| excluded = exclude_user_ids or set() | |
| with self._lock: | |
| return [ | |
| session | |
| for session in self._sessions.values() | |
| if session.valid | |
| and session.age < self.session_max_age | |
| and session.user_id not in excluded | |
| ] | |
| async def _ensure_capacity(self) -> bool: | |
| """补齐匿名会话池容量。返回 True 表示补齐成功或无需补齐,False 表示补齐失败(未建出任何会话)。""" | |
| async with self._capacity_lock: | |
| while True: | |
| valid_sessions = self._list_valid_sessions() | |
| need = self.pool_size - len(valid_sessions) | |
| if need <= 0: | |
| return True | |
| logger.debug( | |
| "[guest_session.ensure_capacity] valid_sessions=%s pool_size=%s need=%s", | |
| len(valid_sessions), | |
| self.pool_size, | |
| need, | |
| ) | |
| results = await asyncio.gather( | |
| *[self._create_session(reason="capacity_refill") for _ in range(need)], | |
| return_exceptions=True, | |
| ) | |
| created = 0 | |
| duplicate_user_ids: set[str] = set() | |
| errors = set() | |
| with self._lock: | |
| for result in results: | |
| if isinstance(result, GuestSession): | |
| if result.user_id in self._sessions: | |
| duplicate_user_ids.add(result.user_id) | |
| continue | |
| self._sessions[result.user_id] = result | |
| created += 1 | |
| elif isinstance(result, Exception): | |
| errors.add( | |
| normalize_httpx_exception( | |
| result, | |
| method="GET", | |
| url=build_upstream_url(AUTH_URL), | |
| context="guest_session.create", | |
| ) | |
| ) | |
| if errors: | |
| logger.warning( | |
| "⚠️ 匿名会话池补齐失败 (成功 %s/%s): %s", | |
| created, | |
| need, | |
| ", ".join(sorted(errors)), | |
| ) | |
| if duplicate_user_ids: | |
| self._next_create_allowed_at = max( | |
| self._next_create_allowed_at, | |
| time.time() + self.maintenance_interval, | |
| ) | |
| logger.warning( | |
| "⚠️ 匿名会话池补齐遇到重复 guest user_id,停止继续补池并冷却: duplicates=%s current_sessions=%s cooldown_seconds=%s", | |
| ", ".join(sorted(duplicate_user_ids)), | |
| len(self._sessions), | |
| self.maintenance_interval, | |
| ) | |
| logger.debug( | |
| "[guest_session.ensure_capacity] refill_result created=%s requested=%s duplicates=%s errors=%s total_sessions=%s", | |
| created, | |
| need, | |
| len(duplicate_user_ids), | |
| len(errors), | |
| len(self._sessions), | |
| ) | |
| if created == 0 or duplicate_user_ids: | |
| return False | |
| async def _maintenance_loop(self): | |
| """后台维护:回收过期/失效会话,并补齐池容量。""" | |
| consecutive_failures = 0 | |
| recovery_wait_seconds = 300 | |
| force_sleep_once = 0 | |
| while True: | |
| try: | |
| if force_sleep_once > 0: | |
| sleep_time = force_sleep_once | |
| force_sleep_once = 0 | |
| elif consecutive_failures > 0: | |
| sleep_time = self.maintenance_interval * (2 ** (consecutive_failures - 1)) | |
| else: | |
| sleep_time = self.maintenance_interval | |
| await asyncio.sleep(sleep_time) | |
| stale_sessions: List[GuestSession] = [] | |
| forced_removed_logs: List[str] = [] | |
| with self._lock: | |
| for user_id, session in list(self._sessions.items()): | |
| force_remove = session.age > (2 * self.session_max_age) | |
| should_remove = ( | |
| force_remove | |
| or ( | |
| (not session.valid or session.age > self.session_max_age) | |
| and session.active_requests == 0 | |
| ) | |
| ) | |
| if should_remove: | |
| removed = self._sessions.pop(user_id) | |
| stale_sessions.append(removed) | |
| if force_remove: | |
| forced_removed_logs.append( | |
| f"{removed.user_id}(age={removed.age:.1f}s, " | |
| f"active_requests={removed.active_requests}, valid={removed.valid})" | |
| ) | |
| if forced_removed_logs: | |
| logger.warning( | |
| "⚠️ 匿名会话池强制回收超龄会话: " + ", ".join(forced_removed_logs) | |
| ) | |
| await self._delete_sessions_concurrently(stale_sessions) | |
| success = await self._ensure_capacity() | |
| if success is False: | |
| consecutive_failures += 1 | |
| logger.debug( | |
| "[guest_session.maintenance] ensure_capacity_failed consecutive_failures=%s", | |
| consecutive_failures, | |
| ) | |
| if consecutive_failures >= self.max_failures: | |
| logger.warning( | |
| f"⚠️ 匿名会话池补齐连续失败 {consecutive_failures} 次达到限值," | |
| f"进入恢复等待 {recovery_wait_seconds} 秒后继续" | |
| ) | |
| consecutive_failures = 0 | |
| force_sleep_once = recovery_wait_seconds | |
| else: | |
| next_sleep = self.maintenance_interval * (2 ** (consecutive_failures - 1)) | |
| logger.warning(f"⚠️ 匿名会话池补齐连续失败 {consecutive_failures} 次, 下次重试将等待 {next_sleep} 秒") | |
| else: | |
| if consecutive_failures > 0: | |
| logger.debug("[pool] guest session pool maintenance success") | |
| consecutive_failures = 0 | |
| except asyncio.CancelledError: | |
| return | |
| except Exception as exc: | |
| logger.warning( | |
| "⚠️ 匿名会话池后台维护异常", | |
| exc_info=settings.DEBUG_LOGGING, | |
| ) | |
| consecutive_failures += 1 | |
| if consecutive_failures >= self.max_failures: | |
| logger.warning( | |
| f"⚠️ 匿名会话池后台维护异常连续 {consecutive_failures} 次达到限值," | |
| f"进入恢复等待 {recovery_wait_seconds} 秒后继续" | |
| ) | |
| consecutive_failures = 0 | |
| force_sleep_once = recovery_wait_seconds | |
| async def initialize(self): | |
| """初始化匿名会话池。""" | |
| if self._maintenance_task: | |
| return | |
| logger.debug( | |
| "[guest_session.initialize] start pool_size=%s maintenance_interval=%s session_max_age=%s", | |
| self.pool_size, | |
| self.maintenance_interval, | |
| self.session_max_age, | |
| ) | |
| results = await asyncio.gather( | |
| *[self._create_session(reason="startup_initialize") for _ in range(self.pool_size)], | |
| return_exceptions=True, | |
| ) | |
| created = 0 | |
| errors = set() | |
| with self._lock: | |
| for result in results: | |
| if isinstance(result, GuestSession): | |
| self._sessions[result.user_id] = result | |
| created += 1 | |
| elif isinstance(result, Exception): | |
| errors.add( | |
| normalize_httpx_exception( | |
| result, | |
| method="GET", | |
| url=build_upstream_url(AUTH_URL), | |
| context="guest_session.create", | |
| ) | |
| ) | |
| if errors: | |
| logger.warning( | |
| "⚠️ 匿名会话池初始化失败 (成功 %s/%s): %s", | |
| created, | |
| self.pool_size, | |
| ", ".join(sorted(errors)), | |
| ) | |
| if created == 0: | |
| try: | |
| fallback = await self._create_session(reason="startup_fallback") | |
| with self._lock: | |
| self._sessions[fallback.user_id] = fallback | |
| created = 1 | |
| logger.debug("[pool] guest session pool success initialize") | |
| except Exception as e: | |
| log_exception( | |
| logger, | |
| "[pool] guest session pool initialize failed,retrying in background" | |
| ) | |
| else: | |
| logger.debug("[pool] guest session pool initialize success, current capacity: %s", created) | |
| self._maintenance_task = asyncio.create_task(self._maintenance_loop()) | |
| async def close(self): | |
| """关闭匿名会话池。""" | |
| if self._maintenance_task: | |
| self._maintenance_task.cancel() | |
| try: | |
| await self._maintenance_task | |
| except asyncio.CancelledError: | |
| pass | |
| self._maintenance_task = None | |
| with self._lock: | |
| sessions = list(self._sessions.values()) | |
| self._sessions.clear() | |
| await self._wait_background_tasks(cancel=True) | |
| idle_sessions = [session for session in sessions if session.active_requests == 0] | |
| try: | |
| await asyncio.wait_for(self._delete_sessions_concurrently(idle_sessions), timeout=3.0) | |
| except asyncio.TimeoutError: | |
| logger.warning("⚠️ 清理匿名会话记录超时,强制关闭") | |
| except Exception as e: | |
| logger.warning( | |
| "⚠️ 清理匿名会话记录异常", | |
| exc_info=settings.DEBUG_LOGGING, | |
| ) | |
| finally: | |
| await self._http_clients.close() | |
| async def acquire( | |
| self, | |
| exclude_user_ids: Optional[Set[str]] = None, | |
| ) -> GuestSession: | |
| """按最小忙碌度获取一个可用匿名会话。""" | |
| excluded = exclude_user_ids or set() | |
| max_sessions = self.pool_size * 3 | |
| while True: | |
| candidates = self._list_valid_sessions(exclude_user_ids=excluded) | |
| if candidates: | |
| session = min( | |
| candidates, | |
| key=lambda item: (item.active_requests, item.created_at), | |
| ) | |
| with self._lock: | |
| current = self._sessions.get(session.user_id) | |
| if current and current.valid and current.user_id not in excluded: | |
| current.active_requests += 1 | |
| logger.debug( | |
| "[guest_session.acquire] reuse user_id=%s active_requests=%s valid_sessions=%s", | |
| current.user_id, | |
| current.active_requests, | |
| len(candidates), | |
| ) | |
| return current | |
| with self._lock: | |
| current_size = len(self._sessions) | |
| if current_size >= max_sessions: | |
| raise RuntimeError( | |
| f"匿名会话池容量超限: current={current_size}, max={max_sessions}" | |
| ) | |
| logger.debug( | |
| "[guest_session.acquire] no reusable session, creating fallback current_size=%s excluded=%s", | |
| current_size, | |
| len(excluded), | |
| ) | |
| new_session = await self._create_session(reason="acquire_fallback") | |
| if new_session.user_id in excluded: | |
| await self._delete_all_chats(new_session) | |
| continue | |
| with self._lock: | |
| current_size = len(self._sessions) | |
| if current_size >= max_sessions: | |
| overflow_message = ( | |
| f"匿名会话池容量超限: current={current_size}, max={max_sessions}" | |
| ) | |
| else: | |
| new_session.active_requests = 1 | |
| self._sessions[new_session.user_id] = new_session | |
| return new_session | |
| await self._delete_all_chats(new_session) | |
| logger.warning(f"⚠️ {overflow_message}, 已放弃新建会话: {new_session.user_id}") | |
| raise RuntimeError(overflow_message) | |
| def release(self, user_id: str): | |
| """释放一个匿名会话占用。""" | |
| with self._lock: | |
| session = self._sessions.get(user_id) | |
| if session: | |
| session.active_requests = max(0, session.active_requests - 1) | |
| logger.debug( | |
| "[guest_session.release] user_id=%s active_requests=%s", | |
| user_id, | |
| session.active_requests, | |
| ) | |
| async def report_failure(self, user_id: Optional[str] = None): | |
| """标记匿名会话失效,并尝试补一个新会话。""" | |
| session: Optional[GuestSession] = None | |
| if user_id: | |
| with self._lock: | |
| session = self._sessions.pop(user_id, None) | |
| if session: | |
| session.valid = False | |
| session.failure_count += 1 | |
| session.last_failure_time = time.time() | |
| session.active_requests = 0 | |
| if session: | |
| self._track_background_task(self._delete_all_chats(session)) | |
| logger.debug("已淘汰匿名会话: %s", session.user_id) | |
| logger.debug( | |
| "[guest_session.report_failure] user_id=%s triggering_capacity_refill=%s", | |
| user_id, | |
| session is not None, | |
| ) | |
| await self._ensure_capacity() | |
| async def refresh_auth(self, failed_user_id: Optional[str] = None): | |
| """兼容 glm-demo 命名:刷新匿名会话。""" | |
| await self.report_failure(failed_user_id) | |
| async def cleanup_idle_chats(self): | |
| """清理当前空闲匿名会话的聊天记录。""" | |
| with self._lock: | |
| idle_sessions = [ | |
| session | |
| for session in self._sessions.values() | |
| if session.valid and session.active_requests == 0 | |
| ] | |
| await self._delete_sessions_concurrently(idle_sessions) | |
| def get_pool_status(self) -> Dict[str, int]: | |
| """获取匿名会话池状态。""" | |
| with self._lock: | |
| sessions = list(self._sessions.values()) | |
| valid_sessions = [ | |
| session for session in sessions if session.valid and session.age < self.session_max_age | |
| ] | |
| busy_sessions = [session for session in valid_sessions if session.active_requests > 0] | |
| return { | |
| "total_sessions": len(sessions), | |
| "valid_sessions": len(valid_sessions), | |
| "available_sessions": len(valid_sessions), | |
| "busy_sessions": len(busy_sessions), | |
| "expired_sessions": len( | |
| [session for session in sessions if session.age >= self.session_max_age] | |
| ), | |
| } | |
| _guest_session_pool: Optional[GuestSessionPool] = None | |
| _guest_pool_lock = Lock() | |
| def get_guest_session_pool() -> Optional[GuestSessionPool]: | |
| """获取全局匿名会话池。""" | |
| return _guest_session_pool | |
| async def initialize_guest_session_pool( | |
| pool_size: int = 3, | |
| session_max_age: int = 480, | |
| maintenance_interval: int = 30, | |
| max_failures: int = 10, | |
| ) -> GuestSessionPool: | |
| """初始化全局匿名会话池。""" | |
| global _guest_session_pool | |
| if not settings.ANONYMOUS_MODE: | |
| raise RuntimeError("ANONYMOUS_MODE is disabled") | |
| with _guest_pool_lock: | |
| if _guest_session_pool is None: | |
| _guest_session_pool = GuestSessionPool( | |
| pool_size=pool_size, | |
| session_max_age=session_max_age, | |
| maintenance_interval=maintenance_interval, | |
| max_failures=max_failures, | |
| ) | |
| pool = _guest_session_pool | |
| await pool.initialize() | |
| return pool | |
| async def close_guest_session_pool(): | |
| """关闭全局匿名会话池。""" | |
| global _guest_session_pool | |
| with _guest_pool_lock: | |
| pool = _guest_session_pool | |
| _guest_session_pool = None | |
| if pool: | |
| await pool.close() | |