Search code examples
pythonmultithreadingquart

Python Quart Unable to shutdown background task


I am working on a Python app, but I am moving from Flask to Quart. The application needs a background task that runs constantly whilst the application is running.

When I try to stop the process using control-c, the thread doesn't close cleanly and sits in the while loop in the shutdown routine.

    while not self._master_thread_class.shutdown_completed:
        if not pro:
            print('[DEBUG] Thread is not complete')
            pro = True

I have followed this Stackoverflow question, but I can't figure out how to cleanly shutdown the background thread so I would love an explanation please as it seems like the Quart Documentation is lacking a bit.

MasterThread class:

import asyncio

class MasterThread:

    def __init__(self, shutdown_requested_event):
        self._shutdown_completed = False
        self._shutdown_requested_event = shutdown_requested_event
        self._shutdown_requested = False

    def __del__(self):
        print('Thread was deleted')

    def run(self, loop) -> None:
        asyncio.set_event_loop(loop)
        loop.run_until_complete(self._async_entrypoint())

    @asyncio.coroutine
    def _async_entrypoint(self) -> None:
        while not self. _shutdown_requested and \
            not self._shutdown_requested_event.isSet():
            #print('_main_loop()')
            pass

            if self._shutdown_requested_event.wait(0.1):
                self. _shutdown_requested = True

        print('[DEBUG] thread has completed....')
        self._shutdown_completed = True

    def _main_loop(self) -> None:
        print('_main_loop()')

Main application module:

import asyncio
import threading
from quart import Quart
from workthr import MasterThread

app = Quart(__name__)

class Service:

    def __init__(self):
        self._shutdown_thread_event = threading.Event()
        self._master_thread = MasterThread(self._shutdown_thread_event)
        self._thread = None

    def __del__(self):
        self.stop()

    def start(self):
        loop = asyncio.get_event_loop()
        self._thread = threading.Thread(target=self._master_thread.run, args=(loop,))
        self._thread.start()
        return True

    def stop(self) -> None:
        print('[DEBUG] Stop signal caught...')
        self._shutdown_thread_event.set()
        while not self._master_thread.shutdown_completed:
            print('[DEBUG] Thread is not complete')

        print('[DEBUG] Thread has completed')

        self._shutdown()

    def _shutdown(self):
        print('Shutting down...')

service = Service()
service.start()

Solution

  • Quart has startup and shutdown methods that allow something to be started before the server starts serving and stopped when the server finishes serving. If your background task is mostly IO bound I'd recommend just using a coroutine function rather than a thread,

    async def background_task():
        while True:
            ...
    
    @app.before_serving
    async def startup():
        app.background_task = asyncio.ensure_future(background_task())
    
    @app.after_serving
    async def shutdown():
        app.background_task.cancel()  # Or use a variable in the while loop
    

    Or you can do the same with your Service,

    @app.before_serving
    async def startup():
        service.start()
    
    @app.after_serving
    async def shutdown():
        service.stop()