claude-mpm 0.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of claude-mpm might be problematic. Click here for more details.
- claude_mpm/__init__.py +17 -0
- claude_mpm/__main__.py +14 -0
- claude_mpm/_version.py +32 -0
- claude_mpm/agents/BASE_AGENT_TEMPLATE.md +88 -0
- claude_mpm/agents/INSTRUCTIONS.md +375 -0
- claude_mpm/agents/__init__.py +118 -0
- claude_mpm/agents/agent_loader.py +621 -0
- claude_mpm/agents/agent_loader_integration.py +229 -0
- claude_mpm/agents/agents_metadata.py +204 -0
- claude_mpm/agents/base_agent.json +27 -0
- claude_mpm/agents/base_agent_loader.py +519 -0
- claude_mpm/agents/schema/agent_schema.json +160 -0
- claude_mpm/agents/system_agent_config.py +587 -0
- claude_mpm/agents/templates/__init__.py +101 -0
- claude_mpm/agents/templates/data_engineer_agent.json +46 -0
- claude_mpm/agents/templates/documentation_agent.json +45 -0
- claude_mpm/agents/templates/engineer_agent.json +49 -0
- claude_mpm/agents/templates/ops_agent.json +46 -0
- claude_mpm/agents/templates/qa_agent.json +45 -0
- claude_mpm/agents/templates/research_agent.json +49 -0
- claude_mpm/agents/templates/security_agent.json +46 -0
- claude_mpm/agents/templates/update-optimized-specialized-agents.json +374 -0
- claude_mpm/agents/templates/version_control_agent.json +46 -0
- claude_mpm/agents/test_fix_deployment/.claude-pm/config/project.json +6 -0
- claude_mpm/cli.py +655 -0
- claude_mpm/cli_main.py +13 -0
- claude_mpm/cli_module/__init__.py +15 -0
- claude_mpm/cli_module/args.py +222 -0
- claude_mpm/cli_module/commands.py +203 -0
- claude_mpm/cli_module/migration_example.py +183 -0
- claude_mpm/cli_module/refactoring_guide.md +253 -0
- claude_mpm/cli_old/__init__.py +1 -0
- claude_mpm/cli_old/ticket_cli.py +102 -0
- claude_mpm/config/__init__.py +5 -0
- claude_mpm/config/hook_config.py +42 -0
- claude_mpm/constants.py +150 -0
- claude_mpm/core/__init__.py +45 -0
- claude_mpm/core/agent_name_normalizer.py +248 -0
- claude_mpm/core/agent_registry.py +627 -0
- claude_mpm/core/agent_registry.py.bak +312 -0
- claude_mpm/core/agent_session_manager.py +273 -0
- claude_mpm/core/base_service.py +747 -0
- claude_mpm/core/base_service.py.bak +406 -0
- claude_mpm/core/config.py +334 -0
- claude_mpm/core/config_aliases.py +292 -0
- claude_mpm/core/container.py +347 -0
- claude_mpm/core/factories.py +281 -0
- claude_mpm/core/framework_loader.py +472 -0
- claude_mpm/core/injectable_service.py +206 -0
- claude_mpm/core/interfaces.py +539 -0
- claude_mpm/core/logger.py +468 -0
- claude_mpm/core/minimal_framework_loader.py +107 -0
- claude_mpm/core/mixins.py +150 -0
- claude_mpm/core/service_registry.py +299 -0
- claude_mpm/core/session_manager.py +190 -0
- claude_mpm/core/simple_runner.py +511 -0
- claude_mpm/core/tool_access_control.py +173 -0
- claude_mpm/hooks/README.md +243 -0
- claude_mpm/hooks/__init__.py +5 -0
- claude_mpm/hooks/base_hook.py +154 -0
- claude_mpm/hooks/builtin/__init__.py +1 -0
- claude_mpm/hooks/builtin/logging_hook_example.py +165 -0
- claude_mpm/hooks/builtin/post_delegation_hook_example.py +124 -0
- claude_mpm/hooks/builtin/pre_delegation_hook_example.py +125 -0
- claude_mpm/hooks/builtin/submit_hook_example.py +100 -0
- claude_mpm/hooks/builtin/ticket_extraction_hook_example.py +237 -0
- claude_mpm/hooks/builtin/todo_agent_prefix_hook.py +239 -0
- claude_mpm/hooks/builtin/workflow_start_hook.py +181 -0
- claude_mpm/hooks/hook_client.py +264 -0
- claude_mpm/hooks/hook_runner.py +370 -0
- claude_mpm/hooks/json_rpc_executor.py +259 -0
- claude_mpm/hooks/json_rpc_hook_client.py +319 -0
- claude_mpm/hooks/tool_call_interceptor.py +204 -0
- claude_mpm/init.py +246 -0
- claude_mpm/orchestration/SUBPROCESS_DESIGN.md +66 -0
- claude_mpm/orchestration/__init__.py +6 -0
- claude_mpm/orchestration/archive/direct_orchestrator.py +195 -0
- claude_mpm/orchestration/archive/factory.py +215 -0
- claude_mpm/orchestration/archive/hook_enabled_orchestrator.py +188 -0
- claude_mpm/orchestration/archive/hook_integration_example.py +178 -0
- claude_mpm/orchestration/archive/interactive_subprocess_orchestrator.py +826 -0
- claude_mpm/orchestration/archive/orchestrator.py +501 -0
- claude_mpm/orchestration/archive/pexpect_orchestrator.py +252 -0
- claude_mpm/orchestration/archive/pty_orchestrator.py +270 -0
- claude_mpm/orchestration/archive/simple_orchestrator.py +82 -0
- claude_mpm/orchestration/archive/subprocess_orchestrator.py +801 -0
- claude_mpm/orchestration/archive/system_prompt_orchestrator.py +278 -0
- claude_mpm/orchestration/archive/wrapper_orchestrator.py +187 -0
- claude_mpm/scripts/__init__.py +1 -0
- claude_mpm/scripts/ticket.py +269 -0
- claude_mpm/services/__init__.py +10 -0
- claude_mpm/services/agent_deployment.py +955 -0
- claude_mpm/services/agent_lifecycle_manager.py +948 -0
- claude_mpm/services/agent_management_service.py +596 -0
- claude_mpm/services/agent_modification_tracker.py +841 -0
- claude_mpm/services/agent_profile_loader.py +606 -0
- claude_mpm/services/agent_registry.py +677 -0
- claude_mpm/services/base_agent_manager.py +380 -0
- claude_mpm/services/framework_agent_loader.py +337 -0
- claude_mpm/services/framework_claude_md_generator/README.md +92 -0
- claude_mpm/services/framework_claude_md_generator/__init__.py +206 -0
- claude_mpm/services/framework_claude_md_generator/content_assembler.py +151 -0
- claude_mpm/services/framework_claude_md_generator/content_validator.py +126 -0
- claude_mpm/services/framework_claude_md_generator/deployment_manager.py +137 -0
- claude_mpm/services/framework_claude_md_generator/section_generators/__init__.py +106 -0
- claude_mpm/services/framework_claude_md_generator/section_generators/agents.py +582 -0
- claude_mpm/services/framework_claude_md_generator/section_generators/claude_pm_init.py +97 -0
- claude_mpm/services/framework_claude_md_generator/section_generators/core_responsibilities.py +27 -0
- claude_mpm/services/framework_claude_md_generator/section_generators/delegation_constraints.py +23 -0
- claude_mpm/services/framework_claude_md_generator/section_generators/environment_config.py +23 -0
- claude_mpm/services/framework_claude_md_generator/section_generators/footer.py +20 -0
- claude_mpm/services/framework_claude_md_generator/section_generators/header.py +26 -0
- claude_mpm/services/framework_claude_md_generator/section_generators/orchestration_principles.py +30 -0
- claude_mpm/services/framework_claude_md_generator/section_generators/role_designation.py +37 -0
- claude_mpm/services/framework_claude_md_generator/section_generators/subprocess_validation.py +111 -0
- claude_mpm/services/framework_claude_md_generator/section_generators/todo_task_tools.py +89 -0
- claude_mpm/services/framework_claude_md_generator/section_generators/troubleshooting.py +39 -0
- claude_mpm/services/framework_claude_md_generator/section_manager.py +106 -0
- claude_mpm/services/framework_claude_md_generator/version_manager.py +121 -0
- claude_mpm/services/framework_claude_md_generator.py +621 -0
- claude_mpm/services/hook_service.py +388 -0
- claude_mpm/services/hook_service_manager.py +223 -0
- claude_mpm/services/json_rpc_hook_manager.py +92 -0
- claude_mpm/services/parent_directory_manager/README.md +83 -0
- claude_mpm/services/parent_directory_manager/__init__.py +577 -0
- claude_mpm/services/parent_directory_manager/backup_manager.py +258 -0
- claude_mpm/services/parent_directory_manager/config_manager.py +210 -0
- claude_mpm/services/parent_directory_manager/deduplication_manager.py +279 -0
- claude_mpm/services/parent_directory_manager/framework_protector.py +143 -0
- claude_mpm/services/parent_directory_manager/operations.py +186 -0
- claude_mpm/services/parent_directory_manager/state_manager.py +624 -0
- claude_mpm/services/parent_directory_manager/template_deployer.py +579 -0
- claude_mpm/services/parent_directory_manager/validation_manager.py +378 -0
- claude_mpm/services/parent_directory_manager/version_control_helper.py +339 -0
- claude_mpm/services/parent_directory_manager/version_manager.py +222 -0
- claude_mpm/services/shared_prompt_cache.py +819 -0
- claude_mpm/services/ticket_manager.py +213 -0
- claude_mpm/services/ticket_manager_di.py +318 -0
- claude_mpm/services/ticketing_service_original.py +508 -0
- claude_mpm/services/version_control/VERSION +1 -0
- claude_mpm/services/version_control/__init__.py +70 -0
- claude_mpm/services/version_control/branch_strategy.py +670 -0
- claude_mpm/services/version_control/conflict_resolution.py +744 -0
- claude_mpm/services/version_control/git_operations.py +784 -0
- claude_mpm/services/version_control/semantic_versioning.py +703 -0
- claude_mpm/ui/__init__.py +1 -0
- claude_mpm/ui/rich_terminal_ui.py +295 -0
- claude_mpm/ui/terminal_ui.py +328 -0
- claude_mpm/utils/__init__.py +16 -0
- claude_mpm/utils/config_manager.py +468 -0
- claude_mpm/utils/import_migration_example.py +80 -0
- claude_mpm/utils/imports.py +182 -0
- claude_mpm/utils/path_operations.py +357 -0
- claude_mpm/utils/paths.py +289 -0
- claude_mpm-0.3.0.dist-info/METADATA +290 -0
- claude_mpm-0.3.0.dist-info/RECORD +159 -0
- claude_mpm-0.3.0.dist-info/WHEEL +5 -0
- claude_mpm-0.3.0.dist-info/entry_points.txt +4 -0
- claude_mpm-0.3.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,747 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Consolidated Base Service for Claude PM Framework.
|
|
3
|
+
|
|
4
|
+
Combines functionality from:
|
|
5
|
+
- base_service.py (basic service infrastructure)
|
|
6
|
+
- enhanced_base_service.py (advanced features)
|
|
7
|
+
|
|
8
|
+
Provides:
|
|
9
|
+
- Lifecycle management (start, stop, health checks)
|
|
10
|
+
- Configuration management
|
|
11
|
+
- Logging with optional structured context
|
|
12
|
+
- Metrics collection and performance monitoring
|
|
13
|
+
- Error handling with optional circuit breaker
|
|
14
|
+
- Optional dependency injection support
|
|
15
|
+
- Service discovery and registration
|
|
16
|
+
- Background task management
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import asyncio
|
|
20
|
+
import logging
|
|
21
|
+
import signal
|
|
22
|
+
import sys
|
|
23
|
+
import os
|
|
24
|
+
import time
|
|
25
|
+
import traceback
|
|
26
|
+
import threading
|
|
27
|
+
from abc import ABC, abstractmethod
|
|
28
|
+
from dataclasses import dataclass, field
|
|
29
|
+
from datetime import datetime, timedelta
|
|
30
|
+
from typing import Any, Dict, List, Optional, Union, Type, Callable
|
|
31
|
+
from pathlib import Path
|
|
32
|
+
from contextlib import asynccontextmanager
|
|
33
|
+
import json
|
|
34
|
+
|
|
35
|
+
from .config import Config
|
|
36
|
+
from .logger import setup_logging
|
|
37
|
+
from .mixins import LoggerMixin
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class ServiceHealth:
|
|
42
|
+
"""Service health status information."""
|
|
43
|
+
status: str # healthy, degraded, unhealthy, unknown
|
|
44
|
+
message: str
|
|
45
|
+
timestamp: str
|
|
46
|
+
metrics: Dict[str, Any] = field(default_factory=dict)
|
|
47
|
+
checks: Dict[str, bool] = field(default_factory=dict)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass
|
|
51
|
+
class ServiceMetrics:
|
|
52
|
+
"""Service metrics collection."""
|
|
53
|
+
requests_total: int = 0
|
|
54
|
+
requests_failed: int = 0
|
|
55
|
+
response_time_avg: float = 0.0
|
|
56
|
+
uptime_seconds: int = 0
|
|
57
|
+
memory_usage_mb: float = 0.0
|
|
58
|
+
custom_metrics: Dict[str, Any] = field(default_factory=dict)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass
|
|
62
|
+
class ServiceContext:
|
|
63
|
+
"""Service execution context with request tracking (enhanced feature)."""
|
|
64
|
+
request_id: str
|
|
65
|
+
start_time: float
|
|
66
|
+
service_name: str
|
|
67
|
+
operation: str
|
|
68
|
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@dataclass
|
|
72
|
+
class CircuitBreakerState:
|
|
73
|
+
"""Circuit breaker state for service resilience (enhanced feature)."""
|
|
74
|
+
failure_count: int = 0
|
|
75
|
+
last_failure_time: Optional[float] = None
|
|
76
|
+
state: str = "closed" # closed, open, half_open
|
|
77
|
+
failure_threshold: int = 5
|
|
78
|
+
timeout_seconds: int = 60
|
|
79
|
+
success_threshold: int = 3 # For half-open state
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class BaseService(LoggerMixin, ABC):
|
|
83
|
+
"""
|
|
84
|
+
Base class for all Claude PM services with optional enhanced features.
|
|
85
|
+
|
|
86
|
+
Basic features (always enabled):
|
|
87
|
+
- Lifecycle management
|
|
88
|
+
- Configuration
|
|
89
|
+
- Logging
|
|
90
|
+
- Metrics
|
|
91
|
+
- Health checks
|
|
92
|
+
- Background tasks
|
|
93
|
+
|
|
94
|
+
Enhanced features (optional):
|
|
95
|
+
- Dependency injection
|
|
96
|
+
- Circuit breaker pattern
|
|
97
|
+
- Request tracking
|
|
98
|
+
- Performance monitoring
|
|
99
|
+
- Structured logging context
|
|
100
|
+
"""
|
|
101
|
+
|
|
102
|
+
def __init__(
|
|
103
|
+
self,
|
|
104
|
+
name: str,
|
|
105
|
+
config: Optional[Dict[str, Any]] = None,
|
|
106
|
+
config_path: Optional[Path] = None,
|
|
107
|
+
enable_enhanced_features: bool = False,
|
|
108
|
+
container: Optional[Any] = None
|
|
109
|
+
):
|
|
110
|
+
"""
|
|
111
|
+
Initialize the base service.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
name: Service name for identification
|
|
115
|
+
config: Optional configuration dictionary
|
|
116
|
+
config_path: Optional path to configuration file
|
|
117
|
+
enable_enhanced_features: Enable circuit breaker, request tracking, etc.
|
|
118
|
+
container: Optional service container for dependency injection
|
|
119
|
+
"""
|
|
120
|
+
self.name = name
|
|
121
|
+
self.config = Config(config or {}, config_path)
|
|
122
|
+
self._enable_enhanced = enable_enhanced_features
|
|
123
|
+
self._container = container
|
|
124
|
+
|
|
125
|
+
# Set custom logger name based on service name
|
|
126
|
+
self._logger_name = f"{self.__class__.__module__}.{self.__class__.__name__}.{name}"
|
|
127
|
+
|
|
128
|
+
# Service state
|
|
129
|
+
self._running = False
|
|
130
|
+
self._start_time: Optional[datetime] = None
|
|
131
|
+
self._stop_event = asyncio.Event()
|
|
132
|
+
self._shutdown_timeout = timedelta(seconds=30)
|
|
133
|
+
|
|
134
|
+
# Health and metrics
|
|
135
|
+
self._health = ServiceHealth(
|
|
136
|
+
status="unknown", message="Service not started", timestamp=datetime.now().isoformat()
|
|
137
|
+
)
|
|
138
|
+
self._metrics = ServiceMetrics()
|
|
139
|
+
self._last_health_check: Optional[float] = None
|
|
140
|
+
|
|
141
|
+
# Background tasks
|
|
142
|
+
self._background_tasks: List[asyncio.Task] = []
|
|
143
|
+
|
|
144
|
+
# Enhanced features (only initialized if enabled)
|
|
145
|
+
if self._enable_enhanced:
|
|
146
|
+
self._init_enhanced_features()
|
|
147
|
+
|
|
148
|
+
# Check for quiet mode
|
|
149
|
+
default_log_level = "INFO"
|
|
150
|
+
if os.getenv('CLAUDE_PM_QUIET_MODE') == 'true':
|
|
151
|
+
default_log_level = "WARNING"
|
|
152
|
+
self.logger.setLevel(logging.WARNING)
|
|
153
|
+
|
|
154
|
+
# Only log if not in quiet mode
|
|
155
|
+
if not os.environ.get('CLAUDE_PM_QUIET_MODE', '').lower() == 'true':
|
|
156
|
+
self.logger.info(f"Initialized {self.name} service")
|
|
157
|
+
|
|
158
|
+
def _init_enhanced_features(self):
|
|
159
|
+
"""Initialize enhanced features when enabled."""
|
|
160
|
+
# Circuit breaker for resilience
|
|
161
|
+
self._circuit_breaker = CircuitBreakerState()
|
|
162
|
+
|
|
163
|
+
# Error tracking
|
|
164
|
+
self._error_counts: Dict[str, int] = {}
|
|
165
|
+
|
|
166
|
+
# Request tracking
|
|
167
|
+
self._request_contexts: Dict[str, ServiceContext] = {}
|
|
168
|
+
|
|
169
|
+
# Performance metrics
|
|
170
|
+
self._performance_metrics: Dict[str, List[float]] = {}
|
|
171
|
+
|
|
172
|
+
# Thread safety
|
|
173
|
+
self._lock = threading.RLock()
|
|
174
|
+
|
|
175
|
+
# Optional service dependencies
|
|
176
|
+
self._health_monitor: Optional[Any] = None
|
|
177
|
+
self._error_handler: Optional[Any] = None
|
|
178
|
+
self._cache: Optional[Any] = None
|
|
179
|
+
|
|
180
|
+
# Register dependencies if container provided
|
|
181
|
+
if self._container:
|
|
182
|
+
self._register_service_dependencies()
|
|
183
|
+
|
|
184
|
+
@property
|
|
185
|
+
def running(self) -> bool:
|
|
186
|
+
"""Check if service is currently running."""
|
|
187
|
+
return self._running
|
|
188
|
+
|
|
189
|
+
@property
|
|
190
|
+
def uptime(self) -> Optional[float]:
|
|
191
|
+
"""Get service uptime in seconds."""
|
|
192
|
+
if self._start_time and self._running:
|
|
193
|
+
return (datetime.now() - self._start_time).total_seconds()
|
|
194
|
+
return None
|
|
195
|
+
|
|
196
|
+
@property
|
|
197
|
+
def health(self) -> ServiceHealth:
|
|
198
|
+
"""Get current service health status."""
|
|
199
|
+
return self._health
|
|
200
|
+
|
|
201
|
+
@property
|
|
202
|
+
def metrics(self) -> ServiceMetrics:
|
|
203
|
+
"""Get current service metrics."""
|
|
204
|
+
if self.uptime:
|
|
205
|
+
self._metrics.uptime_seconds = int(self.uptime)
|
|
206
|
+
return self._metrics
|
|
207
|
+
|
|
208
|
+
async def start(self) -> None:
|
|
209
|
+
"""Start the service."""
|
|
210
|
+
if self._running:
|
|
211
|
+
self.logger.warning(f"Service {self.name} is already running")
|
|
212
|
+
return
|
|
213
|
+
|
|
214
|
+
self.logger.info(f"Starting {self.name} service...")
|
|
215
|
+
|
|
216
|
+
try:
|
|
217
|
+
# Use context manager if enhanced features enabled
|
|
218
|
+
if self._enable_enhanced:
|
|
219
|
+
async with self._service_operation("start"):
|
|
220
|
+
await self._start_internal()
|
|
221
|
+
else:
|
|
222
|
+
await self._start_internal()
|
|
223
|
+
|
|
224
|
+
self.logger.info(f"Service {self.name} started successfully")
|
|
225
|
+
|
|
226
|
+
except Exception as e:
|
|
227
|
+
self.logger.error(f"Failed to start service {self.name}: {e}")
|
|
228
|
+
self._health = ServiceHealth(
|
|
229
|
+
status="unhealthy",
|
|
230
|
+
message=f"Startup failed: {str(e)}",
|
|
231
|
+
timestamp=datetime.now().isoformat(),
|
|
232
|
+
checks={"startup": False},
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
if self._enable_enhanced and hasattr(self, '_handle_error'):
|
|
236
|
+
await self._handle_error(e, {"operation": "start", "service": self.name})
|
|
237
|
+
raise
|
|
238
|
+
|
|
239
|
+
async def _start_internal(self):
|
|
240
|
+
"""Internal start logic."""
|
|
241
|
+
# Setup signal handlers
|
|
242
|
+
self._setup_signal_handlers()
|
|
243
|
+
|
|
244
|
+
# Initialize dependencies if enhanced
|
|
245
|
+
if self._enable_enhanced and hasattr(self, '_initialize_dependencies'):
|
|
246
|
+
await self._initialize_dependencies()
|
|
247
|
+
|
|
248
|
+
# Initialize service
|
|
249
|
+
await self._initialize()
|
|
250
|
+
|
|
251
|
+
# Start background tasks
|
|
252
|
+
await self._start_background_tasks()
|
|
253
|
+
|
|
254
|
+
# Register with health monitor if available
|
|
255
|
+
if self._enable_enhanced and hasattr(self, '_register_with_health_monitor'):
|
|
256
|
+
await self._register_with_health_monitor()
|
|
257
|
+
|
|
258
|
+
# Mark as running
|
|
259
|
+
self._running = True
|
|
260
|
+
self._start_time = datetime.now()
|
|
261
|
+
|
|
262
|
+
# Update health status
|
|
263
|
+
self._health = ServiceHealth(
|
|
264
|
+
status="healthy",
|
|
265
|
+
message="Service started successfully",
|
|
266
|
+
timestamp=datetime.now().isoformat(),
|
|
267
|
+
checks={"startup": True},
|
|
268
|
+
metrics=self._get_health_metrics() if self._enable_enhanced else {}
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
async def stop(self) -> None:
|
|
272
|
+
"""Stop the service gracefully."""
|
|
273
|
+
if not self._running:
|
|
274
|
+
self.logger.warning(f"Service {self.name} is not running")
|
|
275
|
+
return
|
|
276
|
+
|
|
277
|
+
self.logger.info(f"Stopping {self.name} service...")
|
|
278
|
+
|
|
279
|
+
try:
|
|
280
|
+
# Apply timeout if enhanced features enabled
|
|
281
|
+
if self._enable_enhanced:
|
|
282
|
+
async with asyncio.timeout(self._shutdown_timeout.total_seconds()):
|
|
283
|
+
await self._stop_internal()
|
|
284
|
+
else:
|
|
285
|
+
await self._stop_internal()
|
|
286
|
+
|
|
287
|
+
self.logger.info(f"Service {self.name} stopped successfully")
|
|
288
|
+
|
|
289
|
+
except asyncio.TimeoutError:
|
|
290
|
+
self.logger.error(f"Service {self.name} shutdown timeout exceeded")
|
|
291
|
+
# Force stop background tasks
|
|
292
|
+
for task in self._background_tasks:
|
|
293
|
+
if not task.done():
|
|
294
|
+
task.cancel()
|
|
295
|
+
raise
|
|
296
|
+
except Exception as e:
|
|
297
|
+
self.logger.error(f"Error stopping service {self.name}: {e}")
|
|
298
|
+
if self._enable_enhanced and hasattr(self, '_handle_error'):
|
|
299
|
+
await self._handle_error(e, {"operation": "stop", "service": self.name})
|
|
300
|
+
raise
|
|
301
|
+
|
|
302
|
+
async def _stop_internal(self):
|
|
303
|
+
"""Internal stop logic."""
|
|
304
|
+
# Signal stop to background tasks
|
|
305
|
+
self._stop_event.set()
|
|
306
|
+
|
|
307
|
+
# Unregister from health monitor if available
|
|
308
|
+
if self._enable_enhanced and hasattr(self, '_unregister_from_health_monitor'):
|
|
309
|
+
await self._unregister_from_health_monitor()
|
|
310
|
+
|
|
311
|
+
# Cancel background tasks
|
|
312
|
+
for task in self._background_tasks:
|
|
313
|
+
if not task.done():
|
|
314
|
+
task.cancel()
|
|
315
|
+
|
|
316
|
+
# Wait for tasks to complete
|
|
317
|
+
if self._background_tasks:
|
|
318
|
+
await asyncio.gather(*self._background_tasks, return_exceptions=True)
|
|
319
|
+
|
|
320
|
+
# Cleanup service
|
|
321
|
+
await self._cleanup()
|
|
322
|
+
|
|
323
|
+
# Mark as stopped
|
|
324
|
+
self._running = False
|
|
325
|
+
|
|
326
|
+
# Update health status
|
|
327
|
+
self._health = ServiceHealth(
|
|
328
|
+
status="unknown",
|
|
329
|
+
message="Service stopped",
|
|
330
|
+
timestamp=datetime.now().isoformat(),
|
|
331
|
+
checks={"running": False},
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
async def restart(self) -> None:
|
|
335
|
+
"""Restart the service."""
|
|
336
|
+
self.logger.info(f"Restarting {self.name} service...")
|
|
337
|
+
await self.stop()
|
|
338
|
+
await self.start()
|
|
339
|
+
|
|
340
|
+
async def health_check(self) -> ServiceHealth:
|
|
341
|
+
"""
|
|
342
|
+
Perform comprehensive health check.
|
|
343
|
+
|
|
344
|
+
Returns:
|
|
345
|
+
ServiceHealth object with current status
|
|
346
|
+
"""
|
|
347
|
+
try:
|
|
348
|
+
# Use enhanced health check if enabled
|
|
349
|
+
if self._enable_enhanced and hasattr(self, '_enhanced_health_check'):
|
|
350
|
+
return await self._enhanced_health_check()
|
|
351
|
+
|
|
352
|
+
# Basic health check
|
|
353
|
+
checks = {}
|
|
354
|
+
|
|
355
|
+
# Basic running check
|
|
356
|
+
checks["running"] = self._running
|
|
357
|
+
|
|
358
|
+
# Custom health checks
|
|
359
|
+
custom_checks = await self._health_check()
|
|
360
|
+
checks.update(custom_checks)
|
|
361
|
+
|
|
362
|
+
# Determine overall status
|
|
363
|
+
if not checks["running"]:
|
|
364
|
+
status = "unhealthy"
|
|
365
|
+
message = "Service is not running"
|
|
366
|
+
elif all(checks.values()):
|
|
367
|
+
status = "healthy"
|
|
368
|
+
message = "All health checks passed"
|
|
369
|
+
elif any(checks.values()):
|
|
370
|
+
status = "degraded"
|
|
371
|
+
message = "Some health checks failed"
|
|
372
|
+
else:
|
|
373
|
+
status = "unhealthy"
|
|
374
|
+
message = "Multiple health checks failed"
|
|
375
|
+
|
|
376
|
+
# Update health status
|
|
377
|
+
self._health = ServiceHealth(
|
|
378
|
+
status=status,
|
|
379
|
+
message=message,
|
|
380
|
+
timestamp=datetime.now().isoformat(),
|
|
381
|
+
checks=checks,
|
|
382
|
+
metrics={
|
|
383
|
+
"uptime": self.uptime,
|
|
384
|
+
"requests_total": self._metrics.requests_total,
|
|
385
|
+
"requests_failed": self._metrics.requests_failed,
|
|
386
|
+
},
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
return self._health
|
|
390
|
+
|
|
391
|
+
except Exception as e:
|
|
392
|
+
self.logger.error(f"Health check failed for {self.name}: {e}")
|
|
393
|
+
self._health = ServiceHealth(
|
|
394
|
+
status="unhealthy",
|
|
395
|
+
message=f"Health check error: {str(e)}",
|
|
396
|
+
timestamp=datetime.now().isoformat(),
|
|
397
|
+
checks={"health_check_error": True},
|
|
398
|
+
)
|
|
399
|
+
return self._health
|
|
400
|
+
|
|
401
|
+
def update_metrics(self, **kwargs) -> None:
|
|
402
|
+
"""Update service metrics."""
|
|
403
|
+
for key, value in kwargs.items():
|
|
404
|
+
if hasattr(self._metrics, key):
|
|
405
|
+
setattr(self._metrics, key, value)
|
|
406
|
+
else:
|
|
407
|
+
self._metrics.custom_metrics[key] = value
|
|
408
|
+
|
|
409
|
+
def get_config(self, key: str, default: Any = None) -> Any:
|
|
410
|
+
"""Get configuration value."""
|
|
411
|
+
return self.config.get(key, default)
|
|
412
|
+
|
|
413
|
+
def _setup_signal_handlers(self) -> None:
|
|
414
|
+
"""Setup signal handlers for graceful shutdown."""
|
|
415
|
+
def signal_handler(signum, frame):
|
|
416
|
+
self.logger.info(f"Received signal {signum}, initiating graceful shutdown...")
|
|
417
|
+
asyncio.create_task(self.stop())
|
|
418
|
+
|
|
419
|
+
signal.signal(signal.SIGINT, signal_handler)
|
|
420
|
+
signal.signal(signal.SIGTERM, signal_handler)
|
|
421
|
+
|
|
422
|
+
async def _start_background_tasks(self) -> None:
|
|
423
|
+
"""Start background tasks."""
|
|
424
|
+
# Health check task
|
|
425
|
+
if self.get_config("enable_health_monitoring", True):
|
|
426
|
+
interval = self.get_config("health_check_interval", 30)
|
|
427
|
+
task = asyncio.create_task(self._health_monitor_task(interval))
|
|
428
|
+
self._background_tasks.append(task)
|
|
429
|
+
|
|
430
|
+
# Metrics collection task
|
|
431
|
+
if self.get_config("enable_metrics", True):
|
|
432
|
+
interval = self.get_config("metrics_interval", 60)
|
|
433
|
+
task = asyncio.create_task(self._metrics_task(interval))
|
|
434
|
+
self._background_tasks.append(task)
|
|
435
|
+
|
|
436
|
+
# Custom background tasks
|
|
437
|
+
custom_tasks = await self._start_custom_tasks()
|
|
438
|
+
if custom_tasks:
|
|
439
|
+
self._background_tasks.extend(custom_tasks)
|
|
440
|
+
|
|
441
|
+
async def _health_monitor_task(self, interval: int) -> None:
|
|
442
|
+
"""Background task for periodic health monitoring."""
|
|
443
|
+
while not self._stop_event.is_set():
|
|
444
|
+
try:
|
|
445
|
+
await self.health_check()
|
|
446
|
+
await asyncio.sleep(interval)
|
|
447
|
+
except asyncio.CancelledError:
|
|
448
|
+
break
|
|
449
|
+
except Exception as e:
|
|
450
|
+
self.logger.error(f"Health monitor task error: {e}")
|
|
451
|
+
await asyncio.sleep(interval)
|
|
452
|
+
|
|
453
|
+
async def _metrics_task(self, interval: int) -> None:
|
|
454
|
+
"""Background task for metrics collection."""
|
|
455
|
+
while not self._stop_event.is_set():
|
|
456
|
+
try:
|
|
457
|
+
await self._collect_metrics()
|
|
458
|
+
await asyncio.sleep(interval)
|
|
459
|
+
except asyncio.CancelledError:
|
|
460
|
+
break
|
|
461
|
+
except Exception as e:
|
|
462
|
+
self.logger.error(f"Metrics task error: {e}")
|
|
463
|
+
await asyncio.sleep(interval)
|
|
464
|
+
|
|
465
|
+
async def _collect_metrics(self) -> None:
|
|
466
|
+
"""Collect service metrics."""
|
|
467
|
+
try:
|
|
468
|
+
# Update uptime
|
|
469
|
+
if self.uptime:
|
|
470
|
+
self._metrics.uptime_seconds = int(self.uptime)
|
|
471
|
+
|
|
472
|
+
# Memory usage (basic implementation)
|
|
473
|
+
try:
|
|
474
|
+
import psutil
|
|
475
|
+
process = psutil.Process()
|
|
476
|
+
self._metrics.memory_usage_mb = process.memory_info().rss / 1024 / 1024
|
|
477
|
+
except ImportError:
|
|
478
|
+
pass
|
|
479
|
+
|
|
480
|
+
# Custom metrics collection
|
|
481
|
+
await self._collect_custom_metrics()
|
|
482
|
+
|
|
483
|
+
except Exception as e:
|
|
484
|
+
self.logger.warning(f"Failed to collect metrics: {e}")
|
|
485
|
+
|
|
486
|
+
# Enhanced features (conditionally included)
|
|
487
|
+
|
|
488
|
+
@asynccontextmanager
|
|
489
|
+
async def _service_operation(self, operation: str):
|
|
490
|
+
"""Context manager for tracking service operations (enhanced feature)."""
|
|
491
|
+
if not self._enable_enhanced:
|
|
492
|
+
yield
|
|
493
|
+
return
|
|
494
|
+
|
|
495
|
+
request_id = f"{self.name}_{operation}_{time.time()}"
|
|
496
|
+
context = ServiceContext(
|
|
497
|
+
request_id=request_id,
|
|
498
|
+
start_time=time.time(),
|
|
499
|
+
service_name=self.name,
|
|
500
|
+
operation=operation
|
|
501
|
+
)
|
|
502
|
+
|
|
503
|
+
self._request_contexts[request_id] = context
|
|
504
|
+
|
|
505
|
+
try:
|
|
506
|
+
yield context
|
|
507
|
+
|
|
508
|
+
# Record success metrics
|
|
509
|
+
duration = time.time() - context.start_time
|
|
510
|
+
self._record_operation_metrics(operation, duration, success=True)
|
|
511
|
+
|
|
512
|
+
except Exception as e:
|
|
513
|
+
# Record failure metrics
|
|
514
|
+
duration = time.time() - context.start_time
|
|
515
|
+
self._record_operation_metrics(operation, duration, success=False)
|
|
516
|
+
|
|
517
|
+
# Update error counts
|
|
518
|
+
error_type = type(e).__name__
|
|
519
|
+
self._error_counts[error_type] = self._error_counts.get(error_type, 0) + 1
|
|
520
|
+
|
|
521
|
+
raise
|
|
522
|
+
|
|
523
|
+
finally:
|
|
524
|
+
# Cleanup context
|
|
525
|
+
self._request_contexts.pop(request_id, None)
|
|
526
|
+
|
|
527
|
+
def _record_operation_metrics(self, operation: str, duration: float, success: bool) -> None:
|
|
528
|
+
"""Record operation performance metrics (enhanced feature)."""
|
|
529
|
+
if not self._enable_enhanced:
|
|
530
|
+
return
|
|
531
|
+
|
|
532
|
+
with self._lock:
|
|
533
|
+
# Update general metrics
|
|
534
|
+
self._metrics.requests_total += 1
|
|
535
|
+
if not success:
|
|
536
|
+
self._metrics.requests_failed += 1
|
|
537
|
+
|
|
538
|
+
# Update performance tracking
|
|
539
|
+
if operation not in self._performance_metrics:
|
|
540
|
+
self._performance_metrics[operation] = []
|
|
541
|
+
|
|
542
|
+
self._performance_metrics[operation].append(duration)
|
|
543
|
+
|
|
544
|
+
# Keep only recent metrics (last 100 operations)
|
|
545
|
+
if len(self._performance_metrics[operation]) > 100:
|
|
546
|
+
self._performance_metrics[operation] = self._performance_metrics[operation][-100:]
|
|
547
|
+
|
|
548
|
+
# Calculate average
|
|
549
|
+
all_times = []
|
|
550
|
+
for times in self._performance_metrics.values():
|
|
551
|
+
all_times.extend(times)
|
|
552
|
+
|
|
553
|
+
if all_times:
|
|
554
|
+
self._metrics.response_time_avg = sum(all_times) / len(all_times)
|
|
555
|
+
|
|
556
|
+
async def _enhanced_health_check(self) -> ServiceHealth:
|
|
557
|
+
"""Enhanced health check with circuit breaker (enhanced feature)."""
|
|
558
|
+
async with self._service_operation("health_check"):
|
|
559
|
+
# Check circuit breaker state
|
|
560
|
+
if self._circuit_breaker.state == "open":
|
|
561
|
+
if self._should_attempt_circuit_recovery():
|
|
562
|
+
self._circuit_breaker.state = "half_open"
|
|
563
|
+
self.logger.info(f"Circuit breaker for {self.name} moved to half-open")
|
|
564
|
+
else:
|
|
565
|
+
return ServiceHealth(
|
|
566
|
+
status="degraded",
|
|
567
|
+
message="Service circuit breaker is open",
|
|
568
|
+
timestamp=datetime.now().isoformat(),
|
|
569
|
+
checks={"circuit_breaker": False},
|
|
570
|
+
metrics=self._get_health_metrics()
|
|
571
|
+
)
|
|
572
|
+
|
|
573
|
+
# Perform standard health check
|
|
574
|
+
health = await self.health_check()
|
|
575
|
+
|
|
576
|
+
# Update circuit breaker
|
|
577
|
+
if health.status in ["healthy", "degraded"]:
|
|
578
|
+
self._record_circuit_success()
|
|
579
|
+
else:
|
|
580
|
+
self._record_circuit_failure()
|
|
581
|
+
|
|
582
|
+
return health
|
|
583
|
+
|
|
584
|
+
def _should_attempt_circuit_recovery(self) -> bool:
|
|
585
|
+
"""Check if circuit breaker should attempt recovery."""
|
|
586
|
+
if not self._enable_enhanced or self._circuit_breaker.last_failure_time is None:
|
|
587
|
+
return True
|
|
588
|
+
|
|
589
|
+
time_since_failure = time.time() - self._circuit_breaker.last_failure_time
|
|
590
|
+
return time_since_failure >= self._circuit_breaker.timeout_seconds
|
|
591
|
+
|
|
592
|
+
def _record_circuit_failure(self) -> None:
|
|
593
|
+
"""Record circuit breaker failure."""
|
|
594
|
+
if not self._enable_enhanced:
|
|
595
|
+
return
|
|
596
|
+
|
|
597
|
+
self._circuit_breaker.failure_count += 1
|
|
598
|
+
self._circuit_breaker.last_failure_time = time.time()
|
|
599
|
+
|
|
600
|
+
if self._circuit_breaker.failure_count >= self._circuit_breaker.failure_threshold:
|
|
601
|
+
if self._circuit_breaker.state != "open":
|
|
602
|
+
self._circuit_breaker.state = "open"
|
|
603
|
+
self.logger.warning(f"Circuit breaker opened for service {self.name}")
|
|
604
|
+
|
|
605
|
+
def _record_circuit_success(self) -> None:
|
|
606
|
+
"""Record circuit breaker success."""
|
|
607
|
+
if not self._enable_enhanced:
|
|
608
|
+
return
|
|
609
|
+
|
|
610
|
+
if self._circuit_breaker.state == "half_open":
|
|
611
|
+
self._circuit_breaker.failure_count = max(0, self._circuit_breaker.failure_count - 1)
|
|
612
|
+
if self._circuit_breaker.failure_count == 0:
|
|
613
|
+
self._circuit_breaker.state = "closed"
|
|
614
|
+
self.logger.info(f"Circuit breaker closed for service {self.name}")
|
|
615
|
+
elif self._circuit_breaker.state == "closed":
|
|
616
|
+
self._circuit_breaker.failure_count = max(0, self._circuit_breaker.failure_count - 1)
|
|
617
|
+
|
|
618
|
+
def _get_health_metrics(self) -> Dict[str, Any]:
|
|
619
|
+
"""Get metrics for health status."""
|
|
620
|
+
base_metrics = {
|
|
621
|
+
"uptime_seconds": int(self.uptime) if self.uptime else 0,
|
|
622
|
+
"requests_total": self._metrics.requests_total,
|
|
623
|
+
"requests_failed": self._metrics.requests_failed,
|
|
624
|
+
"response_time_avg": self._metrics.response_time_avg,
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
if self._enable_enhanced:
|
|
628
|
+
base_metrics.update({
|
|
629
|
+
"circuit_breaker_state": self._circuit_breaker.state,
|
|
630
|
+
"error_counts": self._error_counts.copy()
|
|
631
|
+
})
|
|
632
|
+
|
|
633
|
+
return base_metrics
|
|
634
|
+
|
|
635
|
+
async def _handle_error(self, error: Exception, context: Dict[str, Any]) -> None:
|
|
636
|
+
"""Handle service errors with context (enhanced feature)."""
|
|
637
|
+
if not self._enable_enhanced:
|
|
638
|
+
return
|
|
639
|
+
|
|
640
|
+
try:
|
|
641
|
+
if hasattr(self, '_error_handler') and self._error_handler:
|
|
642
|
+
await self._error_handler.handle_error(error, context)
|
|
643
|
+
else:
|
|
644
|
+
# Default error handling
|
|
645
|
+
self.logger.error(
|
|
646
|
+
f"Service error in {self.name}: {error}",
|
|
647
|
+
extra={
|
|
648
|
+
"service": self.name,
|
|
649
|
+
"context": context,
|
|
650
|
+
"traceback": traceback.format_exc()
|
|
651
|
+
}
|
|
652
|
+
)
|
|
653
|
+
except Exception as handler_error:
|
|
654
|
+
self.logger.error(f"Error handler failed: {handler_error}")
|
|
655
|
+
|
|
656
|
+
def _register_service_dependencies(self) -> None:
|
|
657
|
+
"""Register service dependencies (enhanced feature)."""
|
|
658
|
+
if not self._container:
|
|
659
|
+
return
|
|
660
|
+
|
|
661
|
+
try:
|
|
662
|
+
# Get optional dependencies from container
|
|
663
|
+
# This is a simplified version - real implementation would use interfaces
|
|
664
|
+
if hasattr(self._container, 'get_service'):
|
|
665
|
+
self._health_monitor = self._container.get_service('IHealthMonitor', None)
|
|
666
|
+
self._error_handler = self._container.get_service('IErrorHandler', None)
|
|
667
|
+
self._cache = self._container.get_service('ICacheService', None)
|
|
668
|
+
except Exception as e:
|
|
669
|
+
self.logger.warning(f"Failed to register dependencies for {self.name}: {e}")
|
|
670
|
+
|
|
671
|
+
async def _initialize_dependencies(self) -> None:
|
|
672
|
+
"""Initialize service dependencies (enhanced feature)."""
|
|
673
|
+
# Override in subclasses for specific dependency setup
|
|
674
|
+
pass
|
|
675
|
+
|
|
676
|
+
async def _register_with_health_monitor(self) -> None:
|
|
677
|
+
"""Register service with health monitor (enhanced feature)."""
|
|
678
|
+
if self._enable_enhanced and self._health_monitor:
|
|
679
|
+
try:
|
|
680
|
+
await self._health_monitor.register_service(self)
|
|
681
|
+
except Exception as e:
|
|
682
|
+
self.logger.warning(f"Failed to register with health monitor: {e}")
|
|
683
|
+
|
|
684
|
+
async def _unregister_from_health_monitor(self) -> None:
|
|
685
|
+
"""Unregister service from health monitor (enhanced feature)."""
|
|
686
|
+
if self._enable_enhanced and self._health_monitor:
|
|
687
|
+
try:
|
|
688
|
+
await self._health_monitor.unregister_service(self.name)
|
|
689
|
+
except Exception as e:
|
|
690
|
+
self.logger.warning(f"Failed to unregister from health monitor: {e}")
|
|
691
|
+
|
|
692
|
+
# Abstract methods to be implemented by subclasses
|
|
693
|
+
|
|
694
|
+
@abstractmethod
|
|
695
|
+
async def _initialize(self) -> None:
|
|
696
|
+
"""Initialize the service. Must be implemented by subclasses."""
|
|
697
|
+
pass
|
|
698
|
+
|
|
699
|
+
@abstractmethod
|
|
700
|
+
async def _cleanup(self) -> None:
|
|
701
|
+
"""Cleanup service resources. Must be implemented by subclasses."""
|
|
702
|
+
pass
|
|
703
|
+
|
|
704
|
+
async def _health_check(self) -> Dict[str, bool]:
|
|
705
|
+
"""
|
|
706
|
+
Perform custom health checks. Override in subclasses.
|
|
707
|
+
|
|
708
|
+
Returns:
|
|
709
|
+
Dictionary of check name -> success boolean
|
|
710
|
+
"""
|
|
711
|
+
return {}
|
|
712
|
+
|
|
713
|
+
async def _start_custom_tasks(self) -> Optional[List[asyncio.Task]]:
|
|
714
|
+
"""
|
|
715
|
+
Start custom background tasks. Override in subclasses.
|
|
716
|
+
|
|
717
|
+
Returns:
|
|
718
|
+
List of asyncio tasks or None
|
|
719
|
+
"""
|
|
720
|
+
return None
|
|
721
|
+
|
|
722
|
+
async def _collect_custom_metrics(self) -> None:
|
|
723
|
+
"""Collect custom metrics. Override in subclasses."""
|
|
724
|
+
pass
|
|
725
|
+
|
|
726
|
+
# Utility methods
|
|
727
|
+
|
|
728
|
+
async def run_forever(self) -> None:
|
|
729
|
+
"""Run the service until stopped."""
|
|
730
|
+
await self.start()
|
|
731
|
+
try:
|
|
732
|
+
# Wait for stop signal
|
|
733
|
+
while self._running:
|
|
734
|
+
await asyncio.sleep(1)
|
|
735
|
+
except KeyboardInterrupt:
|
|
736
|
+
self.logger.info("Received keyboard interrupt")
|
|
737
|
+
finally:
|
|
738
|
+
await self.stop()
|
|
739
|
+
|
|
740
|
+
def __repr__(self) -> str:
|
|
741
|
+
"""String representation of the service."""
|
|
742
|
+
enhanced_str = " (enhanced)" if self._enable_enhanced else ""
|
|
743
|
+
return f"<{self.__class__.__name__}(name='{self.name}', running={self._running}{enhanced_str})>"
|
|
744
|
+
|
|
745
|
+
|
|
746
|
+
# For backwards compatibility, create an alias
|
|
747
|
+
EnhancedBaseService = BaseService
|