fix(worker): bound blocking ops so hung builtin workers are recoverable (#148)#24
Conversation
…ncel survive a sync-pinned inner (spacedriveapp#148) A hung builtin worker survived ~26h 'running' past its 1800s budget because run() polled run_inner INLINE in the select!. A synchronous, non-yielding stretch inside run_inner pins the task's poll, so the sleep(timeout) branch is never evaluated and cancel_worker_with_reason's handle.abort() (also an .await-point operation) cannot fire — run() never completes, so the completion/cleanup in spawn_worker_task never runs and the pool slot/DB record stay 'running'. Fix: spawn run_inner as its own task; select! on its JoinHandle vs the timer. The timer/cancel now live on a task the inner work cannot pin, so the worker's slot and record are freed promptly on timeout. An AbortOnDrop guard tears the inner task down on timeout, completion, or external cancel. A sync-pinned inner still leaks one blocking thread until it returns or the process restarts (P0 remains the ultimate backstop) — true force-kill needs process isolation. refs shipyard#148
Review: fix(worker): bound blocking ops so hung builtin workers are recoverableSummaryThis PR fixes a critical issue (spacedriveapp#148) where hung builtin workers could survive past their wall-clock timeout by running synchronous, non-yielding code that pins the tokio runtime thread. The fix spawns FindingsP1 (Blocking)
P2 (Should Fix)
P3 (Nit)
SecurityNo security concerns identified. The change is internal task management with no new trust boundaries or input validation changes. VerdictAPPROVE — The fix correctly addresses the root cause of spacedriveapp#148 with a well-designed solution. Tests are comprehensive and the implementation is sound. The acknowledged thread-leak edge case is acceptable given the process-level backstop. Consider the P2 suggestions for future robustness, but they are not blockers. AI Review · Verdict: COMMENT |
Problem (shipyard#148, P1c)
A builtin worker hung ~26.4h 'running' past its 1800s wall-clock budget, unkillable except by restarting spacebot (P0).
Root cause
The abort machinery already exists —
worker.rs::run()hasselect!vssleep(1800s),channel.rstracksworker_handles+cancel_worker_with_reasoncallshandle.abort(), andspawn_worker_taskcleans up on every outcome. Butrun()polledrun_innerinline in the select!. A synchronous, non-yielding stretch insiderun_inner(a tool/index path that never reaches an.await) pins that task's poll → thesleepbranch is never evaluated,abort()(also.await-gated) can't fire →run()never completes → cleanup never runs → slot/DB row stayrunningforever.Fix (one file, surgical)
Drive
run_inneron its own tokio task;select!on itsJoinHandlevs the timer. The timer + external cancel now run on a task the inner work cannot pin → slot/record freed promptly on timeout. AnAbortOnDropguard tears the inner task down on timeout/completion/cancel. Panic + cancel semantics preserved.Residual (honest)
A sync-pinned inner still leaks one blocking thread until it returns or the process restarts — true OS-level force-kill needs process isolation (a larger follow-up). P0's restart stays the ultimate backstop; this stops the pool-level zombie, complementing spacedriveapp#147's slot reclamation.
Tests
abort_guard_tests: AbortOnDrop cancels on drop; a sync-pinned spawned task doesn't starve a concurrent select! timer.Native deps (lance/fastembed) need the full build env — must be gated by
docker compose build spacebotbefore deploy. refs shipyard#148