Skip to content

Dump wedged-worker stacks with a faulthandler watchdog in CI#592

Merged
tobocop2 merged 1 commit into
mainfrom
diag/hang-stack-dump
Jul 22, 2026
Merged

Dump wedged-worker stacks with a faulthandler watchdog in CI#592
tobocop2 merged 1 commit into
mainfrom
diag/hang-stack-dump

Conversation

@tobocop2

Copy link
Copy Markdown
Owner

Problem

  • A wedged pytest-xdist worker (ProactorEventLoop / reactive-timing thread accumulation) hangs the job silently until timeout-minutes kills it, with no traceback.
  • pytest-timeout is configured but on Windows runs its thread method: the timeout callback is Python and never runs while a worker holds the GIL, so it cannot break this wedge.

Solution

  • Opt-in faulthandler watchdog (tests/_hang_watchdog.py, gated on LILBEE_TEST_HANG_DUMP_S), armed per test above the 60s pytest-timeout. faulthandler's C watchdog thread dumps every thread without acquiring the GIL, so it fires where pytest-timeout can't; exit=True then _exits the worker so the run fails fast with a traceback (exit 1 serial, 3 under xdist) instead of hanging.
  • CI sets the var on the test jobs and prints the per-worker dump in an if: always() step, since the wedged worker can't print its own.

Diagnostic only, no-op when the var is unset. Verified locally: dumps the exact frozen frame for a GIL-releasing sleep and a GIL-holding busy loop; no misfire on fast tests under xdist.

pytest-timeout is configured but on Windows runs its thread method: a
threading.Timer whose callback is Python. When a worker wedges holding the GIL
(the ProactorEventLoop / reactive-timing thread-accumulation case), no Python
runs, the callback never fires, and the job hangs silently until timeout-minutes
kills it with no traceback.

faulthandler.dump_traceback_later runs a C watchdog thread that dumps every
thread without acquiring the GIL, so it fires where pytest-timeout cannot.
exit=True then _exits the wedged worker, so a hang fails fast with a traceback
instead of burning the job budget. Opt-in via LILBEE_TEST_HANG_DUMP_S, set above
the 60s per-test pytest-timeout so it is only ever the backstop. CI surfaces the
per-worker dump in a step that runs even when the test step was killed.

Verified locally: dumps the exact frozen frame and _exits the worker for both a
GIL-releasing sleep and a GIL-holding busy loop; no-op when the var is unset; no
misfire on fast tests under xdist.
@tobocop2
tobocop2 merged commit e599268 into main Jul 22, 2026
16 of 17 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