Skip to content

Commit be789c5

Browse files
committed
Reap fork children off the proxy loop
1 parent 0f4443a commit be789c5

1 file changed

Lines changed: 25 additions & 11 deletions

File tree

verifiers/v1/mcp/multiplex.py

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
from __future__ import annotations
3232

3333
import asyncio
34+
import concurrent.futures
3435
import contextlib
3536
import hashlib
3637
import logging
@@ -148,8 +149,7 @@ async def ensure(key: str, state_url: str) -> _Child:
148149
child = children.get(key)
149150
if child is not None and child.ready.is_set():
150151
return child
151-
# The lock is held ONLY for the synchronous fork()+register, never the readiness wait — so a
152-
# cold fork serializes other forks (fork-safety) but doesn't stall traffic to other children.
152+
# Readiness waits stay outside the lock, so traffic to ready children remains lock-free.
153153
async with forking:
154154
child = children.get(key)
155155
creating = child is None
@@ -177,15 +177,29 @@ async def ensure(key: str, state_url: str) -> _Child:
177177
return child
178178

179179
async def reap(key: str) -> None:
180-
child = children.pop(key, None)
181-
if not child:
182-
return
183-
child.ready.set() # wake any waiter blocked in `ensure` — it re-checks `children` and re-forks
184-
with contextlib.suppress(Exception):
185-
os.kill(child.pid, signal.SIGKILL)
186-
with contextlib.suppress(Exception):
187-
os.waitpid(child.pid, 0)
188-
shutil.rmtree(child.cwd, ignore_errors=True)
180+
# Keep fork() out of the cleanup thread's lifetime; ready children bypass this lock.
181+
async with forking:
182+
child = children.pop(key, None)
183+
if not child:
184+
return
185+
child.ready.set() # wake waiters; they re-check `children` before re-forking
186+
with contextlib.suppress(Exception):
187+
os.kill(child.pid, signal.SIGKILL)
188+
189+
def cleanup() -> None:
190+
with contextlib.suppress(Exception):
191+
os.waitpid(child.pid, 0)
192+
shutil.rmtree(child.cwd, ignore_errors=True)
193+
194+
# One worker keeps cleanup ordered and lets it finish if this task is cancelled.
195+
loop = asyncio.get_running_loop()
196+
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor:
197+
future = loop.run_in_executor(executor, cleanup)
198+
try:
199+
await asyncio.shield(future)
200+
except asyncio.CancelledError:
201+
await future
202+
raise
189203
logger.info("fork: reaped child pid=%d", child.pid)
190204

191205
async def _respond(send, status: int, body: bytes) -> None:

0 commit comments

Comments
 (0)