Skip to content

Commit 060907e

Browse files
committed
fix - uvloop freeze when something other than uvloop invoke process fork and execute non-python process
Problem: Uvloop for each loop register `atFork` handler that is called after fork is executed by forked child. This handler works fine when fork was invoked by uvloop. In case fork is invoked by something else (such as external library) uvloop freeze in this handler because: - GIL is acquired inside `atFork` handler -> in case forked child does not contain python runtime `atFork` handler freeze at obtaining GIL - when compiled in debug mode (`make debug`) cython trace calls are inserted inside compiled `atFork` handler -> in case forked child does not contain python runtime `atFork` handler freeze at providing trace call Solution: This fix solve described problems by implementing `atFork` handler as C function so that forked child can call it safely whether or not contains python runtime.
1 parent 6b91061 commit 060907e

File tree

5 files changed

+140
-11
lines changed

5 files changed

+140
-11
lines changed

tests/test_process_spawning.py

+109
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import asyncio
2+
import ctypes.util
3+
import logging
4+
from concurrent.futures import ThreadPoolExecutor
5+
from threading import Thread
6+
from unittest import TestCase
7+
8+
import uvloop
9+
10+
11+
class ProcessSpawningTestCollection(TestCase):
12+
13+
def test_spawning_external_process(self):
14+
"""Test spawning external process (using `popen` system call) that
15+
cause loop freeze."""
16+
17+
async def run(loop):
18+
event = asyncio.Event(loop=loop)
19+
20+
dummy_workers = [simulate_loop_activity(loop, event)
21+
for _ in range(5)]
22+
spawn_worker = spawn_external_process(loop, event)
23+
done, pending = await asyncio.wait([spawn_worker] + dummy_workers,
24+
loop=loop)
25+
exceptions = [result.exception()
26+
for result in done if result.exception()]
27+
if exceptions:
28+
raise exceptions[0]
29+
30+
return True
31+
32+
async def simulate_loop_activity(loop, done_event):
33+
"""Simulate loop activity by busy waiting for event."""
34+
while True:
35+
try:
36+
await asyncio.wait_for(done_event.wait(),
37+
timeout=0.1, loop=loop)
38+
except asyncio.TimeoutError:
39+
pass
40+
41+
if done_event.is_set():
42+
return None
43+
44+
async def spawn_external_process(loop, event):
45+
executor = ThreadPoolExecutor()
46+
try:
47+
call = loop.run_in_executor(executor, spawn_process)
48+
await asyncio.wait_for(call, loop=loop, timeout=3600)
49+
finally:
50+
event.set()
51+
executor.shutdown(wait=False)
52+
return True
53+
54+
BUFFER_LENGTH = 1025
55+
BufferType = ctypes.c_char * (BUFFER_LENGTH - 1)
56+
57+
def run_echo(popen, fread, pclose):
58+
fd = popen('echo test'.encode('ASCII'), 'r'.encode('ASCII'))
59+
try:
60+
while True:
61+
buffer = BufferType()
62+
data = ctypes.c_void_p(ctypes.addressof(buffer))
63+
64+
# -> this call will freeze whole loop in case of bug
65+
read = fread(data, 1, BUFFER_LENGTH, fd)
66+
if not read:
67+
break
68+
except Exception:
69+
logging.getLogger().exception('read error')
70+
raise
71+
finally:
72+
pclose(fd)
73+
74+
def spawn_process():
75+
"""Spawn external process via `popen` system call."""
76+
77+
stdio = ctypes.CDLL(ctypes.util.find_library('c'))
78+
79+
# popen system call
80+
popen = stdio.popen
81+
popen.argtypes = (ctypes.c_char_p, ctypes.c_char_p)
82+
popen.restype = ctypes.c_void_p
83+
84+
# pclose system call
85+
pclose = stdio.pclose
86+
pclose.argtypes = (ctypes.c_void_p,)
87+
pclose.restype = ctypes.c_int
88+
89+
# fread system call
90+
fread = stdio.fread
91+
fread.argtypes = (ctypes.c_void_p, ctypes.c_size_t,
92+
ctypes.c_size_t, ctypes.c_void_p)
93+
fread.restype = ctypes.c_size_t
94+
95+
for iteration in range(1000):
96+
t = Thread(target=run_echo,
97+
args=(popen, fread, pclose),
98+
daemon=True)
99+
t.start()
100+
t.join(timeout=10.0)
101+
if t.is_alive():
102+
raise Exception('process freeze detected at {}'
103+
.format(iteration))
104+
105+
return True
106+
107+
loop = uvloop.new_event_loop()
108+
proc = loop.run_until_complete(run(loop))
109+
self.assertTrue(proc)

