Files
g0v0-server/app/service/realtime_online_cleanup.py
2025-08-22 13:06:23 +08:00

197 lines
7.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
实时在线状态清理服务
此模块提供实时的在线状态清理功能,确保用户在断开连接后立即从在线列表中移除。
"""
from __future__ import annotations
import asyncio
import json
from datetime import datetime, timedelta
from app.dependencies.database import get_redis, get_redis_message
from app.log import logger
from app.router.v2.stats import (
REDIS_ONLINE_USERS_KEY,
REDIS_PLAYING_USERS_KEY,
_redis_exec,
)
class RealtimeOnlineCleanup:
"""实时在线状态清理器"""
def __init__(self):
self._running = False
self._task = None
async def start(self):
"""启动实时清理服务"""
if self._running:
return
self._running = True
self._task = asyncio.create_task(self._realtime_cleanup_loop())
logger.info("[RealtimeOnlineCleanup] Started realtime online cleanup service")
async def stop(self):
"""停止实时清理服务"""
if not self._running:
return
self._running = False
if self._task:
self._task.cancel()
try:
await self._task
except asyncio.CancelledError:
pass
logger.info("[RealtimeOnlineCleanup] Stopped realtime online cleanup service")
async def _realtime_cleanup_loop(self):
"""实时清理循环 - 每30秒检查一次"""
while self._running:
try:
# 执行快速清理
cleaned_count = await self._quick_cleanup_stale_users()
if cleaned_count > 0:
logger.debug(f"[RealtimeOnlineCleanup] Quick cleanup: removed {cleaned_count} stale users")
# 等待30秒
await asyncio.sleep(30)
except asyncio.CancelledError:
break
except Exception as e:
logger.error(f"[RealtimeOnlineCleanup] Error in cleanup loop: {e}")
# 出错时等待30秒再重试
await asyncio.sleep(30)
async def _quick_cleanup_stale_users(self) -> int:
"""快速清理过期用户,返回清理数量"""
redis_sync = get_redis_message()
redis_async = get_redis()
total_cleaned = 0
try:
# 获取所有在线用户
online_users = await _redis_exec(redis_sync.smembers, REDIS_ONLINE_USERS_KEY)
# 快速检查只检查metadata键是否存在
stale_users = []
for user_id in online_users:
user_id_str = user_id.decode() if isinstance(user_id, bytes) else str(user_id)
metadata_key = f"metadata:online:{user_id_str}"
# 如果metadata标记不存在立即标记为过期
if not await redis_async.exists(metadata_key):
stale_users.append(user_id_str)
# 立即清理过期用户
if stale_users:
# 从在线用户集合中移除
await _redis_exec(redis_sync.srem, REDIS_ONLINE_USERS_KEY, *stale_users)
# 同时从游玩用户集合中移除(如果存在)
await _redis_exec(redis_sync.srem, REDIS_PLAYING_USERS_KEY, *stale_users)
total_cleaned = len(stale_users)
logger.info(f"[RealtimeOnlineCleanup] Immediately cleaned {total_cleaned} stale users")
except Exception as e:
logger.error(f"[RealtimeOnlineCleanup] Error in quick cleanup: {e}")
return total_cleaned
async def verify_user_online_status(self, user_id: int) -> bool:
"""实时验证用户在线状态"""
try:
redis = get_redis()
metadata_key = f"metadata:online:{user_id}"
# 检查metadata键是否存在
exists = await redis.exists(metadata_key)
# 如果不存在,从在线集合中移除该用户
if not exists:
redis_sync = get_redis_message()
await _redis_exec(redis_sync.srem, REDIS_ONLINE_USERS_KEY, str(user_id))
await _redis_exec(redis_sync.srem, REDIS_PLAYING_USERS_KEY, str(user_id))
logger.debug(f"[RealtimeOnlineCleanup] Verified user {user_id} is offline, removed from sets")
return bool(exists)
except Exception as e:
logger.error(f"[RealtimeOnlineCleanup] Error verifying user {user_id} status: {e}")
return False
async def force_refresh_online_list(self):
"""强制刷新整个在线用户列表"""
try:
redis_sync = get_redis_message()
redis_async = get_redis()
# 获取所有在线用户
online_users = await _redis_exec(redis_sync.smembers, REDIS_ONLINE_USERS_KEY)
playing_users = await _redis_exec(redis_sync.smembers, REDIS_PLAYING_USERS_KEY)
# 验证每个用户的在线状态
valid_online_users = []
valid_playing_users = []
for user_id in online_users:
user_id_str = user_id.decode() if isinstance(user_id, bytes) else str(user_id)
metadata_key = f"metadata:online:{user_id_str}"
if await redis_async.exists(metadata_key):
valid_online_users.append(user_id_str)
for user_id in playing_users:
user_id_str = user_id.decode() if isinstance(user_id, bytes) else str(user_id)
metadata_key = f"metadata:online:{user_id_str}"
if await redis_async.exists(metadata_key):
valid_playing_users.append(user_id_str)
# 重建在线用户集合
if online_users:
await _redis_exec(redis_sync.delete, REDIS_ONLINE_USERS_KEY)
if valid_online_users:
await _redis_exec(redis_sync.sadd, REDIS_ONLINE_USERS_KEY, *valid_online_users)
# 设置过期时间
await redis_async.expire(REDIS_ONLINE_USERS_KEY, 3 * 3600)
# 重建游玩用户集合
if playing_users:
await _redis_exec(redis_sync.delete, REDIS_PLAYING_USERS_KEY)
if valid_playing_users:
await _redis_exec(redis_sync.sadd, REDIS_PLAYING_USERS_KEY, *valid_playing_users)
# 设置过期时间
await redis_async.expire(REDIS_PLAYING_USERS_KEY, 3 * 3600)
cleaned_online = len(online_users) - len(valid_online_users)
cleaned_playing = len(playing_users) - len(valid_playing_users)
if cleaned_online > 0 or cleaned_playing > 0:
logger.info(f"[RealtimeOnlineCleanup] Force refresh: removed {cleaned_online} stale online users, {cleaned_playing} stale playing users")
return cleaned_online + cleaned_playing
except Exception as e:
logger.error(f"[RealtimeOnlineCleanup] Error in force refresh: {e}")
return 0
# 全局实例
realtime_cleanup = RealtimeOnlineCleanup()
def start_realtime_cleanup():
"""启动实时清理服务"""
asyncio.create_task(realtime_cleanup.start())
def stop_realtime_cleanup():
"""停止实时清理服务"""
asyncio.create_task(realtime_cleanup.stop())