Make a Python asyncio call from a Flask route

前端 未结 5 591
迷失自我
迷失自我 2020-12-07 15:31

I want to execute an async function every time the Flask route is executed. Why is the abar function never executed?

import asyncio
from flask          


        
相关标签:
5条回答
  • 2020-12-07 16:16

    A simpler solution to your problem (in my biased view) is to switch to Quart from Flask. If so your snippet simplifies to,

    import asyncio
    from quart import Quart
    
    async def abar(a):
        print(a)
    
    app = Quart(__name__)
    
    @app.route("/")
    async def notify():
        await abar("abar")
        return "OK"
    
    if __name__ == "__main__":
        app.run(debug=False)
    

    As noted in the other answers the Flask app run is blocking, and does not interact with an asyncio loop. Quart on the other hand is the Flask API built on asyncio, so it should work how you expect.

    Also as an update, Flask-Aiohttp is no longer maintained.

    0 讨论(0)
  • 2020-12-07 16:16

    For same reason you won't see this print:

    if __name__ == "__main__":
        app.run(debug=False, use_reloader=False)
        print('Hey!')
        loop.run_forever()
    

    loop.run_forever() is never called since as @dirn already noted app.run is also blocking.

    Running global blocking event loop - is only way you can run asyncio coroutines and tasks, but it's not compatible with running blocking Flask app (or with any other such thing in general).

    If you want to use asynchronous web framework you should choose one created to be asynchronous. For example, probably most popular now is aiohttp:

    from aiohttp import web
    
    
    async def hello(request):
        return web.Response(text="Hello, world")
    
    
    if __name__ == "__main__":
        app = web.Application()
        app.router.add_get('/', hello)
        web.run_app(app)  # this runs asyncio event loop inside
    

    Upd:

    About your try to run event loop in background thread. I didn't investigate much, but it seems problem somehow related with tread-safety: many asyncio objects are not thread-safe. If you change your code this way, it'll work:

    def _create_task():
        asyncio.ensure_future(abar("abar"), loop=worker_loop)
    
    @app.route("/")
    def notify():
        worker_loop.call_soon_threadsafe(_create_task)
        return "OK"
    

    But again, this is very bad idea. It's not only very inconvenient, but I guess wouldn't make much sense: if you're going to use thread to start asyncio, why don't just use threads in Flask instead of asyncio? You will have Flask you want and parallelization.

    If I still didn't convince you, at least take a look at Flask-aiohttp project. It has close to Flask api and I think still better that what you're trying to do.

    0 讨论(0)
  • 2020-12-07 16:19

    You can incorporate some async functionality into Flask apps without having to completely convert them to asyncio.

    import asyncio
    from flask import Flask
    
    async def abar(a):
        print(a)
    
    loop = asyncio.get_event_loop()
    app = Flask(__name__)
    
    @app.route("/")
    def notify():
        loop.run_until_complete(abar("abar"))
        return "OK"
    
    if __name__ == "__main__":
        app.run(debug=False, use_reloader=False)
    

    This will block the Flask response until the async function returns, but it still allows you to do some clever things. I've used this pattern to perform many external requests in parallel using aiohttp, and then when they are complete, I'm back into traditional flask for data processing and template rendering.

    import aiohttp
    import asyncio
    import async_timeout
    from flask import Flask
    
    loop = asyncio.get_event_loop()
    app = Flask(__name__)
    
    async def fetch(url):
        async with aiohttp.ClientSession() as session, async_timeout.timeout(10):
            async with session.get(url) as response:
                return await response.text()
    
    def fight(responses):
        return "Why can't we all just get along?"
    
    @app.route("/")
    def index():
        # perform multiple async requests concurrently
        responses = loop.run_until_complete(asyncio.gather(
            fetch("https://google.com/"),
            fetch("https://bing.com/"),
            fetch("https://duckduckgo.com"),
            fetch("http://www.dogpile.com"),
        ))
    
        # do something with the results
        return fight(responses)
    
    if __name__ == "__main__":
        app.run(debug=False, use_reloader=False)
    
    0 讨论(0)
  • 2020-12-07 16:23

    Thanks for JL Diaz ( From RealPython ) for providing a working code for the above that was not working.

    If anything here should be changed, feel free to comment.

    import aiohttp
    import asyncio
    import async_timeout
    from quart import Quart, jsonify
    
    app = Quart(__name__)
    
    async def fetch(url):
        async with aiohttp.ClientSession() as session, async_timeout.timeout(10):
            async with session.get(url) as response:
                return await response.text()
    
    def fight(responses):
        return jsonify([len(r) for r in responses])
    
    @app.route("/")
    async def index():
        # perform multiple async requests concurrently
        responses = await asyncio.gather(
            fetch("https://google.com/"),
            fetch("https://bing.com/"),
            fetch("https://duckduckgo.com"),
            fetch("http://www.dogpile.com"),
        )
    
        # do something with the results
        return fight(responses)
    
    if __name__ == "__main__":
        app.run(debug=False, use_reloader=False)
    
    0 讨论(0)
  • 2020-12-07 16:35

    Your mistake is to try to run the asyncio event loop after calling app.run(). The latter doesn't return, it instead runs the Flask development server.

    In fact, that's how most WSGI setups will work; either the main thread is going to busy dispatching requests, or the Flask server is imported as a module in a WSGI server, and you can't start an event loop here either.

    You'll instead have to run your asyncio event loop in a separate thread, then run your coroutines in that separate thread via asyncio.run_coroutine_threadsafe(). See the Coroutines and Multithreading section in the documentation for what this entails.

    Here is an implementation of a module that will run such an event loop thread, and gives you the utilities to schedule coroutines to be run in that loop:

    import asyncio
    import itertools
    import time
    import threading
    
    __all__ = ["EventLoopThread", "get_event_loop", "stop_event_loop", "run_coroutine"]
    
    class EventLoopThread(threading.Thread):
        loop = None
        _count = itertools.count(0)
    
        def __init__(self):
            name = f"{type(self).__name__}-{next(self._count)}"
            super().__init__(name=name, daemon=True)
    
        def __repr__(self):
            loop, r, c, d = self.loop, False, True, False
            if loop is not None:
                r, c, d = loop.is_running(), loop.is_closed(), loop.get_debug()
            return (
                f"<{type(self).__name__} {self.name} id={self.ident} "
                f"running={r} closed={c} debug={d}>"
            )
    
        def run(self):
            self.loop = loop = asyncio.new_event_loop()
            asyncio.set_event_loop(loop)
    
            try:
                loop.run_forever()
            finally:
                try:
                    shutdown_asyncgens = loop.shutdown_asyncgens()
                except AttributeError:
                    pass
                else:
                    loop.run_until_complete(shutdown_asyncgens)
                loop.close()
                asyncio.set_event_loop(None)
    
        def stop(self):
            loop, self.loop = self.loop, None
            if loop is None:
                return
            loop.call_soon_threadsafe(loop.stop)
            self.join()
    
    _lock = threading.Lock()
    _loop_thread = None
    
    def get_event_loop():
        global _loop_thread
        with _lock:
            if _loop_thread is None:
                _loop_thread = EventLoopThread()
                _loop_thread.start()
            return _loop_thread.loop
    
    def stop_event_loop():
        global _loop_thread
        with _lock:
            if _loop_thread is not None:
                _loop_thread.stop()
                _loop_thread = None
    
    def run_coroutine(coro):
        """Run the coroutine in the event loop running in a separate thread
    
        Returns a Future, call Future.result() to get the output
    
        """
        return asyncio.run_coroutine_threadsafe(coro, get_event_loop())
    

    You can use the run_coroutine() function defined here to schedule asyncio routines. Use the returned Future instance to control the coroutine:

    • Get the result with Future.result(). You can give this a timeout; if no result is produced within the timeout, the coroutine is automatically cancelled.
    • You can query the state of the coroutine with the .cancelled(), .running() and .done() methods.
    • You can add callbacks to the future, which will be called when the coroutine has completed, or is cancelled or raised an exception (take into account that this is probably going to be called from the event loop thread, not the thread that you called run_coroutine() in).

    For your specific example, where abar() doesn't return any result, you can just ignore the returned future, like this:

    @app.route("/")
    def notify():
        run_coroutine(abar("abar"))
        return "OK"
    

    Note that before Python 3.8 that you can't use an event loop running on a separate thread to create subprocesses with! See my answer to Python3 Flask asyncio subprocess in route hangs for backport of the Python 3.8 ThreadedChildWatcher class for a work-around for this.

    0 讨论(0)
提交回复
热议问题