3

I am working with FastAPI and uvloop to serve a REST API in an efficient way.

I have a lot of asynchronous code that make calls to remote resources such as a database, a storage, etc, those functions looks like this:

async def _get_remote_resource(key: str) -> Resource:
    # do some async work
    return resource

I'm implementing an interface to an existing Abstract Base Class where I need to use the asynchronous function from above in a synchronous method. I have done something like:

class Resource:
     def __str__(self):
         resource = asyncio.run_until_complete(_get_remote_resource(self.key))
         return f"{resource.pk}"

Great! Now I do an endpoint in fastapi to make this work accesible:

@app.get("")
async def get(key):
     return str(Resource(key))

The problem is that FastAPI already gets and event loop running, using uvloop, and then the asynchronous code fails because the loop is already running.

Is there any way I can call the asynchronous method from the synchronous method in the class? Or do I have to rethink the structure of the code?

Nicolas Martinez
  • 719
  • 1
  • 6
  • 23

2 Answers2

2

The runtime error is designed precisely to prevent what you are trying to do. run_until_complete is a blocking call, and using it inside an async def will halt the outer event loop.

The straightforward fix is to expose the needed functionality through an actual async method, e.g.:

class Resource:
    def name(self):
        return loop.run_until_complete(self.name_async())

    async def name_async(self):
        resource = await _get_remote_resource(self.key)
        return f"{resource.pk}"

Then in fastapi you'd access the API in the native way:

@app.get("")
async def get(key):
     return await Resource(key).name_async()

You could also define __str__(self) to return self.name(), but that's best avoided because something as basic as str() should be callable from within asyncio as well (due to use in logging, debugging, etc.).

user4815162342
  • 141,790
  • 18
  • 296
  • 355
  • Yes, I agree the straightforward fix is using an async method, however, the class Resource is actually an implementation of a Abstract Base Class so the methods are already defined. – Nicolas Martinez Mar 03 '21 at 21:54
  • 1
    @NicolasMartinez That's new information. I don't think such a design is going to work - you'll need to switch to an async-aware ABC. – user4815162342 Mar 03 '21 at 22:14
  • Yes, I forgot to include that in the question! Editing ASAP. Thanks for your help. – Nicolas Martinez Mar 03 '21 at 22:35
  • 1
    We did switch to an async-aware aBC (which provides sync and async methods for almost everything) did fix the problem. Thanks! – Nicolas Martinez Nov 30 '22 at 15:56
1

I would like to complement the @user4815162342 answer.

FastAPI is an asychronous framework. I would suggest sticking to a few principles:

  • Do not execute IO operations in synchronous functions in a blocking way. Prepare this resource asynchronously and already pass the ready data to the synchronous function (this principle can be called an asynchronous dependency for synchronous code).
  • If you still need to perform a blocking IO operation in a synchronous code, then do it in a separate thread. And wait for this result asynchronously by means of asyncio (def endpoint, run_in_executor with ThreadPoolExecutor or def background task).
  • If you need to do a blocking CPU-bound operation, then delegate its execution to a separate process (the simplest way run_in_executor with ProcessPoolExecutor or any task queue).
alex_noname
  • 26,459
  • 5
  • 69
  • 86
  • Should "a synchronous code" in the second bullet be "asynchronous code"? – user4815162342 Mar 04 '21 at 07:21
  • By synchronous code, I mean here a piece of code that does not contain asynchronous calls, but includes preparatory actions and a call to a synchronous blocking function. – alex_noname Mar 04 '21 at 17:26