microsoft / playwright-python

Python version of the Playwright testing and automation library.
https://playwright.dev/python/
Apache License 2.0
11.89k stars 906 forks source link

[Bug]: Task cancellation raises asyncio.InvalidState due to a cancelled future being set #2581

Closed blubber closed 1 month ago

blubber commented 1 month ago

Version

1.47.0

Steps to reproduce

  1. Use the async_playwright context manager to open a context.
  2. Launch a new browser (only tested with Chromium).
  3. Create a new page.
  4. Within the async_playwright context create a task group, add at least two tasks, one should be a playwright task (i.e. page.<some_locator>.inner_text()).
  5. Add a second task that raises an exception.
  6. The exception will result in the task group being cancelled, which is not gracefully handled by playwright.

Expected behavior

I expected the task group to cancel the playwright task and than rereaise the original exception. This does happen, however, in cleaning up after cancellation playwright attempts to call set_exception on a future that is already cancelled.ror.

The problem appears to be in the cleanup method where set_exception is called on an already cancelled future. I verified this by putting a bunch of print statements in that particular code, and print(callback.fugure) printed <Future cancelled>. Calling set or set_exception an an already cancelled exception causes asyncio to raise a InvalidState error.

Actual behavior

Traceback (most recent call last):
  File "/Users/tiemo/Workspace/playwright_bug/test.py", line 21, in <module>
    asyncio.run(use_playwright())
  File "/opt/homebrew/Cellar/python@3.12/3.12.5/Frameworks/Python.framework/Versions/3.12/lib/python3.12/asyncio/runners.py", line 194, in run
    return runner.run(main)
           ^^^^^^^^^^^^^^^^
  File "/opt/homebrew/Cellar/python@3.12/3.12.5/Frameworks/Python.framework/Versions/3.12/lib/python3.12/asyncio/runners.py", line 118, in run
    return self._loop.run_until_complete(task)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/homebrew/Cellar/python@3.12/3.12.5/Frameworks/Python.framework/Versions/3.12/lib/python3.12/asyncio/base_events.py", line 687, in run_until_complet
e
    return future.result()
           ^^^^^^^^^^^^^^^
  File "/Users/tiemo/Workspace/playwright_bug/test.py", line 11, in use_playwright
    async with async_playwright() as sp:
  File "/Users/tiemo/Workspace/playwright_bug/venv12/lib/python3.12/site-packages/playwright/async_api/_context_manager.py", line 57, in __aexit__
    await self._connection.stop_async()
  File "/Users/tiemo/Workspace/playwright_bug/venv12/lib/python3.12/site-packages/playwright/_impl/_connection.py", line 285, in stop_async
    self.cleanup()
  File "/Users/tiemo/Workspace/playwright_bug/venv12/lib/python3.12/site-packages/playwright/_impl/_connection.py", line 297, in cleanup
    callback.future.set_exception(self._closed_error)
asyncio.exceptions.InvalidStateError: invalid state

Additional context

Below code reliably reproduces the error:

import asyncio

from playwright.async_api import async_playwright

async def raise_exception():
    raise ValueError("Something went wrong")

async def use_playwright():
    async with async_playwright() as sp:
        browser = await sp.chromium.launch()
        page = await browser.new_page()
        await page.goto("https://example.com")

        async with asyncio.TaskGroup() as group:
            group.create_task(page.locator("css=h1").inner_text())
            group.create_task(raise_exception())

asyncio.run(use_playwright())

Environment

- OS: macOs 15
- CPU: Apple M1
- Python version: 3.12.5
- Browsers: Chromium