Skip to content

Instantly share code, notes, and snippets.

@blink1073
Created June 16, 2023 00:12
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save blink1073/3873afd23977a8ab94a423127f2ca2ce to your computer and use it in GitHub Desktop.
Save blink1073/3873afd23977a8ab94a423127f2ca2ce to your computer and use it in GitHub Desktop.
Async PyMongo with Greenlet

Async PyMongo with Greenlet

June 12, 2023

Proposal

Use greenletio to add asyncio support in PyMongo, with minimal changes to existing classes.

Background

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.

Implementation

  • 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.

Considerations

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.

Code Sketch

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
"""
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment