Skip to content

Commit b881df4

Browse files
gh-139894: fix incorrect sharing of current task while forking in asyncio (#139897)
Fix incorrect sharing of current task with the forked child process by clearing thread state's current task and current loop in `PyOS_AfterFork_Child`.
1 parent c7f1da9 commit b881df4

File tree

3 files changed

+72
-25
lines changed

3 files changed

+72
-25
lines changed

Lib/test/test_asyncio/test_unix_events.py

Lines changed: 61 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1180,32 +1180,68 @@ async def runner():
11801180

11811181

11821182
@support.requires_fork()
1183-
class TestFork(unittest.IsolatedAsyncioTestCase):
1183+
class TestFork(unittest.TestCase):
11841184

1185-
async def test_fork_not_share_event_loop(self):
1186-
with warnings_helper.ignore_fork_in_thread_deprecation_warnings():
1187-
# The forked process should not share the event loop with the parent
1188-
loop = asyncio.get_running_loop()
1189-
r, w = os.pipe()
1190-
self.addCleanup(os.close, r)
1191-
self.addCleanup(os.close, w)
1192-
pid = os.fork()
1193-
if pid == 0:
1194-
# child
1195-
try:
1196-
loop = asyncio.get_event_loop()
1197-
os.write(w, b'LOOP:' + str(id(loop)).encode())
1198-
except RuntimeError:
1199-
os.write(w, b'NO LOOP')
1200-
except BaseException as e:
1201-
os.write(w, b'ERROR:' + ascii(e).encode())
1202-
finally:
1203-
os._exit(0)
1204-
else:
1205-
# parent
1206-
result = os.read(r, 100)
1207-
self.assertEqual(result, b'NO LOOP')
1208-
wait_process(pid, exitcode=0)
1185+
@warnings_helper.ignore_fork_in_thread_deprecation_warnings()
1186+
def test_fork_not_share_current_task(self):
1187+
loop = object()
1188+
task = object()
1189+
asyncio._set_running_loop(loop)
1190+
self.addCleanup(asyncio._set_running_loop, None)
1191+
asyncio.tasks._enter_task(loop, task)
1192+
self.addCleanup(asyncio.tasks._leave_task, loop, task)
1193+
self.assertIs(asyncio.current_task(), task)
1194+
r, w = os.pipe()
1195+
self.addCleanup(os.close, r)
1196+
self.addCleanup(os.close, w)
1197+
pid = os.fork()
1198+
if pid == 0:
1199+
# child
1200+
try:
1201+
asyncio._set_running_loop(loop)
1202+
current_task = asyncio.current_task()
1203+
if current_task is None:
1204+
os.write(w, b'NO TASK')
1205+
else:
1206+
os.write(w, b'TASK:' + str(id(current_task)).encode())
1207+
except BaseException as e:
1208+
os.write(w, b'ERROR:' + ascii(e).encode())
1209+
finally:
1210+
asyncio._set_running_loop(None)
1211+
os._exit(0)
1212+
else:
1213+
# parent
1214+
result = os.read(r, 100)
1215+
self.assertEqual(result, b'NO TASK')
1216+
wait_process(pid, exitcode=0)
1217+
1218+
@warnings_helper.ignore_fork_in_thread_deprecation_warnings()
1219+
def test_fork_not_share_event_loop(self):
1220+
# The forked process should not share the event loop with the parent
1221+
loop = object()
1222+
asyncio._set_running_loop(loop)
1223+
self.assertIs(asyncio.get_running_loop(), loop)
1224+
self.addCleanup(asyncio._set_running_loop, None)
1225+
r, w = os.pipe()
1226+
self.addCleanup(os.close, r)
1227+
self.addCleanup(os.close, w)
1228+
pid = os.fork()
1229+
if pid == 0:
1230+
# child
1231+
try:
1232+
loop = asyncio.get_event_loop()
1233+
os.write(w, b'LOOP:' + str(id(loop)).encode())
1234+
except RuntimeError:
1235+
os.write(w, b'NO LOOP')
1236+
except BaseException as e:
1237+
os.write(w, b'ERROR:' + ascii(e).encode())
1238+
finally:
1239+
os._exit(0)
1240+
else:
1241+
# parent
1242+
result = os.read(r, 100)
1243+
self.assertEqual(result, b'NO LOOP')
1244+
wait_process(pid, exitcode=0)
12091245

12101246
@warnings_helper.ignore_fork_in_thread_deprecation_warnings()
12111247
@hashlib_helper.requires_hashdigest('md5')
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix incorrect sharing of current task with the child process while forking in :mod:`asyncio`. Patch by Kumar Aditya.

Modules/posixmodule.c

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -689,6 +689,14 @@ reset_remotedebug_data(PyThreadState *tstate)
689689
_Py_MAX_SCRIPT_PATH_SIZE);
690690
}
691691

692+
static void
693+
reset_asyncio_state(_PyThreadStateImpl *tstate)
694+
{
695+
llist_init(&tstate->asyncio_tasks_head);
696+
tstate->asyncio_running_loop = NULL;
697+
tstate->asyncio_running_task = NULL;
698+
}
699+
692700

693701
void
694702
PyOS_AfterFork_Child(void)
@@ -725,6 +733,8 @@ PyOS_AfterFork_Child(void)
725733

726734
reset_remotedebug_data(tstate);
727735

736+
reset_asyncio_state((_PyThreadStateImpl *)tstate);
737+
728738
// Remove the dead thread states. We "start the world" once we are the only
729739
// thread state left to undo the stop the world call in `PyOS_BeforeFork`.
730740
// That needs to happen before `_PyThreadState_DeleteList`, because that

0 commit comments

Comments
 (0)