Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to call async method from greenlet (playwright)

My framework (Locust, https://github.com/locustio/locust) is based on gevent and greenlets. But I would like to leverage Playwright (https://playwright.dev/python/), which is built on asyncio.

Naively using Playwrights sync api doesnt work and gives an exception:

playwright._impl._api_types.Error: It looks like you are using Playwright Sync API inside the asyncio loop.
Please use the Async API instead.

I'm looking for some kind of best practice on how to use async in combination with gevent.

I've tried a couple different approaches but I dont know if I'm close or if what I'm trying to do is even possible (I have some experience with gevent, but havent really used asyncio before)

Edit: I kind of have something working now (I've removed Locust and just directly spawned some greenlets to make it easier to understan). Is this as good as it gets, or is there a better solution?

import asyncio
import threading
from playwright.async_api import async_playwright
import gevent


def thr(i):
    loop = asyncio.new_event_loop()
    asyncio.set_event_loop(loop)
    loop.run_until_complete(do_stuff(i))
    loop.close()


async def do_stuff(i):
    playwright = await async_playwright().start()
    browser = await playwright.chromium.launch(headless=False)
    page = await browser.new_page()
    await page.wait_for_timeout(5000)
    await page.goto(f"https://google.com")
    await page.close()
    print(i)


def green(i):
    t = threading.Thread(target=thr, args=(i,))
    t.start()
    # t.join() # joining doesnt work, but I couldnt be bothered right now :)



g1 = gevent.spawn(green, 1)
g2 = gevent.spawn(green, 2)
g1.join()
g2.join()
like image 487
Cyberwiz Avatar asked Sep 05 '25 03:09

Cyberwiz


2 Answers

Insipred by @user4815162342 's comment, I went with something like this:

from playwright.async_api import async_playwright # need to import this first
from gevent import monkey, spawn
import asyncio
import gevent

monkey.patch_all()
loop = asyncio.new_event_loop()


async def f():
    print("start")
    playwright = await async_playwright().start()
    browser = await playwright.chromium.launch(headless=True)
    context = await browser.new_context()
    page = await context.new_page()
    await page.goto(f"https://www.google.com")
    print("done")


def greeny():
    while True:  # and not other_exit_condition
        future = asyncio.run_coroutine_threadsafe(f(), loop)
        while not future.done():
            gevent.sleep(1)


greenlet1 = spawn(greeny)
greenlet2 = spawn(greeny)
loop.run_forever()

The actual implementation will end up in Locust some day, probably after some optimization (reusing browser instance etc)

like image 139
Cyberwiz Avatar answered Sep 07 '25 23:09

Cyberwiz


Here's a simple way to integrate asyncio and gevent:

  • Run an asyncio loop in a dedicated thread
  • Use asyncio.run_coroutine_threadsafe() to run a coroutine
  • Use gevent.event.Event to wait until the coroutine resolves
import asyncio
import threading
import gevent

loop = asyncio.new_event_loop()
loop_thread = threading.Thread(target=loop.run_forever, daemon=True)
loop_thread.start()

async def your_coro():
  # ...

def wait_until_complete(coro):
  future = asyncio.run_coroutine_threadsafe(coro, loop)

  event = gevent.event.Event()
  future.add_dome_callback(lambda _: event.set())
  event.wait()

  return future.result()

result = wait_until_complete(your_coro())
like image 40
Panic Avatar answered Sep 08 '25 01:09

Panic