Jens Geyer created THRIFT-6082:
----------------------------------

             Summary: Python TProcessPoolServer test shutdown can deadlock 
(signal handler reenters Condition.notify())
                 Key: THRIFT-6082
                 URL: https://issues.apache.org/jira/browse/THRIFT-6082
             Project: Thrift
          Issue Type: Bug
          Components: Python - Library
            Reporter: Jens Geyer


h3. Summary
{{test/py/TestServer.py}}'s SIGALRM-based test shutdown can deadlock the server 
subprocess indefinitely. This surfaced as a {{lib-python-macos}} CI job that 
ran until GitHub Actions' 6-hour job ceiling before being cancelled, even 
though all 24 client-side test assertions had already passed.

h3. Root cause
{{TestServer.py}} runs the {{TProcessPoolServer}} server flavor under a 
{{signal.alarm(4)}} self-timer. When the alarm fires, the handler 
({{clean_shutdown}}) terminates the worker processes and calls 
{{server.stop()}} ({{lib/py/src/server/TProcessPoolServer.py}}).

{{TProcessPoolServer.serve()}} blocks its main thread in 
{{multiprocessing.Condition.wait()}} while waiting to be told to stop. Python 
signal handlers run synchronously, nested inside whatever the interrupted 
thread was doing -- so {{clean_shutdown}} reenters on the *same* OS thread that 
is parked inside that {{wait()}} call. {{stop()}}'s call to 
{{Condition.notify()}} then blocks waiting for the sleeper to post a wake 
acknowledgement (the {{_woken_count}} handshake in 
{{multiprocessing/synchronize.py}}) -- but the "sleeper" is the very thread now 
trapped inside the nested signal handler, so it can never reach that code. The 
two frames wait on each other forever.

This reproduces deterministically (10/10 runs, both {{fork}} and {{spawn}} 
multiprocessing start methods) against the unmodified library code, confirmed 
with a {{faulthandler}} stack trace showing the frozen frame inside 
{{Condition.notify()}}.

h3. Impact
* In CI: a single {{TProcessPoolServer}} test iteration hangs, and nothing else 
in that job runs afterward.
* In production: any application that calls {{TProcessPoolServer.stop()}} from 
a signal handler (a common graceful-shutdown pattern, e.g. on SIGTERM/SIGINT) 
can hit the identical deadlock if the signal happens to arrive while the 
server's main thread is parked in {{wait()}}.

h3. Fix
{{test/py/TestServer.py}}'s shutdown handler should terminate the workers and 
exit the process directly instead of coordinating through 
{{TProcessPoolServer.stop()}}'s {{Condition}}, avoiding the reentrant 
{{notify()}} call. PR to follow.

h3. Out of scope
The underlying signal-handler/{{Condition}} reentrancy hazard inside 
{{TProcessPoolServer}} itself (library-level, not just the test harness) is not 
addressed by the fix above and would need separate design work (e.g. a 
signal-safe polling loop instead of {{Condition.wait()}}) to harden it for 
production callers.



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

Reply via email to