527 lines
19 KiB
Python
Raw Normal View History

#!/usr/bin/env python3
"""
會話清理管理器
==============
統一管理 Web 會話的清理策略統計和性能監控
與內存監控系統深度集成提供智能清理決策
"""
import threading
2025-06-11 03:25:08 +08:00
import time
from collections.abc import Callable
from dataclasses import dataclass
from datetime import datetime
from enum import Enum
2025-06-11 03:25:08 +08:00
from typing import Any
from ...debug import web_debug_log as debug_log
from ...utils.error_handler import ErrorHandler, ErrorType
from ..models.feedback_session import CleanupReason, SessionStatus
@dataclass
class CleanupPolicy:
"""清理策略配置"""
2025-06-11 03:25:08 +08:00
max_idle_time: int = 1800 # 最大空閒時間(秒)
max_session_age: int = 7200 # 最大會話年齡(秒)
max_sessions: int = 10 # 最大會話數量
cleanup_interval: int = 300 # 清理間隔(秒)
memory_pressure_threshold: float = 0.8 # 內存壓力閾值
enable_auto_cleanup: bool = True # 啟用自動清理
preserve_active_session: bool = True # 保護活躍會話
@dataclass
class CleanupStats:
"""清理統計數據"""
2025-06-11 03:25:08 +08:00
total_cleanups: int = 0
expired_cleanups: int = 0
memory_pressure_cleanups: int = 0
manual_cleanups: int = 0
auto_cleanups: int = 0
total_sessions_cleaned: int = 0
total_cleanup_time: float = 0.0
average_cleanup_time: float = 0.0
2025-06-11 03:25:08 +08:00
last_cleanup_time: datetime | None = None
cleanup_efficiency: float = 0.0 # 清理效率(清理的會話數/總會話數)
class CleanupTrigger(Enum):
"""清理觸發器類型"""
2025-06-11 03:25:08 +08:00
AUTO = "auto" # 自動清理
MEMORY_PRESSURE = "memory_pressure" # 內存壓力
MANUAL = "manual" # 手動清理
EXPIRED = "expired" # 過期清理
CAPACITY = "capacity" # 容量限制
class SessionCleanupManager:
"""會話清理管理器"""
2025-06-11 03:25:08 +08:00
def __init__(self, web_ui_manager, policy: CleanupPolicy = None):
"""
初始化會話清理管理器
2025-06-11 03:25:08 +08:00
Args:
web_ui_manager: WebUIManager 實例
policy: 清理策略配置
"""
self.web_ui_manager = web_ui_manager
self.policy = policy or CleanupPolicy()
self.stats = CleanupStats()
2025-06-11 03:25:08 +08:00
# 清理狀態
self.is_running = False
2025-06-11 03:25:08 +08:00
self.cleanup_thread: threading.Thread | None = None
self._stop_event = threading.Event()
2025-06-11 03:25:08 +08:00
# 回調函數
2025-06-11 03:25:08 +08:00
self.cleanup_callbacks: list[Callable] = []
self.stats_callbacks: list[Callable] = []
# 清理歷史記錄
2025-06-11 03:25:08 +08:00
self.cleanup_history: list[dict[str, Any]] = []
self.max_history = 100
2025-06-11 03:25:08 +08:00
debug_log("SessionCleanupManager 初始化完成")
2025-06-11 03:25:08 +08:00
def start_auto_cleanup(self) -> bool:
"""啟動自動清理"""
if not self.policy.enable_auto_cleanup:
debug_log("自動清理已禁用")
return False
2025-06-11 03:25:08 +08:00
if self.is_running:
debug_log("自動清理已在運行")
return True
2025-06-11 03:25:08 +08:00
try:
self.is_running = True
self._stop_event.clear()
2025-06-11 03:25:08 +08:00
self.cleanup_thread = threading.Thread(
target=self._auto_cleanup_loop,
name="SessionCleanupManager",
2025-06-11 03:25:08 +08:00
daemon=True,
)
self.cleanup_thread.start()
2025-06-11 03:25:08 +08:00
debug_log(f"自動清理已啟動,間隔 {self.policy.cleanup_interval}")
return True
2025-06-11 03:25:08 +08:00
except Exception as e:
self.is_running = False
error_id = ErrorHandler.log_error_with_context(
2025-06-11 03:25:08 +08:00
e, context={"operation": "啟動自動清理"}, error_type=ErrorType.SYSTEM
)
debug_log(f"啟動自動清理失敗 [錯誤ID: {error_id}]: {e}")
return False
2025-06-11 03:25:08 +08:00
def stop_auto_cleanup(self) -> bool:
"""停止自動清理"""
if not self.is_running:
debug_log("自動清理未在運行")
return True
2025-06-11 03:25:08 +08:00
try:
self.is_running = False
self._stop_event.set()
2025-06-11 03:25:08 +08:00
if self.cleanup_thread and self.cleanup_thread.is_alive():
self.cleanup_thread.join(timeout=5)
2025-06-11 03:25:08 +08:00
debug_log("自動清理已停止")
return True
2025-06-11 03:25:08 +08:00
except Exception as e:
error_id = ErrorHandler.log_error_with_context(
2025-06-11 03:25:08 +08:00
e, context={"operation": "停止自動清理"}, error_type=ErrorType.SYSTEM
)
debug_log(f"停止自動清理失敗 [錯誤ID: {error_id}]: {e}")
return False
2025-06-11 03:25:08 +08:00
def _auto_cleanup_loop(self):
"""自動清理主循環"""
debug_log("自動清理循環開始")
2025-06-11 03:25:08 +08:00
while not self._stop_event.is_set():
try:
# 執行清理檢查
self._perform_auto_cleanup()
2025-06-11 03:25:08 +08:00
# 等待下次清理
if self._stop_event.wait(self.policy.cleanup_interval):
break
2025-06-11 03:25:08 +08:00
except Exception as e:
error_id = ErrorHandler.log_error_with_context(
e,
context={"operation": "自動清理循環"},
2025-06-11 03:25:08 +08:00
error_type=ErrorType.SYSTEM,
)
debug_log(f"自動清理循環錯誤 [錯誤ID: {error_id}]: {e}")
2025-06-11 03:25:08 +08:00
# 發生錯誤時等待較短時間後重試
if self._stop_event.wait(30):
break
2025-06-11 03:25:08 +08:00
debug_log("自動清理循環結束")
2025-06-11 03:25:08 +08:00
def _perform_auto_cleanup(self):
"""執行自動清理"""
cleanup_start_time = time.time()
cleaned_sessions = 0
2025-06-11 03:25:08 +08:00
try:
# 1. 檢查會話數量限制
if len(self.web_ui_manager.sessions) > self.policy.max_sessions:
cleaned = self._cleanup_by_capacity()
cleaned_sessions += cleaned
debug_log(f"容量限制清理了 {cleaned} 個會話")
2025-06-11 03:25:08 +08:00
# 2. 清理過期會話
cleaned = self._cleanup_expired_sessions()
cleaned_sessions += cleaned
2025-06-11 03:25:08 +08:00
# 3. 清理空閒會話
cleaned = self._cleanup_idle_sessions()
cleaned_sessions += cleaned
2025-06-11 03:25:08 +08:00
# 4. 更新統計
cleanup_duration = time.time() - cleanup_start_time
self._update_cleanup_stats(
2025-06-11 03:25:08 +08:00
CleanupTrigger.AUTO, cleaned_sessions, cleanup_duration
)
2025-06-11 03:25:08 +08:00
if cleaned_sessions > 0:
2025-06-11 03:25:08 +08:00
debug_log(
f"自動清理完成,清理了 {cleaned_sessions} 個會話,耗時: {cleanup_duration:.2f}"
)
except Exception as e:
error_id = ErrorHandler.log_error_with_context(
2025-06-11 03:25:08 +08:00
e, context={"operation": "執行自動清理"}, error_type=ErrorType.SYSTEM
)
debug_log(f"執行自動清理失敗 [錯誤ID: {error_id}]: {e}")
2025-06-11 03:25:08 +08:00
def trigger_cleanup(self, trigger: CleanupTrigger, force: bool = False) -> int:
"""觸發清理操作"""
cleanup_start_time = time.time()
cleaned_sessions = 0
2025-06-11 03:25:08 +08:00
try:
debug_log(f"觸發清理操作,觸發器: {trigger.value},強制: {force}")
2025-06-11 03:25:08 +08:00
if trigger == CleanupTrigger.MEMORY_PRESSURE:
2025-06-11 03:25:08 +08:00
cleaned_sessions = (
self.web_ui_manager.cleanup_sessions_by_memory_pressure(force)
)
elif trigger == CleanupTrigger.EXPIRED:
cleaned_sessions = self.web_ui_manager.cleanup_expired_sessions()
elif trigger == CleanupTrigger.CAPACITY:
cleaned_sessions = self._cleanup_by_capacity()
elif trigger == CleanupTrigger.MANUAL:
# 手動清理:組合多種策略
cleaned_sessions += self.web_ui_manager.cleanup_expired_sessions()
if force:
2025-06-11 03:25:08 +08:00
cleaned_sessions += (
self.web_ui_manager.cleanup_sessions_by_memory_pressure(force)
)
else:
# 自動清理
self._perform_auto_cleanup()
return 0 # 統計已在 _perform_auto_cleanup 中更新
2025-06-11 03:25:08 +08:00
# 更新統計
cleanup_duration = time.time() - cleanup_start_time
self._update_cleanup_stats(trigger, cleaned_sessions, cleanup_duration)
2025-06-11 03:25:08 +08:00
debug_log(
f"清理操作完成,清理了 {cleaned_sessions} 個會話,耗時: {cleanup_duration:.2f}"
)
return cleaned_sessions
2025-06-11 03:25:08 +08:00
except Exception as e:
error_id = ErrorHandler.log_error_with_context(
e,
2025-06-11 03:25:08 +08:00
context={
"operation": "觸發清理",
"trigger": trigger.value,
"force": force,
},
error_type=ErrorType.SYSTEM,
)
debug_log(f"觸發清理操作失敗 [錯誤ID: {error_id}]: {e}")
return 0
2025-06-11 03:25:08 +08:00
def _cleanup_by_capacity(self) -> int:
"""根據容量限制清理會話"""
sessions = self.web_ui_manager.sessions
if len(sessions) <= self.policy.max_sessions:
return 0
2025-06-11 03:25:08 +08:00
# 計算需要清理的會話數量
excess_count = len(sessions) - self.policy.max_sessions
2025-06-11 03:25:08 +08:00
# 按優先級排序會話(優先清理舊的、非活躍的會話)
session_priorities = []
for session_id, session in sessions.items():
# 跳過當前活躍會話(如果啟用保護)
2025-06-11 03:25:08 +08:00
if (
self.policy.preserve_active_session
and self.web_ui_manager.current_session
and session.session_id == self.web_ui_manager.current_session.session_id
):
continue
2025-06-11 03:25:08 +08:00
# 計算優先級分數(分數越高越優先清理)
priority_score = 0
2025-06-11 03:25:08 +08:00
# 狀態優先級
2025-06-11 03:25:08 +08:00
if session.status in [
SessionStatus.COMPLETED,
SessionStatus.ERROR,
SessionStatus.TIMEOUT,
]:
priority_score += 100
elif session.status == SessionStatus.FEEDBACK_SUBMITTED:
priority_score += 50
2025-06-11 03:25:08 +08:00
# 年齡優先級
age = session.get_age()
priority_score += age / 60 # 每分鐘加1分
2025-06-11 03:25:08 +08:00
# 空閒時間優先級
idle_time = session.get_idle_time()
priority_score += idle_time / 30 # 每30秒加1分
2025-06-11 03:25:08 +08:00
session_priorities.append((session_id, session, priority_score))
2025-06-11 03:25:08 +08:00
# 按優先級排序並清理
session_priorities.sort(key=lambda x: x[2], reverse=True)
cleaned_count = 0
2025-06-11 03:25:08 +08:00
for i in range(min(excess_count, len(session_priorities))):
session_id, session, _ = session_priorities[i]
try:
session._cleanup_sync_enhanced(CleanupReason.MANUAL)
del self.web_ui_manager.sessions[session_id]
cleaned_count += 1
except Exception as e:
debug_log(f"容量清理會話 {session_id} 失敗: {e}")
2025-06-11 03:25:08 +08:00
return cleaned_count
def _cleanup_expired_sessions(self) -> int:
"""清理過期會話"""
expired_sessions = []
current_time = time.time()
for session_id, session in self.web_ui_manager.sessions.items():
# 檢查是否過期
2025-06-11 03:25:08 +08:00
if session.is_expired() or session.get_age() > self.policy.max_session_age:
expired_sessions.append(session_id)
# 清理過期會話
cleaned_count = 0
for session_id in expired_sessions:
try:
session = self.web_ui_manager.sessions.get(session_id)
if session:
session._cleanup_sync_enhanced(CleanupReason.EXPIRED)
del self.web_ui_manager.sessions[session_id]
cleaned_count += 1
# 如果清理的是當前活躍會話,清空當前會話
2025-06-11 03:25:08 +08:00
if (
self.web_ui_manager.current_session
and self.web_ui_manager.current_session.session_id == session_id
):
self.web_ui_manager.current_session = None
except Exception as e:
debug_log(f"清理過期會話 {session_id} 失敗: {e}")
return cleaned_count
def _cleanup_idle_sessions(self) -> int:
"""清理空閒會話"""
idle_sessions = []
for session_id, session in self.web_ui_manager.sessions.items():
# 跳過當前活躍會話(如果啟用保護)
2025-06-11 03:25:08 +08:00
if (
self.policy.preserve_active_session
and self.web_ui_manager.current_session
and session.session_id == self.web_ui_manager.current_session.session_id
):
continue
# 檢查是否空閒時間過長
if session.get_idle_time() > self.policy.max_idle_time:
idle_sessions.append(session_id)
# 清理空閒會話
cleaned_count = 0
for session_id in idle_sessions:
try:
session = self.web_ui_manager.sessions.get(session_id)
if session:
session._cleanup_sync_enhanced(CleanupReason.EXPIRED)
del self.web_ui_manager.sessions[session_id]
cleaned_count += 1
except Exception as e:
debug_log(f"清理空閒會話 {session_id} 失敗: {e}")
return cleaned_count
2025-06-11 03:25:08 +08:00
def _update_cleanup_stats(
self, trigger: CleanupTrigger, cleaned_count: int, duration: float
):
"""更新清理統計"""
self.stats.total_cleanups += 1
self.stats.total_sessions_cleaned += cleaned_count
self.stats.total_cleanup_time += duration
self.stats.last_cleanup_time = datetime.now()
# 更新平均清理時間
if self.stats.total_cleanups > 0:
2025-06-11 03:25:08 +08:00
self.stats.average_cleanup_time = (
self.stats.total_cleanup_time / self.stats.total_cleanups
)
# 更新清理效率
total_sessions = len(self.web_ui_manager.sessions) + cleaned_count
if total_sessions > 0:
self.stats.cleanup_efficiency = cleaned_count / total_sessions
# 根據觸發器類型更新統計
if trigger == CleanupTrigger.AUTO:
self.stats.auto_cleanups += 1
elif trigger == CleanupTrigger.MEMORY_PRESSURE:
self.stats.memory_pressure_cleanups += 1
elif trigger == CleanupTrigger.EXPIRED:
self.stats.expired_cleanups += 1
elif trigger == CleanupTrigger.MANUAL:
self.stats.manual_cleanups += 1
# 記錄清理歷史
cleanup_record = {
"timestamp": datetime.now().isoformat(),
"trigger": trigger.value,
"cleaned_count": cleaned_count,
"duration": duration,
"total_sessions_before": total_sessions,
2025-06-11 03:25:08 +08:00
"total_sessions_after": len(self.web_ui_manager.sessions),
}
self.cleanup_history.append(cleanup_record)
# 限制歷史記錄數量
if len(self.cleanup_history) > self.max_history:
2025-06-11 03:25:08 +08:00
self.cleanup_history = self.cleanup_history[-self.max_history :]
# 調用統計回調
for callback in self.stats_callbacks:
try:
callback(self.stats, cleanup_record)
except Exception as e:
debug_log(f"統計回調執行失敗: {e}")
2025-06-11 03:25:08 +08:00
def get_cleanup_statistics(self) -> dict[str, Any]:
"""獲取清理統計數據"""
stats_dict = {
"total_cleanups": self.stats.total_cleanups,
"expired_cleanups": self.stats.expired_cleanups,
"memory_pressure_cleanups": self.stats.memory_pressure_cleanups,
"manual_cleanups": self.stats.manual_cleanups,
"auto_cleanups": self.stats.auto_cleanups,
"total_sessions_cleaned": self.stats.total_sessions_cleaned,
"total_cleanup_time": round(self.stats.total_cleanup_time, 2),
"average_cleanup_time": round(self.stats.average_cleanup_time, 2),
"cleanup_efficiency": round(self.stats.cleanup_efficiency, 3),
2025-06-11 03:25:08 +08:00
"last_cleanup_time": self.stats.last_cleanup_time.isoformat()
if self.stats.last_cleanup_time
else None,
"is_auto_cleanup_running": self.is_running,
"current_sessions": len(self.web_ui_manager.sessions),
"policy": {
"max_idle_time": self.policy.max_idle_time,
"max_session_age": self.policy.max_session_age,
"max_sessions": self.policy.max_sessions,
"cleanup_interval": self.policy.cleanup_interval,
"enable_auto_cleanup": self.policy.enable_auto_cleanup,
2025-06-11 03:25:08 +08:00
"preserve_active_session": self.policy.preserve_active_session,
},
}
return stats_dict
2025-06-11 03:25:08 +08:00
def get_cleanup_history(self, limit: int = 20) -> list[dict[str, Any]]:
"""獲取清理歷史記錄"""
return self.cleanup_history[-limit:] if self.cleanup_history else []
def add_cleanup_callback(self, callback: Callable):
"""添加清理回調函數"""
if callback not in self.cleanup_callbacks:
self.cleanup_callbacks.append(callback)
debug_log("添加清理回調函數")
def add_stats_callback(self, callback: Callable):
"""添加統計回調函數"""
if callback not in self.stats_callbacks:
self.stats_callbacks.append(callback)
debug_log("添加統計回調函數")
def update_policy(self, **kwargs):
"""更新清理策略"""
for key, value in kwargs.items():
if hasattr(self.policy, key):
setattr(self.policy, key, value)
debug_log(f"更新清理策略 {key} = {value}")
else:
debug_log(f"未知的策略參數: {key}")
def reset_stats(self):
"""重置統計數據"""
self.stats = CleanupStats()
self.cleanup_history.clear()
debug_log("清理統計數據已重置")
def force_cleanup_all(self, exclude_current: bool = True) -> int:
"""強制清理所有會話"""
sessions_to_clean = []
for session_id, session in self.web_ui_manager.sessions.items():
# 是否排除當前活躍會話
2025-06-11 03:25:08 +08:00
if (
exclude_current
and self.web_ui_manager.current_session
and session.session_id == self.web_ui_manager.current_session.session_id
):
continue
sessions_to_clean.append(session_id)
# 清理會話
cleaned_count = 0
for session_id in sessions_to_clean:
try:
session = self.web_ui_manager.sessions.get(session_id)
if session:
session._cleanup_sync_enhanced(CleanupReason.MANUAL)
del self.web_ui_manager.sessions[session_id]
cleaned_count += 1
except Exception as e:
debug_log(f"強制清理會話 {session_id} 失敗: {e}")
# 更新統計
self._update_cleanup_stats(CleanupTrigger.MANUAL, cleaned_count, 0.0)
debug_log(f"強制清理完成,清理了 {cleaned_count} 個會話")
return cleaned_count