Skip to content

Commit 00e4554

Browse files
authored
Merge pull request #112 from UiPath/fix/hosted_graceful_shutdown
fix: graceful shutdown for hosted servers
2 parents 28eb2e3 + 2a4eb1d commit 00e4554

File tree

1 file changed

+65
-41
lines changed

1 file changed

+65
-41
lines changed

src/uipath_mcp/_cli/_runtime/_runtime.py

Lines changed: 65 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -98,30 +98,39 @@ async def execute(self) -> Optional[UiPathRuntimeResult]:
9898
await self._register()
9999

100100
run_task = asyncio.create_task(self._signalr_client.run())
101-
102-
# Set up a task to wait for cancellation
103101
cancel_task = asyncio.create_task(self._cancel_event.wait())
104-
105102
self._keep_alive_task = asyncio.create_task(self._keep_alive())
106103

107-
# Keep the runtime alive
108-
# Wait for either the run to complete or cancellation
109-
done, pending = await asyncio.wait(
110-
[run_task, cancel_task], return_when=asyncio.FIRST_COMPLETED
111-
)
112-
113-
# Cancel any pending tasks
114-
for task in pending:
115-
task.cancel()
104+
try:
105+
# Wait for either the run to complete or cancellation
106+
done, pending = await asyncio.wait(
107+
[run_task, cancel_task], return_when=asyncio.FIRST_COMPLETED
108+
)
109+
except KeyboardInterrupt:
110+
logger.info(
111+
"Received keyboard interrupt, shutting down gracefully..."
112+
)
113+
self._cancel_event.set()
114+
finally:
115+
# Cancel any pending tasks gracefully
116+
for task in [run_task, cancel_task, self._keep_alive_task]:
117+
if task and not task.done():
118+
task.cancel()
119+
try:
120+
await asyncio.wait_for(task, timeout=2.0)
121+
except (asyncio.CancelledError, asyncio.TimeoutError):
122+
pass
116123

117124
output_result = {}
118125
if self._session_output:
119126
output_result["content"] = self._session_output
120127

121128
self.context.result = UiPathRuntimeResult(output=output_result)
122-
123129
return self.context.result
124130

131+
except KeyboardInterrupt:
132+
logger.info("Keyboard interrupt received")
133+
return None
125134
except Exception as e:
126135
if isinstance(e, UiPathMcpRuntimeError):
127136
raise
@@ -133,7 +142,9 @@ async def execute(self) -> Optional[UiPathRuntimeResult]:
133142
UiPathErrorCategory.USER,
134143
) from e
135144
finally:
136-
self.trace_provider.shutdown()
145+
await self.cleanup()
146+
if hasattr(self, "trace_provider") and self.trace_provider:
147+
self.trace_provider.shutdown()
137148

138149
async def validate(self) -> None:
139150
"""Validate runtime inputs and load MCP server configuration."""
@@ -442,36 +453,49 @@ async def _keep_alive(self) -> None:
442453
"""
443454
Heartbeat to keep the runtime available.
444455
"""
445-
while not self._cancel_event.is_set():
446-
try:
456+
try:
457+
while not self._cancel_event.is_set():
458+
try:
459+
460+
async def on_keep_alive_response(
461+
response: CompletionMessage,
462+
) -> None:
463+
if response.error:
464+
logger.error(f"Error during keep-alive: {response.error}")
465+
return
466+
session_ids = response.result
467+
logger.info(f"Active sessions: {session_ids}")
468+
# If there are no active sessions and this is a sandbox environment
469+
# We need to cancel the runtime
470+
# eg: when user kills the agent that triggered the runtime, before we subscribe to events
471+
if (
472+
not session_ids
473+
and self.sandboxed
474+
and not self._cancel_event.is_set()
475+
):
476+
logger.error(
477+
"No active sessions, cancelling sandboxed runtime..."
478+
)
479+
self._cancel_event.set()
447480

448-
async def on_keep_alive_response(response: CompletionMessage) -> None:
449-
if response.error:
450-
logger.error(f"Error during keep-alive: {response.error}")
451-
return
452-
session_ids = response.result
453-
logger.info(f"Active sessions: {session_ids}")
454-
# If there are no active sessions and this is a sandbox environment
455-
# We need to cancel the runtime
456-
# eg: when user kills the agent that triggered the runtime, before we subscribe to events
457-
if (
458-
not session_ids
459-
and self.sandboxed
460-
and not self._cancel_event.is_set()
461-
):
462-
logger.error(
463-
"No active sessions, cancelling sandboxed runtime..."
481+
if self._signalr_client:
482+
await self._signalr_client.send(
483+
method="OnKeepAlive",
484+
arguments=[],
485+
on_invocation=on_keep_alive_response,
464486
)
465-
self._cancel_event.set()
487+
except Exception as e:
488+
if not self._cancel_event.is_set():
489+
logger.error(f"Error during keep-alive: {e}")
466490

467-
await self._signalr_client.send(
468-
method="OnKeepAlive",
469-
arguments=[],
470-
on_invocation=on_keep_alive_response,
471-
)
472-
except Exception as e:
473-
logger.error(f"Error during keep-alive: {e}")
474-
await asyncio.sleep(60)
491+
try:
492+
await asyncio.wait_for(self._cancel_event.wait(), timeout=60)
493+
break
494+
except asyncio.TimeoutError:
495+
continue
496+
except asyncio.CancelledError:
497+
logger.info("Keep-alive task cancelled")
498+
raise
475499

476500
async def _on_runtime_abort(self) -> None:
477501
"""

0 commit comments

Comments
 (0)