Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions tests/tools/test_mcp_stability.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ def test_kill_orphaned_uses_sigkill_when_available(self, monkeypatch):
# bpo-14484). Return True so the SIGKILL escalation fires.
with patch("tools.mcp_tool.os.kill") as mock_kill, \
patch("gateway.status._pid_exists", return_value=True), \
patch("tools.mcp_tool.time.sleep") as mock_sleep:
patch("tools.mcp_tool._orphan_reap_sleep") as mock_sleep:
_kill_orphaned_mcp_children()

# SIGTERM then SIGKILL; the alive check no longer touches os.kill.
Expand Down Expand Up @@ -163,12 +163,12 @@ def test_kill_orphaned_falls_back_without_sigkill(self, monkeypatch):
monkeypatch.delattr(signal, "SIGKILL", raising=False)

with patch("tools.mcp_tool.os.kill") as mock_kill, \
patch("tools.mcp_tool.time.sleep") as mock_sleep:
patch("tools.mcp_tool._orphan_reap_sleep") as mock_sleep:
_kill_orphaned_mcp_children()

# SIGTERM phase, alive check raises (process gone), no escalation
mock_kill.assert_any_call(fake_pid, signal.SIGTERM)
assert mock_sleep.called
mock_sleep.assert_called_once_with(2)

with _lock:
assert fake_pid not in _orphan_stdio_pids
Expand Down
14 changes: 13 additions & 1 deletion tools/mcp_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -3500,6 +3500,18 @@ async def _shutdown():
_stop_mcp_loop()


def _orphan_reap_sleep(seconds: float) -> None:
"""Indirection over ``time.sleep`` for the orphan-reap SIGTERM→SIGKILL gap.

Tests patch this symbol instead of ``time.sleep`` so unrelated background
sleepers (pytest-xdist workers, MCP heartbeat threads, etc.) don't pollute
the mock's call list — patching ``time.sleep`` (or ``tools.mcp_tool.time.sleep``,
which resolves to the same module attribute) intercepts every thread in the
process.
"""
time.sleep(seconds)


def _kill_orphaned_mcp_children(include_active: bool = False) -> None:
"""Best-effort graceful shutdown of stdio MCP subprocesses to reap orphans.

Expand Down Expand Up @@ -3542,7 +3554,7 @@ def _kill_orphaned_mcp_children(include_active: bool = False) -> None:
pass

# Phase 2: Wait for graceful exit
time.sleep(2)
_orphan_reap_sleep(2)

# Phase 3: SIGKILL any survivors
_sigkill = getattr(_signal, "SIGKILL", _signal.SIGTERM)
Expand Down