python-betterproto icon indicating copy to clipboard operation
python-betterproto copied to clipboard

Option for generating code without async?

Open adisunw opened this issue 5 years ago • 10 comments

I have some infrastructure developed around the previously gen'd grpc code, however I would really like to adopt betterproto for all its extra pythonic features.

The only hurdle i'm facing is that the async implementation is turned on by default. Is there a way to turn this off and not have the async gen code?

adisunw avatar Feb 12 '20 19:02 adisunw

Similar issue: https://github.com/danielgtaylor/python-betterproto/issues/44

nat-n avatar May 21 '20 15:05 nat-n

Just to clarify beyond all doubt here -- for the moment the answer here is no, correct? We can't turn off async/await in the generated stub?

githorse avatar May 27 '20 16:05 githorse

@githorse your assessment is correct.

nat-n avatar May 27 '20 16:05 nat-n

This library does not work for Cloud Run due to the lack of this functionality. If it worked for sure betterproto would be the best option.

hiagopdutra avatar May 04 '21 17:05 hiagopdutra

What about using native Python's async-to-sync functionality? That is, given an async RPC method called via result = await ServiceStub.some_method(), you could do either:

result = asyncio.run(ServiceStub.some_method())

...if you wanted to start and stop the event loop just for that method. If that incurs too much overhead, you could call into an existing event loop via something like:

result = asyncio.run_coroutine_threadsafe(ServiceStub.some_method(), loop=asyncio.get_event_loop()).result()

@nat-n if that satisfies user requirements, would you accept a PR documenting that technique?

zbentley avatar Oct 10 '21 16:10 zbentley

I tried that as a workaround. The problem was that

  • grpclib channel constructor requires a running working loop. Constructor can't be marked as async so using those run in async functions wouldn't work. So you'd have to set a main loop before calling that. I'm not sure about the impact of doing so in asyncio-based web servers like gunicorn w/aiohttp or uvicorn. From running in production for the last quarter I never had any issue.
  • That loop is used to maintain connection keepalive. Running RPC and the event loop to RPC completion works until the remote end timeout the connection (because no loop is running to reply to pings). I believe normal gRPC library has automatic retry in this case but grpclib would just stop working entirely.
  • One workaround I use is to call channel.close() after every RPC to always get a fresh connection, but that's not efficient at all.

whs avatar Jan 29 '22 04:01 whs

If I may suggest, protobuf-ts has a swappable transport implementation for RPC where you can swap between gRPC, twirp and grpcweb.

whs avatar Jan 29 '22 04:01 whs

any news? need sync grpc client...

theartofdevel avatar Sep 22 '22 19:09 theartofdevel

I have a similar requirement for a client; and I thought I'd share my current workaround. I didn't want to edit the generated client stubs because I may need to re-generate them frequently. I also wanted to hide the async stuff from the user, and let them call the service methods as if they're synchronous.

To achieve this, I created two decorators.

import asyncio
from functools import wraps


def synced(async_func):
    """Wrap the asynchronous function so that it becomes synchronous"""

    @wraps(async_func)
    def wrapped(*args, **kwargs):
        return asyncio.get_event_loop().run_until_complete(async_func(*args, **kwargs))

    return wrapped


def sync(cls):
    """Decorator for the service stubs to make them synchronized"""
    # Find all service methods (endpoints) by inspecting the passed class
    endpoints = tuple(
        getattr(cls, endpoint)
        for endpoint in dir(cls)
        if callable(getattr(cls, endpoint)) and not endpoint.startswith("_")
    )
   # Replace the methods by their `synced` variants
    for endpoint in endpoints:
        setattr(cls, endpoint.__name__, synced(endpoint))
    return cls

The decorator synced creates a single async event loop each time the method is called and runs the loop until the method is complete. The decorator sync is then used to apply the synced decorator to each service method residing in the service stub class. it assumes that the service methods are callables that don't start with an underscore.

To get a synchronized service from the generated asynchronous ServiceStub, you initiate it as follows:

synchronized_service = sync(ServiceStub(channel=...))  # enter a channel as you normally would

The upside of this method is that your IDE will pick up the service methods. The downside of this method is that tools like mypy get confused because by applying these decorators, we are (dynamically) changing the return types.

wpdonders avatar Dec 12 '22 09:12 wpdonders

@wpdonders that solution works, but may break in surprising ways in the presence of multiprocessing or other threads. I suggest using the asgiref.sync package (specifically async_to_sync), which provides the same functionality without those drawbacks.

zbentley avatar Dec 12 '22 16:12 zbentley