Skip to content

Pass a stop signal that another thread received on to the main thread while a job blocks the loop - #20

Merged
Piergiuseppe merged 3 commits into
mainfrom
fix/stop-signals-reach-the-main-thread
Nov 15, 2026
Merged

Piergiuseppe merged 3 commits into
mainfrom
fix/stop-signals-reach-the-main-thread

Conversation

@Piergiuseppe

Copy link
Copy Markdown
Owner

What was wrong

A stop signal sent to the process is delivered by the kernel to any one thread. Python runs its handlers only on the main thread, and a signal another thread received does not interrupt a blocking call the main thread is in. With a job blocking the worker's loop, a Ctrl-C or SIGTERM that landed on a helper thread waited for the job to return, and two presses of the same signal that waited together ran the handler once, so the second press did not force the stop. This is the rare CI failure "the worker ignored a second Ctrl-C (own, blocking)": about 1 in 1,200 to 1,700 forced stops on Linux.

What changes

Python writes the number of every signal the process receives, on whichever thread, to the loop's wakeup channel. While a run's handlers are in place a thread watches that channel without reading it. A stop signal found unread twice, 50 ms apart, means the loop is blocked, and the thread sends the main thread one SIGURG, whose handler is empty: the interrupted system call makes Python run the pending stop handler, which is what happens when the kernel picks the main thread in the first place.

  • A loop that reads its signals in time is never nudged; nothing is sent that a stop signal did not cause.
  • SIGURG is taken only when it is unhandled in Python and at the sigaction level, and is put back afterwards, in a forked child too.
  • A run started without handle_signals gives Python its loop's wakeup channel while it lasts, if none is set.
  • No forwarding on a loop that is not asyncio's own (uvloop): behaviour there is as before.

Two designs were tried and dropped on this branch before this one: blocking the stop signals in tinyjobs' threads (masks are inherited by user threads and child processes), and periodic nudges while a stopping loop did not answer (the probe below still hung, because the common case is the first press landing elsewhere).

Measured

  • Forced stop with each signal sent to a chosen thread (main/main, main/other, other/main, other/other): 0.81 to 0.91 s in every order; 6 s before for the orders with another thread.
  • Linux, 3 x 1,700 forced stops of a worker whose loop a job blocks: 0 hung. The same probe on the previous design: 3 hung.
  • Full suite locally (SQLite, Redis, Postgres 17): 2,526 tests, green except the Postgres DROP TABLE deadlock that Give a process room to exit in the tests that time one #19 fixed, now on main.

Also here

  • test_a_stopped_worker_waits_for_a_def_job_on_an_extra_loop stopped its worker when the store counted two running jobs, not when both had begun, so a claim still on its way was handed back. It now waits for both to begin.

Docs

docs/design.md (stop-signal section), docs/operating-it.md, CHANGELOG.md (0.19.0, Fixed).

…im still on its way to the worker at the stop is not handed back to the queue
@read-the-docs-community

read-the-docs-community Bot commented Nov 15, 2026 •

Copy link
Copy Markdown

Documentation build overview

📚 tinyjobs | 🛠️ Build #35906885 | 📁 Comparing 0b161d5 against latest (31b6259)

  🔍 Preview build  

2 files changed
± design/index.html
± operating-it/index.html

@Piergiuseppe
Piergiuseppe force-pushed the fix/stop-signals-reach-the-main-thread branch from 74c8475 to b91ec51 Compare November 15, 2026 13:26
… while a job blocks the loop, so a second Ctrl-C or SIGTERM forces the stop wherever each one landed instead of waiting for the job to return
…ork gate for a moment, so a write still finishing after the interrupt is not taken for a leak
@Piergiuseppe
Piergiuseppe force-pushed the fix/stop-signals-reach-the-main-thread branch from b91ec51 to 0b161d5 Compare November 15, 2026 14:05
@Piergiuseppe
Piergiuseppe merged commit f9fd40d into main Nov 15, 2026
14 checks passed
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.

1 participant