Skip to content

Commit d70ede3

Browse files
gh-157259: Record awaited-by edge for asyncio subprocess pipe-connect task
When BaseSubprocessTransport creates a task for _connect_pipes(waiter), register the awaited-by relationship between task and waiter so that asyncio.print_call_graph() and asyncio.capture_call_graph() connect the background pipe-connect task to the awaiting parent task. Discard the edge in a done callback once _connect_pipes completes or is cancelled.
1 parent 15f3d7f commit d70ede3

3 files changed

Lines changed: 54 additions & 1 deletion

File tree

‎Lib/asyncio/base_subprocess.py‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import signal
66
import sys
77

8+
from . import futures
89
from . import protocols
910
from . import transports
1011
from .log import logger
@@ -53,7 +54,12 @@ def __init__(self, loop, protocol, args, shell,
5354
logger.debug('process %r created: pid %s',
5455
program, self._pid)
5556

56-
self._loop.create_task(self._connect_pipes(waiter))
57+
task = self._loop.create_task(self._connect_pipes(waiter))
58+
if waiter is not None:
59+
futures.future_add_to_awaited_by(task, waiter)
60+
task.add_done_callback(
61+
lambda t: futures.future_discard_from_awaited_by(t, waiter)
62+
)
5763

5864
def __repr__(self):
5965
info = [self.__class__.__name__]

‎Lib/test/test_asyncio/test_graph.py‎

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -450,6 +450,52 @@ async def main(t):
450450
]
451451
])
452452

453+
async def test_stack_subprocess_connect_pipes(self):
454+
# gh-157259: create_subprocess_exec must record the awaiting waiter / task
455+
# on the internal _connect_pipes task
456+
loop = asyncio.get_running_loop()
457+
in_connect_pipes = asyncio.Event()
458+
can_finish = asyncio.Event()
459+
460+
pipe_task = None
461+
orig_connect = loop.connect_write_pipe
462+
async def mocked_connect(*args, **kwargs):
463+
nonlocal pipe_task
464+
pipe_task = asyncio.current_task()
465+
in_connect_pipes.set()
466+
await can_finish.wait()
467+
return await orig_connect(*args, **kwargs)
468+
469+
loop.connect_write_pipe = mocked_connect
470+
spawn = None
471+
try:
472+
spawn = asyncio.create_task(
473+
asyncio.create_subprocess_exec(
474+
sys.executable, '-c', 'pass',
475+
stdin=asyncio.subprocess.PIPE,
476+
),
477+
name='spawn',
478+
)
479+
480+
await in_connect_pipes.wait()
481+
self.assertIsNotNone(pipe_task)
482+
483+
stack = capture_test_stack(fut=pipe_task)
484+
485+
self.assertEqual(stack[0][0], 'T<anon>')
486+
awaited_by = stack[0][2]
487+
self.assertEqual(len(awaited_by), 1)
488+
self.assertEqual(awaited_by[0][0], 'F')
489+
self.assertEqual(awaited_by[0][2][0][0], 'T<spawn>')
490+
finally:
491+
can_finish.set()
492+
loop.connect_write_pipe = orig_connect
493+
if spawn is not None:
494+
proc = await spawn
495+
await proc.wait()
496+
497+
self.assertFalse(pipe_task._asyncio_awaited_by)
498+
453499
async def test_stack_task(self):
454500

455501
stack_for_inner = None
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Record the awaiting task in the :mod:`asyncio` call graph for internal pipe-connection tasks created during subprocess spawning.

0 commit comments

Comments
 (0)