Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ctrl-c / KeyboardInterrupt makes event loop exceptions #8

Closed
mguijarr opened this issue Nov 16, 2022 · 6 comments · Fixed by #10
Closed

ctrl-c / KeyboardInterrupt makes event loop exceptions #8

mguijarr opened this issue Nov 16, 2022 · 6 comments · Fixed by #10

Comments

@mguijarr
Copy link

Exiting asyncio.run(<a main function()>) with SIGINT (ctrl-c, KeyboardInterrupt exception) makes bad RuntimeError exceptions. I am affected by this in a prompt-toolkit application relying on gevent.

The minimum code to reproduce the problem:

import gevent
import gevent.monkey; gevent.monkey.patch_all()
import asyncio
import asyncio_gevent
import time

asyncio.set_event_loop_policy(asyncio_gevent.EventLoopPolicy())

async def my_main():
    gevent.sleep(10)

# press CTRL-C to exit the Python program before 10 seconds
asyncio.run(my_main())
python test.py
^CKeyboardInterrupt
2022-11-16T14:00:43Z
Traceback (most recent call last):
  File "/home/matias/miniconda3/envs/bliss/lib/python3.7/asyncio/runners.py", line 43, in run
    return loop.run_until_complete(main)
  File "/home/matias/miniconda3/envs/bliss/lib/python3.7/asyncio/base_events.py", line 574, in run_until_complete
    self.run_forever()
  File "/home/matias/dev/asyncio-gevent/asyncio_gevent/event_loop.py", line 19, in run_forever
    greenlet.join()
  File "src/gevent/greenlet.py", line 833, in gevent._gevent_cgreenlet.Greenlet.join
  File "src/gevent/greenlet.py", line 859, in gevent._gevent_cgreenlet.Greenlet.join
  File "src/gevent/greenlet.py", line 848, in gevent._gevent_cgreenlet.Greenlet.join
  File "src/gevent/_greenlet_primitives.py", line 61, in gevent._gevent_c_greenlet_primitives.SwitchOutGreenletWithLoop.switch
  File "src/gevent/_greenlet_primitives.py", line 61, in gevent._gevent_c_greenlet_primitives.SwitchOutGreenletWithLoop.switch
  File "src/gevent/_greenlet_primitives.py", line 65, in gevent._gevent_c_greenlet_primitives.SwitchOutGreenletWithLoop.switch
  File "src/gevent/_gevent_c_greenlet_primitives.pxd", line 35, in gevent._gevent_c_greenlet_primitives._greenlet_switch
KeyboardInterrupt

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/home/matias/miniconda3/envs/bliss/lib/python3.7/asyncio/runners.py", line 46, in run
    _cancel_all_tasks(loop)
  File "/home/matias/miniconda3/envs/bliss/lib/python3.7/asyncio/runners.py", line 62, in _cancel_all_tasks
    tasks.gather(*to_cancel, loop=loop, return_exceptions=True))
  File "/home/matias/miniconda3/envs/bliss/lib/python3.7/asyncio/base_events.py", line 563, in run_until_complete
    self._check_runnung()
  File "/home/matias/miniconda3/envs/bliss/lib/python3.7/asyncio/base_events.py", line 523, in _check_runnung
    raise RuntimeError('This event loop is already running')
RuntimeError: This event loop is already running

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/tmp/bla.py", line 12, in <module>
    asyncio.run(my_main())
  File "/home/matias/miniconda3/envs/bliss/lib/python3.7/asyncio/runners.py", line 50, in run
    loop.close()
  File "/home/matias/miniconda3/envs/bliss/lib/python3.7/asyncio/unix_events.py", line 55, in close
    super().close()
  File "/home/matias/miniconda3/envs/bliss/lib/python3.7/asyncio/selector_events.py", line 88, in close
    raise RuntimeError("Cannot close a running event loop")
RuntimeError: Cannot close a running event loop

@gfmio
Copy link
Owner

