Skip to content

Let a SIGINT leave the reaper task and restart it on the next connect - #255

Merged
quinnj-bot[bot] merged 4 commits into
mainfrom
claude/reaper-interrupt
Nov 5, 2026
Merged

quinnj-bot[bot] merged 4 commits into
mainfrom
claude/reaper-interrupt

Conversation

@quinnj-bot

@quinnj-bot quinnj-bot Bot commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

Why

Once MySQL.jl has opened a connection, a Ctrl-C during a long statement can vanish, and reaping stops afterwards (#254). Julia throws a SIGINT's InterruptException into whichever task is resident on thread 1. The reaper wakes every 0.5 s and parks again, so during a long statement it is usually that task. Its catch around wait(t) took the exception, ended the loop, and left the timer open.

What changed

  • reaper_loop catches nothing around wait(t). An InterruptException now leaves the task, which is what Base's task_done_hook needs to hand it to the REPL backend.
  • ensure_reaper! tracks the task in REAPER_TASK and starts a new one on the next connect when the old one is done.
  • The reaper task goes on the default pool, so with an interactive pool (-t N,M, or -t auto on 1.12) it never parks on thread 1 while the user's work runs there.

Scope

  • Reaper only. The statement-id parking path has no background task.
  • Does not change where Julia delivers a SIGINT. With one thread the reaper is still the resident task most of the time. The interrupt then ends the reaper; Base forwards it to the REPL when one is evaluating, and a script sees what it sees for any finished task on thread 1: the first Ctrl-C is lost, the second terminates the process. Base's own Timer(cb) task and the Profile SIGINFO listener behave the same way.
  • Between the interrupt and the next connect, queued transports wait. The restarted reaper or the atexit hook closes them.

Tradeoffs

  • Keeping the catch and continuing (option 2 in The reaper task swallows a Ctrl-C (InterruptException) meant for the main task, and then stops reaping #254) keeps the reaper alive, but every later Ctrl-C is lost too, and a one-thread script can no longer be stopped with Ctrl-C at all. Today its second Ctrl-C terminates it.
  • Threads.@spawn :default is the public way to pick a pool, but its closure thunk is not reachable from the static call edge --trim needs. Task(reaper_loop) plus Threads._spawn_set_thrpool keeps the trim story as it is.

Blast Radius

The reaper's task lifecycle only. The CAS states, exactly-once close, reap_now!, and the atexit hook are untouched. The reaper now runs on default threads when an interactive pool exists; transport close was already reached from any thread.

Verification

  • The repro from The reaper task swallows a Ctrl-C (InterruptException) meant for the main task, and then stops reaping #254 (no server), Julia 1.12.6, macOS. Before, with -t 2,1, 3 of 3 runs lost the interrupt and left the reaper done with its timer open. After, 2 of 3 runs delivered InterruptException to the main task after 1.1 s, and the reaper kept reaping in all 3; the third run lost the interrupt inside the runtime, the same 1-in-3 drop a MySQL-free control shows on this Mac. With -t 1, before: swallowed and reaping stopped; after: the interrupt ends the reaper task and ensure_reaper!() restarts it.
  • The new testset in test/protocol/native_tests.jl fails on main (5 of 8 assertions: the task ends normally, nothing restarts it, a queued entry is never closed) and passes with the fix on -t 1 and on -t auto,2, on 1.12.6 and 1.10.11. The test finds the parked reaper through the timer's wait queue under its lock, which holds tasks before Julia 1.15 and WaitEntry records on 1.15-dev (the first CI round failed only the nightly jobs, on that check).
  • Serverless suite (MYSQL_INTEGRATION=0 Pkg.test()), macOS arm64: Julia 1.12.6 passed 1823 of 1823 (the --trim=safe lane compiled and ran the workload, which exercises the reaper), Julia 1.10.11 passed 1816 of 1816.

Review notes

Checked as a reviewer: ensure_reaper! has one caller (register! in connect.jl); wait(::Timer) throws only EOFError for a closed timer, which nothing closes, or an exception thrown into the task; the restart runs under REAPER_SETUP_LOCK, so two concurrent connects start one task; atexit is still registered once; the test holds the timer's lock between the parked check and the throw, so schedule(..., error=true) never reaches a running task; _spawn_set_thrpool has the same signature on 1.10, 1.12, and master.

Fixes #254

🤖 Generated with Claude Code

quinnj and others added 2 commits November 4, 2026 22:17
Track the reaper task in REAPER_TASK so a test can reach it. The new
testset throws an InterruptException into the reaper while it waits on
its timer, the way a Ctrl-C does when the reaper is the task resident
on thread 1, and asserts that the exception leaves the task and that
the next ensure_reaper! call starts a reaper that still reaps.

Before the fix the testset fails: the catch around wait(t) swallows the
InterruptException, the task ends normally, and nothing restarts it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…nnect

Julia throws a SIGINT's InterruptException into whichever task is
resident on thread 1. The reaper wakes every 0.5 s and parks again, so
during a long statement it is usually that task. Its catch around
wait(t) took the InterruptException, ended the loop, and left the timer
open, so the Ctrl-C never reached the user's task and nothing reaped
again until exit.

Remove the catch: the exception now leaves the task, which is what
Base's task_done_hook needs to hand it to the REPL backend. Track the
reaper task and let ensure_reaper! start a new one when the old one is
done. Place the reaper on the default pool, so that when an interactive
pool exists it never parks on thread 1 while the user's work runs there.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
quinnj and others added 2 commits November 4, 2026 23:05
The parked check compared `reaper.queue` with the timer's wait queue,
which no longer holds on Julia 1.15-dev: a parked task registers a
WaitEntry there and `queue` stays `nothing`, so the nightly jobs never
injected the interrupt. Check membership in the timer's wait queue
instead, under its lock, reading the entry's atomic `task` field on
1.15 and the entry itself before that.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…Entry records

Julia 1.13 keeps tasks in a Timer's wait queue; 1.14.0-alpha1 and later
keep WaitEntry records.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

The reaper task swallows a Ctrl-C (InterruptException) meant for the main task, and then stops reaping

1 participant