Skip to content

Keep a stop signal from hanging an idle worker - #46

Merged
PhiLily merged 1 commit into
mainfrom
fix/stop-signal-hang
Sep 15, 2026
Merged

PhiLily merged 1 commit into
mainfrom
fix/stop-signal-hang

Conversation

@PhiLily

@PhiLily PhiLily commented Sep 15, 2026

Copy link
Copy Markdown
Member

Closes #45

A stop signal could hang an idle ox_worker instead of draining it. The handler set a threading.Event while the main thread could be inside wait() on that Event with its lock held, and set() then waited on a lock its own thread owned.

What changed

  • Worker. The SIGTERM/SIGINT handler only records the signal on a queue.SimpleQueue. A helper thread stops the worker and then logs. A second signal still exits with 130, now without logging first, since nothing may stand between the handler and that exit.
  • Supervisor. Its handler also only records. The run loop logs, stops, forwards and escalates, including between child starts and during the final wait, so a second or third signal still escalates after the loop has failed.
  • Start-up orphans. The supervisor passes its pid to each child in OX_SUPERVISOR_PID. The child removes the variable, arms the parent-death signal after its handlers are in place, and compares its parent with that pid on every pass. A child whose supervisor died while it was starting now drains having claimed nothing. An older child ignores the variable, so a rollback does not stop children starting.

Tests

tests/test_stop_signals.py runs the handler at the exact moment the Event's lock is held, by wrapping Condition.__exit__ and calling signal.raise_signal, so it fails on the old code every time rather than by timing. Other tests cover the second-signal exit, what the handler touches, the helper's order, the supervisor's signal processing and start-up, and the orphan check through the real command.

Beyond the suite, a loop sending SIGTERM to an idle worker ran about 80,000 times on Linux with Python 3.12 and 3.14 without a hang. The old code hung within a few hundred runs.

The changelog has one Changed entry, for the forced exit no longer logging, and three Fixed entries.

ox_worker's SIGTERM and SIGINT handler logged and called
worker.request_stop(), which sets a threading.Event. Python runs signal
handlers on the main thread, and an idle worker's main thread spends
its time in Event.wait() on that same Event. A signal that arrived
while Event.wait() held the Event's lock ran a handler that waited for
that lock on the thread holding it, and the worker hung instead of
draining.

The handler now only records the signal on a SimpleQueue, which is
documented as safe to call from a signal handler, and a second signal
calls os._exit(130) with nothing before it. A daemon thread requests
the stop and then logs. The handlers are installed before that thread
starts, and if it cannot start the previous handlers come back and a
signal already queued stops the worker.

The supervisor's handler also only records. Its run loop logs, stops,
forwards and escalates, including between child starts and while it
waits for children to exit after the loop has ended, so a second or
third signal still reaches SIGKILL after an error.

The supervisor sets OX_SUPERVISOR_PID in each child's environment. The
child removes it, arms PR_SET_PDEATHSIG after its handlers are in
place, and compares its parent with that pid before every poll, so a
child whose supervisor died while it was starting drains having
claimed nothing. A child running an older release ignores the
variable.

The regression test raises SIGTERM from inside Condition.__exit__ on
the worker's stop Event, where the lock is still held, so it fails on
the old code every time rather than by timing.
@PhiLily
PhiLily merged commit 3e75490 into main Sep 15, 2026
30 checks passed
@PhiLily
PhiLily deleted the fix/stop-signal-hang branch September 15, 2026 16:04
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.

A stop signal can hang an idle worker instead of draining it

1 participant