lmcache--lmcache
585 行
18 KiB
Python
585 行
18 KiB
Python
# SPDX-License-Identifier: Apache-2.0
|
|
"""
|
|
Unified PeriodicThread abstraction for LMCache background threads.
|
|
|
|
This module provides a standardized way to create and manage periodic
|
|
background threads with proper naming, monitoring, and lifecycle management.
|
|
"""
|
|
|
|
# Standard
|
|
from abc import ABC, abstractmethod
|
|
from dataclasses import dataclass, field
|
|
from enum import Enum
|
|
from typing import TYPE_CHECKING, Callable, Dict, List, Optional
|
|
import threading
|
|
import time
|
|
|
|
# First Party
|
|
from lmcache.logging import init_logger
|
|
from lmcache.v1.exceptions import IrrecoverableException
|
|
|
|
if TYPE_CHECKING:
|
|
# First Party
|
|
pass
|
|
|
|
logger = init_logger(__name__)
|
|
|
|
|
|
class ThreadLevel(Enum):
|
|
"""
|
|
Thread importance level.
|
|
|
|
CRITICAL: Thread failure causes severe system degradation or data loss.
|
|
Examples: health-monitor-thread, PinMonitor-thread
|
|
|
|
HIGH: Thread failure significantly impacts performance or functionality.
|
|
Examples: storage-manager-event-loop, lookup-server threads
|
|
|
|
MEDIUM: Thread failure causes noticeable degradation but system remains functional.
|
|
Examples: stats-logger-thread, batched-message-sender-thread
|
|
|
|
LOW: Thread failure has minimal impact on system operation.
|
|
Examples: lazy-memory-expand-thread
|
|
"""
|
|
|
|
CRITICAL = "critical"
|
|
HIGH = "high"
|
|
MEDIUM = "medium"
|
|
LOW = "low"
|
|
|
|
|
|
@dataclass
|
|
class ThreadRunSummary:
|
|
"""Summary of a single thread execution cycle."""
|
|
|
|
timestamp: float = 0.0
|
|
duration_ms: float = 0.0
|
|
success: bool = True
|
|
message: str = ""
|
|
extra_info: Dict[str, str] = field(default_factory=dict)
|
|
|
|
|
|
class PeriodicThread(ABC):
|
|
"""
|
|
Abstract base class for periodic background threads.
|
|
|
|
This class provides a standardized framework for creating periodic
|
|
background threads with proper naming, monitoring, and lifecycle management.
|
|
|
|
Attributes:
|
|
name: Human-readable name of the thread
|
|
interval: Time interval between executions in seconds
|
|
level: Importance level of the thread
|
|
init_wait: Initial wait time before first execution in seconds
|
|
|
|
Features:
|
|
- Named threads for easy identification
|
|
- Configurable execution interval
|
|
- Initial wait time before first execution
|
|
- Automatic tracking of last run time and summary
|
|
- Thread level classification for monitoring
|
|
- Interruptible sleep for graceful shutdown
|
|
|
|
Usage:
|
|
class MyPeriodicThread(PeriodicThread):
|
|
def __init__(self):
|
|
super().__init__(
|
|
name="my-periodic-thread",
|
|
interval=30.0,
|
|
level=ThreadLevel.MEDIUM,
|
|
init_wait=5.0,
|
|
)
|
|
|
|
def _execute(self) -> ThreadRunSummary:
|
|
# Perform periodic work
|
|
return ThreadRunSummary(
|
|
timestamp=time.time(),
|
|
success=True,
|
|
message="Completed successfully"
|
|
)
|
|
|
|
thread = MyPeriodicThread()
|
|
thread.start()
|
|
# ... later ...
|
|
thread.stop()
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
name: str,
|
|
interval: float,
|
|
level: ThreadLevel = ThreadLevel.MEDIUM,
|
|
init_wait: float = 0.0,
|
|
):
|
|
"""
|
|
Initialize a PeriodicThread.
|
|
|
|
Args:
|
|
name: Thread name for identification
|
|
interval: Execution interval in seconds
|
|
level: Thread importance level
|
|
init_wait: Initial wait time before first execution in seconds
|
|
"""
|
|
self._name = name
|
|
self._interval = interval
|
|
self._level = level
|
|
self._init_wait = init_wait
|
|
|
|
# Thread state
|
|
self._thread: Optional[threading.Thread] = None
|
|
self._stop_event = threading.Event()
|
|
self._running = False
|
|
|
|
# Execution tracking
|
|
self._last_run_time: float = 0.0
|
|
self._last_summary: Optional[ThreadRunSummary] = None
|
|
self._total_runs: int = 0
|
|
self._failed_runs: int = 0
|
|
self._lock = threading.RLock()
|
|
|
|
# Start time (set when thread starts)
|
|
self._start_time: float = 0.0
|
|
|
|
@property
|
|
def name(self) -> str:
|
|
"""Get the thread name."""
|
|
return self._name
|
|
|
|
@property
|
|
def interval(self) -> float:
|
|
"""Get the execution interval in seconds."""
|
|
return self._interval
|
|
|
|
@property
|
|
def level(self) -> ThreadLevel:
|
|
"""Get the thread importance level."""
|
|
return self._level
|
|
|
|
@property
|
|
def init_wait(self) -> float:
|
|
"""Get the initial wait time in seconds."""
|
|
return self._init_wait
|
|
|
|
@property
|
|
def last_run_time(self) -> float:
|
|
"""Get the timestamp of the last execution."""
|
|
with self._lock:
|
|
return self._last_run_time
|
|
|
|
@property
|
|
def last_summary(self) -> Optional[ThreadRunSummary]:
|
|
"""Get the summary of the last execution."""
|
|
with self._lock:
|
|
return self._last_summary
|
|
|
|
@property
|
|
def total_runs(self) -> int:
|
|
"""Get the total number of executions."""
|
|
with self._lock:
|
|
return self._total_runs
|
|
|
|
@property
|
|
def failed_runs(self) -> int:
|
|
"""Get the number of failed executions."""
|
|
with self._lock:
|
|
return self._failed_runs
|
|
|
|
@property
|
|
def is_running(self) -> bool:
|
|
"""Check if the thread is currently running."""
|
|
return self._running and self._thread is not None and self._thread.is_alive()
|
|
|
|
@property
|
|
def stop_requested(self) -> bool:
|
|
"""Whether a stop has been requested (set by stop, reset by start)."""
|
|
return self._stop_event.is_set()
|
|
|
|
@property
|
|
def is_active(self) -> bool:
|
|
"""
|
|
Check if the thread is active (running and recently executed).
|
|
|
|
A thread is considered active if:
|
|
1. It is running
|
|
2. The time since last run is less than 3 * interval
|
|
|
|
Returns:
|
|
bool: True if active, False otherwise
|
|
"""
|
|
if not self.is_running:
|
|
return False
|
|
|
|
with self._lock:
|
|
if self._last_run_time == 0:
|
|
# Thread started but hasn't run yet
|
|
# Consider active if within init_wait + 3 * interval from start
|
|
time_since_start = time.time() - self._start_time
|
|
return time_since_start < self._init_wait + 3 * self._interval
|
|
|
|
time_since_last_run = time.time() - self._last_run_time
|
|
return time_since_last_run < 3 * self._interval
|
|
|
|
def start(self) -> Optional[threading.Thread]:
|
|
"""
|
|
Start the periodic thread.
|
|
|
|
Returns:
|
|
Optional[threading.Thread]: The started thread, or None if already running
|
|
"""
|
|
if self._running:
|
|
logger.warning("PeriodicThread %s is already running", self._name)
|
|
return None
|
|
|
|
self._stop_event.clear()
|
|
self._running = True
|
|
self._start_time = time.time()
|
|
|
|
self._thread = threading.Thread(
|
|
target=self._run_loop,
|
|
daemon=True,
|
|
name=self._name,
|
|
)
|
|
self._thread.start()
|
|
|
|
logger.info(
|
|
"Started PeriodicThread: %s (level=%s, interval=%.1fs, init_wait=%.1fs)",
|
|
self._name,
|
|
self._level.value,
|
|
self._interval,
|
|
self._init_wait,
|
|
)
|
|
return self._thread
|
|
|
|
def stop(self, timeout: float = 5.0) -> None:
|
|
"""
|
|
Stop the periodic thread.
|
|
|
|
Args:
|
|
timeout: Maximum time to wait for thread termination in seconds
|
|
"""
|
|
if not self._running:
|
|
return
|
|
|
|
logger.info("Stopping PeriodicThread: %s", self._name)
|
|
self._running = False
|
|
self._stop_event.set()
|
|
|
|
if self._thread is not None and self._thread.is_alive():
|
|
self._thread.join(timeout=timeout)
|
|
if self._thread.is_alive():
|
|
logger.warning(
|
|
"PeriodicThread %s did not terminate within %.1fs timeout",
|
|
self._name,
|
|
timeout,
|
|
)
|
|
|
|
def _run_loop(self) -> None:
|
|
"""Main thread loop."""
|
|
# Initial wait
|
|
if self._init_wait > 0:
|
|
if self._stop_event.wait(timeout=self._init_wait):
|
|
logger.info("PeriodicThread %s stopped during init_wait", self._name)
|
|
return
|
|
|
|
logger.info(
|
|
"PeriodicThread %s entering main loop (interval=%.1fs)",
|
|
self._name,
|
|
self._interval,
|
|
)
|
|
|
|
while not self._stop_event.is_set():
|
|
start_time = time.time()
|
|
|
|
try:
|
|
summary = self._execute()
|
|
summary.timestamp = start_time
|
|
summary.duration_ms = (time.time() - start_time) * 1000
|
|
|
|
with self._lock:
|
|
self._last_run_time = start_time
|
|
self._last_summary = summary
|
|
self._total_runs += 1
|
|
if not summary.success:
|
|
self._failed_runs += 1
|
|
|
|
except IrrecoverableException as e:
|
|
logger.error(
|
|
"IrrecoverableException in PeriodicThread %s: %s",
|
|
self._name,
|
|
e,
|
|
exc_info=True,
|
|
)
|
|
summary = ThreadRunSummary(
|
|
timestamp=start_time,
|
|
duration_ms=(time.time() - start_time) * 1000,
|
|
success=False,
|
|
message=str(e),
|
|
)
|
|
with self._lock:
|
|
self._last_run_time = start_time
|
|
self._last_summary = summary
|
|
self._total_runs += 1
|
|
self._failed_runs += 1
|
|
# Stop the loop on irrecoverable exceptions
|
|
logger.info(
|
|
"PeriodicThread %s stopping due to IrrecoverableException",
|
|
self._name,
|
|
)
|
|
break
|
|
except Exception as e:
|
|
logger.error(
|
|
"Error in PeriodicThread %s: %s", self._name, e, exc_info=True
|
|
)
|
|
summary = ThreadRunSummary(
|
|
timestamp=start_time,
|
|
duration_ms=(time.time() - start_time) * 1000,
|
|
success=False,
|
|
message=str(e),
|
|
)
|
|
with self._lock:
|
|
self._last_run_time = start_time
|
|
self._last_summary = summary
|
|
self._total_runs += 1
|
|
self._failed_runs += 1
|
|
|
|
# Wait for next interval
|
|
if self._stop_event.wait(timeout=self._interval):
|
|
break
|
|
|
|
logger.info("PeriodicThread %s loop stopped", self._name)
|
|
|
|
@abstractmethod
|
|
def _execute(self) -> ThreadRunSummary:
|
|
"""
|
|
Execute one cycle of the periodic task.
|
|
|
|
This method should be overridden by subclasses to implement
|
|
the actual periodic work.
|
|
|
|
Returns:
|
|
ThreadRunSummary: Summary of this execution cycle
|
|
"""
|
|
pass
|
|
|
|
def get_status(self) -> Dict:
|
|
"""
|
|
Get the current status of the thread.
|
|
|
|
Returns:
|
|
Dict: Thread status information
|
|
"""
|
|
with self._lock:
|
|
last_summary_dict = None
|
|
if self._last_summary:
|
|
last_summary_dict = {
|
|
"timestamp": self._last_summary.timestamp,
|
|
"duration_ms": self._last_summary.duration_ms,
|
|
"success": self._last_summary.success,
|
|
"message": self._last_summary.message,
|
|
"extra_info": self._last_summary.extra_info,
|
|
}
|
|
|
|
return {
|
|
"name": self._name,
|
|
"level": self._level.value,
|
|
"interval": self._interval,
|
|
"init_wait": self._init_wait,
|
|
"is_running": self.is_running,
|
|
"is_active": self.is_active,
|
|
"last_run_time": self._last_run_time,
|
|
"last_run_ago": time.time() - self._last_run_time
|
|
if self._last_run_time > 0
|
|
else None,
|
|
"total_runs": self._total_runs,
|
|
"failed_runs": self._failed_runs,
|
|
"success_rate": (
|
|
(self._total_runs - self._failed_runs) / self._total_runs * 100
|
|
if self._total_runs > 0
|
|
else None
|
|
),
|
|
"last_summary": last_summary_dict,
|
|
}
|
|
|
|
|
|
class PeriodicThreadRegistry:
|
|
"""
|
|
Global registry for all PeriodicThread instances.
|
|
|
|
This class provides a centralized way to track and manage all
|
|
periodic threads in the system.
|
|
"""
|
|
|
|
_instance: Optional["PeriodicThreadRegistry"] = None
|
|
_lock = threading.Lock()
|
|
|
|
def __init__(self):
|
|
self._threads: Dict[str, PeriodicThread] = {}
|
|
self._registry_lock = threading.RLock()
|
|
|
|
@classmethod
|
|
def get_instance(cls) -> "PeriodicThreadRegistry":
|
|
"""Get or create the singleton instance."""
|
|
if cls._instance is None:
|
|
with cls._lock:
|
|
if cls._instance is None:
|
|
cls._instance = PeriodicThreadRegistry()
|
|
return cls._instance
|
|
|
|
@classmethod
|
|
def reset(cls) -> None:
|
|
"""Reset the singleton instance. Mainly for testing."""
|
|
with cls._lock:
|
|
if cls._instance is not None:
|
|
cls._instance.unregister_all()
|
|
cls._instance = None
|
|
|
|
def register(self, thread: PeriodicThread) -> None:
|
|
"""
|
|
Register a periodic thread.
|
|
|
|
Args:
|
|
thread: The PeriodicThread to register
|
|
"""
|
|
with self._registry_lock:
|
|
if thread.name in self._threads:
|
|
logger.warning(
|
|
"PeriodicThread %s is already registered, replacing",
|
|
thread.name,
|
|
)
|
|
self._threads[thread.name] = thread
|
|
logger.debug("Registered PeriodicThread: %s", thread.name)
|
|
|
|
def unregister(self, name: str) -> Optional[PeriodicThread]:
|
|
"""
|
|
Unregister a periodic thread by name.
|
|
|
|
Args:
|
|
name: The name of the thread to unregister
|
|
|
|
Returns:
|
|
The unregistered thread, or None if not found
|
|
"""
|
|
with self._registry_lock:
|
|
thread = self._threads.pop(name, None)
|
|
if thread:
|
|
logger.debug("Unregistered PeriodicThread: %s", name)
|
|
return thread
|
|
|
|
def unregister_all(self) -> None:
|
|
"""Unregister all periodic threads."""
|
|
with self._registry_lock:
|
|
self._threads.clear()
|
|
logger.debug("Unregistered all PeriodicThreads")
|
|
|
|
def get(self, name: str) -> Optional[PeriodicThread]:
|
|
"""Get a registered thread by name."""
|
|
with self._registry_lock:
|
|
return self._threads.get(name)
|
|
|
|
def get_all(self) -> List[PeriodicThread]:
|
|
"""Get all registered threads."""
|
|
with self._registry_lock:
|
|
return list(self._threads.values())
|
|
|
|
def get_by_level(self, level: ThreadLevel) -> List[PeriodicThread]:
|
|
"""Get all threads with the specified level."""
|
|
with self._registry_lock:
|
|
return [t for t in self._threads.values() if t.level == level]
|
|
|
|
def get_running_count(self) -> int:
|
|
"""Get the count of running threads."""
|
|
with self._registry_lock:
|
|
return sum(1 for t in self._threads.values() if t.is_running)
|
|
|
|
def get_active_count(self) -> int:
|
|
"""Get the count of active threads."""
|
|
with self._registry_lock:
|
|
return sum(1 for t in self._threads.values() if t.is_active)
|
|
|
|
def get_count_by_level(self, level: ThreadLevel) -> Dict[str, int]:
|
|
"""
|
|
Get counts of threads for a specific level.
|
|
|
|
Returns:
|
|
Dict with keys: total, running, active
|
|
"""
|
|
with self._registry_lock:
|
|
threads = [t for t in self._threads.values() if t.level == level]
|
|
return {
|
|
"total": len(threads),
|
|
"running": sum(1 for t in threads if t.is_running),
|
|
"active": sum(1 for t in threads if t.is_active),
|
|
}
|
|
|
|
def get_summary(self) -> Dict:
|
|
"""
|
|
Get a summary of all registered threads.
|
|
|
|
Returns:
|
|
Dict containing:
|
|
- total_count: Total number of registered threads
|
|
- running_count: Number of running threads
|
|
- active_count: Number of active threads
|
|
- by_level: Counts by thread level
|
|
- threads: List of thread statuses
|
|
"""
|
|
with self._registry_lock:
|
|
by_level = {}
|
|
for level in ThreadLevel:
|
|
by_level[level.value] = self.get_count_by_level(level)
|
|
|
|
return {
|
|
"total_count": len(self._threads),
|
|
"running_count": self.get_running_count(),
|
|
"active_count": self.get_active_count(),
|
|
"by_level": by_level,
|
|
"threads": [t.get_status() for t in self._threads.values()],
|
|
}
|
|
|
|
|
|
def create_periodic_thread(
|
|
name: str,
|
|
interval: float,
|
|
execute_fn: Callable[[], ThreadRunSummary],
|
|
level: ThreadLevel = ThreadLevel.MEDIUM,
|
|
init_wait: float = 0.0,
|
|
auto_register: bool = True,
|
|
) -> PeriodicThread:
|
|
"""
|
|
Factory function to create a simple PeriodicThread.
|
|
|
|
This is a convenience function for creating periodic threads
|
|
without needing to define a subclass.
|
|
|
|
Args:
|
|
name: Thread name
|
|
interval: Execution interval in seconds
|
|
execute_fn: Function to execute on each cycle
|
|
level: Thread importance level
|
|
init_wait: Initial wait time before first execution
|
|
auto_register: Whether to automatically register with the global registry
|
|
|
|
Returns:
|
|
PeriodicThread: The created thread instance
|
|
"""
|
|
|
|
class SimplePeriodicThread(PeriodicThread):
|
|
def __init__(self):
|
|
super().__init__(
|
|
name=name,
|
|
interval=interval,
|
|
level=level,
|
|
init_wait=init_wait,
|
|
)
|
|
self._execute_fn = execute_fn
|
|
|
|
def _execute(self) -> ThreadRunSummary:
|
|
return self._execute_fn()
|
|
|
|
thread = SimplePeriodicThread()
|
|
|
|
if auto_register:
|
|
PeriodicThreadRegistry.get_instance().register(thread)
|
|
|
|
return thread
|