zai / app /core /session /session_manager.py
bg5's picture
Upload 85 files
e0b68d2 verified
Raw History Blame Contribute Delete
18.2 kB
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""ไผš่ฏ็ฎก็†ๅ™จ๏ผš่ฟž็ปญไผš่ฏ็š„ๆ ธๅฟƒๅ่ฐƒๆจกๅ—ใ€‚
่Œ่ดฃ๏ผš
1. ้€š่ฟ‡ๆŒ‡็บนๅŒน้…ๆฃ€ๆต‹่ฟž็ปญไผš่ฏ
2. ็ปดๆŠค chat_id / parent_id ็š„็”Ÿๅ‘ฝๅ‘จๆœŸ
3. ๅŽๅฐๅฎšๆœŸๆธ…็†่ฟ‡ๆœŸไผš่ฏ
"""
from __future__ import annotations
import asyncio
import time
import uuid
from dataclasses import dataclass, field
from typing import Any, Dict, List, Optional
from app.core.session.session_fingerprint import SessionFingerprint
from app.core.session.session_store import SessionStore
from app.core.config import settings
from app.utils.logger import logger
def _new_uuid() -> str:
return str(uuid.uuid4())
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
# ๆ•ฐๆฎ็ฑป
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
@dataclass
class SessionResult:
"""ไผš่ฏๆŸฅๆ‰พ/ๅˆ›ๅปบ็š„็ป“ๆžœ๏ผŒไพ› transform_request ไฝฟ็”จใ€‚"""
chat_id: str
"""ๅค็”จๆˆ–ๆ–ฐๅปบ็š„ๅฏน่ฏ IDใ€‚"""
message_id: str
"""ๆœฌๆฌก่ฏทๆฑ‚็”Ÿๆˆ็š„็”จๆˆทๆถˆๆฏ ID๏ผˆcurrent_user_message_id๏ผ‰ใ€‚"""
parent_id: Optional[str]
"""่ฟž็ปญไผš่ฏๆ—ถๆŒ‡ๅ‘ไธŠๆฌก็š„ current_user_message_id๏ผŒๆ–ฐๅฏน่ฏๆ—ถไธบ Noneใ€‚"""
is_new: bool
"""True = ๆ–ฐๅปบๅฏน่ฏ๏ผŒFalse = ๅค็”จๅทฒๆœ‰ไผš่ฏใ€‚"""
bound_token: Optional[str] = None
"""ๅค็”จไผš่ฏๆ—ถ็ป‘ๅฎš็š„ๅŽŸๅง‹ token๏ผŒๆ–ฐๅฏน่ฏๆ—ถไธบ Noneใ€‚"""
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
# ๅญ˜ๅ‚จ้”ฎ่พ…ๅŠฉ
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
_SESSION_PREFIX = "session"
_FP_INDEX_PREFIX = "fp_index"
def _session_key(client_fp: str, chat_id: str) -> str:
return f"{_SESSION_PREFIX}:{client_fp}:{chat_id}"
def _fp_index_key(client_fp: str) -> str:
return f"{_FP_INDEX_PREFIX}:{client_fp}"
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
# SessionManager
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
class SessionManager:
"""่ฟž็ปญไผš่ฏ็ฎก็†ๅ™จใ€‚
ๅ•ไพ‹ไฝฟ็”จๆ–นๅผ๏ผˆ็”ฑ UpstreamClient ๆŒๆœ‰๏ผ‰๏ผš
self._session_manager = SessionManager()
"""
def __init__(
self,
session_ttl: int = 3600,
max_sessions_per_client: int = 50,
cleanup_interval: int = 300,
) -> None:
self._ttl = session_ttl
self._max_per_client = max(1, max_sessions_per_client)
self._cleanup_interval = cleanup_interval
self._store = SessionStore()
self._fp = SessionFingerprint()
self._cleanup_task: Optional[asyncio.Task] = None
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
# ๅ…ฌๅ…ฑๆŽฅๅฃ
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
async def find_session(
self,
model: str,
messages: List[Dict[str, Any]],
client_id: Optional[str] = None,
) -> Optional[SessionResult]:
"""Find matching continuous session by fingerprint. Returns None if not found.
This method is intentionally read-only. It only returns the matched
session and the IDs needed for the current request; the caller must
commit the new current_user_message_id after the upstream request is
accepted.
Args:
model: Model name used for index grouping.
messages: Full message list (OpenAI format) for fingerprint matching.
client_id: Optional explicit client ID (used for index key if provided).
Returns:
SessionResult on match, None otherwise.
"""
self._ensure_cleanup_started()
index_identifier = client_id or model
client_fp = self._fp.generate_client_fingerprint(index_identifier, model)
match = await self._match_session(client_fp, messages)
if not match:
return None
session = match
message_id = _new_uuid()
chat_id = session["chat_id"]
parent_id = session.get("last_message_id") or None
logger.debug(
"่ฟž็ปญไผš่ฏๅค็”จ: chat_id=%s... "
"parent_id=%s...",
chat_id[:8],
parent_id[:8] if parent_id else 'None',
)
return SessionResult(
chat_id=chat_id,
message_id=message_id,
parent_id=parent_id,
is_new=False,
bound_token=session.get("auth_token"),
)
async def create_session(
self,
auth_token: str,
model: str,
messages: List[Dict[str, Any]],
chat_id: str,
message_id: str,
client_id: Optional[str] = None,
) -> SessionResult:
"""Create a new session with an externally-provided chat_id (from upstream).
This is the counterpart to find_session: the caller has already obtained
a chat_id from the upstream /chats/new endpoint and wants to store it
for future reuse.
Args:
auth_token: Upstream auth token (stored for bound_token on reuse).
model: Model name for index grouping.
messages: Full message list (OpenAI format) for fingerprint collection.
chat_id: Chat ID returned by the upstream pre-create endpoint.
message_id: current_user_message_id generated for this turn.
client_id: Optional explicit client ID (used for index key if provided).
Returns:
SessionResult with is_new=True.
"""
self._ensure_cleanup_started()
index_identifier = client_id or model
client_fp = self._fp.generate_client_fingerprint(index_identifier, model)
fingerprints = self._fp.collect_fingerprints(messages)
record: Dict[str, Any] = {
"chat_id": chat_id,
"client_fingerprint": client_fp,
"fingerprints": fingerprints,
"last_message_id": message_id,
"model": model,
"token_hash": self._fp.hash_token(auth_token),
"auth_token": auth_token,
"created_at": time.time(),
"last_update": time.time(),
}
await self._save_session(client_fp, chat_id, record)
await self._update_fp_index(client_fp, chat_id)
logger.debug("ๆ–ฐๅปบไผš่ฏ: chat_id=%s... model=%s", chat_id[:8], model)
return SessionResult(
chat_id=chat_id,
message_id=message_id,
parent_id=None,
is_new=True,
)
async def commit_session_turn(
self,
model: str,
messages: List[Dict[str, Any]],
chat_id: str,
message_id: str,
client_id: Optional[str] = None,
) -> None:
"""ๅœจไธŠๆธธๆˆๅŠŸๆŽฅๆ”ถ่ฏทๆฑ‚ๅŽ๏ผŒๆŽจ่ฟ›่ฟž็ปญไผš่ฏ้“พๆŒ‡้’ˆใ€‚
Args:
model: Model name for index grouping.
messages: Full message list (OpenAI format) for fingerprint update.
chat_id: Existing upstream chat ID.
message_id: Accepted current_user_message_id for this turn.
client_id: Optional explicit client ID (used for index key if provided).
"""
self._ensure_cleanup_started()
index_identifier = client_id or model
client_fp = self._fp.generate_client_fingerprint(index_identifier, model)
session = await self._store.get(_session_key(client_fp, chat_id))
if not session:
logger.warning(
"ๆไบค่ฟž็ปญไผš่ฏๅคฑ่ดฅ: ๆœชๆ‰พๅˆฐ chat_id=%s ็š„ไผš่ฏ่ฎฐๅฝ•",
chat_id[:8],
)
return
session["fingerprints"] = self._fp.collect_fingerprints(messages)
session["last_message_id"] = message_id
session["last_update"] = time.time()
await self._save_session(client_fp, chat_id, session)
await self._touch_fp_index(client_fp, chat_id)
logger.debug(
"่ฟž็ปญไผš่ฏๅทฒๆไบค: chat_id=%s last_message_id=%s",
chat_id[:8],
message_id[:8],
)
async def update_session_message_id(
self,
client_fp_or_chat_id: str,
chat_id: str,
new_message_id: str,
) -> None:
"""๏ผˆๅฏ้€‰๏ผ‰ๆ‰‹ๅŠจๅˆทๆ–ฐไผš่ฏ็š„ last_message_idใ€‚
้€šๅธธๆ— ้œ€่ฐƒ็”จโ€”โ€”ไผš่ฏ็Šถๆ€ไผšๅœจไธŠๆธธๆˆๅŠŸๆŽฅๆ”ถ่ฏทๆฑ‚ๅŽๆไบคใ€‚
ๅฝ“้œ€่ฆๆ˜พๅผไฟฎๆญฃ current_user_message_id ๆ—ถๅฏ่ฐƒ็”จใ€‚
"""
# ๅ…ˆๅฐ่ฏ•ๆŒ‰ chat_id ๆ‰พ client_fp๏ผˆ้ๅކ fp_index๏ผ‰
# ็ฎ€ๅŒ–๏ผš่‹ฅ่ฐƒ็”จๆ–นๅทฒ็Ÿฅ client_fp ๅˆ™็›ดๆŽฅๆ“ไฝœ
session = await self._store.get(_session_key(client_fp_or_chat_id, chat_id))
if not session:
return
session["last_message_id"] = new_message_id
session["last_update"] = time.time()
await self._store.set(
_session_key(client_fp_or_chat_id, chat_id), session, ttl=self._ttl
)
async def clear_session(self, client_fp: str, chat_id: str) -> None:
"""ๆธ…้™คๅ•ไธชไผš่ฏๅŠๅ…ถๆŒ‡็บน็ดขๅผ•ๆก็›ฎใ€‚"""
await self._store.delete(_session_key(client_fp, chat_id))
await self._remove_from_fp_index(client_fp, chat_id)
async def get_stats(self) -> Dict[str, Any]:
"""่ฟ”ๅ›žๅญ˜ๅ‚จ็ปŸ่ฎกไฟกๆฏ๏ผˆ็”จไบŽ Admin ้ขๆฟ๏ผ‰ใ€‚"""
session_keys = await self._store.keys(f"{_SESSION_PREFIX}:")
fp_keys = await self._store.keys(f"{_FP_INDEX_PREFIX}:")
return {
"total_sessions": len(session_keys),
"total_clients": len(fp_keys),
"store_size": self._store.size(),
"session_ttl": self._ttl,
"max_per_client": self._max_per_client,
}
async def close(self) -> None:
"""ๅœๆญขๅŽๅฐๆธ…็†ไปปๅŠกใ€‚"""
if self._cleanup_task and not self._cleanup_task.done():
self._cleanup_task.cancel()
try:
await self._cleanup_task
except asyncio.CancelledError:
pass
async with self._store._lock:
self._store._data.clear()
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
# ๅ†…้ƒจ๏ผšไผš่ฏๅŒน้…
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
async def _match_session(
self,
client_fp: str,
messages: List[Dict[str, Any]],
) -> Optional[Dict[str, Any]]:
"""ๅœจ่ฏฅๅฎขๆˆท็ซฏ็š„ไผš่ฏๅˆ—่กจไธญๆŸฅๆ‰พๅŒน้…็š„่ฟž็ปญไผš่ฏใ€‚"""
index = await self._store.get(_fp_index_key(client_fp))
if not index:
return None
chat_ids: List[str] = index.get("chat_ids", [])
if not chat_ids:
return None
# ๅพ…็กฎ่ฎคไผš่ฏ็ฒพ็กฎๅŒน้…ๆ—ถๆƒฐๆ€ง่ฎก็ฎ—
pending_fps: Optional[List[str]] = None
# ไปŽๆœ€ๆ–ฐ็š„ไผš่ฏๅผ€ๅง‹ๅ€’ๅบๅŒน้…
for chat_id in reversed(chat_ids):
session = await self._store.get(_session_key(client_fp, chat_id))
if not session:
# ๅทฒ่ฟ‡ๆœŸ๏ผŒไปŽ็ดขๅผ•ไธญๆธ…็†
await self._remove_from_fp_index(client_fp, chat_id)
continue
cached_fps: List[str] = session.get("fingerprints", [])
# ๅพ…็กฎ่ฎคไผš่ฏ๏ผˆprecreate ๆˆๅŠŸไฝ† completions ๆœช็กฎ่ฎค๏ผ‰๏ผš
# is_continuous_session ๅฏน้ฆ–่ฝฎๆถˆๆฏ๏ผˆโ‰ค2 ๆก๏ผ‰่ฟ”ๅ›ž False๏ผŒ
# ๅ› ๆญคๆ”น็”จ็ฒพ็กฎๆŒ‡็บนๆฏ”ๅฏนๆฅๅŒน้…ๅŒไธ€่ฏทๆฑ‚็š„้‡่ฏ•ใ€‚
if not session.get("last_message_id"):
if pending_fps is None:
pending_fps = self._fp.collect_fingerprints(messages)
if pending_fps == cached_fps:
return session
continue
if self._fp.is_continuous_session(messages, cached_fps):
return session
return None
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
# ๅ†…้ƒจ๏ผšๅญ˜ๅ‚จๆ“ไฝœ
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
async def _save_session(
self, client_fp: str, chat_id: str, session: Dict[str, Any]
) -> None:
key = _session_key(client_fp, chat_id)
await self._store.set(key, session, ttl=self._ttl)
async def _update_fp_index(self, client_fp: str, chat_id: str) -> None:
"""ๆ–ฐๅขž chat_id ๅˆฐๆŒ‡็บน็ดขๅผ•๏ผŒ่ถ…ๅ‡บไธŠ้™ๆ—ถ้ฉฑ้€ๆœ€ๆ—ง็š„ใ€‚"""
index_key = _fp_index_key(client_fp)
index = await self._store.get(index_key) or {
"client_fingerprint": client_fp,
"chat_ids": [],
"created_at": time.time(),
}
chat_ids: List[str] = index.get("chat_ids", [])
if chat_id in chat_ids:
chat_ids.remove(chat_id)
chat_ids.append(chat_id)
# ่ถ…ๅ‡บไธŠ้™๏ผš้ฉฑ้€ๆœ€ๆ—ง็š„ไผš่ฏ
while len(chat_ids) > self._max_per_client:
evict_id = chat_ids.pop(0)
await self._store.delete(_session_key(client_fp, evict_id))
logger.debug("้ฉฑ้€่ถ…้™ไผš่ฏ: %s...", evict_id[:8])
index["chat_ids"] = chat_ids
index["most_recent"] = chat_id
index["last_update"] = time.time()
await self._store.set(index_key, index, ttl=self._ttl)
async def _touch_fp_index(self, client_fp: str, chat_id: str) -> None:
"""ๅฐ†ๅทฒๅŒน้…็š„ chat_id ็งปๅˆฐๅˆ—่กจๆœซๅฐพ๏ผˆๆœ€่ฟ‘ไฝฟ็”จ๏ผ‰๏ผŒๅนถๅˆทๆ–ฐ TTLใ€‚"""
index_key = _fp_index_key(client_fp)
index = await self._store.get(index_key)
if not index:
return
chat_ids: List[str] = index.get("chat_ids", [])
if chat_id in chat_ids:
chat_ids.remove(chat_id)
chat_ids.append(chat_id)
index["chat_ids"] = chat_ids
index["most_recent"] = chat_id
index["last_update"] = time.time()
await self._store.set(index_key, index, ttl=self._ttl)
async def _remove_from_fp_index(self, client_fp: str, chat_id: str) -> None:
"""ไปŽๆŒ‡็บน็ดขๅผ•ไธญ็งป้™ค chat_id๏ผˆไผš่ฏๅทฒ่ฟ‡ๆœŸๆˆ–ๅทฒๅˆ ้™ค๏ผ‰ใ€‚"""
index_key = _fp_index_key(client_fp)
index = await self._store.get(index_key)
if not index:
return
chat_ids: List[str] = index.get("chat_ids", [])
if chat_id in chat_ids:
chat_ids.remove(chat_id)
if chat_ids:
index["chat_ids"] = chat_ids
index["last_update"] = time.time()
await self._store.set(index_key, index, ttl=self._ttl)
else:
await self._store.delete(index_key)
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
# ๅ†…้ƒจ๏ผšๅŽๅฐๆธ…็†
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
def _ensure_cleanup_started(self) -> None:
"""ๆƒฐๆ€งๅฏๅŠจๅŽๅฐๆธ…็†ๅ็จ‹๏ผˆไป…ๅœจไบ‹ไปถๅพช็Žฏไธญ้ฆ–ๆฌก่ฐƒ็”จๆ—ถๅฏๅŠจ๏ผ‰ใ€‚"""
if self._cleanup_task is None or self._cleanup_task.done():
try:
self._cleanup_task = asyncio.get_event_loop().create_task(
self._cleanup_loop()
)
except RuntimeError:
pass # ๆ— ไบ‹ไปถๅพช็Žฏ๏ผˆๆต‹่ฏ•็Žฏๅขƒ๏ผ‰ๆ—ถๅฟฝ็•ฅ
async def _cleanup_loop(self) -> None:
"""ๅฎšๆœŸๆธ…็†่ฟ‡ๆœŸไผš่ฏใ€‚"""
while True:
try:
await asyncio.sleep(self._cleanup_interval)
removed = await self._store.cleanup_expired()
if removed:
logger.debug("ไผš่ฏๆธ…็†๏ผšๆธ…้™ค %s ๆก่ฟ‡ๆœŸ่ฎฐๅฝ•", removed)
except asyncio.CancelledError:
break
except Exception as exc:
logger.warning("ไผš่ฏๆธ…็†ๅผ‚ๅธธ", exc_info=settings.DEBUG_LOGGING)