fix(runtime): bound worker joins during scheduler teardown - #2876
Open
gertybotbot wants to merge 1 commit into
Open
fix(runtime): bound worker joins during scheduler teardown#2876gertybotbot wants to merge 1 commit into
gertybotbot wants to merge 1 commit into
Conversation
Closes hew-lang#2508. teardown_workers hard-joined every worker with no timeout. A worker parked in a blocking syscall (e.g. accept()) never reaches the park-recheck, so that join is unbounded and can hang shutdown forever. Poll is_finished() against a single deadline shared by the whole worker set (not per-handle, which would multiply the bound by N), and join only once a thread is known finished so the join itself cannot block. A worker still running at the deadline is DETACHED rather than force-killed: it may still be touching scheduler-owned memory and there is no safe way to interrupt it, so leaking the handle is the fail-closed choice. Scope note: hew-lang#2508 describes this as unreachable for compiled binaries. That understates it -- codegen emits hew_sched_shutdown directly into native main for supervisor programs (llvm.rs emit_immediate_shutdown_epilogue), and hew_sched_shutdown calls teardown_workers, so the unbounded join is reachable today, not only from the not-yet-wired host-embedder path.
This was referenced Aug 9, 2026
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.
Closes #2508.
The defect
teardown_workers(hew-runtime/src/scheduler.rs) hard-joined every worker thread with no timeout:A worker genuinely parked in a blocking syscall (e.g.
accept()) never reaches the park-recheck, sojoin()never returns and shutdown hangs forever.Scope correction
The issue says this "does NOT affect any compiled
.hewbinary today" and is latent hardening for the not-yet-wired host-embedder path. That understates it.teardown_workershas three callers, and one ishew_sched_shutdown— which codegen emits directly into nativemainfor supervisor programs (hew-codegen-rs/src/llvm.rs,emit_immediate_shutdown_epilogue, alongside thehew_sched_shutdownABI declaration inruntime_abi.rs). Supervisor programs deliberately bypass the generic idle drain and go straight to closing and joining workers, so they reach the unbounded join today.hew_runtime_cleanupbeing unreachable is true but is not the only route.The fix is the same either way; the reachability is just wider than filed.
The fix
Poll
is_finished()against a deadline, andjoin()only once the thread is known finished so the join itself cannot block.Two deliberate choices:
mem::forgetand reported on stderr. Bounding shutdown must not introduce a use-after-free; leaking a thread is the fail-closed choice. This mirrors the existing task-scope reaper precedent.WORKER_JOIN_TIMEOUTis 5s, matchingshutdown::DEFAULT_DRAIN_TIMEOUT_MS. By the time teardown runs, the drain phase has already had its own budget, so this is a backstop against a worker that cannot return — not a second drain window. The polling shape follows the house convention inshutdown::drain_until_idle.The stale doc comment on
hew_sched_shutdown(which promised an unconditional join of every worker) is updated to state the bound.Verification
Three tests, proving discrimination rather than presence:
teardown_joins_workers_that_exitteardown_abandons_a_worker_that_never_exitsteardown_deadline_is_shared_across_workersRevert-control: with the join loop reverted to
h.join()but the tests kept,teardown_abandons_a_worker_that_never_exitsruns past 60s and has to be killed — i.e. it reproduces exactly the hang #2508 describes. With the fix it passes in ~5s. The test is a real discriminator, not a tautology.Green bare (exit codes read directly, not through a pipe):
cargo test -p hew-runtime --lib— 2243 passed, RC=0cargo clippy -p hew-runtime --all-targets -- -D warnings— RC=0cargo fmt -p hew-runtime -- --check— RC=0The pre-existing
spawn_failure_teardown_joins_partial_worker_set_and_drops_schedulerregression test still passes.Unrelated flake found while verifying
transport::tests::framed_send_to_broken_pipe_fails_closed_without_signalfails intermittently in full-suite runs. I initially suspected my change and ran it down:main, 6 full-suite runs: 5 ok / 1 FAILED — so it is pre-existing and not caused by this PR.left: 4096— a fully successful 4096-byte write where-1was expected. So the send did not hit EPIPE at all: afterdrop(far)closes the peer fd, a concurrent thread elsewhere in the suite opens a new fd that reuses that number, and the write lands on a live socket. Classic fd-reuse race inherent to the test, not a defect in the SIGPIPE suppression it is meant to guard.My diff contains no fd-touching code (only sleeps,
is_finished(),join, andmem::forget), though the added ~5s of wall-clock does widen the window. Filing separately rather than folding an unrelated fix into this PR.