mirror of
https://github.com/Minidoracat/mcp-feedback-enhanced.git
synced 2025-07-27 10:42:25 +08:00
✨ 新增會話清理管理器,整合自動清理策略、統計與性能監控,提升系統資源管理效率。擴展 WebFeedbackSession 及 SessionCleanupManager 功能,支持過期與內存壓力清理,並新增測試模組以確保功能正確性。
This commit is contained in:
parent
90deebdee5
commit
b9d781f147
@ -17,7 +17,8 @@ import threading
|
|||||||
import time
|
import time
|
||||||
import webbrowser
|
import webbrowser
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Dict, Optional
|
from typing import Dict, Optional, List
|
||||||
|
from datetime import datetime
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
from fastapi import FastAPI, Request, Response
|
from fastapi import FastAPI, Request, Response
|
||||||
@ -26,7 +27,7 @@ from fastapi.templating import Jinja2Templates
|
|||||||
from fastapi.middleware.gzip import GZipMiddleware
|
from fastapi.middleware.gzip import GZipMiddleware
|
||||||
import uvicorn
|
import uvicorn
|
||||||
|
|
||||||
from .models import WebFeedbackSession, FeedbackResult
|
from .models import WebFeedbackSession, FeedbackResult, CleanupReason, SessionStatus
|
||||||
from .routes import setup_routes
|
from .routes import setup_routes
|
||||||
from .utils import find_free_port, get_browser_opener
|
from .utils import find_free_port, get_browser_opener
|
||||||
from .utils.port_manager import PortManager
|
from .utils.port_manager import PortManager
|
||||||
@ -85,6 +86,17 @@ class WebUIManager:
|
|||||||
# 會話更新通知標記
|
# 會話更新通知標記
|
||||||
self._pending_session_update = False
|
self._pending_session_update = False
|
||||||
|
|
||||||
|
# 會話清理統計
|
||||||
|
self.cleanup_stats = {
|
||||||
|
"total_cleanups": 0,
|
||||||
|
"expired_cleanups": 0,
|
||||||
|
"memory_pressure_cleanups": 0,
|
||||||
|
"manual_cleanups": 0,
|
||||||
|
"last_cleanup_time": None,
|
||||||
|
"total_cleanup_duration": 0.0,
|
||||||
|
"sessions_cleaned": 0
|
||||||
|
}
|
||||||
|
|
||||||
self.server_thread = None
|
self.server_thread = None
|
||||||
self.server_process = None
|
self.server_process = None
|
||||||
self.i18n = get_i18n_manager()
|
self.i18n = get_i18n_manager()
|
||||||
@ -148,16 +160,46 @@ class WebUIManager:
|
|||||||
# 添加 Web 應用特定的警告回調
|
# 添加 Web 應用特定的警告回調
|
||||||
def web_memory_alert(alert):
|
def web_memory_alert(alert):
|
||||||
debug_log(f"Web UI 內存警告 [{alert.level}]: {alert.message}")
|
debug_log(f"Web UI 內存警告 [{alert.level}]: {alert.message}")
|
||||||
# 可以在這裡添加更多 Web 特定的處理邏輯
|
|
||||||
# 例如:通過 WebSocket 通知前端、記錄到特定日誌等
|
# 根據警告級別觸發不同的清理策略
|
||||||
|
if alert.level == "critical":
|
||||||
|
# 危險級別:清理過期會話
|
||||||
|
cleaned = self.cleanup_expired_sessions()
|
||||||
|
debug_log(f"內存危險警告觸發,清理了 {cleaned} 個過期會話")
|
||||||
|
elif alert.level == "emergency":
|
||||||
|
# 緊急級別:強制清理會話
|
||||||
|
cleaned = self.cleanup_sessions_by_memory_pressure(force=True)
|
||||||
|
debug_log(f"內存緊急警告觸發,強制清理了 {cleaned} 個會話")
|
||||||
|
|
||||||
self.memory_monitor.add_alert_callback(web_memory_alert)
|
self.memory_monitor.add_alert_callback(web_memory_alert)
|
||||||
|
|
||||||
|
# 添加會話清理回調到內存監控
|
||||||
|
def session_cleanup_callback(force: bool = False):
|
||||||
|
"""內存監控觸發的會話清理回調"""
|
||||||
|
try:
|
||||||
|
if force:
|
||||||
|
# 強制清理:包括內存壓力清理
|
||||||
|
cleaned = self.cleanup_sessions_by_memory_pressure(force=True)
|
||||||
|
debug_log(f"內存監控強制清理了 {cleaned} 個會話")
|
||||||
|
else:
|
||||||
|
# 常規清理:只清理過期會話
|
||||||
|
cleaned = self.cleanup_expired_sessions()
|
||||||
|
debug_log(f"內存監控清理了 {cleaned} 個過期會話")
|
||||||
|
except Exception as e:
|
||||||
|
error_id = ErrorHandler.log_error_with_context(
|
||||||
|
e,
|
||||||
|
context={"operation": "內存監控會話清理", "force": force},
|
||||||
|
error_type=ErrorType.SYSTEM
|
||||||
|
)
|
||||||
|
debug_log(f"內存監控會話清理失敗 [錯誤ID: {error_id}]: {e}")
|
||||||
|
|
||||||
|
self.memory_monitor.add_cleanup_callback(session_cleanup_callback)
|
||||||
|
|
||||||
# 確保內存監控已啟動(ResourceManager 可能已經啟動了)
|
# 確保內存監控已啟動(ResourceManager 可能已經啟動了)
|
||||||
if not self.memory_monitor.is_monitoring:
|
if not self.memory_monitor.is_monitoring:
|
||||||
self.memory_monitor.start_monitoring()
|
self.memory_monitor.start_monitoring()
|
||||||
|
|
||||||
debug_log("Web UI 內存監控設置完成")
|
debug_log("Web UI 內存監控設置完成,已集成會話清理回調")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
error_id = ErrorHandler.log_error_with_context(
|
error_id = ErrorHandler.log_error_with_context(
|
||||||
@ -563,13 +605,176 @@ class WebUIManager:
|
|||||||
"""獲取伺服器 URL"""
|
"""獲取伺服器 URL"""
|
||||||
return f"http://{self.host}:{self.port}"
|
return f"http://{self.host}:{self.port}"
|
||||||
|
|
||||||
|
def cleanup_expired_sessions(self) -> int:
|
||||||
|
"""清理過期會話"""
|
||||||
|
cleanup_start_time = time.time()
|
||||||
|
expired_sessions = []
|
||||||
|
|
||||||
|
# 掃描過期會話
|
||||||
|
for session_id, session in self.sessions.items():
|
||||||
|
if session.is_expired():
|
||||||
|
expired_sessions.append(session_id)
|
||||||
|
|
||||||
|
# 批量清理過期會話
|
||||||
|
cleaned_count = 0
|
||||||
|
for session_id in expired_sessions:
|
||||||
|
try:
|
||||||
|
session = self.sessions.get(session_id)
|
||||||
|
if session:
|
||||||
|
# 使用增強清理方法
|
||||||
|
session._cleanup_sync_enhanced(CleanupReason.EXPIRED)
|
||||||
|
del self.sessions[session_id]
|
||||||
|
cleaned_count += 1
|
||||||
|
|
||||||
|
# 如果清理的是當前活躍會話,清空當前會話
|
||||||
|
if self.current_session and self.current_session.session_id == session_id:
|
||||||
|
self.current_session = None
|
||||||
|
debug_log("清空過期的當前活躍會話")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
error_id = ErrorHandler.log_error_with_context(
|
||||||
|
e,
|
||||||
|
context={"session_id": session_id, "operation": "清理過期會話"},
|
||||||
|
error_type=ErrorType.SYSTEM
|
||||||
|
)
|
||||||
|
debug_log(f"清理過期會話 {session_id} 失敗 [錯誤ID: {error_id}]: {e}")
|
||||||
|
|
||||||
|
# 更新統計
|
||||||
|
cleanup_duration = time.time() - cleanup_start_time
|
||||||
|
self.cleanup_stats.update({
|
||||||
|
"total_cleanups": self.cleanup_stats["total_cleanups"] + 1,
|
||||||
|
"expired_cleanups": self.cleanup_stats["expired_cleanups"] + 1,
|
||||||
|
"last_cleanup_time": datetime.now().isoformat(),
|
||||||
|
"total_cleanup_duration": self.cleanup_stats["total_cleanup_duration"] + cleanup_duration,
|
||||||
|
"sessions_cleaned": self.cleanup_stats["sessions_cleaned"] + cleaned_count
|
||||||
|
})
|
||||||
|
|
||||||
|
if cleaned_count > 0:
|
||||||
|
debug_log(f"清理了 {cleaned_count} 個過期會話,耗時: {cleanup_duration:.2f}秒")
|
||||||
|
|
||||||
|
return cleaned_count
|
||||||
|
|
||||||
|
def cleanup_sessions_by_memory_pressure(self, force: bool = False) -> int:
|
||||||
|
"""根據內存壓力清理會話"""
|
||||||
|
cleanup_start_time = time.time()
|
||||||
|
sessions_to_clean = []
|
||||||
|
|
||||||
|
# 根據優先級選擇要清理的會話
|
||||||
|
# 優先級:已完成 > 已提交反饋 > 錯誤狀態 > 空閒時間最長
|
||||||
|
for session_id, session in self.sessions.items():
|
||||||
|
# 跳過當前活躍會話(除非強制清理)
|
||||||
|
if not force and self.current_session and session.session_id == self.current_session.session_id:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 優先清理已完成或錯誤狀態的會話
|
||||||
|
if session.status in [SessionStatus.COMPLETED, SessionStatus.ERROR, SessionStatus.TIMEOUT]:
|
||||||
|
sessions_to_clean.append((session_id, session, 1)) # 高優先級
|
||||||
|
elif session.status == SessionStatus.FEEDBACK_SUBMITTED:
|
||||||
|
# 已提交反饋但空閒時間較長的會話
|
||||||
|
if session.get_idle_time() > 300: # 5分鐘空閒
|
||||||
|
sessions_to_clean.append((session_id, session, 2)) # 中優先級
|
||||||
|
elif session.get_idle_time() > 600: # 10分鐘空閒
|
||||||
|
sessions_to_clean.append((session_id, session, 3)) # 低優先級
|
||||||
|
|
||||||
|
# 按優先級排序
|
||||||
|
sessions_to_clean.sort(key=lambda x: x[2])
|
||||||
|
|
||||||
|
# 清理會話(限制數量避免過度清理)
|
||||||
|
max_cleanup = min(len(sessions_to_clean), 5 if not force else len(sessions_to_clean))
|
||||||
|
cleaned_count = 0
|
||||||
|
|
||||||
|
for i in range(max_cleanup):
|
||||||
|
session_id, session, priority = sessions_to_clean[i]
|
||||||
|
try:
|
||||||
|
# 使用增強清理方法
|
||||||
|
session._cleanup_sync_enhanced(CleanupReason.MEMORY_PRESSURE)
|
||||||
|
del self.sessions[session_id]
|
||||||
|
cleaned_count += 1
|
||||||
|
|
||||||
|
# 如果清理的是當前活躍會話,清空當前會話
|
||||||
|
if self.current_session and self.current_session.session_id == session_id:
|
||||||
|
self.current_session = None
|
||||||
|
debug_log("因內存壓力清空當前活躍會話")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
error_id = ErrorHandler.log_error_with_context(
|
||||||
|
e,
|
||||||
|
context={"session_id": session_id, "operation": "內存壓力清理"},
|
||||||
|
error_type=ErrorType.SYSTEM
|
||||||
|
)
|
||||||
|
debug_log(f"內存壓力清理會話 {session_id} 失敗 [錯誤ID: {error_id}]: {e}")
|
||||||
|
|
||||||
|
# 更新統計
|
||||||
|
cleanup_duration = time.time() - cleanup_start_time
|
||||||
|
self.cleanup_stats.update({
|
||||||
|
"total_cleanups": self.cleanup_stats["total_cleanups"] + 1,
|
||||||
|
"memory_pressure_cleanups": self.cleanup_stats["memory_pressure_cleanups"] + 1,
|
||||||
|
"last_cleanup_time": datetime.now().isoformat(),
|
||||||
|
"total_cleanup_duration": self.cleanup_stats["total_cleanup_duration"] + cleanup_duration,
|
||||||
|
"sessions_cleaned": self.cleanup_stats["sessions_cleaned"] + cleaned_count
|
||||||
|
})
|
||||||
|
|
||||||
|
if cleaned_count > 0:
|
||||||
|
debug_log(f"因內存壓力清理了 {cleaned_count} 個會話,耗時: {cleanup_duration:.2f}秒")
|
||||||
|
|
||||||
|
return cleaned_count
|
||||||
|
|
||||||
|
def get_session_cleanup_stats(self) -> dict:
|
||||||
|
"""獲取會話清理統計"""
|
||||||
|
stats = self.cleanup_stats.copy()
|
||||||
|
stats.update({
|
||||||
|
"active_sessions": len(self.sessions),
|
||||||
|
"current_session_id": self.current_session.session_id if self.current_session else None,
|
||||||
|
"expired_sessions": sum(1 for s in self.sessions.values() if s.is_expired()),
|
||||||
|
"idle_sessions": sum(1 for s in self.sessions.values() if s.get_idle_time() > 300),
|
||||||
|
"memory_usage_mb": 0 # 將在下面計算
|
||||||
|
})
|
||||||
|
|
||||||
|
# 計算內存使用(如果可能)
|
||||||
|
try:
|
||||||
|
import psutil
|
||||||
|
process = psutil.Process()
|
||||||
|
stats["memory_usage_mb"] = round(process.memory_info().rss / (1024 * 1024), 2)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return stats
|
||||||
|
|
||||||
|
def _scan_expired_sessions(self) -> List[str]:
|
||||||
|
"""掃描過期會話ID列表"""
|
||||||
|
expired_sessions = []
|
||||||
|
for session_id, session in self.sessions.items():
|
||||||
|
if session.is_expired():
|
||||||
|
expired_sessions.append(session_id)
|
||||||
|
return expired_sessions
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
"""停止 Web UI 服務"""
|
"""停止 Web UI 服務"""
|
||||||
# 清理所有會話
|
# 清理所有會話
|
||||||
|
cleanup_start_time = time.time()
|
||||||
|
session_count = len(self.sessions)
|
||||||
|
|
||||||
for session in list(self.sessions.values()):
|
for session in list(self.sessions.values()):
|
||||||
session.cleanup()
|
try:
|
||||||
|
session._cleanup_sync_enhanced(CleanupReason.SHUTDOWN)
|
||||||
|
except Exception as e:
|
||||||
|
debug_log(f"停止服務時清理會話失敗: {e}")
|
||||||
|
|
||||||
self.sessions.clear()
|
self.sessions.clear()
|
||||||
|
self.current_session = None
|
||||||
|
|
||||||
|
# 更新統計
|
||||||
|
cleanup_duration = time.time() - cleanup_start_time
|
||||||
|
self.cleanup_stats.update({
|
||||||
|
"total_cleanups": self.cleanup_stats["total_cleanups"] + 1,
|
||||||
|
"manual_cleanups": self.cleanup_stats["manual_cleanups"] + 1,
|
||||||
|
"last_cleanup_time": datetime.now().isoformat(),
|
||||||
|
"total_cleanup_duration": self.cleanup_stats["total_cleanup_duration"] + cleanup_duration,
|
||||||
|
"sessions_cleaned": self.cleanup_stats["sessions_cleaned"] + session_count
|
||||||
|
})
|
||||||
|
|
||||||
|
debug_log(f"停止服務時清理了 {session_count} 個會話,耗時: {cleanup_duration:.2f}秒")
|
||||||
|
|
||||||
# 停止伺服器(注意:uvicorn 的 graceful shutdown 需要額外處理)
|
# 停止伺服器(注意:uvicorn 的 graceful shutdown 需要額外處理)
|
||||||
if self.server_thread and self.server_thread.is_alive():
|
if self.server_thread and self.server_thread.is_alive():
|
||||||
debug_log("正在停止 Web UI 服務")
|
debug_log("正在停止 Web UI 服務")
|
||||||
|
@ -7,10 +7,12 @@ Web UI 資料模型模組
|
|||||||
定義 Web UI 相關的資料結構和型別。
|
定義 Web UI 相關的資料結構和型別。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from .feedback_session import WebFeedbackSession
|
from .feedback_session import WebFeedbackSession, SessionStatus, CleanupReason
|
||||||
from .feedback_result import FeedbackResult
|
from .feedback_result import FeedbackResult
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
'WebFeedbackSession',
|
'WebFeedbackSession',
|
||||||
|
'SessionStatus',
|
||||||
|
'CleanupReason',
|
||||||
'FeedbackResult'
|
'FeedbackResult'
|
||||||
]
|
]
|
@ -11,14 +11,17 @@ import asyncio
|
|||||||
import base64
|
import base64
|
||||||
import subprocess
|
import subprocess
|
||||||
import threading
|
import threading
|
||||||
|
import time
|
||||||
|
from datetime import datetime, timedelta
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Dict, List, Optional
|
from typing import Dict, List, Optional, Callable
|
||||||
|
|
||||||
from fastapi import WebSocket
|
from fastapi import WebSocket
|
||||||
|
|
||||||
from ...debug import web_debug_log as debug_log
|
from ...debug import web_debug_log as debug_log
|
||||||
from ...utils.resource_manager import get_resource_manager, register_process
|
from ...utils.resource_manager import get_resource_manager, register_process
|
||||||
|
from ...utils.error_handler import ErrorHandler, ErrorType
|
||||||
|
|
||||||
|
|
||||||
class SessionStatus(Enum):
|
class SessionStatus(Enum):
|
||||||
@ -29,6 +32,17 @@ class SessionStatus(Enum):
|
|||||||
COMPLETED = "completed" # 已完成
|
COMPLETED = "completed" # 已完成
|
||||||
TIMEOUT = "timeout" # 超時
|
TIMEOUT = "timeout" # 超時
|
||||||
ERROR = "error" # 錯誤
|
ERROR = "error" # 錯誤
|
||||||
|
EXPIRED = "expired" # 已過期
|
||||||
|
|
||||||
|
|
||||||
|
class CleanupReason(Enum):
|
||||||
|
"""清理原因枚舉"""
|
||||||
|
TIMEOUT = "timeout" # 超時清理
|
||||||
|
EXPIRED = "expired" # 過期清理
|
||||||
|
MEMORY_PRESSURE = "memory_pressure" # 內存壓力清理
|
||||||
|
MANUAL = "manual" # 手動清理
|
||||||
|
ERROR = "error" # 錯誤清理
|
||||||
|
SHUTDOWN = "shutdown" # 系統關閉清理
|
||||||
|
|
||||||
# 常數定義
|
# 常數定義
|
||||||
MAX_IMAGE_SIZE = 1 * 1024 * 1024 # 1MB 圖片大小限制
|
MAX_IMAGE_SIZE = 1 * 1024 * 1024 # 1MB 圖片大小限制
|
||||||
@ -39,7 +53,8 @@ TEMP_DIR = Path.home() / ".cache" / "interactive-feedback-mcp-web"
|
|||||||
class WebFeedbackSession:
|
class WebFeedbackSession:
|
||||||
"""Web 回饋會話管理"""
|
"""Web 回饋會話管理"""
|
||||||
|
|
||||||
def __init__(self, session_id: str, project_directory: str, summary: str):
|
def __init__(self, session_id: str, project_directory: str, summary: str,
|
||||||
|
auto_cleanup_delay: int = 3600, max_idle_time: int = 1800):
|
||||||
self.session_id = session_id
|
self.session_id = session_id
|
||||||
self.project_directory = project_directory
|
self.project_directory = project_directory
|
||||||
self.summary = summary
|
self.summary = summary
|
||||||
@ -55,21 +70,49 @@ class WebFeedbackSession:
|
|||||||
# 新增:會話狀態管理
|
# 新增:會話狀態管理
|
||||||
self.status = SessionStatus.WAITING
|
self.status = SessionStatus.WAITING
|
||||||
self.status_message = "等待用戶回饋"
|
self.status_message = "等待用戶回饋"
|
||||||
self.created_at = asyncio.get_event_loop().time()
|
# 統一使用 time.time() 以避免時間基準不一致
|
||||||
|
self.created_at = time.time()
|
||||||
self.last_activity = self.created_at
|
self.last_activity = self.created_at
|
||||||
|
|
||||||
|
# 新增:自動清理配置
|
||||||
|
self.auto_cleanup_delay = auto_cleanup_delay # 自動清理延遲時間(秒)
|
||||||
|
self.max_idle_time = max_idle_time # 最大空閒時間(秒)
|
||||||
|
self.cleanup_timer: Optional[threading.Timer] = None
|
||||||
|
self.cleanup_callbacks: List[Callable] = [] # 清理回調函數列表
|
||||||
|
|
||||||
|
# 新增:清理統計
|
||||||
|
self.cleanup_stats = {
|
||||||
|
"cleanup_count": 0,
|
||||||
|
"last_cleanup_time": None,
|
||||||
|
"cleanup_reason": None,
|
||||||
|
"cleanup_duration": 0.0,
|
||||||
|
"memory_freed": 0,
|
||||||
|
"resources_cleaned": 0
|
||||||
|
}
|
||||||
|
|
||||||
# 確保臨時目錄存在
|
# 確保臨時目錄存在
|
||||||
TEMP_DIR.mkdir(parents=True, exist_ok=True)
|
TEMP_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
# 獲取資源管理器實例
|
# 獲取資源管理器實例
|
||||||
self.resource_manager = get_resource_manager()
|
self.resource_manager = get_resource_manager()
|
||||||
|
|
||||||
|
# 啟動自動清理定時器
|
||||||
|
self._schedule_auto_cleanup()
|
||||||
|
|
||||||
|
debug_log(f"會話 {self.session_id} 初始化完成,自動清理延遲: {auto_cleanup_delay}秒,最大空閒: {max_idle_time}秒")
|
||||||
|
|
||||||
def update_status(self, status: SessionStatus, message: str = None):
|
def update_status(self, status: SessionStatus, message: str = None):
|
||||||
"""更新會話狀態"""
|
"""更新會話狀態"""
|
||||||
self.status = status
|
self.status = status
|
||||||
if message:
|
if message:
|
||||||
self.status_message = message
|
self.status_message = message
|
||||||
self.last_activity = asyncio.get_event_loop().time()
|
# 統一使用 time.time()
|
||||||
|
self.last_activity = time.time()
|
||||||
|
|
||||||
|
# 如果會話變為活躍狀態,重置清理定時器
|
||||||
|
if status in [SessionStatus.ACTIVE, SessionStatus.FEEDBACK_SUBMITTED]:
|
||||||
|
self._schedule_auto_cleanup()
|
||||||
|
|
||||||
debug_log(f"會話 {self.session_id} 狀態更新: {status.value} - {self.status_message}")
|
debug_log(f"會話 {self.session_id} 狀態更新: {status.value} - {self.status_message}")
|
||||||
|
|
||||||
def get_status_info(self) -> dict:
|
def get_status_info(self) -> dict:
|
||||||
@ -90,6 +133,117 @@ class WebFeedbackSession:
|
|||||||
"""檢查會話是否活躍"""
|
"""檢查會話是否活躍"""
|
||||||
return self.status in [SessionStatus.WAITING, SessionStatus.ACTIVE, SessionStatus.FEEDBACK_SUBMITTED]
|
return self.status in [SessionStatus.WAITING, SessionStatus.ACTIVE, SessionStatus.FEEDBACK_SUBMITTED]
|
||||||
|
|
||||||
|
def is_expired(self) -> bool:
|
||||||
|
"""檢查會話是否已過期"""
|
||||||
|
# 統一使用 time.time()
|
||||||
|
current_time = time.time()
|
||||||
|
|
||||||
|
# 檢查是否超過最大空閒時間
|
||||||
|
idle_time = current_time - self.last_activity
|
||||||
|
if idle_time > self.max_idle_time:
|
||||||
|
debug_log(f"會話 {self.session_id} 空閒時間過長: {idle_time:.1f}秒 > {self.max_idle_time}秒")
|
||||||
|
return True
|
||||||
|
|
||||||
|
# 檢查是否處於已過期狀態
|
||||||
|
if self.status == SessionStatus.EXPIRED:
|
||||||
|
return True
|
||||||
|
|
||||||
|
# 檢查是否處於錯誤或超時狀態且超過一定時間
|
||||||
|
if self.status in [SessionStatus.ERROR, SessionStatus.TIMEOUT]:
|
||||||
|
error_time = current_time - self.last_activity
|
||||||
|
if error_time > 300: # 錯誤狀態超過5分鐘視為過期
|
||||||
|
debug_log(f"會話 {self.session_id} 錯誤狀態時間過長: {error_time:.1f}秒")
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_age(self) -> float:
|
||||||
|
"""獲取會話年齡(秒)"""
|
||||||
|
current_time = time.time()
|
||||||
|
return current_time - self.created_at
|
||||||
|
|
||||||
|
def get_idle_time(self) -> float:
|
||||||
|
"""獲取會話空閒時間(秒)"""
|
||||||
|
current_time = time.time()
|
||||||
|
return current_time - self.last_activity
|
||||||
|
|
||||||
|
def _schedule_auto_cleanup(self):
|
||||||
|
"""安排自動清理定時器"""
|
||||||
|
if self.cleanup_timer:
|
||||||
|
self.cleanup_timer.cancel()
|
||||||
|
|
||||||
|
def auto_cleanup():
|
||||||
|
"""自動清理回調"""
|
||||||
|
try:
|
||||||
|
if not self._cleanup_done and self.is_expired():
|
||||||
|
debug_log(f"會話 {self.session_id} 觸發自動清理(過期)")
|
||||||
|
# 使用異步方式執行清理
|
||||||
|
import asyncio
|
||||||
|
try:
|
||||||
|
loop = asyncio.get_event_loop()
|
||||||
|
loop.create_task(self._cleanup_resources_enhanced(CleanupReason.EXPIRED))
|
||||||
|
except RuntimeError:
|
||||||
|
# 如果沒有事件循環,使用同步清理
|
||||||
|
self._cleanup_sync_enhanced(CleanupReason.EXPIRED)
|
||||||
|
else:
|
||||||
|
# 如果還沒過期,重新安排定時器
|
||||||
|
self._schedule_auto_cleanup()
|
||||||
|
except Exception as e:
|
||||||
|
error_id = ErrorHandler.log_error_with_context(
|
||||||
|
e,
|
||||||
|
context={"session_id": self.session_id, "operation": "自動清理"},
|
||||||
|
error_type=ErrorType.SYSTEM
|
||||||
|
)
|
||||||
|
debug_log(f"自動清理失敗 [錯誤ID: {error_id}]: {e}")
|
||||||
|
|
||||||
|
self.cleanup_timer = threading.Timer(self.auto_cleanup_delay, auto_cleanup)
|
||||||
|
self.cleanup_timer.daemon = True
|
||||||
|
self.cleanup_timer.start()
|
||||||
|
debug_log(f"會話 {self.session_id} 自動清理定時器已設置,{self.auto_cleanup_delay}秒後觸發")
|
||||||
|
|
||||||
|
def extend_cleanup_timer(self, additional_time: int = None):
|
||||||
|
"""延長清理定時器"""
|
||||||
|
if additional_time is None:
|
||||||
|
additional_time = self.auto_cleanup_delay
|
||||||
|
|
||||||
|
if self.cleanup_timer:
|
||||||
|
self.cleanup_timer.cancel()
|
||||||
|
|
||||||
|
self.cleanup_timer = threading.Timer(additional_time, lambda: None)
|
||||||
|
self.cleanup_timer.daemon = True
|
||||||
|
self.cleanup_timer.start()
|
||||||
|
|
||||||
|
debug_log(f"會話 {self.session_id} 清理定時器已延長 {additional_time} 秒")
|
||||||
|
|
||||||
|
def add_cleanup_callback(self, callback: Callable):
|
||||||
|
"""添加清理回調函數"""
|
||||||
|
if callback not in self.cleanup_callbacks:
|
||||||
|
self.cleanup_callbacks.append(callback)
|
||||||
|
debug_log(f"會話 {self.session_id} 添加清理回調函數")
|
||||||
|
|
||||||
|
def remove_cleanup_callback(self, callback: Callable):
|
||||||
|
"""移除清理回調函數"""
|
||||||
|
if callback in self.cleanup_callbacks:
|
||||||
|
self.cleanup_callbacks.remove(callback)
|
||||||
|
debug_log(f"會話 {self.session_id} 移除清理回調函數")
|
||||||
|
|
||||||
|
def get_cleanup_stats(self) -> dict:
|
||||||
|
"""獲取清理統計信息"""
|
||||||
|
stats = self.cleanup_stats.copy()
|
||||||
|
stats.update({
|
||||||
|
"session_id": self.session_id,
|
||||||
|
"age": self.get_age(),
|
||||||
|
"idle_time": self.get_idle_time(),
|
||||||
|
"is_expired": self.is_expired(),
|
||||||
|
"is_active": self.is_active(),
|
||||||
|
"status": self.status.value,
|
||||||
|
"has_websocket": self.websocket is not None,
|
||||||
|
"has_process": self.process is not None,
|
||||||
|
"command_logs_count": len(self.command_logs),
|
||||||
|
"images_count": len(self.images)
|
||||||
|
})
|
||||||
|
return stats
|
||||||
|
|
||||||
async def wait_for_feedback(self, timeout: int = 600) -> dict:
|
async def wait_for_feedback(self, timeout: int = 600) -> dict:
|
||||||
"""
|
"""
|
||||||
等待用戶回饋,包含圖片,支援超時自動清理
|
等待用戶回饋,包含圖片,支援超時自動清理
|
||||||
@ -321,33 +475,72 @@ class WebFeedbackSession:
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
async def _cleanup_resources_on_timeout(self):
|
async def _cleanup_resources_on_timeout(self):
|
||||||
"""超時時清理所有資源"""
|
"""超時時清理所有資源(保持向後兼容)"""
|
||||||
|
await self._cleanup_resources_enhanced(CleanupReason.TIMEOUT)
|
||||||
|
|
||||||
|
async def _cleanup_resources_enhanced(self, reason: CleanupReason):
|
||||||
|
"""增強的資源清理方法"""
|
||||||
if self._cleanup_done:
|
if self._cleanup_done:
|
||||||
return # 避免重複清理
|
return # 避免重複清理
|
||||||
|
|
||||||
|
cleanup_start_time = time.time()
|
||||||
self._cleanup_done = True
|
self._cleanup_done = True
|
||||||
debug_log(f"開始清理會話 {self.session_id} 的資源...")
|
|
||||||
|
debug_log(f"開始清理會話 {self.session_id} 的資源,原因: {reason.value}")
|
||||||
|
|
||||||
|
# 更新清理統計
|
||||||
|
self.cleanup_stats["cleanup_count"] += 1
|
||||||
|
self.cleanup_stats["cleanup_reason"] = reason.value
|
||||||
|
self.cleanup_stats["last_cleanup_time"] = datetime.now().isoformat()
|
||||||
|
|
||||||
|
resources_cleaned = 0
|
||||||
|
memory_before = 0
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# 1. 關閉 WebSocket 連接
|
# 記錄清理前的內存使用(如果可能)
|
||||||
|
try:
|
||||||
|
import psutil
|
||||||
|
process = psutil.Process()
|
||||||
|
memory_before = process.memory_info().rss
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# 1. 取消自動清理定時器
|
||||||
|
if self.cleanup_timer:
|
||||||
|
self.cleanup_timer.cancel()
|
||||||
|
self.cleanup_timer = None
|
||||||
|
resources_cleaned += 1
|
||||||
|
|
||||||
|
# 2. 關閉 WebSocket 連接
|
||||||
if self.websocket:
|
if self.websocket:
|
||||||
try:
|
try:
|
||||||
# 先通知前端超時
|
# 根據清理原因發送不同的通知消息
|
||||||
|
message_map = {
|
||||||
|
CleanupReason.TIMEOUT: "會話已超時,介面將自動關閉",
|
||||||
|
CleanupReason.EXPIRED: "會話已過期,介面將自動關閉",
|
||||||
|
CleanupReason.MEMORY_PRESSURE: "系統內存不足,會話將被清理",
|
||||||
|
CleanupReason.MANUAL: "會話已被手動清理",
|
||||||
|
CleanupReason.ERROR: "會話發生錯誤,將被清理",
|
||||||
|
CleanupReason.SHUTDOWN: "系統正在關閉,會話將被清理"
|
||||||
|
}
|
||||||
|
|
||||||
await self.websocket.send_json({
|
await self.websocket.send_json({
|
||||||
"type": "session_timeout",
|
"type": "session_cleanup",
|
||||||
"message": "會話已超時,介面將自動關閉"
|
"reason": reason.value,
|
||||||
|
"message": message_map.get(reason, "會話將被清理")
|
||||||
})
|
})
|
||||||
await asyncio.sleep(0.1) # 給前端一點時間處理消息
|
await asyncio.sleep(0.1) # 給前端一點時間處理消息
|
||||||
|
|
||||||
# 安全關閉 WebSocket
|
# 安全關閉 WebSocket
|
||||||
await self._safe_close_websocket()
|
await self._safe_close_websocket()
|
||||||
debug_log(f"會話 {self.session_id} WebSocket 已關閉")
|
debug_log(f"會話 {self.session_id} WebSocket 已關閉")
|
||||||
|
resources_cleaned += 1
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
debug_log(f"關閉 WebSocket 時發生錯誤: {e}")
|
debug_log(f"關閉 WebSocket 時發生錯誤: {e}")
|
||||||
finally:
|
finally:
|
||||||
self.websocket = None
|
self.websocket = None
|
||||||
|
|
||||||
# 2. 終止正在運行的命令進程
|
# 3. 終止正在運行的命令進程
|
||||||
if self.process:
|
if self.process:
|
||||||
try:
|
try:
|
||||||
self.process.terminate()
|
self.process.terminate()
|
||||||
@ -357,67 +550,213 @@ class WebFeedbackSession:
|
|||||||
except subprocess.TimeoutExpired:
|
except subprocess.TimeoutExpired:
|
||||||
self.process.kill()
|
self.process.kill()
|
||||||
debug_log(f"會話 {self.session_id} 命令進程已強制終止")
|
debug_log(f"會話 {self.session_id} 命令進程已強制終止")
|
||||||
|
resources_cleaned += 1
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
debug_log(f"終止命令進程時發生錯誤: {e}")
|
debug_log(f"終止命令進程時發生錯誤: {e}")
|
||||||
finally:
|
finally:
|
||||||
self.process = None
|
self.process = None
|
||||||
|
|
||||||
# 3. 設置完成事件(防止其他地方還在等待)
|
# 4. 設置完成事件(防止其他地方還在等待)
|
||||||
self.feedback_completed.set()
|
self.feedback_completed.set()
|
||||||
|
|
||||||
# 4. 清理臨時數據
|
# 5. 清理臨時數據
|
||||||
|
logs_count = len(self.command_logs)
|
||||||
|
images_count = len(self.images)
|
||||||
|
|
||||||
self.command_logs.clear()
|
self.command_logs.clear()
|
||||||
self.images.clear()
|
self.images.clear()
|
||||||
|
self.settings.clear()
|
||||||
debug_log(f"會話 {self.session_id} 資源清理完成")
|
|
||||||
|
if logs_count > 0 or images_count > 0:
|
||||||
|
resources_cleaned += logs_count + images_count
|
||||||
|
debug_log(f"清理了 {logs_count} 條日誌和 {images_count} 張圖片")
|
||||||
|
|
||||||
|
# 6. 更新會話狀態
|
||||||
|
if reason == CleanupReason.EXPIRED:
|
||||||
|
self.status = SessionStatus.EXPIRED
|
||||||
|
elif reason == CleanupReason.TIMEOUT:
|
||||||
|
self.status = SessionStatus.TIMEOUT
|
||||||
|
elif reason == CleanupReason.ERROR:
|
||||||
|
self.status = SessionStatus.ERROR
|
||||||
|
else:
|
||||||
|
self.status = SessionStatus.COMPLETED
|
||||||
|
|
||||||
|
# 7. 調用清理回調函數
|
||||||
|
for callback in self.cleanup_callbacks:
|
||||||
|
try:
|
||||||
|
if asyncio.iscoroutinefunction(callback):
|
||||||
|
await callback(self, reason)
|
||||||
|
else:
|
||||||
|
callback(self, reason)
|
||||||
|
except Exception as e:
|
||||||
|
debug_log(f"清理回調執行失敗: {e}")
|
||||||
|
|
||||||
|
# 8. 計算清理效果
|
||||||
|
cleanup_duration = time.time() - cleanup_start_time
|
||||||
|
memory_after = 0
|
||||||
|
try:
|
||||||
|
import psutil
|
||||||
|
process = psutil.Process()
|
||||||
|
memory_after = process.memory_info().rss
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
memory_freed = max(0, memory_before - memory_after)
|
||||||
|
|
||||||
|
# 更新清理統計
|
||||||
|
self.cleanup_stats.update({
|
||||||
|
"cleanup_duration": cleanup_duration,
|
||||||
|
"memory_freed": memory_freed,
|
||||||
|
"resources_cleaned": resources_cleaned
|
||||||
|
})
|
||||||
|
|
||||||
|
debug_log(f"會話 {self.session_id} 資源清理完成,耗時: {cleanup_duration:.2f}秒,"
|
||||||
|
f"清理資源: {resources_cleaned}個,釋放內存: {memory_freed}字節")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
debug_log(f"清理會話 {self.session_id} 資源時發生錯誤: {e}")
|
error_id = ErrorHandler.log_error_with_context(
|
||||||
|
e,
|
||||||
|
context={
|
||||||
|
"session_id": self.session_id,
|
||||||
|
"cleanup_reason": reason.value,
|
||||||
|
"operation": "增強資源清理"
|
||||||
|
},
|
||||||
|
error_type=ErrorType.SYSTEM
|
||||||
|
)
|
||||||
|
debug_log(f"清理會話 {self.session_id} 資源時發生錯誤 [錯誤ID: {error_id}]: {e}")
|
||||||
|
|
||||||
|
# 即使發生錯誤也要更新統計
|
||||||
|
self.cleanup_stats["cleanup_duration"] = time.time() - cleanup_start_time
|
||||||
|
|
||||||
def _cleanup_sync(self):
|
def _cleanup_sync(self):
|
||||||
"""同步清理會話資源(但保留 WebSocket 連接)"""
|
"""同步清理會話資源(但保留 WebSocket 連接)- 保持向後兼容"""
|
||||||
if self._cleanup_done:
|
self._cleanup_sync_enhanced(CleanupReason.MANUAL, preserve_websocket=True)
|
||||||
|
|
||||||
|
def _cleanup_sync_enhanced(self, reason: CleanupReason, preserve_websocket: bool = False):
|
||||||
|
"""增強的同步清理會話資源"""
|
||||||
|
if self._cleanup_done and not preserve_websocket:
|
||||||
return
|
return
|
||||||
|
|
||||||
debug_log(f"同步清理會話 {self.session_id} 資源(保留 WebSocket)...")
|
cleanup_start_time = time.time()
|
||||||
|
debug_log(f"同步清理會話 {self.session_id} 資源,原因: {reason.value},保留WebSocket: {preserve_websocket}")
|
||||||
|
|
||||||
# 只清理進程,不清理 WebSocket 連接
|
# 更新清理統計
|
||||||
if self.process:
|
self.cleanup_stats["cleanup_count"] += 1
|
||||||
|
self.cleanup_stats["cleanup_reason"] = reason.value
|
||||||
|
self.cleanup_stats["last_cleanup_time"] = datetime.now().isoformat()
|
||||||
|
|
||||||
|
resources_cleaned = 0
|
||||||
|
memory_before = 0
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 記錄清理前的內存使用
|
||||||
try:
|
try:
|
||||||
self.process.terminate()
|
import psutil
|
||||||
self.process.wait(timeout=5)
|
process = psutil.Process()
|
||||||
|
memory_before = process.memory_info().rss
|
||||||
except:
|
except:
|
||||||
try:
|
pass
|
||||||
self.process.kill()
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
self.process = None
|
|
||||||
|
|
||||||
# 清理臨時數據
|
# 1. 取消自動清理定時器
|
||||||
self.command_logs.clear()
|
if self.cleanup_timer:
|
||||||
# 注意:不設置 _cleanup_done = True,因為還需要清理 WebSocket
|
self.cleanup_timer.cancel()
|
||||||
|
self.cleanup_timer = None
|
||||||
|
resources_cleaned += 1
|
||||||
|
|
||||||
|
# 2. 清理進程
|
||||||
|
if self.process:
|
||||||
|
try:
|
||||||
|
self.process.terminate()
|
||||||
|
self.process.wait(timeout=5)
|
||||||
|
debug_log(f"會話 {self.session_id} 命令進程已正常終止")
|
||||||
|
resources_cleaned += 1
|
||||||
|
except:
|
||||||
|
try:
|
||||||
|
self.process.kill()
|
||||||
|
debug_log(f"會話 {self.session_id} 命令進程已強制終止")
|
||||||
|
resources_cleaned += 1
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
self.process = None
|
||||||
|
|
||||||
|
# 3. 清理臨時數據
|
||||||
|
logs_count = len(self.command_logs)
|
||||||
|
images_count = len(self.images)
|
||||||
|
|
||||||
|
self.command_logs.clear()
|
||||||
|
if not preserve_websocket:
|
||||||
|
self.images.clear()
|
||||||
|
self.settings.clear()
|
||||||
|
resources_cleaned += images_count
|
||||||
|
|
||||||
|
resources_cleaned += logs_count
|
||||||
|
|
||||||
|
# 4. 設置完成事件
|
||||||
|
if not preserve_websocket:
|
||||||
|
self.feedback_completed.set()
|
||||||
|
|
||||||
|
# 5. 更新狀態
|
||||||
|
if not preserve_websocket:
|
||||||
|
if reason == CleanupReason.EXPIRED:
|
||||||
|
self.status = SessionStatus.EXPIRED
|
||||||
|
elif reason == CleanupReason.TIMEOUT:
|
||||||
|
self.status = SessionStatus.TIMEOUT
|
||||||
|
elif reason == CleanupReason.ERROR:
|
||||||
|
self.status = SessionStatus.ERROR
|
||||||
|
else:
|
||||||
|
self.status = SessionStatus.COMPLETED
|
||||||
|
|
||||||
|
self._cleanup_done = True
|
||||||
|
|
||||||
|
# 6. 調用清理回調函數(同步版本)
|
||||||
|
for callback in self.cleanup_callbacks:
|
||||||
|
try:
|
||||||
|
if not asyncio.iscoroutinefunction(callback):
|
||||||
|
callback(self, reason)
|
||||||
|
except Exception as e:
|
||||||
|
debug_log(f"同步清理回調執行失敗: {e}")
|
||||||
|
|
||||||
|
# 7. 計算清理效果
|
||||||
|
cleanup_duration = time.time() - cleanup_start_time
|
||||||
|
memory_after = 0
|
||||||
|
try:
|
||||||
|
import psutil
|
||||||
|
process = psutil.Process()
|
||||||
|
memory_after = process.memory_info().rss
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
memory_freed = max(0, memory_before - memory_after)
|
||||||
|
|
||||||
|
# 更新清理統計
|
||||||
|
self.cleanup_stats.update({
|
||||||
|
"cleanup_duration": cleanup_duration,
|
||||||
|
"memory_freed": memory_freed,
|
||||||
|
"resources_cleaned": resources_cleaned
|
||||||
|
})
|
||||||
|
|
||||||
|
debug_log(f"會話 {self.session_id} 同步清理完成,耗時: {cleanup_duration:.2f}秒,"
|
||||||
|
f"清理資源: {resources_cleaned}個,釋放內存: {memory_freed}字節")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
error_id = ErrorHandler.log_error_with_context(
|
||||||
|
e,
|
||||||
|
context={
|
||||||
|
"session_id": self.session_id,
|
||||||
|
"cleanup_reason": reason.value,
|
||||||
|
"preserve_websocket": preserve_websocket,
|
||||||
|
"operation": "同步資源清理"
|
||||||
|
},
|
||||||
|
error_type=ErrorType.SYSTEM
|
||||||
|
)
|
||||||
|
debug_log(f"同步清理會話 {self.session_id} 資源時發生錯誤 [錯誤ID: {error_id}]: {e}")
|
||||||
|
|
||||||
|
# 即使發生錯誤也要更新統計
|
||||||
|
self.cleanup_stats["cleanup_duration"] = time.time() - cleanup_start_time
|
||||||
|
|
||||||
def cleanup(self):
|
def cleanup(self):
|
||||||
"""同步清理會話資源(保持向後兼容)"""
|
"""同步清理會話資源(保持向後兼容)"""
|
||||||
if self._cleanup_done:
|
self._cleanup_sync_enhanced(CleanupReason.MANUAL)
|
||||||
return
|
|
||||||
|
|
||||||
self._cleanup_done = True
|
|
||||||
debug_log(f"同步清理會話 {self.session_id} 資源...")
|
|
||||||
|
|
||||||
if self.process:
|
|
||||||
try:
|
|
||||||
self.process.terminate()
|
|
||||||
self.process.wait(timeout=5)
|
|
||||||
except:
|
|
||||||
try:
|
|
||||||
self.process.kill()
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
self.process = None
|
|
||||||
|
|
||||||
# 設置完成事件
|
|
||||||
self.feedback_completed.set()
|
|
||||||
|
|
||||||
async def _safe_close_websocket(self):
|
async def _safe_close_websocket(self):
|
||||||
"""安全關閉 WebSocket 連接,避免事件循環衝突"""
|
"""安全關閉 WebSocket 連接,避免事件循環衝突"""
|
||||||
|
504
src/mcp_feedback_enhanced/web/utils/session_cleanup_manager.py
Normal file
504
src/mcp_feedback_enhanced/web/utils/session_cleanup_manager.py
Normal file
@ -0,0 +1,504 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
會話清理管理器
|
||||||
|
==============
|
||||||
|
|
||||||
|
統一管理 Web 會話的清理策略、統計和性能監控。
|
||||||
|
與內存監控系統深度集成,提供智能清理決策。
|
||||||
|
"""
|
||||||
|
|
||||||
|
import time
|
||||||
|
import threading
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from typing import Dict, List, Optional, Callable, Any
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
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:
|
||||||
|
"""清理策略配置"""
|
||||||
|
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:
|
||||||
|
"""清理統計數據"""
|
||||||
|
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
|
||||||
|
last_cleanup_time: Optional[datetime] = None
|
||||||
|
cleanup_efficiency: float = 0.0 # 清理效率(清理的會話數/總會話數)
|
||||||
|
|
||||||
|
|
||||||
|
class CleanupTrigger(Enum):
|
||||||
|
"""清理觸發器類型"""
|
||||||
|
AUTO = "auto" # 自動清理
|
||||||
|
MEMORY_PRESSURE = "memory_pressure" # 內存壓力
|
||||||
|
MANUAL = "manual" # 手動清理
|
||||||
|
EXPIRED = "expired" # 過期清理
|
||||||
|
CAPACITY = "capacity" # 容量限制
|
||||||
|
|
||||||
|
|
||||||
|
class SessionCleanupManager:
|
||||||
|
"""會話清理管理器"""
|
||||||
|
|
||||||
|
def __init__(self, web_ui_manager, policy: CleanupPolicy = None):
|
||||||
|
"""
|
||||||
|
初始化會話清理管理器
|
||||||
|
|
||||||
|
Args:
|
||||||
|
web_ui_manager: WebUIManager 實例
|
||||||
|
policy: 清理策略配置
|
||||||
|
"""
|
||||||
|
self.web_ui_manager = web_ui_manager
|
||||||
|
self.policy = policy or CleanupPolicy()
|
||||||
|
self.stats = CleanupStats()
|
||||||
|
|
||||||
|
# 清理狀態
|
||||||
|
self.is_running = False
|
||||||
|
self.cleanup_thread: Optional[threading.Thread] = None
|
||||||
|
self._stop_event = threading.Event()
|
||||||
|
|
||||||
|
# 回調函數
|
||||||
|
self.cleanup_callbacks: List[Callable] = []
|
||||||
|
self.stats_callbacks: List[Callable] = []
|
||||||
|
|
||||||
|
# 清理歷史記錄
|
||||||
|
self.cleanup_history: List[Dict[str, Any]] = []
|
||||||
|
self.max_history = 100
|
||||||
|
|
||||||
|
debug_log("SessionCleanupManager 初始化完成")
|
||||||
|
|
||||||
|
def start_auto_cleanup(self) -> bool:
|
||||||
|
"""啟動自動清理"""
|
||||||
|
if not self.policy.enable_auto_cleanup:
|
||||||
|
debug_log("自動清理已禁用")
|
||||||
|
return False
|
||||||
|
|
||||||
|
if self.is_running:
|
||||||
|
debug_log("自動清理已在運行")
|
||||||
|
return True
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.is_running = True
|
||||||
|
self._stop_event.clear()
|
||||||
|
|
||||||
|
self.cleanup_thread = threading.Thread(
|
||||||
|
target=self._auto_cleanup_loop,
|
||||||
|
name="SessionCleanupManager",
|
||||||
|
daemon=True
|
||||||
|
)
|
||||||
|
self.cleanup_thread.start()
|
||||||
|
|
||||||
|
debug_log(f"自動清理已啟動,間隔 {self.policy.cleanup_interval} 秒")
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.is_running = False
|
||||||
|
error_id = ErrorHandler.log_error_with_context(
|
||||||
|
e,
|
||||||
|
context={"operation": "啟動自動清理"},
|
||||||
|
error_type=ErrorType.SYSTEM
|
||||||
|
)
|
||||||
|
debug_log(f"啟動自動清理失敗 [錯誤ID: {error_id}]: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def stop_auto_cleanup(self) -> bool:
|
||||||
|
"""停止自動清理"""
|
||||||
|
if not self.is_running:
|
||||||
|
debug_log("自動清理未在運行")
|
||||||
|
return True
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.is_running = False
|
||||||
|
self._stop_event.set()
|
||||||
|
|
||||||
|
if self.cleanup_thread and self.cleanup_thread.is_alive():
|
||||||
|
self.cleanup_thread.join(timeout=5)
|
||||||
|
|
||||||
|
debug_log("自動清理已停止")
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
error_id = ErrorHandler.log_error_with_context(
|
||||||
|
e,
|
||||||
|
context={"operation": "停止自動清理"},
|
||||||
|
error_type=ErrorType.SYSTEM
|
||||||
|
)
|
||||||
|
debug_log(f"停止自動清理失敗 [錯誤ID: {error_id}]: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _auto_cleanup_loop(self):
|
||||||
|
"""自動清理主循環"""
|
||||||
|
debug_log("自動清理循環開始")
|
||||||
|
|
||||||
|
while not self._stop_event.is_set():
|
||||||
|
try:
|
||||||
|
# 執行清理檢查
|
||||||
|
self._perform_auto_cleanup()
|
||||||
|
|
||||||
|
# 等待下次清理
|
||||||
|
if self._stop_event.wait(self.policy.cleanup_interval):
|
||||||
|
break
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
error_id = ErrorHandler.log_error_with_context(
|
||||||
|
e,
|
||||||
|
context={"operation": "自動清理循環"},
|
||||||
|
error_type=ErrorType.SYSTEM
|
||||||
|
)
|
||||||
|
debug_log(f"自動清理循環錯誤 [錯誤ID: {error_id}]: {e}")
|
||||||
|
|
||||||
|
# 發生錯誤時等待較短時間後重試
|
||||||
|
if self._stop_event.wait(30):
|
||||||
|
break
|
||||||
|
|
||||||
|
debug_log("自動清理循環結束")
|
||||||
|
|
||||||
|
def _perform_auto_cleanup(self):
|
||||||
|
"""執行自動清理"""
|
||||||
|
cleanup_start_time = time.time()
|
||||||
|
cleaned_sessions = 0
|
||||||
|
|
||||||
|
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} 個會話")
|
||||||
|
|
||||||
|
# 2. 清理過期會話
|
||||||
|
cleaned = self._cleanup_expired_sessions()
|
||||||
|
cleaned_sessions += cleaned
|
||||||
|
|
||||||
|
# 3. 清理空閒會話
|
||||||
|
cleaned = self._cleanup_idle_sessions()
|
||||||
|
cleaned_sessions += cleaned
|
||||||
|
|
||||||
|
# 4. 更新統計
|
||||||
|
cleanup_duration = time.time() - cleanup_start_time
|
||||||
|
self._update_cleanup_stats(
|
||||||
|
CleanupTrigger.AUTO,
|
||||||
|
cleaned_sessions,
|
||||||
|
cleanup_duration
|
||||||
|
)
|
||||||
|
|
||||||
|
if cleaned_sessions > 0:
|
||||||
|
debug_log(f"自動清理完成,清理了 {cleaned_sessions} 個會話,耗時: {cleanup_duration:.2f}秒")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
error_id = ErrorHandler.log_error_with_context(
|
||||||
|
e,
|
||||||
|
context={"operation": "執行自動清理"},
|
||||||
|
error_type=ErrorType.SYSTEM
|
||||||
|
)
|
||||||
|
debug_log(f"執行自動清理失敗 [錯誤ID: {error_id}]: {e}")
|
||||||
|
|
||||||
|
def trigger_cleanup(self, trigger: CleanupTrigger, force: bool = False) -> int:
|
||||||
|
"""觸發清理操作"""
|
||||||
|
cleanup_start_time = time.time()
|
||||||
|
cleaned_sessions = 0
|
||||||
|
|
||||||
|
try:
|
||||||
|
debug_log(f"觸發清理操作,觸發器: {trigger.value},強制: {force}")
|
||||||
|
|
||||||
|
if trigger == CleanupTrigger.MEMORY_PRESSURE:
|
||||||
|
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:
|
||||||
|
cleaned_sessions += self.web_ui_manager.cleanup_sessions_by_memory_pressure(force)
|
||||||
|
else:
|
||||||
|
# 自動清理
|
||||||
|
self._perform_auto_cleanup()
|
||||||
|
return 0 # 統計已在 _perform_auto_cleanup 中更新
|
||||||
|
|
||||||
|
# 更新統計
|
||||||
|
cleanup_duration = time.time() - cleanup_start_time
|
||||||
|
self._update_cleanup_stats(trigger, cleaned_sessions, cleanup_duration)
|
||||||
|
|
||||||
|
debug_log(f"清理操作完成,清理了 {cleaned_sessions} 個會話,耗時: {cleanup_duration:.2f}秒")
|
||||||
|
return cleaned_sessions
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
error_id = ErrorHandler.log_error_with_context(
|
||||||
|
e,
|
||||||
|
context={"operation": "觸發清理", "trigger": trigger.value, "force": force},
|
||||||
|
error_type=ErrorType.SYSTEM
|
||||||
|
)
|
||||||
|
debug_log(f"觸發清理操作失敗 [錯誤ID: {error_id}]: {e}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def _cleanup_by_capacity(self) -> int:
|
||||||
|
"""根據容量限制清理會話"""
|
||||||
|
sessions = self.web_ui_manager.sessions
|
||||||
|
if len(sessions) <= self.policy.max_sessions:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
# 計算需要清理的會話數量
|
||||||
|
excess_count = len(sessions) - self.policy.max_sessions
|
||||||
|
|
||||||
|
# 按優先級排序會話(優先清理舊的、非活躍的會話)
|
||||||
|
session_priorities = []
|
||||||
|
for session_id, session in sessions.items():
|
||||||
|
# 跳過當前活躍會話(如果啟用保護)
|
||||||
|
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
|
||||||
|
|
||||||
|
# 計算優先級分數(分數越高越優先清理)
|
||||||
|
priority_score = 0
|
||||||
|
|
||||||
|
# 狀態優先級
|
||||||
|
if session.status in [SessionStatus.COMPLETED, SessionStatus.ERROR, SessionStatus.TIMEOUT]:
|
||||||
|
priority_score += 100
|
||||||
|
elif session.status == SessionStatus.FEEDBACK_SUBMITTED:
|
||||||
|
priority_score += 50
|
||||||
|
|
||||||
|
# 年齡優先級
|
||||||
|
age = session.get_age()
|
||||||
|
priority_score += age / 60 # 每分鐘加1分
|
||||||
|
|
||||||
|
# 空閒時間優先級
|
||||||
|
idle_time = session.get_idle_time()
|
||||||
|
priority_score += idle_time / 30 # 每30秒加1分
|
||||||
|
|
||||||
|
session_priorities.append((session_id, session, priority_score))
|
||||||
|
|
||||||
|
# 按優先級排序並清理
|
||||||
|
session_priorities.sort(key=lambda x: x[2], reverse=True)
|
||||||
|
cleaned_count = 0
|
||||||
|
|
||||||
|
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}")
|
||||||
|
|
||||||
|
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():
|
||||||
|
# 檢查是否過期
|
||||||
|
if session.is_expired():
|
||||||
|
expired_sessions.append(session_id)
|
||||||
|
# 檢查是否超過最大年齡
|
||||||
|
elif 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
|
||||||
|
|
||||||
|
# 如果清理的是當前活躍會話,清空當前會話
|
||||||
|
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():
|
||||||
|
# 跳過當前活躍會話(如果啟用保護)
|
||||||
|
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
|
||||||
|
|
||||||
|
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:
|
||||||
|
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,
|
||||||
|
"total_sessions_after": len(self.web_ui_manager.sessions)
|
||||||
|
}
|
||||||
|
|
||||||
|
self.cleanup_history.append(cleanup_record)
|
||||||
|
|
||||||
|
# 限制歷史記錄數量
|
||||||
|
if len(self.cleanup_history) > self.max_history:
|
||||||
|
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}")
|
||||||
|
|
||||||
|
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),
|
||||||
|
"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,
|
||||||
|
"preserve_active_session": self.policy.preserve_active_session
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return stats_dict
|
||||||
|
|
||||||
|
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():
|
||||||
|
# 是否排除當前活躍會話
|
||||||
|
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
|
366
tests/test_session_cleanup.py
Normal file
366
tests/test_session_cleanup.py
Normal file
@ -0,0 +1,366 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
會話清理優化測試
|
||||||
|
================
|
||||||
|
|
||||||
|
測試 WebFeedbackSession 和 SessionCleanupManager 的清理功能。
|
||||||
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import pytest
|
||||||
|
import time
|
||||||
|
import threading
|
||||||
|
from unittest.mock import Mock, patch, MagicMock
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
|
||||||
|
|
||||||
|
from src.mcp_feedback_enhanced.web.models.feedback_session import (
|
||||||
|
WebFeedbackSession, SessionStatus, CleanupReason
|
||||||
|
)
|
||||||
|
from src.mcp_feedback_enhanced.web.utils.session_cleanup_manager import (
|
||||||
|
SessionCleanupManager, CleanupPolicy, CleanupTrigger
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestWebFeedbackSessionCleanup:
|
||||||
|
"""測試 WebFeedbackSession 清理功能"""
|
||||||
|
|
||||||
|
def setup_method(self):
|
||||||
|
"""測試前設置"""
|
||||||
|
self.session_id = "test_session_001"
|
||||||
|
self.project_dir = "/tmp/test_project"
|
||||||
|
self.summary = "測試會話摘要"
|
||||||
|
|
||||||
|
# 創建測試會話
|
||||||
|
self.session = WebFeedbackSession(
|
||||||
|
self.session_id,
|
||||||
|
self.project_dir,
|
||||||
|
self.summary,
|
||||||
|
auto_cleanup_delay=60, # 1分鐘自動清理
|
||||||
|
max_idle_time=30 # 30秒最大空閒時間
|
||||||
|
)
|
||||||
|
|
||||||
|
def teardown_method(self):
|
||||||
|
"""測試後清理"""
|
||||||
|
if hasattr(self, 'session') and self.session:
|
||||||
|
try:
|
||||||
|
self.session._cleanup_sync_enhanced(CleanupReason.MANUAL)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def test_session_initialization(self):
|
||||||
|
"""測試會話初始化"""
|
||||||
|
assert self.session.session_id == self.session_id
|
||||||
|
assert self.session.project_directory == self.project_dir
|
||||||
|
assert self.session.summary == self.summary
|
||||||
|
assert self.session.status == SessionStatus.WAITING
|
||||||
|
assert self.session.auto_cleanup_delay == 60
|
||||||
|
assert self.session.max_idle_time == 30
|
||||||
|
assert self.session.cleanup_timer is not None
|
||||||
|
assert len(self.session.cleanup_stats) > 0
|
||||||
|
|
||||||
|
def test_is_expired_by_idle_time(self):
|
||||||
|
"""測試空閒時間過期檢測"""
|
||||||
|
# 新創建的會話不應該過期
|
||||||
|
assert not self.session.is_expired()
|
||||||
|
|
||||||
|
# 模擬空閒時間過長
|
||||||
|
self.session.last_activity = time.time() - 40 # 40秒前
|
||||||
|
assert self.session.is_expired()
|
||||||
|
|
||||||
|
def test_is_expired_by_status(self):
|
||||||
|
"""測試狀態過期檢測"""
|
||||||
|
# 設置為錯誤狀態
|
||||||
|
self.session.status = SessionStatus.ERROR
|
||||||
|
self.session.last_activity = time.time() - 400 # 400秒前
|
||||||
|
assert self.session.is_expired()
|
||||||
|
|
||||||
|
# 設置為已過期狀態
|
||||||
|
self.session.status = SessionStatus.EXPIRED
|
||||||
|
assert self.session.is_expired()
|
||||||
|
|
||||||
|
def test_get_age_and_idle_time(self):
|
||||||
|
"""測試年齡和空閒時間計算"""
|
||||||
|
# 測試年齡
|
||||||
|
age = self.session.get_age()
|
||||||
|
assert age >= 0
|
||||||
|
assert age < 1 # 剛創建,應該小於1秒
|
||||||
|
|
||||||
|
# 測試空閒時間
|
||||||
|
idle_time = self.session.get_idle_time()
|
||||||
|
assert idle_time >= 0
|
||||||
|
assert idle_time < 1 # 剛創建,應該小於1秒
|
||||||
|
|
||||||
|
def test_cleanup_timer_scheduling(self):
|
||||||
|
"""測試清理定時器調度"""
|
||||||
|
# 檢查定時器是否已設置
|
||||||
|
assert self.session.cleanup_timer is not None
|
||||||
|
assert self.session.cleanup_timer.is_alive()
|
||||||
|
|
||||||
|
# 測試延長定時器
|
||||||
|
old_timer = self.session.cleanup_timer
|
||||||
|
self.session.extend_cleanup_timer(120)
|
||||||
|
|
||||||
|
# 應該創建新的定時器
|
||||||
|
assert self.session.cleanup_timer != old_timer
|
||||||
|
assert self.session.cleanup_timer.is_alive()
|
||||||
|
|
||||||
|
def test_cleanup_callbacks(self):
|
||||||
|
"""測試清理回調函數"""
|
||||||
|
callback_called = False
|
||||||
|
callback_session = None
|
||||||
|
callback_reason = None
|
||||||
|
|
||||||
|
def test_callback(session, reason):
|
||||||
|
nonlocal callback_called, callback_session, callback_reason
|
||||||
|
callback_called = True
|
||||||
|
callback_session = session
|
||||||
|
callback_reason = reason
|
||||||
|
|
||||||
|
# 添加回調
|
||||||
|
self.session.add_cleanup_callback(test_callback)
|
||||||
|
assert len(self.session.cleanup_callbacks) == 1
|
||||||
|
|
||||||
|
# 執行清理
|
||||||
|
self.session._cleanup_sync_enhanced(CleanupReason.MANUAL)
|
||||||
|
|
||||||
|
# 檢查回調是否被調用
|
||||||
|
assert callback_called
|
||||||
|
assert callback_session == self.session
|
||||||
|
assert callback_reason == CleanupReason.MANUAL
|
||||||
|
|
||||||
|
# 移除回調
|
||||||
|
self.session.remove_cleanup_callback(test_callback)
|
||||||
|
assert len(self.session.cleanup_callbacks) == 0
|
||||||
|
|
||||||
|
def test_cleanup_stats(self):
|
||||||
|
"""測試清理統計"""
|
||||||
|
# 初始統計
|
||||||
|
stats = self.session.get_cleanup_stats()
|
||||||
|
assert stats["cleanup_count"] == 0
|
||||||
|
assert stats["session_id"] == self.session_id
|
||||||
|
assert stats["is_active"] == True
|
||||||
|
|
||||||
|
# 執行清理
|
||||||
|
self.session._cleanup_sync_enhanced(CleanupReason.EXPIRED)
|
||||||
|
|
||||||
|
# 檢查統計更新
|
||||||
|
stats = self.session.get_cleanup_stats()
|
||||||
|
assert stats["cleanup_count"] == 1
|
||||||
|
assert stats["cleanup_reason"] == CleanupReason.EXPIRED.value
|
||||||
|
assert stats["last_cleanup_time"] is not None
|
||||||
|
assert stats["cleanup_duration"] >= 0
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_async_cleanup(self):
|
||||||
|
"""測試異步清理"""
|
||||||
|
# 模擬 WebSocket 連接
|
||||||
|
mock_websocket = Mock()
|
||||||
|
mock_websocket.send_json = Mock(return_value=asyncio.Future())
|
||||||
|
mock_websocket.send_json.return_value.set_result(None)
|
||||||
|
mock_websocket.close = Mock(return_value=asyncio.Future())
|
||||||
|
mock_websocket.close.return_value.set_result(None)
|
||||||
|
mock_websocket.client_state.DISCONNECTED = False
|
||||||
|
|
||||||
|
self.session.websocket = mock_websocket
|
||||||
|
|
||||||
|
# 執行異步清理
|
||||||
|
await self.session._cleanup_resources_enhanced(CleanupReason.TIMEOUT)
|
||||||
|
|
||||||
|
# 檢查 WebSocket 是否被正確處理
|
||||||
|
mock_websocket.send_json.assert_called_once()
|
||||||
|
|
||||||
|
# 檢查清理統計
|
||||||
|
stats = self.session.get_cleanup_stats()
|
||||||
|
assert stats["cleanup_count"] == 1
|
||||||
|
assert stats["cleanup_reason"] == CleanupReason.TIMEOUT.value
|
||||||
|
|
||||||
|
def test_status_update_resets_timer(self):
|
||||||
|
"""測試狀態更新重置定時器"""
|
||||||
|
old_timer = self.session.cleanup_timer
|
||||||
|
|
||||||
|
# 更新狀態為活躍
|
||||||
|
self.session.update_status(SessionStatus.ACTIVE, "測試活躍狀態")
|
||||||
|
|
||||||
|
# 檢查定時器是否被重置
|
||||||
|
assert self.session.cleanup_timer != old_timer
|
||||||
|
assert self.session.cleanup_timer.is_alive()
|
||||||
|
assert self.session.status == SessionStatus.ACTIVE
|
||||||
|
|
||||||
|
|
||||||
|
class TestSessionCleanupManager:
|
||||||
|
"""測試 SessionCleanupManager 功能"""
|
||||||
|
|
||||||
|
def setup_method(self):
|
||||||
|
"""測試前設置"""
|
||||||
|
# 創建模擬的 WebUIManager
|
||||||
|
self.mock_web_ui_manager = Mock()
|
||||||
|
self.mock_web_ui_manager.sessions = {}
|
||||||
|
self.mock_web_ui_manager.current_session = None
|
||||||
|
self.mock_web_ui_manager.cleanup_expired_sessions = Mock(return_value=0)
|
||||||
|
self.mock_web_ui_manager.cleanup_sessions_by_memory_pressure = Mock(return_value=0)
|
||||||
|
|
||||||
|
# 創建清理策略
|
||||||
|
self.policy = CleanupPolicy(
|
||||||
|
max_idle_time=30,
|
||||||
|
max_session_age=300,
|
||||||
|
max_sessions=5,
|
||||||
|
cleanup_interval=10,
|
||||||
|
enable_auto_cleanup=True
|
||||||
|
)
|
||||||
|
|
||||||
|
# 創建清理管理器
|
||||||
|
self.cleanup_manager = SessionCleanupManager(
|
||||||
|
self.mock_web_ui_manager,
|
||||||
|
self.policy
|
||||||
|
)
|
||||||
|
|
||||||
|
def teardown_method(self):
|
||||||
|
"""測試後清理"""
|
||||||
|
if hasattr(self, 'cleanup_manager'):
|
||||||
|
self.cleanup_manager.stop_auto_cleanup()
|
||||||
|
|
||||||
|
def test_cleanup_manager_initialization(self):
|
||||||
|
"""測試清理管理器初始化"""
|
||||||
|
assert self.cleanup_manager.web_ui_manager == self.mock_web_ui_manager
|
||||||
|
assert self.cleanup_manager.policy == self.policy
|
||||||
|
assert not self.cleanup_manager.is_running
|
||||||
|
assert self.cleanup_manager.cleanup_thread is None
|
||||||
|
assert len(self.cleanup_manager.cleanup_callbacks) == 0
|
||||||
|
assert len(self.cleanup_manager.cleanup_history) == 0
|
||||||
|
|
||||||
|
def test_auto_cleanup_start_stop(self):
|
||||||
|
"""測試自動清理啟動和停止"""
|
||||||
|
# 啟動自動清理
|
||||||
|
result = self.cleanup_manager.start_auto_cleanup()
|
||||||
|
assert result == True
|
||||||
|
assert self.cleanup_manager.is_running == True
|
||||||
|
assert self.cleanup_manager.cleanup_thread is not None
|
||||||
|
assert self.cleanup_manager.cleanup_thread.is_alive()
|
||||||
|
|
||||||
|
# 停止自動清理
|
||||||
|
result = self.cleanup_manager.stop_auto_cleanup()
|
||||||
|
assert result == True
|
||||||
|
assert self.cleanup_manager.is_running == False
|
||||||
|
|
||||||
|
def test_trigger_cleanup_memory_pressure(self):
|
||||||
|
"""測試內存壓力清理觸發"""
|
||||||
|
# 設置模擬返回值
|
||||||
|
self.mock_web_ui_manager.cleanup_sessions_by_memory_pressure.return_value = 3
|
||||||
|
|
||||||
|
# 觸發內存壓力清理
|
||||||
|
cleaned = self.cleanup_manager.trigger_cleanup(CleanupTrigger.MEMORY_PRESSURE, force=True)
|
||||||
|
|
||||||
|
# 檢查結果
|
||||||
|
assert cleaned == 3
|
||||||
|
self.mock_web_ui_manager.cleanup_sessions_by_memory_pressure.assert_called_once_with(True)
|
||||||
|
|
||||||
|
# 檢查統計更新
|
||||||
|
stats = self.cleanup_manager.get_cleanup_statistics()
|
||||||
|
assert stats["total_cleanups"] == 1
|
||||||
|
assert stats["memory_pressure_cleanups"] == 1
|
||||||
|
assert stats["total_sessions_cleaned"] == 3
|
||||||
|
|
||||||
|
def test_trigger_cleanup_expired(self):
|
||||||
|
"""測試過期清理觸發"""
|
||||||
|
# 設置模擬返回值
|
||||||
|
self.mock_web_ui_manager.cleanup_expired_sessions.return_value = 2
|
||||||
|
|
||||||
|
# 觸發過期清理
|
||||||
|
cleaned = self.cleanup_manager.trigger_cleanup(CleanupTrigger.EXPIRED)
|
||||||
|
|
||||||
|
# 檢查結果
|
||||||
|
assert cleaned == 2
|
||||||
|
self.mock_web_ui_manager.cleanup_expired_sessions.assert_called_once()
|
||||||
|
|
||||||
|
# 檢查統計更新
|
||||||
|
stats = self.cleanup_manager.get_cleanup_statistics()
|
||||||
|
assert stats["total_cleanups"] == 1
|
||||||
|
assert stats["expired_cleanups"] == 1
|
||||||
|
assert stats["total_sessions_cleaned"] == 2
|
||||||
|
|
||||||
|
def test_cleanup_statistics(self):
|
||||||
|
"""測試清理統計功能"""
|
||||||
|
# 初始統計
|
||||||
|
stats = self.cleanup_manager.get_cleanup_statistics()
|
||||||
|
assert stats["total_cleanups"] == 0
|
||||||
|
assert stats["total_sessions_cleaned"] == 0
|
||||||
|
assert stats["is_auto_cleanup_running"] == False
|
||||||
|
|
||||||
|
# 執行一些清理操作
|
||||||
|
self.mock_web_ui_manager.cleanup_expired_sessions.return_value = 1
|
||||||
|
self.cleanup_manager.trigger_cleanup(CleanupTrigger.EXPIRED)
|
||||||
|
|
||||||
|
self.mock_web_ui_manager.cleanup_sessions_by_memory_pressure.return_value = 2
|
||||||
|
self.cleanup_manager.trigger_cleanup(CleanupTrigger.MEMORY_PRESSURE)
|
||||||
|
|
||||||
|
# 檢查統計
|
||||||
|
stats = self.cleanup_manager.get_cleanup_statistics()
|
||||||
|
assert stats["total_cleanups"] == 2
|
||||||
|
assert stats["expired_cleanups"] == 1
|
||||||
|
assert stats["memory_pressure_cleanups"] == 1
|
||||||
|
assert stats["total_sessions_cleaned"] == 3
|
||||||
|
assert stats["average_cleanup_time"] >= 0
|
||||||
|
|
||||||
|
def test_cleanup_history(self):
|
||||||
|
"""測試清理歷史記錄"""
|
||||||
|
# 初始歷史為空
|
||||||
|
history = self.cleanup_manager.get_cleanup_history()
|
||||||
|
assert len(history) == 0
|
||||||
|
|
||||||
|
# 執行清理操作
|
||||||
|
self.mock_web_ui_manager.cleanup_expired_sessions.return_value = 1
|
||||||
|
self.cleanup_manager.trigger_cleanup(CleanupTrigger.EXPIRED)
|
||||||
|
|
||||||
|
# 檢查歷史記錄
|
||||||
|
history = self.cleanup_manager.get_cleanup_history()
|
||||||
|
assert len(history) == 1
|
||||||
|
|
||||||
|
record = history[0]
|
||||||
|
assert record["trigger"] == CleanupTrigger.EXPIRED.value
|
||||||
|
assert record["cleaned_count"] == 1
|
||||||
|
assert "timestamp" in record
|
||||||
|
assert "duration" in record
|
||||||
|
|
||||||
|
def test_policy_update(self):
|
||||||
|
"""測試策略更新"""
|
||||||
|
# 更新策略
|
||||||
|
self.cleanup_manager.update_policy(
|
||||||
|
max_idle_time=60,
|
||||||
|
max_sessions=10,
|
||||||
|
enable_auto_cleanup=False
|
||||||
|
)
|
||||||
|
|
||||||
|
# 檢查策略是否更新
|
||||||
|
assert self.cleanup_manager.policy.max_idle_time == 60
|
||||||
|
assert self.cleanup_manager.policy.max_sessions == 10
|
||||||
|
assert self.cleanup_manager.policy.enable_auto_cleanup == False
|
||||||
|
|
||||||
|
def test_stats_reset(self):
|
||||||
|
"""測試統計重置"""
|
||||||
|
# 執行一些操作產生統計
|
||||||
|
self.mock_web_ui_manager.cleanup_expired_sessions.return_value = 1
|
||||||
|
self.cleanup_manager.trigger_cleanup(CleanupTrigger.EXPIRED)
|
||||||
|
|
||||||
|
# 檢查有統計數據
|
||||||
|
stats = self.cleanup_manager.get_cleanup_statistics()
|
||||||
|
assert stats["total_cleanups"] > 0
|
||||||
|
|
||||||
|
# 重置統計
|
||||||
|
self.cleanup_manager.reset_stats()
|
||||||
|
|
||||||
|
# 檢查統計已重置
|
||||||
|
stats = self.cleanup_manager.get_cleanup_statistics()
|
||||||
|
assert stats["total_cleanups"] == 0
|
||||||
|
assert stats["total_sessions_cleaned"] == 0
|
||||||
|
|
||||||
|
history = self.cleanup_manager.get_cleanup_history()
|
||||||
|
assert len(history) == 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
pytest.main([__file__, "-v"])
|
Loading…
x
Reference in New Issue
Block a user