mcp-mesh 0.5.5__py3-none-any.whl → 0.5.6__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.
- _mcp_mesh/__init__.py +1 -1
- _mcp_mesh/pipeline/mcp_heartbeat/lifespan_integration.py +13 -0
- _mcp_mesh/pipeline/mcp_startup/fastapiserver_setup.py +40 -4
- _mcp_mesh/pipeline/mcp_startup/startup_orchestrator.py +4 -154
- _mcp_mesh/shared/simple_shutdown.py +217 -0
- {mcp_mesh-0.5.5.dist-info → mcp_mesh-0.5.6.dist-info}/METADATA +1 -1
- {mcp_mesh-0.5.5.dist-info → mcp_mesh-0.5.6.dist-info}/RECORD +10 -10
- mesh/decorators.py +29 -20
- _mcp_mesh/shared/graceful_shutdown_manager.py +0 -236
- {mcp_mesh-0.5.5.dist-info → mcp_mesh-0.5.6.dist-info}/WHEEL +0 -0
- {mcp_mesh-0.5.5.dist-info → mcp_mesh-0.5.6.dist-info}/licenses/LICENSE +0 -0
_mcp_mesh/__init__.py
CHANGED
|
@@ -44,6 +44,19 @@ async def heartbeat_lifespan_task(heartbeat_config: dict[str, Any]) -> None:
|
|
|
44
44
|
|
|
45
45
|
try:
|
|
46
46
|
while True:
|
|
47
|
+
# Check if shutdown is complete before executing heartbeat
|
|
48
|
+
try:
|
|
49
|
+
from ...shared.simple_shutdown import should_stop_heartbeat
|
|
50
|
+
|
|
51
|
+
if should_stop_heartbeat():
|
|
52
|
+
logger.info(
|
|
53
|
+
f"🛑 Heartbeat stopped for agent '{agent_id}' due to shutdown"
|
|
54
|
+
)
|
|
55
|
+
break
|
|
56
|
+
except ImportError:
|
|
57
|
+
# If simple_shutdown is not available, continue normally
|
|
58
|
+
pass
|
|
59
|
+
|
|
47
60
|
try:
|
|
48
61
|
# Execute heartbeat pipeline
|
|
49
62
|
success = await heartbeat_orchestrator.execute_heartbeat(
|
|
@@ -96,6 +96,8 @@ class FastAPIServerSetupStep(PipelineStep):
|
|
|
96
96
|
result.add_error(f"Failed to wrap server '{server_key}': {e}")
|
|
97
97
|
|
|
98
98
|
# Create FastAPI application with proper FastMCP lifespan integration (AFTER wrappers)
|
|
99
|
+
# Store context for shutdown coordination
|
|
100
|
+
self._current_context = context
|
|
99
101
|
fastapi_app = self._create_fastapi_app(
|
|
100
102
|
agent_config, fastmcp_servers, heartbeat_config, mcp_wrappers
|
|
101
103
|
)
|
|
@@ -333,8 +335,24 @@ class FastAPIServerSetupStep(PipelineStep):
|
|
|
333
335
|
try:
|
|
334
336
|
yield
|
|
335
337
|
finally:
|
|
336
|
-
#
|
|
337
|
-
|
|
338
|
+
# Registry cleanup using simple shutdown
|
|
339
|
+
context = getattr(self, "_current_context", {})
|
|
340
|
+
registry_url = context.get(
|
|
341
|
+
"registry_url", "http://localhost:8001"
|
|
342
|
+
)
|
|
343
|
+
agent_id = context.get("agent_id", "unknown")
|
|
344
|
+
|
|
345
|
+
try:
|
|
346
|
+
from ...shared.simple_shutdown import (
|
|
347
|
+
_simple_shutdown_coordinator,
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
_simple_shutdown_coordinator.set_shutdown_context(
|
|
351
|
+
registry_url, agent_id
|
|
352
|
+
)
|
|
353
|
+
await _simple_shutdown_coordinator.perform_registry_cleanup()
|
|
354
|
+
except Exception as e:
|
|
355
|
+
self.logger.error(f"❌ Registry cleanup error: {e}")
|
|
338
356
|
|
|
339
357
|
# Clean up all lifespans in reverse order
|
|
340
358
|
for ctx in reversed(lifespan_contexts):
|
|
@@ -359,8 +377,24 @@ class FastAPIServerSetupStep(PipelineStep):
|
|
|
359
377
|
try:
|
|
360
378
|
yield
|
|
361
379
|
finally:
|
|
362
|
-
#
|
|
363
|
-
|
|
380
|
+
# Registry cleanup using simple shutdown
|
|
381
|
+
context = getattr(self, "_current_context", {})
|
|
382
|
+
registry_url = context.get(
|
|
383
|
+
"registry_url", "http://localhost:8001"
|
|
384
|
+
)
|
|
385
|
+
agent_id = context.get("agent_id", "unknown")
|
|
386
|
+
|
|
387
|
+
try:
|
|
388
|
+
from ...shared.simple_shutdown import (
|
|
389
|
+
_simple_shutdown_coordinator,
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
_simple_shutdown_coordinator.set_shutdown_context(
|
|
393
|
+
registry_url, agent_id
|
|
394
|
+
)
|
|
395
|
+
await _simple_shutdown_coordinator.perform_registry_cleanup()
|
|
396
|
+
except Exception as e:
|
|
397
|
+
self.logger.error(f"❌ Registry cleanup error: {e}")
|
|
364
398
|
|
|
365
399
|
primary_lifespan = graceful_shutdown_only_lifespan
|
|
366
400
|
|
|
@@ -373,6 +407,8 @@ class FastAPIServerSetupStep(PipelineStep):
|
|
|
373
407
|
lifespan=primary_lifespan,
|
|
374
408
|
)
|
|
375
409
|
|
|
410
|
+
# Registry cleanup is now integrated directly into the lifespan above
|
|
411
|
+
|
|
376
412
|
# Store app reference for global shutdown coordination
|
|
377
413
|
app.state.shutdown_step = self
|
|
378
414
|
|
|
@@ -305,10 +305,8 @@ class DebounceCoordinator:
|
|
|
305
305
|
|
|
306
306
|
except KeyboardInterrupt:
|
|
307
307
|
self.logger.info(
|
|
308
|
-
"🔴 Received KeyboardInterrupt,
|
|
308
|
+
"🔴 Received KeyboardInterrupt, shutdown will be handled by FastAPI lifespan"
|
|
309
309
|
)
|
|
310
|
-
# Perform graceful shutdown before exiting
|
|
311
|
-
self._perform_graceful_shutdown()
|
|
312
310
|
except Exception as e:
|
|
313
311
|
self.logger.error(f"❌ FastAPI server error: {e}")
|
|
314
312
|
raise
|
|
@@ -427,64 +425,7 @@ class DebounceCoordinator:
|
|
|
427
425
|
self.logger.warning(f"⚠️ Could not setup MCP heartbeat: {e}")
|
|
428
426
|
# Don't fail - heartbeat is optional for MCP agents
|
|
429
427
|
|
|
430
|
-
|
|
431
|
-
"""Perform graceful shutdown by unregistering from registry."""
|
|
432
|
-
try:
|
|
433
|
-
# Run graceful shutdown asynchronously
|
|
434
|
-
import asyncio
|
|
435
|
-
|
|
436
|
-
asyncio.run(self._graceful_shutdown_async())
|
|
437
|
-
except Exception as e:
|
|
438
|
-
self.logger.error(f"❌ Graceful shutdown failed: {e}")
|
|
439
|
-
|
|
440
|
-
async def _graceful_shutdown_async(self) -> None:
|
|
441
|
-
"""Async graceful shutdown implementation."""
|
|
442
|
-
try:
|
|
443
|
-
# Get the latest pipeline context from the orchestrator
|
|
444
|
-
if self._orchestrator is None:
|
|
445
|
-
self.logger.warning(
|
|
446
|
-
"🚨 No orchestrator available for graceful shutdown"
|
|
447
|
-
)
|
|
448
|
-
return
|
|
449
|
-
|
|
450
|
-
# Access the pipeline context through the orchestrator
|
|
451
|
-
pipeline_context = getattr(self._orchestrator.pipeline, "_last_context", {})
|
|
452
|
-
|
|
453
|
-
# Get registry configuration
|
|
454
|
-
registry_url = pipeline_context.get("registry_url")
|
|
455
|
-
agent_id = pipeline_context.get("agent_id")
|
|
456
|
-
|
|
457
|
-
if not registry_url or not agent_id:
|
|
458
|
-
self.logger.warning(
|
|
459
|
-
f"🚨 Cannot perform graceful shutdown: missing registry_url={registry_url} or agent_id={agent_id}"
|
|
460
|
-
)
|
|
461
|
-
return
|
|
462
|
-
|
|
463
|
-
# Create registry client for shutdown
|
|
464
|
-
from ...generated.mcp_mesh_registry_client.api_client import ApiClient
|
|
465
|
-
from ...generated.mcp_mesh_registry_client.configuration import (
|
|
466
|
-
Configuration,
|
|
467
|
-
)
|
|
468
|
-
from ...shared.registry_client_wrapper import RegistryClientWrapper
|
|
469
|
-
|
|
470
|
-
config = Configuration(host=registry_url)
|
|
471
|
-
api_client = ApiClient(configuration=config)
|
|
472
|
-
registry_wrapper = RegistryClientWrapper(api_client)
|
|
473
|
-
|
|
474
|
-
# Perform graceful unregistration
|
|
475
|
-
success = await registry_wrapper.unregister_agent(agent_id)
|
|
476
|
-
if success:
|
|
477
|
-
self.logger.info(
|
|
478
|
-
f"🏁 Graceful shutdown completed for agent '{agent_id}'"
|
|
479
|
-
)
|
|
480
|
-
else:
|
|
481
|
-
self.logger.warning(
|
|
482
|
-
f"⚠️ Graceful shutdown failed for agent '{agent_id}' - continuing shutdown"
|
|
483
|
-
)
|
|
484
|
-
|
|
485
|
-
except Exception as e:
|
|
486
|
-
# Don't fail the shutdown process due to unregistration errors
|
|
487
|
-
self.logger.error(f"❌ Graceful shutdown error: {e} - continuing shutdown")
|
|
428
|
+
# Graceful shutdown is now handled by FastAPI lifespan in simple_shutdown.py
|
|
488
429
|
|
|
489
430
|
def _check_auto_run_enabled(self) -> bool:
|
|
490
431
|
"""Check if auto-run is enabled (defaults to True for persistent service behavior)."""
|
|
@@ -703,9 +644,7 @@ def start_runtime() -> None:
|
|
|
703
644
|
|
|
704
645
|
logger.info("🔧 Starting MCP Mesh runtime with debouncing")
|
|
705
646
|
|
|
706
|
-
#
|
|
707
|
-
# This restores the DELETE /heartbeats call that was removed during DNS fix
|
|
708
|
-
_install_minimal_signal_handlers()
|
|
647
|
+
# Signal handlers removed - cleanup now handled by FastAPI lifespan
|
|
709
648
|
|
|
710
649
|
# Create orchestrator and set up debouncing
|
|
711
650
|
orchestrator = get_global_orchestrator()
|
|
@@ -722,96 +661,7 @@ def start_runtime() -> None:
|
|
|
722
661
|
# through the debounce coordinator
|
|
723
662
|
|
|
724
663
|
|
|
725
|
-
|
|
726
|
-
"""
|
|
727
|
-
Install minimal signal handlers that only perform graceful shutdown.
|
|
728
|
-
|
|
729
|
-
This restores the DELETE /heartbeats functionality that was removed during
|
|
730
|
-
the DNS threading fix while avoiding complex operations that could conflict
|
|
731
|
-
with DNS resolution in containerized environments.
|
|
732
|
-
"""
|
|
733
|
-
import signal
|
|
734
|
-
|
|
735
|
-
def graceful_shutdown_signal_handler(signum, frame):
|
|
736
|
-
"""Handle shutdown signals by performing graceful unregistration."""
|
|
737
|
-
try:
|
|
738
|
-
logger.info(f"🚨 Received signal {signum}, performing graceful shutdown...")
|
|
739
|
-
|
|
740
|
-
# Get the global orchestrator to access pipeline context
|
|
741
|
-
orchestrator = get_global_orchestrator()
|
|
742
|
-
if orchestrator and hasattr(orchestrator.pipeline, "_last_context"):
|
|
743
|
-
pipeline_context = orchestrator.pipeline._last_context
|
|
744
|
-
|
|
745
|
-
# Get registry configuration
|
|
746
|
-
registry_url = pipeline_context.get("registry_url")
|
|
747
|
-
agent_id = pipeline_context.get("agent_id")
|
|
748
|
-
|
|
749
|
-
if registry_url and agent_id:
|
|
750
|
-
# Perform synchronous graceful shutdown
|
|
751
|
-
import asyncio
|
|
752
|
-
|
|
753
|
-
try:
|
|
754
|
-
# Create new event loop for shutdown
|
|
755
|
-
loop = asyncio.new_event_loop()
|
|
756
|
-
asyncio.set_event_loop(loop)
|
|
757
|
-
|
|
758
|
-
# Run graceful shutdown
|
|
759
|
-
loop.run_until_complete(
|
|
760
|
-
_perform_signal_graceful_shutdown(registry_url, agent_id)
|
|
761
|
-
)
|
|
762
|
-
loop.close()
|
|
763
|
-
|
|
764
|
-
logger.info(
|
|
765
|
-
f"🏁 Graceful shutdown completed for agent '{agent_id}'"
|
|
766
|
-
)
|
|
767
|
-
except Exception as e:
|
|
768
|
-
logger.error(f"❌ Graceful shutdown error: {e}")
|
|
769
|
-
else:
|
|
770
|
-
logger.warning(
|
|
771
|
-
f"🚨 Cannot perform graceful shutdown: missing registry_url={registry_url} or agent_id={agent_id}"
|
|
772
|
-
)
|
|
773
|
-
else:
|
|
774
|
-
logger.warning(
|
|
775
|
-
"🚨 No orchestrator context available for graceful shutdown"
|
|
776
|
-
)
|
|
777
|
-
|
|
778
|
-
except Exception as e:
|
|
779
|
-
logger.error(f"❌ Signal handler error: {e}")
|
|
780
|
-
finally:
|
|
781
|
-
# Exit cleanly
|
|
782
|
-
logger.info("🛑 Exiting...")
|
|
783
|
-
sys.exit(0)
|
|
784
|
-
|
|
785
|
-
# Install handlers for common termination signals
|
|
786
|
-
signal.signal(signal.SIGTERM, graceful_shutdown_signal_handler)
|
|
787
|
-
signal.signal(signal.SIGINT, graceful_shutdown_signal_handler)
|
|
788
|
-
|
|
789
|
-
logger.debug("🛡️ Minimal signal handlers installed for graceful shutdown")
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
async def _perform_signal_graceful_shutdown(registry_url: str, agent_id: str) -> None:
|
|
793
|
-
"""Perform graceful shutdown from signal handler context."""
|
|
794
|
-
try:
|
|
795
|
-
# Create registry client for shutdown
|
|
796
|
-
from ...generated.mcp_mesh_registry_client.api_client import ApiClient
|
|
797
|
-
from ...generated.mcp_mesh_registry_client.configuration import Configuration
|
|
798
|
-
from ...shared.registry_client_wrapper import RegistryClientWrapper
|
|
799
|
-
|
|
800
|
-
config = Configuration(host=registry_url)
|
|
801
|
-
api_client = ApiClient(configuration=config)
|
|
802
|
-
registry_wrapper = RegistryClientWrapper(api_client)
|
|
803
|
-
|
|
804
|
-
# Perform graceful unregistration (this sends DELETE /heartbeats)
|
|
805
|
-
success = await registry_wrapper.unregister_agent(agent_id)
|
|
806
|
-
if success:
|
|
807
|
-
logger.info(
|
|
808
|
-
f"✅ Agent '{agent_id}' successfully unregistered from registry"
|
|
809
|
-
)
|
|
810
|
-
else:
|
|
811
|
-
logger.warning(f"⚠️ Failed to unregister agent '{agent_id}' from registry")
|
|
812
|
-
|
|
813
|
-
except Exception as e:
|
|
814
|
-
logger.error(f"❌ Signal graceful shutdown error: {e}")
|
|
664
|
+
# Signal handlers removed - cleanup now handled by FastAPI lifespan in simple_shutdown.py
|
|
815
665
|
|
|
816
666
|
|
|
817
667
|
# Minimal signal handlers restored to provide graceful shutdown with DELETE /heartbeats
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Simple shutdown coordination for MCP Mesh agents.
|
|
3
|
+
|
|
4
|
+
Provides clean shutdown via FastAPI lifespan events and basic signal handling.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import asyncio
|
|
8
|
+
import logging
|
|
9
|
+
import signal
|
|
10
|
+
from contextlib import asynccontextmanager
|
|
11
|
+
from typing import Any, Optional
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class SimpleShutdownCoordinator:
|
|
17
|
+
"""Lightweight shutdown coordination using FastAPI lifespan."""
|
|
18
|
+
|
|
19
|
+
def __init__(self):
|
|
20
|
+
self._shutdown_requested = False
|
|
21
|
+
self._registry_url: Optional[str] = None
|
|
22
|
+
self._agent_id: Optional[str] = None
|
|
23
|
+
self._shutdown_complete = False # Flag to prevent race conditions
|
|
24
|
+
|
|
25
|
+
def set_shutdown_context(self, registry_url: str, agent_id: str) -> None:
|
|
26
|
+
"""Set context for shutdown cleanup."""
|
|
27
|
+
self._registry_url = registry_url
|
|
28
|
+
self._agent_id = agent_id
|
|
29
|
+
logger.debug(
|
|
30
|
+
f"🔧 Shutdown context set: agent_id={agent_id}, registry_url={registry_url}"
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
def install_signal_handlers(self) -> None:
|
|
34
|
+
"""Install minimal signal handlers as backup."""
|
|
35
|
+
|
|
36
|
+
def shutdown_signal_handler(signum, frame):
|
|
37
|
+
# Avoid logging in signal handler to prevent reentrant call issues
|
|
38
|
+
self._shutdown_requested = True
|
|
39
|
+
|
|
40
|
+
signal.signal(signal.SIGINT, shutdown_signal_handler)
|
|
41
|
+
signal.signal(signal.SIGTERM, shutdown_signal_handler)
|
|
42
|
+
logger.debug("📡 Signal handlers installed")
|
|
43
|
+
|
|
44
|
+
def is_shutdown_requested(self) -> bool:
|
|
45
|
+
"""Check if shutdown was requested via signal."""
|
|
46
|
+
return self._shutdown_requested
|
|
47
|
+
|
|
48
|
+
def is_shutdown_complete(self) -> bool:
|
|
49
|
+
"""Check if shutdown cleanup is complete."""
|
|
50
|
+
return self._shutdown_complete
|
|
51
|
+
|
|
52
|
+
def mark_shutdown_complete(self) -> None:
|
|
53
|
+
"""Mark shutdown cleanup as complete to prevent further operations."""
|
|
54
|
+
self._shutdown_complete = True
|
|
55
|
+
logger.debug("🏁 Shutdown marked as complete")
|
|
56
|
+
|
|
57
|
+
async def perform_registry_cleanup(self) -> None:
|
|
58
|
+
"""Perform registry cleanup by calling DELETE /agents/{agent_id}."""
|
|
59
|
+
# Try to get the actual agent_id from DecoratorRegistry if available
|
|
60
|
+
actual_agent_id = self._agent_id
|
|
61
|
+
try:
|
|
62
|
+
from _mcp_mesh.engine.decorator_registry import DecoratorRegistry
|
|
63
|
+
|
|
64
|
+
agent_config = DecoratorRegistry.get_resolved_agent_config()
|
|
65
|
+
if agent_config and "agent_id" in agent_config:
|
|
66
|
+
resolved_agent_id = agent_config["agent_id"]
|
|
67
|
+
if resolved_agent_id and resolved_agent_id != "unknown":
|
|
68
|
+
actual_agent_id = resolved_agent_id
|
|
69
|
+
logger.debug(
|
|
70
|
+
f"🔧 Using resolved agent_id from DecoratorRegistry: {actual_agent_id}"
|
|
71
|
+
)
|
|
72
|
+
except Exception as e:
|
|
73
|
+
logger.debug(f"Could not get agent_id from DecoratorRegistry: {e}")
|
|
74
|
+
|
|
75
|
+
if (
|
|
76
|
+
not self._registry_url
|
|
77
|
+
or not actual_agent_id
|
|
78
|
+
or actual_agent_id == "unknown"
|
|
79
|
+
):
|
|
80
|
+
logger.warning(
|
|
81
|
+
f"⚠️ Missing registry URL or agent ID for cleanup: registry_url={self._registry_url}, agent_id={actual_agent_id}"
|
|
82
|
+
)
|
|
83
|
+
return
|
|
84
|
+
|
|
85
|
+
try:
|
|
86
|
+
from _mcp_mesh.generated.mcp_mesh_registry_client.api_client import (
|
|
87
|
+
ApiClient,
|
|
88
|
+
)
|
|
89
|
+
from _mcp_mesh.generated.mcp_mesh_registry_client.configuration import (
|
|
90
|
+
Configuration,
|
|
91
|
+
)
|
|
92
|
+
from _mcp_mesh.shared.registry_client_wrapper import RegistryClientWrapper
|
|
93
|
+
|
|
94
|
+
config = Configuration(host=self._registry_url)
|
|
95
|
+
api_client = ApiClient(configuration=config)
|
|
96
|
+
registry_wrapper = RegistryClientWrapper(api_client)
|
|
97
|
+
|
|
98
|
+
success = await registry_wrapper.unregister_agent(actual_agent_id)
|
|
99
|
+
if success:
|
|
100
|
+
logger.info(f"✅ Agent '{actual_agent_id}' unregistered from registry")
|
|
101
|
+
self.mark_shutdown_complete()
|
|
102
|
+
else:
|
|
103
|
+
logger.warning(f"⚠️ Failed to unregister agent '{actual_agent_id}'")
|
|
104
|
+
self.mark_shutdown_complete() # Mark complete even on failure to prevent loops
|
|
105
|
+
|
|
106
|
+
except Exception as e:
|
|
107
|
+
logger.error(f"❌ Registry cleanup error: {e}")
|
|
108
|
+
self.mark_shutdown_complete() # Mark complete even on error to prevent loops
|
|
109
|
+
|
|
110
|
+
def create_shutdown_lifespan(self, original_lifespan=None):
|
|
111
|
+
"""Create lifespan function that includes registry cleanup."""
|
|
112
|
+
|
|
113
|
+
@asynccontextmanager
|
|
114
|
+
async def shutdown_lifespan(app):
|
|
115
|
+
# Startup phase
|
|
116
|
+
if original_lifespan:
|
|
117
|
+
# If user had a lifespan, run their startup code
|
|
118
|
+
async with original_lifespan(app):
|
|
119
|
+
yield
|
|
120
|
+
else:
|
|
121
|
+
yield
|
|
122
|
+
|
|
123
|
+
# Shutdown phase
|
|
124
|
+
logger.info("🔄 FastAPI shutdown initiated, performing registry cleanup...")
|
|
125
|
+
await self.perform_registry_cleanup()
|
|
126
|
+
logger.info("🏁 Registry cleanup completed")
|
|
127
|
+
|
|
128
|
+
return shutdown_lifespan
|
|
129
|
+
|
|
130
|
+
def inject_shutdown_lifespan(self, app, registry_url: str, agent_id: str) -> None:
|
|
131
|
+
"""Inject shutdown lifespan into FastAPI app."""
|
|
132
|
+
self.set_shutdown_context(registry_url, agent_id)
|
|
133
|
+
|
|
134
|
+
# Store original lifespan if it exists
|
|
135
|
+
original_lifespan = getattr(app, "router", {}).get("lifespan", None)
|
|
136
|
+
|
|
137
|
+
# Replace with our shutdown-aware lifespan
|
|
138
|
+
new_lifespan = self.create_shutdown_lifespan(original_lifespan)
|
|
139
|
+
app.router.lifespan = new_lifespan
|
|
140
|
+
|
|
141
|
+
logger.info(f"🔌 Shutdown lifespan injected for agent '{agent_id}'")
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
# Global instance
|
|
145
|
+
_simple_shutdown_coordinator = SimpleShutdownCoordinator()
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def inject_shutdown_lifespan(app, registry_url: str, agent_id: str) -> None:
|
|
149
|
+
"""Inject shutdown lifespan into FastAPI app (module-level function)."""
|
|
150
|
+
_simple_shutdown_coordinator.inject_shutdown_lifespan(app, registry_url, agent_id)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def install_signal_handlers() -> None:
|
|
154
|
+
"""Install signal handlers (module-level function)."""
|
|
155
|
+
_simple_shutdown_coordinator.install_signal_handlers()
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def should_stop_heartbeat() -> bool:
|
|
159
|
+
"""Check if heartbeat should stop due to shutdown."""
|
|
160
|
+
return _simple_shutdown_coordinator.is_shutdown_complete()
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def start_blocking_loop_with_shutdown_support(thread) -> None:
|
|
164
|
+
"""
|
|
165
|
+
Keep main thread alive while uvicorn in the thread handles requests.
|
|
166
|
+
|
|
167
|
+
Install signal handlers in main thread for proper registry cleanup since
|
|
168
|
+
signals to threads can be unreliable for FastAPI lifespan shutdown.
|
|
169
|
+
"""
|
|
170
|
+
logger.info("🔒 MAIN THREAD: Installing signal handlers for registry cleanup")
|
|
171
|
+
|
|
172
|
+
# Install signal handlers for proper registry cleanup
|
|
173
|
+
_simple_shutdown_coordinator.install_signal_handlers()
|
|
174
|
+
|
|
175
|
+
logger.info(
|
|
176
|
+
"🔒 MAIN THREAD: Waiting for uvicorn thread - signals handled by main thread"
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
try:
|
|
180
|
+
# Wait for thread while handling signals in main thread
|
|
181
|
+
while thread.is_alive():
|
|
182
|
+
thread.join(timeout=1.0)
|
|
183
|
+
|
|
184
|
+
# Check if shutdown was requested via signal
|
|
185
|
+
if _simple_shutdown_coordinator.is_shutdown_requested():
|
|
186
|
+
logger.info(
|
|
187
|
+
"🔄 MAIN THREAD: Shutdown requested, performing registry cleanup..."
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
# Perform registry cleanup in main thread
|
|
191
|
+
import asyncio
|
|
192
|
+
|
|
193
|
+
try:
|
|
194
|
+
# Run cleanup in main thread
|
|
195
|
+
asyncio.run(_simple_shutdown_coordinator.perform_registry_cleanup())
|
|
196
|
+
except Exception as e:
|
|
197
|
+
logger.error(f"❌ Registry cleanup error: {e}")
|
|
198
|
+
|
|
199
|
+
logger.info("🏁 MAIN THREAD: Registry cleanup completed, exiting")
|
|
200
|
+
break
|
|
201
|
+
|
|
202
|
+
except KeyboardInterrupt:
|
|
203
|
+
logger.info(
|
|
204
|
+
"🔄 MAIN THREAD: KeyboardInterrupt received, performing registry cleanup..."
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
# Perform registry cleanup on Ctrl+C
|
|
208
|
+
import asyncio
|
|
209
|
+
|
|
210
|
+
try:
|
|
211
|
+
asyncio.run(_simple_shutdown_coordinator.perform_registry_cleanup())
|
|
212
|
+
except Exception as e:
|
|
213
|
+
logger.error(f"❌ Registry cleanup error: {e}")
|
|
214
|
+
|
|
215
|
+
logger.info("🏁 MAIN THREAD: Registry cleanup completed")
|
|
216
|
+
|
|
217
|
+
logger.info("🏁 MAIN THREAD: Uvicorn thread completed")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mcp-mesh
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.6
|
|
4
4
|
Summary: Kubernetes-native platform for distributed MCP applications
|
|
5
5
|
Project-URL: Homepage, https://github.com/dhyansraj/mcp-mesh
|
|
6
6
|
Project-URL: Documentation, https://github.com/dhyansraj/mcp-mesh/tree/main/docs
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
_mcp_mesh/__init__.py,sha256=
|
|
1
|
+
_mcp_mesh/__init__.py,sha256=uw4Yj9HoBfp-_CyH8izKGYKrrCI-dF9uwMLMNgkIMN0,2719
|
|
2
2
|
_mcp_mesh/engine/__init__.py,sha256=2ennzbo7yJcpkXO9BqN69TruLjJfmJY4Y5VEsG644K4,3630
|
|
3
3
|
_mcp_mesh/engine/async_mcp_client.py,sha256=UcbQjxtgVfeRw6DHTZhAzN1gkcKlTg-lUPEePRPQWAU,6306
|
|
4
4
|
_mcp_mesh/engine/decorator_registry.py,sha256=0-GI7DILpbB6GRLIYhZu0DcdHEA62d89jDYxkK1ywiM,22422
|
|
@@ -78,17 +78,17 @@ _mcp_mesh/pipeline/mcp_heartbeat/fast_heartbeat_check.py,sha256=QTzYdL81WERkOaBV
|
|
|
78
78
|
_mcp_mesh/pipeline/mcp_heartbeat/heartbeat_orchestrator.py,sha256=uB9o298X7GbOaKUw4ij_fUAOCpK0n2brx_oWqWGTXFY,11296
|
|
79
79
|
_mcp_mesh/pipeline/mcp_heartbeat/heartbeat_pipeline.py,sha256=Jb7EVJO13trUVO3aCSgzGqAtoc4vie5kDrYLZtOkiXg,11601
|
|
80
80
|
_mcp_mesh/pipeline/mcp_heartbeat/heartbeat_send.py,sha256=ydVx-Vb_RgW1WPCboHVdZfEwNbgDngFV6Y9elZIBrAw,3602
|
|
81
|
-
_mcp_mesh/pipeline/mcp_heartbeat/lifespan_integration.py,sha256=
|
|
81
|
+
_mcp_mesh/pipeline/mcp_heartbeat/lifespan_integration.py,sha256=4XPPlaJ6rz-FkDO3bxzVxHmVF-aq1CCaTW4vIBXrB0c,3016
|
|
82
82
|
_mcp_mesh/pipeline/mcp_heartbeat/registry_connection.py,sha256=4abbOKN3echwX82PV0RvxF6cJZUu0pMgisOpILZ_ZzY,2875
|
|
83
83
|
_mcp_mesh/pipeline/mcp_startup/__init__.py,sha256=gS0xNmVx66bkLUMw64olMsN40ZLPH3ymwlLixZ4NuTs,1239
|
|
84
84
|
_mcp_mesh/pipeline/mcp_startup/configuration.py,sha256=6LRLIxrqFMU76qrBb6GjGknUlKPZZ9iqOlxE7F9ZhLs,2808
|
|
85
85
|
_mcp_mesh/pipeline/mcp_startup/decorator_collection.py,sha256=RHC6MHtfP9aP0hZ-IJjISZu72e0Pml3LU0qr7dc284w,2294
|
|
86
|
-
_mcp_mesh/pipeline/mcp_startup/fastapiserver_setup.py,sha256=
|
|
86
|
+
_mcp_mesh/pipeline/mcp_startup/fastapiserver_setup.py,sha256=ktCn1IB8J3Iz7T0iUJF_ytwwO_RRbJN3dNQ6qZLY6iI,40229
|
|
87
87
|
_mcp_mesh/pipeline/mcp_startup/fastmcpserver_discovery.py,sha256=ktsE9EZYdyZbCtCKB6HVdzGFMQ0E9n0-7I55LRO99sE,10270
|
|
88
88
|
_mcp_mesh/pipeline/mcp_startup/heartbeat_loop.py,sha256=v85B0ynomvYu87eIvLe-aSZ7-Iwov2VtM4Fg3PkmrZs,3865
|
|
89
89
|
_mcp_mesh/pipeline/mcp_startup/heartbeat_preparation.py,sha256=v3Fl0PvW5s7Ib_Cy7WtXA7gDvsFGiz54a-IlQRTcLPg,10410
|
|
90
90
|
_mcp_mesh/pipeline/mcp_startup/server_discovery.py,sha256=i3t12Dd2nEg3nbifyMGvm2SYr3WYiYJbicBakS3ZeuM,8007
|
|
91
|
-
_mcp_mesh/pipeline/mcp_startup/startup_orchestrator.py,sha256=
|
|
91
|
+
_mcp_mesh/pipeline/mcp_startup/startup_orchestrator.py,sha256=KldSG3xfGNm0iexnCMPkDsi3nuIVXBwneDBuoT5gJO4,26756
|
|
92
92
|
_mcp_mesh/pipeline/mcp_startup/startup_pipeline.py,sha256=Y_VeZcvyT3y9phWtGD7cX92NzKZIzF2J6kRJUO8q-9U,2291
|
|
93
93
|
_mcp_mesh/pipeline/shared/__init__.py,sha256=s9xmdf6LkoetrVRGd7Zp3NUxcJCW6YZ_yNKzUBcnYys,352
|
|
94
94
|
_mcp_mesh/pipeline/shared/base_step.py,sha256=kyPbNUX79NyGrE_0Q-e-Aek7m1J0TW036njWfv0iZ0I,1080
|
|
@@ -101,11 +101,11 @@ _mcp_mesh/shared/content_extractor.py,sha256=culjhieFl_J6EMDv1VFKvS38O3IMhWMs8fH
|
|
|
101
101
|
_mcp_mesh/shared/defaults.py,sha256=5qazybkn7QHi418dXCS0b6QlNQl3DMg97ItzNGkc8d4,1851
|
|
102
102
|
_mcp_mesh/shared/fast_heartbeat_status.py,sha256=OquEsX9ZTbxY1lIsll0Mbb2KDzSJD76sLMOlr3Z73Sc,5188
|
|
103
103
|
_mcp_mesh/shared/fastapi_middleware_manager.py,sha256=_h10dSL9mgQstpJW_ZM2cpkU6yTKaYKlZaKXMk2i6IA,14638
|
|
104
|
-
_mcp_mesh/shared/graceful_shutdown_manager.py,sha256=wH7iz9TNDr-lMnmvQH6UFSu-VrG6WgHB47Cu98icnGk,9239
|
|
105
104
|
_mcp_mesh/shared/host_resolver.py,sha256=ycs6gXnI1zJX5KiqiLJPX5GkHX8r4j8NMHQOlG2J2X8,2964
|
|
106
105
|
_mcp_mesh/shared/logging_config.py,sha256=n9AqShZ5BZgyrkoTlvux6ECRVpM9dUYvmGB0NPMl-Ak,2477
|
|
107
106
|
_mcp_mesh/shared/registry_client_wrapper.py,sha256=d8yL-MiCrQr_WYdRFStOd531qaLv9kZjh0zJAmCJ-Cc,16976
|
|
108
107
|
_mcp_mesh/shared/server_discovery.py,sha256=W5nsN-GvEVFD-7XkbMTxh-9FUIEiyWOxP3GYr8GNi3E,13142
|
|
108
|
+
_mcp_mesh/shared/simple_shutdown.py,sha256=jnF1rTR2yR619LZnEjNlu-ZdKlB3PovxKqG0VZ3HDgE,8319
|
|
109
109
|
_mcp_mesh/shared/sse_parser.py,sha256=OEPnfL9xL3rsjQrbyvfUO82WljPSDeO6Z61uUwN1NAo,8035
|
|
110
110
|
_mcp_mesh/shared/support_types.py,sha256=k-ICF_UwDkHxQ1D5LwFZrp-UrNb4E5dzw02CRuLW9iI,7264
|
|
111
111
|
_mcp_mesh/tracing/agent_context_helper.py,sha256=BIJ3Kc4Znd6emMAu97aUhSoxSIza3qYUmObLgc9ONiA,4765
|
|
@@ -116,9 +116,9 @@ _mcp_mesh/tracing/redis_metadata_publisher.py,sha256=F78E34qnI3D0tOmbHUTBsLbDst2
|
|
|
116
116
|
_mcp_mesh/tracing/trace_context_helper.py,sha256=6tEkwjWFqMBe45zBlhacktmIpzJWTF950ph3bwL3cNc,5994
|
|
117
117
|
_mcp_mesh/tracing/utils.py,sha256=t9lJuTH7CeuzAiiAaD0WxsJMFJPdzZFR0w6-vyR9f2E,3849
|
|
118
118
|
mesh/__init__.py,sha256=l5RSMV8Kx0h7cvku8YkZPbTHjEPWciGT0bcEB2O_eNU,3242
|
|
119
|
-
mesh/decorators.py,sha256=
|
|
119
|
+
mesh/decorators.py,sha256=oBWoRE-FA3qUGygAUtk3-eAYBckwTGfTzvXOgCag4ys,36678
|
|
120
120
|
mesh/types.py,sha256=g37DXAzya-xGPa1_WKlW3T3_VqyTn8ZVepIDSrhBTkc,10815
|
|
121
|
-
mcp_mesh-0.5.
|
|
122
|
-
mcp_mesh-0.5.
|
|
123
|
-
mcp_mesh-0.5.
|
|
124
|
-
mcp_mesh-0.5.
|
|
121
|
+
mcp_mesh-0.5.6.dist-info/METADATA,sha256=Mh8nwL5wjm53WMFPOqcVHCRmcvbPa4sgMmX7bQV-yiw,4879
|
|
122
|
+
mcp_mesh-0.5.6.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
123
|
+
mcp_mesh-0.5.6.dist-info/licenses/LICENSE,sha256=_EBQHRQThv9FPOLc5eFOUdeeRO0mYwChC7cx60dM1tM,1078
|
|
124
|
+
mcp_mesh-0.5.6.dist-info/RECORD,,
|
mesh/decorators.py
CHANGED
|
@@ -12,6 +12,7 @@ from typing import Any, TypeVar
|
|
|
12
12
|
# Import from _mcp_mesh for registry and runtime integration
|
|
13
13
|
from _mcp_mesh.engine.decorator_registry import DecoratorRegistry
|
|
14
14
|
from _mcp_mesh.shared.config_resolver import ValidationRule, get_config_value
|
|
15
|
+
from _mcp_mesh.shared.simple_shutdown import start_blocking_loop_with_shutdown_support
|
|
15
16
|
|
|
16
17
|
logger = logging.getLogger(__name__)
|
|
17
18
|
|
|
@@ -98,50 +99,49 @@ def _start_uvicorn_immediately(http_host: str, http_port: int):
|
|
|
98
99
|
f"🚀 IMMEDIATE UVICORN: Starting uvicorn server on {http_host}:{port}"
|
|
99
100
|
)
|
|
100
101
|
|
|
101
|
-
#
|
|
102
|
-
# This prevents the dual event loop conflict
|
|
102
|
+
# Use uvicorn.run() for proper signal handling (enables FastAPI lifespan shutdown)
|
|
103
103
|
logger.info(
|
|
104
|
-
"⚡ IMMEDIATE UVICORN:
|
|
105
|
-
)
|
|
106
|
-
|
|
107
|
-
config = uvicorn.Config(app=app, host=http_host, port=port, log_level="info")
|
|
108
|
-
server = uvicorn.Server(config)
|
|
109
|
-
|
|
110
|
-
# DON'T start the server here - let the pipeline handle the event loop
|
|
111
|
-
logger.info(
|
|
112
|
-
"✅ IMMEDIATE UVICORN: Uvicorn server configured (will be started by pipeline)"
|
|
104
|
+
"⚡ IMMEDIATE UVICORN: Starting server with uvicorn.run() for proper signal handling"
|
|
113
105
|
)
|
|
114
106
|
|
|
115
107
|
# Start uvicorn server in background thread (NON-daemon to keep process alive)
|
|
116
108
|
def run_server():
|
|
117
|
-
"""Run uvicorn server in background thread
|
|
109
|
+
"""Run uvicorn server in background thread with proper signal handling."""
|
|
118
110
|
try:
|
|
119
111
|
logger.info(
|
|
120
112
|
f"🌟 IMMEDIATE UVICORN: Starting server on {http_host}:{port}"
|
|
121
113
|
)
|
|
122
|
-
|
|
114
|
+
# Use uvicorn.run() instead of Server().run() for proper signal handling
|
|
115
|
+
uvicorn.run(
|
|
116
|
+
app,
|
|
117
|
+
host=http_host,
|
|
118
|
+
port=port,
|
|
119
|
+
log_level="info",
|
|
120
|
+
timeout_graceful_shutdown=30, # Allow time for registry cleanup
|
|
121
|
+
access_log=False, # Reduce noise
|
|
122
|
+
)
|
|
123
123
|
except Exception as e:
|
|
124
124
|
logger.error(f"❌ IMMEDIATE UVICORN: Server failed: {e}")
|
|
125
125
|
import traceback
|
|
126
126
|
|
|
127
127
|
logger.error(f"Server traceback: {traceback.format_exc()}")
|
|
128
128
|
|
|
129
|
-
# Start server in daemon thread
|
|
130
|
-
thread = threading.Thread(target=run_server, daemon=
|
|
129
|
+
# Start server in non-daemon thread so it can handle signals properly
|
|
130
|
+
thread = threading.Thread(target=run_server, daemon=False)
|
|
131
131
|
thread.start()
|
|
132
132
|
|
|
133
133
|
logger.info(
|
|
134
|
-
"🔒 IMMEDIATE UVICORN: Server thread started (daemon=
|
|
134
|
+
"🔒 IMMEDIATE UVICORN: Server thread started (daemon=False) - can handle signals"
|
|
135
135
|
)
|
|
136
136
|
|
|
137
137
|
# Store server reference in DecoratorRegistry BEFORE starting (critical timing)
|
|
138
138
|
server_info = {
|
|
139
139
|
"app": app,
|
|
140
|
-
"server":
|
|
141
|
-
"config":
|
|
140
|
+
"server": None, # No server object with uvicorn.run()
|
|
141
|
+
"config": None, # No config object needed
|
|
142
142
|
"host": http_host,
|
|
143
143
|
"port": port,
|
|
144
|
-
"thread": thread, # Server thread (daemon)
|
|
144
|
+
"thread": thread, # Server thread (non-daemon)
|
|
145
145
|
"type": "immediate_uvicorn_running",
|
|
146
146
|
"status": "running", # Server is now running in background thread
|
|
147
147
|
}
|
|
@@ -162,9 +162,18 @@ def _start_uvicorn_immediately(http_host: str, http_port: int):
|
|
|
162
162
|
f"✅ IMMEDIATE UVICORN: Uvicorn server running on {http_host}:{port} (daemon thread)"
|
|
163
163
|
)
|
|
164
164
|
|
|
165
|
+
# Set up registry context for shutdown cleanup (use defaults initially)
|
|
166
|
+
import os
|
|
167
|
+
|
|
168
|
+
from _mcp_mesh.shared.simple_shutdown import _simple_shutdown_coordinator
|
|
169
|
+
|
|
170
|
+
registry_url = os.getenv("MCP_MESH_REGISTRY_URL", "http://localhost:8000")
|
|
171
|
+
agent_id = "unknown" # Will be updated by pipeline when available
|
|
172
|
+
_simple_shutdown_coordinator.set_shutdown_context(registry_url, agent_id)
|
|
173
|
+
|
|
165
174
|
# CRITICAL FIX: Keep main thread alive to prevent shutdown state
|
|
166
175
|
# This matches the working test setup pattern that prevents DNS resolution failures
|
|
167
|
-
# Uses
|
|
176
|
+
# Uses simple shutdown with signal handlers for clean registry cleanup
|
|
168
177
|
start_blocking_loop_with_shutdown_support(thread)
|
|
169
178
|
|
|
170
179
|
except Exception as e:
|
|
@@ -1,236 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Graceful Shutdown Manager for MCP Mesh agents.
|
|
3
|
-
|
|
4
|
-
This utility class manages graceful shutdown functionality, including signal handlers,
|
|
5
|
-
shutdown context management, and coordination with FastAPI lifecycle and registry.
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
|
-
import asyncio
|
|
9
|
-
import logging
|
|
10
|
-
import os
|
|
11
|
-
import signal
|
|
12
|
-
import sys
|
|
13
|
-
from typing import Any, Optional
|
|
14
|
-
|
|
15
|
-
logger = logging.getLogger(__name__)
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
class GracefulShutdownManager:
|
|
19
|
-
"""Manages graceful shutdown for MCP Mesh agents."""
|
|
20
|
-
|
|
21
|
-
def __init__(self):
|
|
22
|
-
self._shutdown_requested: bool = False
|
|
23
|
-
self._shutdown_context: dict[str, Any] = {}
|
|
24
|
-
|
|
25
|
-
def set_shutdown_context(self, context: dict[str, Any]) -> None:
|
|
26
|
-
"""Set context for graceful shutdown (called from pipeline)."""
|
|
27
|
-
self._shutdown_context.update(context)
|
|
28
|
-
|
|
29
|
-
# Extract FastAPI app from context for coordinated shutdown
|
|
30
|
-
fastapi_app = context.get("fastapi_app")
|
|
31
|
-
if fastapi_app:
|
|
32
|
-
self._shutdown_context["fastapi_app"] = fastapi_app
|
|
33
|
-
logger.debug(
|
|
34
|
-
f"🔧 FastAPI app stored for coordinated shutdown: {type(fastapi_app)}"
|
|
35
|
-
)
|
|
36
|
-
|
|
37
|
-
logger.debug(
|
|
38
|
-
f"🔧 Shutdown context updated: agent_id={context.get('agent_id')}, registry_url={context.get('registry_url')}"
|
|
39
|
-
)
|
|
40
|
-
|
|
41
|
-
def install_signal_handlers(self) -> None:
|
|
42
|
-
"""Install signal handlers that set the shutdown flag."""
|
|
43
|
-
|
|
44
|
-
def shutdown_signal_handler(signum, frame):
|
|
45
|
-
"""Handle shutdown signals by setting the shutdown flag."""
|
|
46
|
-
logger.info(
|
|
47
|
-
f"🚨 SIGNAL HANDLER: Received signal {signum}, setting shutdown flag"
|
|
48
|
-
)
|
|
49
|
-
self._shutdown_requested = True
|
|
50
|
-
|
|
51
|
-
# Install handlers for common termination signals
|
|
52
|
-
signal.signal(signal.SIGTERM, shutdown_signal_handler)
|
|
53
|
-
signal.signal(signal.SIGINT, shutdown_signal_handler)
|
|
54
|
-
|
|
55
|
-
logger.info("🛡️ Signal handlers installed for graceful shutdown")
|
|
56
|
-
|
|
57
|
-
def is_shutdown_requested(self) -> bool:
|
|
58
|
-
"""Check if shutdown has been requested."""
|
|
59
|
-
return self._shutdown_requested
|
|
60
|
-
|
|
61
|
-
def perform_graceful_shutdown_from_main_thread(self) -> None:
|
|
62
|
-
"""Perform graceful shutdown from main thread (non-async context)."""
|
|
63
|
-
try:
|
|
64
|
-
# Check if we have FastAPI app for coordinated shutdown
|
|
65
|
-
fastapi_app = self._shutdown_context.get("fastapi_app")
|
|
66
|
-
|
|
67
|
-
if (
|
|
68
|
-
fastapi_app
|
|
69
|
-
and hasattr(fastapi_app, "state")
|
|
70
|
-
and hasattr(fastapi_app.state, "shutdown_step")
|
|
71
|
-
):
|
|
72
|
-
# Use FastAPI lifespan shutdown mechanism
|
|
73
|
-
logger.info("🚨 Triggering coordinated FastAPI shutdown...")
|
|
74
|
-
shutdown_step = fastapi_app.state.shutdown_step
|
|
75
|
-
|
|
76
|
-
# Create minimal context and call the existing graceful shutdown
|
|
77
|
-
loop = asyncio.new_event_loop()
|
|
78
|
-
asyncio.set_event_loop(loop)
|
|
79
|
-
|
|
80
|
-
try:
|
|
81
|
-
loop.run_until_complete(
|
|
82
|
-
shutdown_step._graceful_shutdown(fastapi_app)
|
|
83
|
-
)
|
|
84
|
-
logger.info("✅ FastAPI coordinated shutdown completed")
|
|
85
|
-
return
|
|
86
|
-
finally:
|
|
87
|
-
loop.close()
|
|
88
|
-
|
|
89
|
-
# Fallback: Direct registry call if no FastAPI coordination available
|
|
90
|
-
logger.info("🚨 Using fallback direct registry shutdown...")
|
|
91
|
-
|
|
92
|
-
# Get registry_url from context or environment (with default)
|
|
93
|
-
registry_url = self._shutdown_context.get("registry_url")
|
|
94
|
-
if not registry_url:
|
|
95
|
-
registry_url = os.getenv(
|
|
96
|
-
"MCP_MESH_REGISTRY_URL", "http://localhost:8000"
|
|
97
|
-
)
|
|
98
|
-
logger.debug(
|
|
99
|
-
f"🔧 Using registry URL from environment/default: {registry_url}"
|
|
100
|
-
)
|
|
101
|
-
|
|
102
|
-
# Get agent_id from context or generate the same one used for registration
|
|
103
|
-
agent_id = self._shutdown_context.get("agent_id")
|
|
104
|
-
if not agent_id:
|
|
105
|
-
agent_id = self._get_or_create_agent_id()
|
|
106
|
-
logger.debug(f"🔧 Using agent ID from shared state: {agent_id}")
|
|
107
|
-
|
|
108
|
-
if not registry_url or not agent_id:
|
|
109
|
-
logger.warning(
|
|
110
|
-
f"⚠️ Cannot perform graceful shutdown: missing registry_url={registry_url} or agent_id={agent_id}"
|
|
111
|
-
)
|
|
112
|
-
return
|
|
113
|
-
|
|
114
|
-
# Create registry client and perform shutdown synchronously
|
|
115
|
-
loop = asyncio.new_event_loop()
|
|
116
|
-
asyncio.set_event_loop(loop)
|
|
117
|
-
|
|
118
|
-
try:
|
|
119
|
-
loop.run_until_complete(
|
|
120
|
-
self._perform_graceful_shutdown_async(registry_url, agent_id)
|
|
121
|
-
)
|
|
122
|
-
finally:
|
|
123
|
-
loop.close()
|
|
124
|
-
|
|
125
|
-
except Exception as e:
|
|
126
|
-
logger.error(f"❌ Graceful shutdown error: {e}")
|
|
127
|
-
|
|
128
|
-
async def _perform_graceful_shutdown_async(
|
|
129
|
-
self, registry_url: str, agent_id: str
|
|
130
|
-
) -> None:
|
|
131
|
-
"""Async graceful shutdown implementation."""
|
|
132
|
-
try:
|
|
133
|
-
# Create registry client for shutdown
|
|
134
|
-
from _mcp_mesh.generated.mcp_mesh_registry_client.api_client import (
|
|
135
|
-
ApiClient,
|
|
136
|
-
)
|
|
137
|
-
from _mcp_mesh.generated.mcp_mesh_registry_client.configuration import (
|
|
138
|
-
Configuration,
|
|
139
|
-
)
|
|
140
|
-
from _mcp_mesh.shared.registry_client_wrapper import RegistryClientWrapper
|
|
141
|
-
|
|
142
|
-
config = Configuration(host=registry_url)
|
|
143
|
-
api_client = ApiClient(configuration=config)
|
|
144
|
-
registry_wrapper = RegistryClientWrapper(api_client)
|
|
145
|
-
|
|
146
|
-
# Perform graceful unregistration (this sends DELETE /heartbeats)
|
|
147
|
-
success = await registry_wrapper.unregister_agent(agent_id)
|
|
148
|
-
if success:
|
|
149
|
-
logger.info(
|
|
150
|
-
f"✅ Agent '{agent_id}' successfully unregistered from registry"
|
|
151
|
-
)
|
|
152
|
-
else:
|
|
153
|
-
logger.warning(
|
|
154
|
-
f"⚠️ Failed to unregister agent '{agent_id}' from registry"
|
|
155
|
-
)
|
|
156
|
-
|
|
157
|
-
except Exception as e:
|
|
158
|
-
logger.error(f"❌ Graceful shutdown error: {e}")
|
|
159
|
-
|
|
160
|
-
def _get_or_create_agent_id(self) -> str:
|
|
161
|
-
"""Get agent ID using the existing decorator registry function."""
|
|
162
|
-
from mesh.decorators import _get_or_create_agent_id
|
|
163
|
-
|
|
164
|
-
return _get_or_create_agent_id()
|
|
165
|
-
|
|
166
|
-
def start_blocking_loop_with_shutdown_support(self, thread) -> None:
|
|
167
|
-
"""
|
|
168
|
-
Start the main thread blocking loop with graceful shutdown support.
|
|
169
|
-
|
|
170
|
-
This keeps the main thread alive while monitoring for shutdown signals
|
|
171
|
-
and maintaining the server thread.
|
|
172
|
-
"""
|
|
173
|
-
logger.info(
|
|
174
|
-
"🔒 MAIN THREAD: Blocking to keep alive (prevents threading shutdown state)"
|
|
175
|
-
)
|
|
176
|
-
|
|
177
|
-
# Install signal handlers
|
|
178
|
-
self.install_signal_handlers()
|
|
179
|
-
|
|
180
|
-
try:
|
|
181
|
-
while True:
|
|
182
|
-
# Check shutdown flag (set by signal handlers)
|
|
183
|
-
if self.is_shutdown_requested():
|
|
184
|
-
logger.info(
|
|
185
|
-
"🚨 MAIN THREAD: Shutdown requested, performing graceful cleanup..."
|
|
186
|
-
)
|
|
187
|
-
self.perform_graceful_shutdown_from_main_thread()
|
|
188
|
-
break
|
|
189
|
-
|
|
190
|
-
if thread.is_alive():
|
|
191
|
-
thread.join(timeout=1) # Check every second
|
|
192
|
-
else:
|
|
193
|
-
logger.warning("⚠️ Server thread died, exiting...")
|
|
194
|
-
break
|
|
195
|
-
except KeyboardInterrupt:
|
|
196
|
-
logger.info("👋 MAIN THREAD: Received interrupt, shutting down gracefully")
|
|
197
|
-
self.perform_graceful_shutdown_from_main_thread()
|
|
198
|
-
except Exception as e:
|
|
199
|
-
logger.error(f"❌ MAIN THREAD: Error in blocking loop: {e}")
|
|
200
|
-
|
|
201
|
-
logger.info("🏁 MAIN THREAD: Exiting blocking loop")
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
# Global instance for backward compatibility with existing decorators.py usage
|
|
205
|
-
_global_shutdown_manager = GracefulShutdownManager()
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
def get_global_shutdown_manager() -> GracefulShutdownManager:
|
|
209
|
-
"""Get the global shutdown manager instance."""
|
|
210
|
-
return _global_shutdown_manager
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
# Convenience functions for backward compatibility
|
|
214
|
-
def set_shutdown_context(context: dict[str, Any]) -> None:
|
|
215
|
-
"""Set context for graceful shutdown (global convenience function)."""
|
|
216
|
-
_global_shutdown_manager.set_shutdown_context(context)
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
def install_graceful_shutdown_handlers() -> None:
|
|
220
|
-
"""Install signal handlers that set the shutdown flag (global convenience function)."""
|
|
221
|
-
_global_shutdown_manager.install_signal_handlers()
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
def is_shutdown_requested() -> bool:
|
|
225
|
-
"""Check if shutdown has been requested (global convenience function)."""
|
|
226
|
-
return _global_shutdown_manager.is_shutdown_requested()
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
def perform_graceful_shutdown_from_main_thread() -> None:
|
|
230
|
-
"""Perform graceful shutdown from main thread (global convenience function)."""
|
|
231
|
-
_global_shutdown_manager.perform_graceful_shutdown_from_main_thread()
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
def start_blocking_loop_with_shutdown_support(thread) -> None:
|
|
235
|
-
"""Start the main thread blocking loop with graceful shutdown support (global convenience function)."""
|
|
236
|
-
_global_shutdown_manager.start_blocking_loop_with_shutdown_support(thread)
|
|
File without changes
|
|
File without changes
|