June 12, 2023
Use greenletio
to add asyncio
support in PyMongo, with minimal changes to existing classes.
The reason that Motor is not "truly asynchronous" is that we have blocking calls in PyMongo that do not enable cooperative multitasking. These include acquiring thread locks and socket i/o. Even if we were to use non-blocking versions of these, the event loop would still be blocked.
The previous proposal required rewriting the entirety of PyMongo to be asyncio-friendly, and gave a small performance hit for synchronous code, especially noticeable for small documents.
GreenletIO allows us to use asyncio-friendly versions of Lock, Condition, socket, SSLContext, etc, while changing nothing else about PyMongo. If GreenletIO is not available, we fall back to existing standard library versions of these classes.
- Add a module that detects the presence of GreenletIO and exposes the appropriate concurrency primitives and socket i/o for the rest of the modules.
- Move the motor classes into a subpackage in pymongo, and use the async_ function from GreenletIO to access asyncio-friendly versions of PyMongo methods (see code sketch below).
- Add an "asyncio" extra for pymongo installation.
We would be dependent on two external libraries for asyncio support: greenlet and greenletio. Greenlet tends to not be available right away for a given version of Python. There is an open PR right now for Python 3.12 support. Python 3.11 support was merged during its alpha phase, and Python 3.10 support was merged during its beta phase.
If GreenletIO is installed and Gevent or Eventlet are in use, we may need additional considerations like detecting if monkey patched, or allowing an env variable to disable GreenletIO usage. It might also be an option to have an example that shows how to use asyncio-gevent.
import asyncio
from time import sleep, time_ns
from greenletio import async_
from greenletio.green.time import sleep as asleep
def sendall(delay):
asleep(delay)
def command(delay):
return inner(delay)
acommand = async_(command)
async def main():
coros = [acommand(1) for _ in range(10)]
start = time_ns()
await asyncio.gather(*coros)
print((time_ns() - start) / 1e9)
print('Async greenletio program, 10 1sec commands)
asyncio.run(main())
asyncio.set_event_loop(asyncio.new_event_loop())
print('Sync greenletio program, 10 0.1sec commands')
start = time_ns()
[sleep(0.1) for _ in range(10)]
print((time_ns() - start) / 1e9)
print('Regular program, 10 0.1sec sleeps')
start = time_ns()
[sleep(0.1) for _ in range(10)]
print((time_ns() - start) / 1e9)
"""
Output:
Async greenletio program, 10 1sec sleeps
1.002226
Sync greenletio program, 10 0.1sec sleeps
1.012643
Regular program, 10 0.1sec sleeps
1.029575
"""