-
Notifications
You must be signed in to change notification settings - Fork 23.7k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Prevent stdio deadlock in forked children (#79522)
* background threads writing to stdout/stderr can cause children to deadlock if a thread in the parent holds the internal lock on the BufferedWriter wrapper * prevent writes to std handles during fork by monkeypatching stdout/stderr during display startup to require a mutex lock with fork(); this ensures no background threads can hold the lock during a fork operation * add integration test that fails reliably on Linux without this fix
- Loading branch information
1 parent
80d2f8d
commit 8a77b2f
Showing
13 changed files
with
400 additions
and
15 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
bugfixes: | ||
- display - reduce risk of post-fork output deadlocks (https://github.com/ansible/ansible/pull/79522) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
shippable/posix/group3 | ||
context/controller | ||
skip/macos |
58 changes: 58 additions & 0 deletions
58
test/integration/targets/fork_safe_stdio/callback_plugins/spewstdio.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
import atexit | ||
import os | ||
import sys | ||
|
||
from ansible.plugins.callback import CallbackBase | ||
from ansible.utils.display import Display | ||
from threading import Thread | ||
|
||
# This callback plugin reliably triggers the deadlock from https://github.com/ansible/ansible-runner/issues/1164 when | ||
# run on a TTY/PTY. It starts a thread in the controller that spews unprintable characters to stdout as fast as | ||
# possible, while causing forked children to write directly to the inherited stdout immediately post-fork. If a fork | ||
# occurs while the spew thread holds stdout's internal BufferedIOWriter lock, the lock will be orphaned in the child, | ||
# and attempts to write to stdout there will hang forever. | ||
|
||
# Any mechanism that ensures non-main threads do not hold locks before forking should allow this test to pass. | ||
|
||
# ref: https://docs.python.org/3/library/io.html#multi-threading | ||
# ref: https://github.com/python/cpython/blob/0547a981ae413248b21a6bb0cb62dda7d236fe45/Modules/_io/bufferedio.c#L268 | ||
|
||
|
||
class CallbackModule(CallbackBase): | ||
CALLBACK_VERSION = 2.0 | ||
CALLBACK_NAME = 'spewstdio' | ||
|
||
def __init__(self): | ||
super().__init__() | ||
self.display = Display() | ||
|
||
if os.environ.get('SPEWSTDIO_ENABLED', '0') != '1': | ||
self.display.warning('spewstdio test plugin loaded but disabled; set SPEWSTDIO_ENABLED=1 to enable') | ||
return | ||
|
||
self.display = Display() | ||
self._keep_spewing = True | ||
|
||
# cause the child to write directly to stdout immediately post-fork | ||
os.register_at_fork(after_in_child=lambda: print(f"hi from forked child pid {os.getpid()}")) | ||
|
||
# in passing cases, stop spewing when the controller is exiting to prevent fatal errors on final flush | ||
atexit.register(self.stop_spew) | ||
|
||
self._spew_thread = Thread(target=self.spew, daemon=True) | ||
self._spew_thread.start() | ||
|
||
def stop_spew(self): | ||
self._keep_spewing = False | ||
|
||
def spew(self): | ||
# dump a message so we know the callback thread has started | ||
self.display.warning("spewstdio STARTING NONPRINTING SPEW ON BACKGROUND THREAD") | ||
|
||
while self._keep_spewing: | ||
# dump a non-printing control character directly to stdout to avoid junking up the screen while still | ||
# doing lots of writes and flushes. | ||
sys.stdout.write('\x1b[K') | ||
sys.stdout.flush() | ||
|
||
self.display.warning("spewstdio STOPPING SPEW") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
[all] | ||
local-[1:10] | ||
|
||
[all:vars] | ||
ansible_connection=local |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
#!/usr/bin/env python | ||
"""Run a command using a PTY.""" | ||
|
||
import sys | ||
|
||
if sys.version_info < (3, 10): | ||
import vendored_pty as pty | ||
else: | ||
import pty | ||
|
||
sys.exit(1 if pty.spawn(sys.argv[1:]) else 0) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
#!/usr/bin/env bash | ||
|
||
set -eu | ||
|
||
echo "testing for stdio deadlock on forked workers (10s timeout)..." | ||
|
||
# Enable a callback that trips deadlocks on forked-child stdout, time out after 10s; forces running | ||
# in a pty, since that tends to be much slower than raw file I/O and thus more likely to trigger the deadlock. | ||
# Redirect stdout to /dev/null since it's full of non-printable garbage we don't want to display unless it failed | ||
ANSIBLE_CALLBACKS_ENABLED=spewstdio SPEWSTDIO_ENABLED=1 python run-with-pty.py timeout 10s ansible-playbook -i hosts -f 5 test.yml > stdout.txt && RC=$? || RC=$? | ||
|
||
if [ $RC != 0 ]; then | ||
echo "failed; likely stdout deadlock. dumping raw output (may be very large)" | ||
cat stdout.txt | ||
exit 1 | ||
fi | ||
|
||
grep -q -e "spewstdio STARTING NONPRINTING SPEW ON BACKGROUND THREAD" stdout.txt || (echo "spewstdio callback was not enabled"; exit 1) | ||
|
||
echo "PASS" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
- hosts: all | ||
gather_facts: no | ||
tasks: | ||
- debug: | ||
msg: yo |
Oops, something went wrong.