Source code for responder.background

import asyncio
import concurrent.futures
import inspect
import multiprocessing
import traceback

from starlette.concurrency import run_in_threadpool

__all__ = ["BackgroundQueue"]


[docs] class BackgroundQueue: """A queue for running tasks in background threads. Uses a ``ThreadPoolExecutor`` sized to the number of CPUs. Access it via ``api.background``. Usage:: # As a decorator — fire and forget @api.background.task def send_email(to, subject): ... send_email("user@example.com", "Hello") # Async functions work too — run via ``asyncio.run`` on the worker @api.background.task async def refresh_cache(): ... # Direct submission future = api.background.run(send_email, "user@example.com", "Hello") # As a callable (supports async functions) await api.background(send_email, "user@example.com", "Hello") """ def __init__(self, n=None): """Create a new background queue. :param n: Number of worker threads. Defaults to CPU count. """ if n is None: n = multiprocessing.cpu_count() self.n = n self.pool = concurrent.futures.ThreadPoolExecutor( max_workers=n, thread_name_prefix="responder-background" ) self.results = []
[docs] def run(self, f, *args, **kwargs): """Submit a function to run in a background thread. Async functions are supported: they are driven to completion on the worker thread via ``asyncio.run`` (previously they silently produced a never-awaited coroutine and the job never ran). :param f: The function to run. :returns: A ``concurrent.futures.Future`` for the result. """ # ``inspect.iscoroutinefunction`` unwraps ``functools.partial`` itself. if inspect.iscoroutinefunction(f): def runner(*args, **kwargs): return asyncio.run(f(*args, **kwargs)) future = self.pool.submit(runner, *args, **kwargs) else: future = self.pool.submit(f, *args, **kwargs) self.results.append(future) future.add_done_callback(self._discard) return future
def _discard(self, future): # Drop completed futures so long-running apps don't accumulate them. try: self.results.remove(future) except ValueError: pass
[docs] def task(self, f): """Decorator that wraps a function to run in the background thread pool. The decorated function returns a ``Future`` instead of blocking. Exceptions are printed to stderr via traceback. :param f: The function to wrap. """ def on_future_done(fs): try: fs.result() except Exception: traceback.print_exc() def do_task(*args, **kwargs): result = self.run(f, *args, **kwargs) result.add_done_callback(on_future_done) return result return do_task
[docs] def shutdown(self, wait=True): """Stop accepting new tasks and, by default, drain in-flight ones. Called automatically at application shutdown so fire-and-forget tasks submitted via :meth:`run`/:meth:`task` are given a chance to finish rather than being abandoned when the process exits. :param wait: Block until running tasks complete (default ``True``). """ self.pool.shutdown(wait=wait)
async def __call__(self, func, *args, **kwargs): """Await ``func`` to completion, off the event loop if it is sync. Async callables are awaited directly; sync callables run in the thread pool so they don't block the loop. This form *awaits* the result — use :meth:`task` or :meth:`run` for true fire-and-forget scheduling. """ if inspect.iscoroutinefunction(func): return await func(*args, **kwargs) return await run_in_threadpool(func, *args, **kwargs)