From c7e835656abeabac81fe3b8cddd70343df0c4eba Mon Sep 17 00:00:00 2001 From: Andrew Svetlov Date: Mon, 11 Sep 2017 11:11:38 +0300 Subject: [PATCH] Document web handlers cancellation (#2257) * Sketch for webhandler cancellation docs * Temp fix * Finish a chapter about tasks cancellation --- docs/web.rst | 96 +++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 95 insertions(+), 1 deletion(-) diff --git a/docs/web.rst b/docs/web.rst index 5a4761d7dd9..a1710420877 100644 --- a/docs/web.rst +++ b/docs/web.rst @@ -320,7 +320,6 @@ viewed using the :meth:`UrlDispatcher.named_resources` method:: :meth:`UrlDispatcher.resources` instead of :meth:`UrlDispatcher.named_routes` / :meth:`UrlDispatcher.routes`. - Alternative ways for registering routes --------------------------------------- @@ -383,6 +382,101 @@ own. .. versionadded:: 2.3 +Web Handler Cancellation +------------------------ + +.. warning:: + + :term:`web-handler` execution could be canceled on every ``await`` + if client drops connection without reading entire response's BODY. + + The behavior is very different from classic WSGI frameworks like + Flask and Django. + +Sometimes it is a desirable behavior: on processing ``GET`` request the +code might fetch data from database or other web resource, the +fetching is potentially slow. + +Canceling this fetch is very good: the peer dropped connection +already, there is no reason to waste time and resources (memory etc) by +getting data from DB without any chance to send it back to peer. + +But sometimes the cancellation is bad: on ``POST`` request very often +is needed to save data to DB regardless to peer closing. + +Cancellation prevention could be implemented in several ways: +* Applying :func:`asyncio.shield` to coroutine that saves data into DB. +* Spawning a new task for DB saving +* Using aiojobs_ or other third party library. + +:func:`asyncio.shield` works pretty good. The only disadvantage is you +need to split web handler into exactly two async functions: one +for handler itself and other for protected code. + +For example the following snippet is not safe:: + + async def handler(request): + await asyncio.shield(write_to_redis(request)) + await asyncio.shield(write_to_postgres(request)) + return web.Response('OK') + +Cancellation might be occurred just after saving data in REDIS, +``write_to_postgres`` will be not called. + +Spawning a new task is much worse: there is no place to ``await`` +spawned tasks:: + + async def handler(request): + request.loop.create_task(write_to_redis(request)) + return web.Response('OK') + +In this case errors from ``write_to_redis`` are not awaited, it leads +to many asyncio log messages *Future exception was never retrieved* +and *Task was destroyed but it is pending!*. + +Moreover on :ref:`aiohttp-web-graceful-shutdown` phase *aiohttp* don't +wait for these tasks, you have a great chance to loose very important +data. + +On other hand aiojobs_ provides an API for spawning new jobs and +awaiting their results etc. It stores all scheduled activity in +internal data structures and could terminate them gracefully:: + + from aiojobs.aiohttp import setup, spawn + + async def coro(timeout): + await asyncio.sleep(timeout) # do something in background + + async def handler(request): + await spawn(request, coro()) + return web.Response() + + app = web.Application() + setup(app) + app.router.add_get('/', handler) + +All not finished jobs will be terminated on +:attr:`aiohttp.web.Application.on_cleanup` signal. + +To prevent cancellation of the whole :term:`web-handler` use +``@atomic`` decorator:: + + from aiojobs.aiohttp import atomic + + @atomic + async def handler(request): + await write_to_db() + return web.Response() + + app = web.Application() + setup(app) + app.router.add_post('/', handler) + +It prevents all ``handler`` async function from cancellation, +``write_to_db`` will be never interrupted. + +.. _aiojobs: http://aiojobs.readthedocs.io/en/latest/ + Custom Routing Criteria -----------------------