[object Object]

← 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

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 →