← back to Exo
fix: unblock MpReceiver.close() to prevent shutdown hang (#1511)
315992549b9f92d7d7538e46fd045558e9ecff73 · 2026-02-18 13:59:02 -0800 · Alex Cheema
## Summary
- `MpReceiver.close()` did not unblock threads stuck on `queue.get()` in
`receive_async()`, causing abandoned threads (via
`abandon_on_cancel=True`) to keep the Python process alive indefinitely
after tests pass
- This caused the `aarch64-darwin` CI jobs in PR #1462 to hang for ~6
hours until the GitHub Actions timeout killed them
- Sends an `_MpEndOfStream` sentinel before closing the buffer,
mirroring what `MpSender.close()` already does
## Test plan
- [x] `uv run basedpyright` — 0 errors
- [x] `uv run ruff check` — clean
- [x] `nix fmt` — 0 changed
- [x] `uv run pytest` — 188 passed, 1 skipped in 12s (no hang)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: rltakashige <rl.takashige@gmail.com>
Co-authored-by: Ryuichi Leo Takashige <leo@exolabs.net>
Files touched
M src/exo/utils/channels.py
Diff
commit 315992549b9f92d7d7538e46fd045558e9ecff73
Author: Alex Cheema <41707476+AlexCheema@users.noreply.github.com>
Date: Wed Feb 18 13:59:02 2026 -0800
fix: unblock MpReceiver.close() to prevent shutdown hang (#1511)
## Summary
- `MpReceiver.close()` did not unblock threads stuck on `queue.get()` in
`receive_async()`, causing abandoned threads (via
`abandon_on_cancel=True`) to keep the Python process alive indefinitely
after tests pass
- This caused the `aarch64-darwin` CI jobs in PR #1462 to hang for ~6
hours until the GitHub Actions timeout killed them
- Sends an `_MpEndOfStream` sentinel before closing the buffer,
mirroring what `MpSender.close()` already does
## Test plan
- [x] `uv run basedpyright` — 0 errors
- [x] `uv run ruff check` — clean
- [x] `nix fmt` — 0 changed
- [x] `uv run pytest` — 188 passed, 1 skipped in 12s (no hang)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: rltakashige <rl.takashige@gmail.com>
Co-authored-by: Ryuichi Leo Takashige <leo@exolabs.net>
---
src/exo/utils/channels.py | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/src/exo/utils/channels.py b/src/exo/utils/channels.py
index 646ac8f6..c9336215 100644
--- a/src/exo/utils/channels.py
+++ b/src/exo/utils/channels.py
@@ -1,3 +1,4 @@
+import contextlib
import multiprocessing as mp
from dataclasses import dataclass, field
from math import inf
@@ -132,7 +133,8 @@ class MpSender[T]:
def close(self) -> None:
if not self._state.closed.is_set():
self._state.closed.set()
- self._state.buffer.put(_MpEndOfStream())
+ with contextlib.suppress(Exception):
+ self._state.buffer.put_nowait(_MpEndOfStream())
self._state.buffer.close()
# == unique to Mp channels ==
@@ -204,6 +206,8 @@ class MpReceiver[T]:
def close(self) -> None:
if not self._state.closed.is_set():
self._state.closed.set()
+ with contextlib.suppress(Exception):
+ self._state.buffer.put_nowait(_MpEndOfStream())
self._state.buffer.close()
# == unique to Mp channels ==
← ce5a65d3 Add MiniMax M2.5 model cards (#1514)
·
back to Exo
·
Cleanup mistakes (#1537) 24e99ce1 →