This commit is contained in:
pacnpal
2024-11-16 22:32:08 +00:00
parent b7d99490cf
commit dac21f2fcd
30 changed files with 5854 additions and 2279 deletions

View File

@@ -2,33 +2,61 @@
import logging
import asyncio
from typing import Dict, Any, Optional, Set, List
from enum import Enum
from typing import Dict, Any, Optional, Set, List, TypedDict, ClassVar, Type, Union, Protocol
from enum import Enum, auto
from datetime import datetime
from pathlib import Path
from ..utils.exceptions import (
ComponentError,
ErrorContext,
ErrorSeverity
)
from ..utils.path_manager import ensure_directory
logger = logging.getLogger("VideoArchiver")
class ComponentState(Enum):
"""Possible states of a component"""
UNREGISTERED = "unregistered"
REGISTERED = "registered"
INITIALIZING = "initializing"
READY = "ready"
ERROR = "error"
SHUTDOWN = "shutdown"
UNREGISTERED = auto()
REGISTERED = auto()
INITIALIZING = auto()
READY = auto()
ERROR = auto()
SHUTDOWN = auto()
class ComponentDependencyError(Exception):
"""Raised when component dependencies cannot be satisfied"""
pass
class ComponentHistory(TypedDict):
"""Type definition for component history entry"""
component: str
state: str
timestamp: str
error: Optional[str]
duration: float
class ComponentLifecycleError(Exception):
"""Raised when component lifecycle operations fail"""
pass
class ComponentStatus(TypedDict):
"""Type definition for component status"""
state: str
registration_time: Optional[str]
initialization_time: Optional[str]
dependencies: Set[str]
dependents: Set[str]
error: Optional[str]
health: bool
class Initializable(Protocol):
"""Protocol for initializable components"""
async def initialize(self) -> None:
"""Initialize the component"""
...
async def shutdown(self) -> None:
"""Shutdown the component"""
...
class Component:
"""Base class for managed components"""
def __init__(self, name: str):
def __init__(self, name: str) -> None:
self.name = name
self.state = ComponentState.UNREGISTERED
self.dependencies: Set[str] = set()
@@ -36,33 +64,74 @@ class Component:
self.registration_time: Optional[datetime] = None
self.initialization_time: Optional[datetime] = None
self.error: Optional[str] = None
self._health_check_task: Optional[asyncio.Task] = None
async def initialize(self) -> None:
"""Initialize the component"""
"""
Initialize the component.
Raises:
ComponentError: If initialization fails
"""
pass
async def shutdown(self) -> None:
"""Shutdown the component"""
pass
"""
Shutdown the component.
Raises:
ComponentError: If shutdown fails
"""
if self._health_check_task:
self._health_check_task.cancel()
try:
await self._health_check_task
except asyncio.CancelledError:
pass
def is_healthy(self) -> bool:
"""Check if component is healthy"""
return self.state == ComponentState.READY and not self.error
class ComponentTracker:
"""Tracks component states and relationships"""
def __init__(self):
self.states: Dict[str, ComponentState] = {}
self.history: List[Dict[str, Any]] = []
MAX_HISTORY: ClassVar[int] = 1000 # Maximum history entries to keep
def update_state(self, name: str, state: ComponentState, error: Optional[str] = None) -> None:
def __init__(self) -> None:
self.states: Dict[str, ComponentState] = {}
self.history: List[ComponentHistory] = []
def update_state(
self,
name: str,
state: ComponentState,
error: Optional[str] = None
) -> None:
"""Update component state"""
self.states[name] = state
self.history.append({
"component": name,
"state": state.value,
"timestamp": datetime.utcnow(),
"error": error
})
# Add history entry
now = datetime.utcnow()
duration = 0.0
if self.history:
last_entry = self.history[-1]
last_time = datetime.fromisoformat(last_entry["timestamp"])
duration = (now - last_time).total_seconds()
def get_component_history(self, name: str) -> List[Dict[str, Any]]:
self.history.append(ComponentHistory(
component=name,
state=state.name,
timestamp=now.isoformat(),
error=error,
duration=duration
))
# Cleanup old history
if len(self.history) > self.MAX_HISTORY:
self.history = self.history[-self.MAX_HISTORY:]
def get_component_history(self, name: str) -> List[ComponentHistory]:
"""Get state history for a component"""
return [
entry for entry in self.history
@@ -72,12 +141,33 @@ class ComponentTracker:
class DependencyManager:
"""Manages component dependencies"""
def __init__(self):
def __init__(self) -> None:
self.dependencies: Dict[str, Set[str]] = {}
self.dependents: Dict[str, Set[str]] = {}
def add_dependency(self, component: str, dependency: str) -> None:
"""Add a dependency relationship"""
"""
Add a dependency relationship.
Args:
component: Component name
dependency: Dependency name
Raises:
ComponentError: If dependency cycle is detected
"""
# Check for cycles
if self._would_create_cycle(component, dependency):
raise ComponentError(
f"Dependency cycle detected: {component} -> {dependency}",
context=ErrorContext(
"DependencyManager",
"add_dependency",
{"component": component, "dependency": dependency},
ErrorSeverity.HIGH
)
)
if component not in self.dependencies:
self.dependencies[component] = set()
self.dependencies[component].add(dependency)
@@ -86,6 +176,23 @@ class DependencyManager:
self.dependents[dependency] = set()
self.dependents[dependency].add(component)
def _would_create_cycle(self, component: str, dependency: str) -> bool:
"""Check if adding dependency would create a cycle"""
visited = set()
def has_path(start: str, end: str) -> bool:
if start == end:
return True
if start in visited:
return False
visited.add(start)
return any(
has_path(dep, end)
for dep in self.dependencies.get(start, set())
)
return has_path(dependency, component)
def get_dependencies(self, component: str) -> Set[str]:
"""Get dependencies for a component"""
return self.dependencies.get(component, set())
@@ -95,27 +202,72 @@ class DependencyManager:
return self.dependents.get(component, set())
def get_initialization_order(self) -> List[str]:
"""Get components in dependency order"""
visited = set()
order = []
"""
Get components in dependency order.
Returns:
List of component names in initialization order
Raises:
ComponentError: If dependency cycle is detected
"""
visited: Set[str] = set()
temp_visited: Set[str] = set()
order: List[str] = []
def visit(component: str) -> None:
if component in temp_visited:
cycle = " -> ".join(
name for name in self.dependencies
if name in temp_visited
)
raise ComponentError(
f"Dependency cycle detected: {cycle}",
context=ErrorContext(
"DependencyManager",
"get_initialization_order",
{"cycle": cycle},
ErrorSeverity.HIGH
)
)
if component in visited:
return
visited.add(component)
temp_visited.add(component)
for dep in self.dependencies.get(component, set()):
visit(dep)
temp_visited.remove(component)
visited.add(component)
order.append(component)
for component in self.dependencies:
visit(component)
try:
for component in self.dependencies:
if component not in visited:
visit(component)
except RecursionError:
raise ComponentError(
"Dependency resolution exceeded maximum recursion depth",
context=ErrorContext(
"DependencyManager",
"get_initialization_order",
None,
ErrorSeverity.HIGH
)
)
return order
class ComponentManager:
"""Manages VideoArchiver components"""
def __init__(self, cog):
CORE_COMPONENTS: ClassVar[Dict[str, Tuple[Type[Any], Set[str]]]] = {
"config_manager": ("..config_manager.ConfigManager", set()),
"processor": ("..processor.core.Processor", {"config_manager"}),
"queue_manager": ("..queue.manager.EnhancedVideoQueueManager", {"config_manager"}),
"ffmpeg_mgr": ("..ffmpeg.ffmpeg_manager.FFmpegManager", set())
}
def __init__(self, cog: Any) -> None:
self.cog = cog
self._components: Dict[str, Component] = {}
self.tracker = ComponentTracker()
@@ -124,21 +276,41 @@ class ComponentManager:
def register(
self,
name: str,
component: Any,
component: Union[Component, Any],
dependencies: Optional[Set[str]] = None
) -> None:
"""Register a component with dependencies"""
"""
Register a component with dependencies.
Args:
name: Component name
component: Component instance
dependencies: Optional set of dependency names
Raises:
ComponentError: If registration fails
"""
try:
# Wrap non-Component objects
if not isinstance(component, Component):
component = Component(name)
wrapped = Component(name)
if isinstance(component, Initializable):
wrapped.initialize = component.initialize
wrapped.shutdown = component.shutdown
component = wrapped
# Register dependencies
if dependencies:
for dep in dependencies:
if dep not in self._components:
raise ComponentDependencyError(
f"Dependency {dep} not registered for {name}"
raise ComponentError(
f"Dependency {dep} not registered for {name}",
context=ErrorContext(
"ComponentManager",
"register",
{"component": name, "dependency": dep},
ErrorSeverity.HIGH
)
)
self.dependency_manager.add_dependency(name, dep)
@@ -149,19 +321,33 @@ class ComponentManager:
logger.debug(f"Registered component: {name}")
except Exception as e:
logger.error(f"Error registering component {name}: {e}")
error = f"Failed to register component {name}: {str(e)}"
logger.error(error, exc_info=True)
self.tracker.update_state(name, ComponentState.ERROR, str(e))
raise ComponentLifecycleError(f"Failed to register component: {str(e)}")
raise ComponentError(
error,
context=ErrorContext(
"ComponentManager",
"register",
{"component": name},
ErrorSeverity.HIGH
)
)
async def initialize_components(self) -> None:
"""Initialize all components in dependency order"""
"""
Initialize all components in dependency order.
Raises:
ComponentError: If initialization fails
"""
try:
# Get initialization order
init_order = self.dependency_manager.get_initialization_order()
# Initialize core components first
await self._initialize_core_components()
# Get initialization order
init_order = self.dependency_manager.get_initialization_order()
# Initialize remaining components
for name in init_order:
if name not in self._components:
@@ -174,88 +360,172 @@ class ComponentManager:
component.initialization_time = datetime.utcnow()
self.tracker.update_state(name, ComponentState.READY)
except Exception as e:
logger.error(f"Error initializing component {name}: {e}")
error = f"Failed to initialize component {name}: {str(e)}"
logger.error(error, exc_info=True)
self.tracker.update_state(name, ComponentState.ERROR, str(e))
raise ComponentLifecycleError(
f"Failed to initialize component {name}: {str(e)}"
raise ComponentError(
error,
context=ErrorContext(
"ComponentManager",
"initialize_components",
{"component": name},
ErrorSeverity.HIGH
)
)
except Exception as e:
logger.error(f"Error during component initialization: {e}")
raise ComponentLifecycleError(f"Component initialization failed: {str(e)}")
error = f"Component initialization failed: {str(e)}"
logger.error(error, exc_info=True)
raise ComponentError(
error,
context=ErrorContext(
"ComponentManager",
"initialize_components",
None,
ErrorSeverity.HIGH
)
)
async def _initialize_core_components(self) -> None:
"""Initialize core system components"""
from ..config_manager import ConfigManager
from ..processor.core import Processor
from ..queue.manager import EnhancedVideoQueueManager
from ..ffmpeg.ffmpeg_manager import FFmpegManager
"""
Initialize core system components.
Raises:
ComponentError: If core component initialization fails
"""
try:
for name, (component_path, deps) in self.CORE_COMPONENTS.items():
module_path, class_name = component_path.rsplit(".", 1)
module = __import__(module_path, fromlist=[class_name])
component_class = getattr(module, class_name)
if name == "processor":
component = component_class(self.cog)
elif name == "ffmpeg_mgr":
component = component_class(self.cog)
else:
component = component_class()
core_components = {
"config_manager": (ConfigManager(self.cog), set()),
"processor": (Processor(self.cog), {"config_manager"}),
"queue_manager": (EnhancedVideoQueueManager(), {"config_manager"}),
"ffmpeg_mgr": (FFmpegManager(self.cog), set())
}
self.register(name, component, deps)
for name, (component, deps) in core_components.items():
self.register(name, component, deps)
# Initialize paths
await self._initialize_paths()
# Initialize paths
await self._initialize_paths()
except Exception as e:
error = f"Failed to initialize core components: {str(e)}"
logger.error(error, exc_info=True)
raise ComponentError(
error,
context=ErrorContext(
"ComponentManager",
"_initialize_core_components",
None,
ErrorSeverity.HIGH
)
)
async def _initialize_paths(self) -> None:
"""Initialize required paths"""
from pathlib import Path
from ..utils.path_manager import ensure_directory
"""
Initialize required paths.
Raises:
ComponentError: If path initialization fails
"""
try:
data_dir = Path(self.cog.bot.data_path) / "VideoArchiver"
download_dir = data_dir / "downloads"
data_dir = Path(self.cog.bot.data_path) / "VideoArchiver"
download_dir = data_dir / "downloads"
# Ensure directories exist
await ensure_directory(data_dir)
await ensure_directory(download_dir)
# Ensure directories exist
await ensure_directory(data_dir)
await ensure_directory(download_dir)
# Register paths
self.register("data_path", data_dir)
self.register("download_path", download_dir)
# Register paths
self.register("data_path", data_dir)
self.register("download_path", download_dir)
except Exception as e:
error = f"Failed to initialize paths: {str(e)}"
logger.error(error, exc_info=True)
raise ComponentError(
error,
context=ErrorContext(
"ComponentManager",
"_initialize_paths",
None,
ErrorSeverity.HIGH
)
)
def get(self, name: str) -> Optional[Any]:
def get(self, name: str) -> Optional[Component]:
"""Get a registered component"""
component = self._components.get(name)
return component if isinstance(component, Component) else None
return self._components.get(name)
async def shutdown_components(self) -> None:
"""Shutdown components in reverse dependency order"""
shutdown_order = reversed(self.dependency_manager.get_initialization_order())
"""
Shutdown components in reverse dependency order.
for name in shutdown_order:
if name not in self._components:
continue
component = self._components[name]
try:
await component.shutdown()
self.tracker.update_state(name, ComponentState.SHUTDOWN)
except Exception as e:
logger.error(f"Error shutting down component {name}: {e}")
self.tracker.update_state(name, ComponentState.ERROR, str(e))
Raises:
ComponentError: If shutdown fails
"""
try:
shutdown_order = reversed(self.dependency_manager.get_initialization_order())
for name in shutdown_order:
if name not in self._components:
continue
component = self._components[name]
try:
await component.shutdown()
self.tracker.update_state(name, ComponentState.SHUTDOWN)
except Exception as e:
error = f"Error shutting down component {name}: {str(e)}"
logger.error(error, exc_info=True)
self.tracker.update_state(name, ComponentState.ERROR, str(e))
raise ComponentError(
error,
context=ErrorContext(
"ComponentManager",
"shutdown_components",
{"component": name},
ErrorSeverity.HIGH
)
)
except Exception as e:
error = f"Component shutdown failed: {str(e)}"
logger.error(error, exc_info=True)
raise ComponentError(
error,
context=ErrorContext(
"ComponentManager",
"shutdown_components",
None,
ErrorSeverity.HIGH
)
)
def clear(self) -> None:
"""Clear all registered components"""
self._components.clear()
logger.debug("Cleared all components")
def get_component_status(self) -> Dict[str, Any]:
"""Get status of all components"""
def get_component_status(self) -> Dict[str, ComponentStatus]:
"""
Get status of all components.
Returns:
Dictionary mapping component names to their status
"""
return {
name: {
"state": self.tracker.states.get(name, ComponentState.UNREGISTERED).value,
"registration_time": component.registration_time,
"initialization_time": component.initialization_time,
"dependencies": self.dependency_manager.get_dependencies(name),
"dependents": self.dependency_manager.get_dependents(name),
"error": component.error
}
name: ComponentStatus(
state=self.tracker.states.get(name, ComponentState.UNREGISTERED).name,
registration_time=component.registration_time.isoformat() if component.registration_time else None,
initialization_time=component.initialization_time.isoformat() if component.initialization_time else None,
dependencies=self.dependency_manager.get_dependencies(name),
dependents=self.dependency_manager.get_dependents(name),
error=component.error,
health=component.is_healthy()
)
for name, component in self._components.items()
}