Repository navigation
fix(ipc): ThreadJobExecutor.aclose() hangs when the runner exits before its ack is read - #7225
Merged
longcw merged 1 commit intoSep 21, 2026
Conversation
…re its ack is read
pabloFuente
added a commit
to OpenVidu/openvidu-agents
that referenced
this pull request
Sep 11, 2026
…ever stopped on SIGQUIT)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Bug description
With
JobExecutorType.THREAD, a graceful shutdown of a worker that has idle prewarmed runners logsjob did not ack shutdown in timefor most of them and then never exits.drain()has already returned by then, sodrain_timeoutdoesn't help. Only a second signal ends the process. On a 16-core host (so 16 idle runners) I've been consistently reproducing this on every shutdown.Why
ThreadJobExecutor._main_task()cancels_monitor_taskas soon as the runner thread joins. An idle runner repliesShutdownRequestAck+ShuttingDownand exits right away, and its join callback (call_soon_threadsafe) is queued ahead of the socket read, so the monitor is cancelled with both messages still unread in theStreamReader. Nothing else resolves_shutting_down_fut, andaclose()awaits it with no timeout right after the ack timeout.SupervisedProcalready handles this case (_on_read_ipc_doneresolves both futures when its reader ends), but the thread executor never got the equivalent and has no kill path to fall back on. The result is the parent process hangs indefinetly instead of dying.Fix
Simple change at job_thread_executor.py
_monitor_taskfinish on its own (bounded to 1 s) before cancelling it. The thread closes its end of the channel before joining, so the monitor reads the pending messages and ends onDuplexClosed._shutdown_ack_futand_shutting_down_futin afinallyof_monitor_task, same asSupervisedProc, so a cancelled or closed reader can never leaveaclose()waiting.Tests
tests/test_ipc.pyhad noThreadJobExecutorcoverage at all. Addedtest_thread_shutdown_no_job,test_thread_job_graceful_shutdownandtest_thread_shutdown_no_job_join_before_ack_is_read, which reproduces the race deterministically by blocking the loop for 0.5 s right afteraclose()sends theShutdownRequest. All three fail onmainwithTimeoutErrorand pass with this change.Minimal reproducible example
The minimal worker below is a proof of the bug: THREAD executor, 32 idle runners, no LiveKit server needed, SIGTERM once the runners are up. On
main27 of the 32 runners logdid not ackand the process never exits. With this change it exits in about a second with no errors.repro.py