diff options
Diffstat (limited to 'subagents')
| -rw-r--r-- | subagents/jobs.lua | 54 |
1 files changed, 43 insertions, 11 deletions
diff --git a/subagents/jobs.lua b/subagents/jobs.lua index a8cde6d..bab8f30 100644 --- a/subagents/jobs.lua +++ b/subagents/jobs.lua @@ -22,6 +22,14 @@ -- A settled result is read and cached the moment it appears, because -- `job:close()` frees it. Jobs are otherwise left open until close_all() ends -- the turn, so a result can still be read after its coroutine resumed. +-- +-- `job:close()` also JOINS the pump, and a pump parked in a tool batch is +-- waiting on the owner thread to come back to the uv loop — the very thread +-- every entry point here runs on. Closing an unsettled job would therefore +-- deadlock, so nothing does: only settle() closes a job, once its pump has +-- exited. close_all() asks what is still running to cancel, marks it +-- close-on-settle, and lets the wake byte that carries the settle drive the +-- close. local ok_uv, uv = pcall(require, "luv") @@ -75,6 +83,16 @@ local function close_pipe(handle) end end +-- Free the binding job. Safe only after the result is cached (close() frees it) +-- and the pump has exited (close() joins it), which together mean: after settle. +local function close_job(handle) + if handle.job then + pcall(handle.job.close, handle.job) + handle.job = nil + end + handle.state = "closed" +end + local function drain_pipe(handle) if not handle.fds then return @@ -114,6 +132,11 @@ local function settle(handle, raw) end handle.state = "settled" close_pipe(handle) + -- The turn ended while this one was still running: close_all() could not + -- join the pump then, but the pump is gone now, so the join is free. + if handle.close_on_settle then + close_job(handle) + end pump_queue() end @@ -283,13 +306,15 @@ function M.start(spec) return handle end --- True while a child with this id has a turn in flight. +-- True while a child with this id has a turn in flight. A child cancelled by +-- close_all is not one: its turn belongs to the turn that ended, and the next +-- one must not be refused because that pump has not finished exiting yet. function M.active(id) if id == nil then return false end for _, handle in ipairs(live) do - if handle.id == id and handle.settled == nil then + if handle.id == id and handle.settled == nil and handle.state ~= "closing" then return true end end @@ -353,7 +378,7 @@ local function can_park(handles) return false end for _, handle in ipairs(handles) do - if handle.state == "running" and handle.poll == nil then + if handle.job ~= nil and handle.settled == nil and handle.poll == nil then return false end end @@ -404,23 +429,30 @@ function M.cancel_all() end end --- The turn is over: join every pump and drop the state. Requests go out first --- so the joins overlap instead of running one child's teardown at a time. +-- The turn is over: cancel every child and drop the state. Never blocks — a +-- child whose pump is still up is left open and closed by its own settle, so +-- the owner thread stays free for the tool batches those pumps are waiting on. function M.close_all() for _, handle in ipairs(live) do if handle.job and handle.settled == nil then pcall(handle.job.request_cancel, handle.job) end end + + -- Whatever is still unsettled stays live so its poll (and any fallback + -- drain) still reaches it; the next close_all sweeps up what settled since. + local closing = {} for _, handle in ipairs(live) do - if handle.job then - pcall(handle.job.close, handle.job) - handle.job = nil + if handle.job and handle.settled == nil then + handle.close_on_settle = true + handle.state = "closing" + closing[#closing + 1] = handle + else + close_pipe(handle) + close_job(handle) end - close_pipe(handle) - handle.state = "closed" end - live, queued, waiters = {}, {}, {} + live, queued, waiters = closing, {}, {} running = 0 end |