uvloop/handles/process.pyx

+3
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ cdef class UVProcess(UVHandle):
2828

2929
global __forking
3030
global __forking_loop
31+
global __forkHandler
3132

3233
cdef int err
3334

@@ -76,6 +77,7 @@ cdef class UVProcess(UVHandle):
7677
loop.active_process_handler = self
7778
__forking = 1
7879
__forking_loop = loop
80+
__forkHandler = <OnForkHandler>&__get_fork_handler
7981

8082
PyOS_BeforeFork()
8183

@@ -85,6 +87,7 @@ cdef class UVProcess(UVHandle):
8587

8688
__forking = 0
8789
__forking_loop = None
90+
__forkHandler = NULL
8891
loop.active_process_handler = None
8992

9093
PyOS_AfterFork_Parent()

uvloop/includes/fork_handler.h

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
2+
typedef void (*OnForkHandler)();
3+
4+
OnForkHandler __forkHandler = NULL;
5+
6+
/* Auxiliary function to call global fork handler if defined.
7+
8+
Note: Fork handler needs to be in C (not cython) otherwise it would require
9+
GIL to be present, but some forks can exec non-python processes.
10+
*/
11+
void handleAtFork() {
12+
if (__forkHandler != NULL) {
13+
__forkHandler();
14+
}
15+
}

uvloop/includes/system.pxd

+4-4
Original file line numberDiff line numberDiff line change
@@ -59,12 +59,12 @@ cdef extern from "unistd.h" nogil:
5959
void _exit(int status)
6060

6161

62-
cdef extern from "pthread.h" nogil:
62+
cdef extern from "pthread.h":
6363

6464
int pthread_atfork(
65-
void (*prepare)() nogil,
66-
void (*parent)() nogil,
67-
void (*child)() nogil)
65+
void (*prepare)(),
66+
void (*parent)(),
67+
void (*child)())
6868

6969

7070
cdef extern from "includes/compat.h" nogil:

uvloop/loop.pyx

+9-7
Original file line numberDiff line numberDiff line change
@@ -3117,19 +3117,21 @@ cdef vint __atfork_installed = 0
31173117
cdef vint __forking = 0
31183118
cdef Loop __forking_loop = None
31193119

3120+
cdef extern from "includes/fork_handler.h":
31203121

3121-
cdef void __atfork_child() nogil:
3122-
# See CPython/posixmodule.c for details
3122+
ctypedef void (*OnForkHandler)()
3123+
cdef OnForkHandler __forkHandler
3124+
void handleAtFork()
3125+
3126+
cdef void __get_fork_handler() nogil:
31233127
global __forking
3128+
global __forking_loop
31243129

31253130
with gil:
3126-
if (__forking and
3127-
__forking_loop is not None and
3131+
if (__forking and __forking_loop is not None and
31283132
__forking_loop.active_process_handler is not None):
3129-
31303133
__forking_loop.active_process_handler._after_fork()
31313134

3132-
31333135
cdef __install_atfork():
31343136
global __atfork_installed
31353137
if __atfork_installed:
@@ -3138,7 +3140,7 @@ cdef __install_atfork():
31383140

31393141
cdef int err
31403142

3141-
err = system.pthread_atfork(NULL, NULL, &__atfork_child)
3143+
err = system.pthread_atfork(NULL, NULL, &handleAtFork)
31423144
if err:
31433145
__atfork_installed = 0
31443146
raise convert_error(-err)

0 commit comments

Comments
 (0)