gfmio commented Nov 21, 2022

Thanks for reporting, I'll look into this!

@gfmio
Copy link
Owner

gfmio commented Nov 22, 2022

I haven't found a complete solution yet, but the problem appears to be that gevent.sleep pauses the current greenlet, so that nothing in it will execute.

@gfmio gfmio closed this as completed in #10 Dec 5, 2022
gfmio added a commit that referenced this issue Dec 5, 2022
Fixes #8. This PR changes the behaviour of `asyncio.greenlet_to_future` by making sure that the greenlet is awaited in a separate thread which prevents blocking of the main thread and makes interrupts safe to use with the code. The PR also adds a note to the README describing how to work with `gevent.sleep`.
@gfmio
Copy link
Owner

gfmio commented Dec 5, 2022

I've found the reason for this behaviour and implemented a fix in #10. The change has been released as version 0.2.3.

The issue is that the gevent.sleep is causing the running greenlet to pause, which pauses the asyncio event loop by default.

The solution is to run any code that runs gevent.sleep in another greenlet using asyncio.sync_to_async or (equivalently) asyncio_gevent.greenlet_to_future(gevent.spawn(f)). This causes the function to be executed in another greenlet which prevents the main greenlet from getting blocked.

Example:

import gevent.monkey

gevent.monkey.patch_all()

import asyncio
import threading

import gevent

import asyncio_gevent

asyncio.set_event_loop_policy(asyncio_gevent.EventLoopPolicy())


async def f():
    print("f", 1)
    await asyncio.sleep(1)
    print("f", 2)


def g():
    print("g", 1)
    gevent.sleep(2)
    print("g", 2)


async def main():
    await asyncio.gather(f(), asyncio_gevent.sync_to_async(g)())
    # OR equivalently
    # await asyncio.gather(f(), asyncio_gevent.greenlet_to_future(gevent.spawn(g)))


if __name__ == "__main__":
    asyncio.run(main())

The output will be (as expected):

g 1
f 1
f 2
g 2

If gevent.sleep is called inside an async function, then the async function needs to first be wrapped in asyncio.async_to_sync.

import gevent.monkey

gevent.monkey.patch_all()

import asyncio

import gevent

import asyncio_gevent

asyncio.set_event_loop_policy(asyncio_gevent.EventLoopPolicy())


async def f():
    print("f", 1)
    await asyncio.sleep(1)
    print("f", 2)


async def g():
    print("g", 1)
    await asyncio.sleep(1)
    gevent.sleep(1)
    print("g", 2)


async def main():
    await asyncio.gather(
        f(), asyncio_gevent.sync_to_async(asyncio_gevent.async_to_sync(g))()
    )


if __name__ == "__main__":
    asyncio.run(main())

The output will again be (as expected):

g 1
f 1
f 2
g 2

In both cases, you can Ctrl+C anytime and you will receive the standard asyncio behaviour of triggering a CancelledError.

I hope this fixes your issue as well. Please re-open the ticket if you still encounter this issue.

@spumer
Copy link

spumer commented Nov 28, 2023

I have a suggestion to optmize this workaround.

Right now we use default executor (None) to join greenlet, and this can produce a lot of threads or can get deadlock if no more threads can be spawned to join greenlet (context switch)

wait_executor = concurrent.futures.ThreadPoolExecutor(max_workers=1)

async def _await_greenlet(
    ...      
        result, _ = await asyncio.gather(
            future, loop.run_in_executor(wait_executor, gevent.sleep, 0)  # or we can use gevent.idle
        )

I was testing it with max_workers=1 and greenlet.join and got stuck on loading django admin. gevent.sleep / gevent.idle allow context switching and instantly free thread. As result no deadlocks and threads overhead and clear traceback at CTR+C

@spumer
Copy link

spumer commented Nov 28, 2023

Can i make a PR? :)

@gfmio
Copy link
Owner

gfmio commented Nov 29, 2023

Sure, go ahead :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants