Skip to content

asyncio gets stuck on event loop close #111604

@fabioz

Description

@fabioz

Bug report

Bug description:

Ok, so, the issue I'm facing is happening when using playwright-python, which uses asyncio under the hood (which is where the problem seems to lie according to my investigation).

What happens is the following:

playwright-python launches a node process using asyncio using code as:

            self._proc = await asyncio.create_subprocess_exec(
                str(self._driver_executable),
                "run-driver",
                stdin=asyncio.subprocess.PIPE,
                stdout=asyncio.subprocess.PIPE,
                stderr=_get_stderr_fileno(),
                limit=32768,
                env=env,
                startupinfo=startupinfo,
            )

Then, later on it goes on to finish the process, but after finishing the process asyncio never appears to get a notification that the process finishes and it gets stuck in the close forever due to that.

Turning on debug on the asyncio event loop shows messages such as:

DEBUG:asyncio:<IocpProactor overlapped#=2 result#=0> is running after closing for 81.1 seconds

and it never leaves the asyncio IocpProactor.close because the while self._cache: always has 2 elements there.

The elements are:

{1744745874048: (
    <_WaitHandleFuture cancelled handle=0x424 signaled>, 
    <_overlapped.Overlapped object at 0x000001963AF54270>, 0, <function IocpProactor._wait_for_handle.<locals>.finish_wait_for_handle at 0x000001963AF54160>), 
1744791716192: (
    <_WaitCancelFuture pending created at py39\lib\asyncio\windows_events.py:699 handle=0x438 signaled wait_handle=0x1963a2f4580>, 
    <_overlapped.Overlapped object at 0x000001963DB0C150>, 0, <function IocpProactor._wait_for_handle.<locals>.finish_wait_for_handle at 0x000001963DB0CD30>)
}

Those seem to be created at:

  File "py39\lib\site-packages\playwright\_impl\_transport.py", line 123, in connect
    self._proc = await asyncio.create_subprocess_exec(
  File "py39\lib\asyncio\subprocess.py", line 236, in create_subprocess_exec
    transport, protocol = await loop.subprocess_exec(
  File "py39\lib\asyncio\base_events.py", line 1676, in subprocess_exec
    transport = await self._make_subprocess_transport(
  File "py39\lib\asyncio\windows_events.py", line 394, in _make_subprocess_transport
    transp = _WindowsSubprocessTransport(self, protocol, args, shell,
  File "py39\lib\asyncio\base_subprocess.py", line 36, in __init__
    self._start(args=args, shell=shell, stdin=stdin, stdout=stdout,
  File "py39\lib\asyncio\windows_events.py", line 904, in _start
    f = self._loop._proactor.wait_for_handle(int(self._proc._handle))
  File "py39\lib\asyncio\windows_events.py", line 675, in wait_for_handle
    return self._wait_for_handle(handle, timeout, False)
  File "py39\lib\asyncio\windows_events.py", line 687, in _wait_for_handle
    traceback.print_stack()

and

  File "py39\lib\asyncio\base_events.py", line 634, in run_until_complete
    self.run_forever()
  File "py39\lib\asyncio\windows_events.py", line 321, in run_forever
    super().run_forever()
  File "py39\lib\asyncio\base_events.py", line 601, in run_forever
    self._run_once()
  File "py39\lib\asyncio\base_events.py", line 1869, in _run_once
    event_list = self._selector.select(timeout)
  File "py39\lib\asyncio\windows_events.py", line 439, in select
    self._poll(timeout)
  File "py39\lib\asyncio\windows_events.py", line 825, in _poll
    f.set_result(value)
  File "py39\lib\asyncio\windows_events.py", line 166, in set_result
    self._unregister_wait()
  File "py39\lib\asyncio\windows_events.py", line 242, in _unregister_wait
    self._event_fut = self._proactor._wait_cancel(self._event,
  File "py39\lib\asyncio\windows_events.py", line 678, in _wait_cancel
    fut = self._wait_for_handle(event, None, True)
  File "py39\lib\asyncio\windows_events.py", line 687, in _wait_for_handle
    traceback.print_stack()

This doesn't always happen. The only difference I can see from both cases is that in one case the callback registered in _WindowsSubprocessTransport is called when it works and when it fails it's not called, but on both cases the subprocess exited.

So, the actual bug seems to be that the function registered in the _WindowsSubprocessTransport:

        f = self._loop._proactor.wait_for_handle(int(self._proc._handle))
        f.add_done_callback(callback)

is never called in one case but it's called in the other, but I can't see a reason for that to happen (and so far I couldn't find any workaround that doesn't involve doing really nasty things to asyncio internals).

Not sure if it makes any difference, but this is on a Windows server 2022. Reproduced with Python 3.9, 3.10 and 3.11 (unable to check it for 3.12 as it misses some other dependencies needed).

CPython versions tested on:

3.9, 3.10, 3.11

Operating systems tested on:

Windows

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    Status

    Todo

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions