From b6df5aa7ce20006ca2b76ffeab33e78fe05c1750 Mon Sep 17 00:00:00 2001 From: Artyom Nikitin Date: Mon, 28 Oct 2019 12:29:32 +0300 Subject: [PATCH 01/12] feat: add graceful mixin/service --- aiomisc/service/graceful.py | 58 +++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 aiomisc/service/graceful.py diff --git a/aiomisc/service/graceful.py b/aiomisc/service/graceful.py new file mode 100644 index 00000000..4a3dfc33 --- /dev/null +++ b/aiomisc/service/graceful.py @@ -0,0 +1,58 @@ +import asyncio +from asyncio import Task +from typing import Coroutine + +from aiomisc import Service + + +class GracefulMixin: + + __tasks = {} + + def create_graceful_task(self, coro: Coroutine, *, cancel: bool): + task = asyncio.create_task(coro) + task.add_done_callback(self.__pop_task) + self.__tasks[task] = cancel + return task + + def __pop_task(self, task: Task): + self.__tasks.pop(task) + + async def graceful_shutdown(self): + if self.__tasks: + items = list(self.__tasks.items()) + to_cancel = [task for task, cancel in items if cancel] + to_wait = [task for task, cancel in items if not cancel] + await self.__wait_tasks(*to_cancel, cancel=True) + await self.__wait_tasks(*to_wait, cancel=False) + self.__tasks.clear() + + @staticmethod + async def __wait_tasks(*tasks: Task, cancel: bool): + if not tasks: + return + + to_stop = [] + + for task in tasks: + if task.done(): + continue + + if cancel: + task.cancel() + + to_stop.append(task) + + await asyncio.gather( + *to_stop, + return_exceptions=True, + ) + + +class GracefulService(Service, GracefulMixin): + + async def start(self): + raise NotImplementedError + + async def stop(self, exception: Exception = None): + await self.graceful_shutdown() From db259d117daf732fe69d28fef852d4f418ec5291 Mon Sep 17 00:00:00 2001 From: Artyom Nikitin Date: Mon, 28 Oct 2019 12:29:48 +0300 Subject: [PATCH 02/12] test: graceful mixin/service --- tests/test_graceful.py | 87 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 tests/test_graceful.py diff --git a/tests/test_graceful.py b/tests/test_graceful.py new file mode 100644 index 00000000..f6818a5e --- /dev/null +++ b/tests/test_graceful.py @@ -0,0 +1,87 @@ +import asyncio +from asyncio import CancelledError, Task + +import pytest + +from aiomisc.service.graceful import GracefulMixin, GracefulService + + +async def test_graceful(): + class Graceful(GracefulMixin): + pass + + async def pho(): + pass + + graceful = Graceful() + + task = graceful.create_graceful_task(pho(), cancel=False) + assert isinstance(task, Task) + + await asyncio.sleep(0.1) + + # Check that done and popped itself from the task store + assert task.done() + assert not graceful._GracefulMixin__tasks + + +@pytest.mark.parametrize( + 'cancel,expected', [ + (False, None), + (True, CancelledError), + ] +) +async def test_graceful_shutdown(cancel, expected): + class Graceful(GracefulMixin): + pass + + async def pho(): + await asyncio.sleep(0.1) + + graceful = Graceful() + + task = graceful.create_graceful_task(pho(), cancel=cancel) + assert isinstance(task, Task) + + await graceful.graceful_shutdown() + assert task.done() + + if expected is None: + assert not task.exception() + else: + with pytest.raises(CancelledError): + task.exception() + + assert not graceful._GracefulMixin__tasks + + +async def test_graceful_service(): + class TestService(GracefulService): + + task_wait = None + task_cancel = None + + async def start(self): + self.task_wait = self.create_graceful_task( + self.pho(), cancel=False + ) + self.task_cancel = self.create_graceful_task( + self.pho(), cancel=True + ) + + async def pho(self): + await asyncio.sleep(0.1) + + service = TestService() + + await service.start() + await service.stop() + + assert service.task_wait.done() + assert service.task_cancel.done() + + assert not service.task_wait.exception() + with pytest.raises(CancelledError): + service.task_cancel.exception() + + assert not service._GracefulMixin__tasks From c4142e683eaa32abc9e8f4d82a13e5f60e292f90 Mon Sep 17 00:00:00 2001 From: Artyom Nikitin Date: Mon, 28 Oct 2019 12:59:35 +0300 Subject: [PATCH 03/12] feat: add wait timeout --- aiomisc/service/graceful.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/aiomisc/service/graceful.py b/aiomisc/service/graceful.py index 4a3dfc33..09f53ab1 100644 --- a/aiomisc/service/graceful.py +++ b/aiomisc/service/graceful.py @@ -1,8 +1,9 @@ import asyncio from asyncio import Task +from contextlib import suppress from typing import Coroutine -from aiomisc import Service +from aiomisc import Service, timeout class GracefulMixin: @@ -18,13 +19,21 @@ def create_graceful_task(self, coro: Coroutine, *, cancel: bool): def __pop_task(self, task: Task): self.__tasks.pop(task) - async def graceful_shutdown(self): + async def graceful_shutdown(self, *, wait_timeout: float = None): if self.__tasks: items = list(self.__tasks.items()) to_cancel = [task for task, cancel in items if cancel] to_wait = [task for task, cancel in items if not cancel] - await self.__wait_tasks(*to_cancel, cancel=True) - await self.__wait_tasks(*to_wait, cancel=False) + + waiter = self.__wait_tasks(*to_cancel, cancel=True) + await waiter + + waiter = timeout(wait_timeout)(self.__wait_tasks)( + *to_wait, cancel=False + ) + with suppress(asyncio.TimeoutError): + await waiter + self.__tasks.clear() @staticmethod From 2b98a0813a1bdb73043433cccacb156c1cf1fb9b Mon Sep 17 00:00:00 2001 From: Artyom Nikitin Date: Mon, 28 Oct 2019 13:00:26 +0300 Subject: [PATCH 04/12] test: wait timeout --- tests/test_graceful.py | 38 ++++++++++++++++++++++++++++++++++---- 1 file changed, 34 insertions(+), 4 deletions(-) diff --git a/tests/test_graceful.py b/tests/test_graceful.py index f6818a5e..b345a04b 100644 --- a/tests/test_graceful.py +++ b/tests/test_graceful.py @@ -26,12 +26,42 @@ async def pho(): @pytest.mark.parametrize( - 'cancel,expected', [ + 'sleep,timeout,error', [ + (0.2, 0.1, CancelledError), + (0.1, 0.2, None), + ] +) +async def test_graceful_timeout(sleep, timeout, error): + class Graceful(GracefulMixin): + pass + + async def pho(): + await asyncio.sleep(sleep) + + graceful = Graceful() + + task = graceful.create_graceful_task(pho(), cancel=False) + assert isinstance(task, Task) + + await graceful.graceful_shutdown(wait_timeout=timeout) + assert task.done() + + if not error: + assert not task.exception() + else: + with pytest.raises(error): + assert task.exception() + + assert not graceful._GracefulMixin__tasks + + +@pytest.mark.parametrize( + 'cancel,error', [ (False, None), (True, CancelledError), ] ) -async def test_graceful_shutdown(cancel, expected): +async def test_graceful_shutdown(cancel, error): class Graceful(GracefulMixin): pass @@ -46,10 +76,10 @@ async def pho(): await graceful.graceful_shutdown() assert task.done() - if expected is None: + if not error: assert not task.exception() else: - with pytest.raises(CancelledError): + with pytest.raises(error): task.exception() assert not graceful._GracefulMixin__tasks From f3fc72a512ecd244bff52b137fb67d2dffe04d94 Mon Sep 17 00:00:00 2001 From: Artyom Nikitin Date: Mon, 28 Oct 2019 14:22:13 +0300 Subject: [PATCH 05/12] test: graceful service timeout --- aiomisc/service/graceful.py | 6 ++++-- tests/test_graceful.py | 30 +++++++++++++++++++++++++++++- 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/aiomisc/service/graceful.py b/aiomisc/service/graceful.py index 09f53ab1..9cd58c2d 100644 --- a/aiomisc/service/graceful.py +++ b/aiomisc/service/graceful.py @@ -29,7 +29,7 @@ async def graceful_shutdown(self, *, wait_timeout: float = None): await waiter waiter = timeout(wait_timeout)(self.__wait_tasks)( - *to_wait, cancel=False + *to_wait, cancel=False, ) with suppress(asyncio.TimeoutError): await waiter @@ -60,8 +60,10 @@ async def __wait_tasks(*tasks: Task, cancel: bool): class GracefulService(Service, GracefulMixin): + graceful_wait_timeout = None # type: float # in seconds + async def start(self): raise NotImplementedError async def stop(self, exception: Exception = None): - await self.graceful_shutdown() + await self.graceful_shutdown(wait_timeout=self.graceful_wait_timeout) diff --git a/tests/test_graceful.py b/tests/test_graceful.py index b345a04b..b60d9285 100644 --- a/tests/test_graceful.py +++ b/tests/test_graceful.py @@ -112,6 +112,34 @@ async def pho(self): assert not service.task_wait.exception() with pytest.raises(CancelledError): - service.task_cancel.exception() + assert not service.task_cancel.exception() + + assert not service._GracefulMixin__tasks + + +async def test_graceful_service_with_timeout(): + class TestService(GracefulService): + + graceful_wait_timeout = 0.1 + + task_wait = None + + async def start(self): + self.task_wait = self.create_graceful_task( + self.pho(), cancel=False + ) + + async def pho(self): + await asyncio.sleep(0.2) + + service = TestService() + + await service.start() + await service.stop() + + assert service.task_wait.done() + + with pytest.raises(CancelledError): + service.task_wait.exception() assert not service._GracefulMixin__tasks From a588698ea9cf4254d1c0c70566f50aa45b4184f4 Mon Sep 17 00:00:00 2001 From: Artyom Nikitin Date: Mon, 28 Oct 2019 15:06:02 +0300 Subject: [PATCH 06/12] doc: add readme --- README.rst | 39 +++++++++++++++++++++++++++++++++++++ aiomisc/service/__init__.py | 2 ++ aiomisc/service/graceful.py | 3 ++- 3 files changed, 43 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index fb1fb063..e1254d7a 100644 --- a/README.rst +++ b/README.rst @@ -615,6 +615,45 @@ Output example: 1 0.000 0.000 0.000 0.000 <...>/lib/python3.7/cProfile.py:50(create_stats) +Graceful +************* + +``GracefulService`` allows creation of tasks that upon service stop will be +either awaited with optional timeout or cancelled and awaited. +Optional parameter ``graceful_wait_timeout`` (in seconds) specifies the allowed +wait time for tasks created with ``create_graceful_task(, cancel=False)``. +Tasks created with ``create_graceful_task(, cancel=True)`` will be cancelled. + + +.. code-block:: python + + import asyncio + from aiomisc.service import GracefulService + + class SwanService(GracefulService): + async def fly(self): + await asyncio.sleep(1) + print('Flew to a lake') + + async def duckify(self): + await asyncio.sleep(1) + print('Became ugly duck') + + async def start(self): + self.create_graceful_task(self.fly(), cancel=False) + self.create_graceful_task(self.duckify(), cancel=True) + + service = SwanService(graceful_wait_timeout=10) + await service.start() + await service.stop() + +Output example: + +.. code-block:: + + Flew to a lake + + timeout decorator +++++++++++++++++ diff --git a/aiomisc/service/__init__.py b/aiomisc/service/__init__.py index 72cced73..4b0173c1 100644 --- a/aiomisc/service/__init__.py +++ b/aiomisc/service/__init__.py @@ -1,4 +1,5 @@ from .base import Service, ServiceMeta, SimpleServer +from .graceful import GracefulMixin, GracefulService from .tcp import TCPServer from .tls import TLSServer from .tracer import MemoryTracer @@ -7,6 +8,7 @@ __all__ = ( + 'GracefulMixin', 'GracefulService', 'MemoryTracer', 'Profiler', 'Service', diff --git a/aiomisc/service/graceful.py b/aiomisc/service/graceful.py index 9cd58c2d..438f5c8c 100644 --- a/aiomisc/service/graceful.py +++ b/aiomisc/service/graceful.py @@ -3,7 +3,8 @@ from contextlib import suppress from typing import Coroutine -from aiomisc import Service, timeout +from . import Service +from ..timeout import timeout class GracefulMixin: From ff3bdad335d6ef1dcaa73d90448256e450c70f0f Mon Sep 17 00:00:00 2001 From: Artyom Nikitin Date: Mon, 28 Oct 2019 19:28:58 +0300 Subject: [PATCH 07/12] fix: py35 --- aiomisc/service/graceful.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/aiomisc/service/graceful.py b/aiomisc/service/graceful.py index 438f5c8c..08fb21a9 100644 --- a/aiomisc/service/graceful.py +++ b/aiomisc/service/graceful.py @@ -1,5 +1,5 @@ import asyncio -from asyncio import Task +from asyncio import Task, get_event_loop from contextlib import suppress from typing import Coroutine @@ -12,7 +12,7 @@ class GracefulMixin: __tasks = {} def create_graceful_task(self, coro: Coroutine, *, cancel: bool): - task = asyncio.create_task(coro) + task = get_event_loop().create_task(coro) task.add_done_callback(self.__pop_task) self.__tasks[task] = cancel return task From 702270b39619db494ad6ead85e5f97546eeefd14 Mon Sep 17 00:00:00 2001 From: Artyom Nikitin Date: Sat, 7 Nov 2020 01:12:39 +0300 Subject: [PATCH 08/12] style: add type annotations --- aiomisc/service/graceful.py | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/aiomisc/service/graceful.py b/aiomisc/service/graceful.py index 08fb21a9..b16173a3 100644 --- a/aiomisc/service/graceful.py +++ b/aiomisc/service/graceful.py @@ -1,7 +1,7 @@ import asyncio from asyncio import Task, get_event_loop from contextlib import suppress -from typing import Coroutine +from typing import Coroutine, Dict from . import Service from ..timeout import timeout @@ -9,18 +9,18 @@ class GracefulMixin: - __tasks = {} + __tasks: Dict[Task, bool] = {} - def create_graceful_task(self, coro: Coroutine, *, cancel: bool): + def create_graceful_task(self, coro: Coroutine, *, cancel: bool) -> Task: task = get_event_loop().create_task(coro) task.add_done_callback(self.__pop_task) self.__tasks[task] = cancel return task - def __pop_task(self, task: Task): + def __pop_task(self, task: Task) -> None: self.__tasks.pop(task) - async def graceful_shutdown(self, *, wait_timeout: float = None): + async def graceful_shutdown(self, *, wait_timeout: float = None) -> None: if self.__tasks: items = list(self.__tasks.items()) to_cancel = [task for task, cancel in items if cancel] @@ -29,16 +29,19 @@ async def graceful_shutdown(self, *, wait_timeout: float = None): waiter = self.__wait_tasks(*to_cancel, cancel=True) await waiter - waiter = timeout(wait_timeout)(self.__wait_tasks)( - *to_wait, cancel=False, - ) + if wait_timeout is None: + waiter = self.__wait_tasks + else: + waiter = timeout(wait_timeout)(self.__wait_tasks) + waiter = waiter(*to_wait, cancel=False) + with suppress(asyncio.TimeoutError): await waiter self.__tasks.clear() @staticmethod - async def __wait_tasks(*tasks: Task, cancel: bool): + async def __wait_tasks(*tasks: Task, cancel: bool) -> None: if not tasks: return @@ -63,8 +66,8 @@ class GracefulService(Service, GracefulMixin): graceful_wait_timeout = None # type: float # in seconds - async def start(self): + async def start(self) -> None: raise NotImplementedError - async def stop(self, exception: Exception = None): + async def stop(self, exception: Exception = None) -> None: await self.graceful_shutdown(wait_timeout=self.graceful_wait_timeout) From 389957b909dcd2e5477efd2fdfaae375955aa72c Mon Sep 17 00:00:00 2001 From: Artyom Nikitin Date: Wed, 11 Nov 2020 21:22:59 +0300 Subject: [PATCH 09/12] fix --- aiomisc/service/graceful.py | 92 +++++++++++++++++++++++++++---------- tests/test_graceful.py | 41 ++++++++++++++--- 2 files changed, 101 insertions(+), 32 deletions(-) diff --git a/aiomisc/service/graceful.py b/aiomisc/service/graceful.py index b16173a3..2aea03e0 100644 --- a/aiomisc/service/graceful.py +++ b/aiomisc/service/graceful.py @@ -1,10 +1,17 @@ import asyncio -from asyncio import Task, get_event_loop -from contextlib import suppress -from typing import Coroutine, Dict +import logging + +try: + from asyncio import create_task +except ImportError: + from asyncio import ensure_future as create_task # type: ignore +from asyncio import Task +from typing import Coroutine, Dict, Optional from . import Service -from ..timeout import timeout +from ..timeout import timeout, Number + +log = logging.getLogger(__name__) class GracefulMixin: @@ -12,34 +19,68 @@ class GracefulMixin: __tasks: Dict[Task, bool] = {} def create_graceful_task(self, coro: Coroutine, *, cancel: bool) -> Task: - task = get_event_loop().create_task(coro) - task.add_done_callback(self.__pop_task) + task = create_task(coro) + # __tasks may be cleared before the task finishes + task.add_done_callback(lambda task: self.__tasks.pop(task, None)) self.__tasks[task] = cancel return task - def __pop_task(self, task: Task) -> None: - self.__tasks.pop(task) - - async def graceful_shutdown(self, *, wait_timeout: float = None) -> None: + async def graceful_shutdown( + self, *, + wait_timeout: Number = None, + cancel_on_timeout: bool = True, + ) -> None: if self.__tasks: items = list(self.__tasks.items()) to_cancel = [task for task, cancel in items if cancel] to_wait = [task for task, cancel in items if not cancel] - waiter = self.__wait_tasks(*to_cancel, cancel=True) - await waiter + log.info( + 'Graceful shutdown: cancel %d and wait for %d tasks', + len(to_cancel), len(to_wait) + ) - if wait_timeout is None: - waiter = self.__wait_tasks - else: - waiter = timeout(wait_timeout)(self.__wait_tasks) - waiter = waiter(*to_wait, cancel=False) - - with suppress(asyncio.TimeoutError): - await waiter + await asyncio.wait([ + self.__cancel_tasks(*to_cancel), + self.__finish_tasks( + *to_wait, + wait_timeout=wait_timeout, + cancel_on_timeout=cancel_on_timeout, + ) + ]) self.__tasks.clear() + async def __cancel_tasks(self, *tasks: Task) -> None: + await self.__wait_tasks(*tasks, cancel=True) + + async def __finish_tasks( + self, *tasks: Task, + wait_timeout: Optional[Number], + cancel_on_timeout: bool, + ) -> None: + + if wait_timeout is None: + await self.__wait_tasks(*tasks, cancel=False) + return + + try: + await timeout(wait_timeout)(self.__wait_tasks)( + *tasks, cancel=False, + ) + except asyncio.TimeoutError: + log.info('Graceful shutdown: wait timeouted') + if not cancel_on_timeout: + return + + to_cancel = [task for task in tasks if not task.done()] + log.info( + 'Graceful shutdown: cancel %d tasks after timeout', + len(to_cancel), + ) + for task in to_cancel: + task.cancel() + @staticmethod async def __wait_tasks(*tasks: Task, cancel: bool) -> None: if not tasks: @@ -56,18 +97,19 @@ async def __wait_tasks(*tasks: Task, cancel: bool) -> None: to_stop.append(task) - await asyncio.gather( - *to_stop, - return_exceptions=True, - ) + await asyncio.wait(to_stop) class GracefulService(Service, GracefulMixin): graceful_wait_timeout = None # type: float # in seconds + cancel_on_timeout = True # type: bool async def start(self) -> None: raise NotImplementedError async def stop(self, exception: Exception = None) -> None: - await self.graceful_shutdown(wait_timeout=self.graceful_wait_timeout) + await self.graceful_shutdown( + wait_timeout=self.graceful_wait_timeout, + cancel_on_timeout=self.cancel_on_timeout, + ) diff --git a/tests/test_graceful.py b/tests/test_graceful.py index b60d9285..4ee6e634 100644 --- a/tests/test_graceful.py +++ b/tests/test_graceful.py @@ -93,10 +93,10 @@ class TestService(GracefulService): async def start(self): self.task_wait = self.create_graceful_task( - self.pho(), cancel=False + self.pho(), cancel=False, ) self.task_cancel = self.create_graceful_task( - self.pho(), cancel=True + self.pho(), cancel=True, ) async def pho(self): @@ -117,7 +117,7 @@ async def pho(self): assert not service._GracefulMixin__tasks -async def test_graceful_service_with_timeout(): +async def test_graceful_service_with_timeout_cancel(): class TestService(GracefulService): graceful_wait_timeout = 0.1 @@ -126,7 +126,7 @@ class TestService(GracefulService): async def start(self): self.task_wait = self.create_graceful_task( - self.pho(), cancel=False + self.pho(), cancel=False, ) async def pho(self): @@ -137,9 +137,36 @@ async def pho(self): await service.start() await service.stop() - assert service.task_wait.done() + assert service.task_wait.cancelled() + assert not service._GracefulMixin__tasks - with pytest.raises(CancelledError): - service.task_wait.exception() +async def test_graceful_service_with_timeout_no_cancel(): + class TestService(GracefulService): + + graceful_wait_timeout = 0.1 + cancel_on_timeout = False + + task_wait = None + + async def start(self): + self.task_wait = self.create_graceful_task( + self.pho(), cancel=False, + ) + + async def pho(self): + await asyncio.sleep(0.2) + return 123 + + service = TestService() + + await service.start() + await service.stop() + + assert not service.task_wait.done() assert not service._GracefulMixin__tasks + + await asyncio.sleep(0.1) + + assert service.task_wait.done() + assert service.task_wait.result() == 123 From cf48d3204261aa604cf353e858f4bcc5e31b0151 Mon Sep 17 00:00:00 2001 From: Artyom Nikitin Date: Wed, 11 Nov 2020 22:15:39 +0300 Subject: [PATCH 10/12] doc: update --- docs/source/services.rst | 48 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/docs/source/services.rst b/docs/source/services.rst index b35e7959..ab1a75bd 100644 --- a/docs/source/services.rst +++ b/docs/source/services.rst @@ -201,6 +201,54 @@ should be proceeded before start loop.run_forever() +GracefulService ++++++++++++++++ + +``GracefulService`` allows creation of tasks that will be either awaited with +optional timeout or cancelled and awaited upon service stop. + +Optional service parameter ``graceful_wait_timeout`` (default ``None``) specifies the allowed +wait time in seconds for tasks created with ``create_graceful_task(coro, cancel=False)``. + +Optional service parameter ``cancel_on_timeout`` (default ``True``) specifies whether +to cancel tasks (without further waiting) that didn't complete within ``graceful_wait_timeout``. + +Tasks created with ``create_graceful_task(coro, cancel=True)`` are cancelled +and awaited when service stops. + +.. code-block:: python + + import asyncio + from aiomisc.service import GracefulService + + class SwanService(GracefulService): + + graceful_wait_timeout = 10 + cancel_on_timeout = False + + async def fly(self): + await asyncio.sleep(1) + print('Flew to a lake') + + async def duckify(self): + await asyncio.sleep(1) + print('Became ugly duck') + + async def start(self): + self.create_graceful_task(self.fly(), cancel=False) + self.create_graceful_task(self.duckify(), cancel=True) + + service = SwanService() + await service.start() + await service.stop() + +Output example: + +.. code-block:: + + Flew to a lake + + Multiple services +++++++++++++++++ From 3eca597d6a541e916fbc35cb0783f0d09b47a4bd Mon Sep 17 00:00:00 2001 From: Artyom Nikitin Date: Wed, 11 Nov 2020 23:05:52 +0300 Subject: [PATCH 11/12] doc: localize --- aiomisc/service/graceful.py | 7 ++ docs/source/locale/ru/LC_MESSAGES/services.po | 70 ++++++++++++++----- docs/source/services.rst | 16 +++-- 3 files changed, 70 insertions(+), 23 deletions(-) diff --git a/aiomisc/service/graceful.py b/aiomisc/service/graceful.py index 2aea03e0..d834bafa 100644 --- a/aiomisc/service/graceful.py +++ b/aiomisc/service/graceful.py @@ -19,6 +19,13 @@ class GracefulMixin: __tasks: Dict[Task, bool] = {} def create_graceful_task(self, coro: Coroutine, *, cancel: bool) -> Task: + """ + Creates a task that will either be awaited or cancelled and awaited + upon service stop. + :param coro: + :param cancel: whether to cancel or await the task on service stop + :return: created task + """ task = create_task(coro) # __tasks may be cleared before the task finishes task.add_done_callback(lambda task: self.__tasks.pop(task, None)) diff --git a/docs/source/locale/ru/LC_MESSAGES/services.po b/docs/source/locale/ru/LC_MESSAGES/services.po index bcc72654..dea6cec4 100644 --- a/docs/source/locale/ru/LC_MESSAGES/services.po +++ b/docs/source/locale/ru/LC_MESSAGES/services.po @@ -182,11 +182,53 @@ msgstr "" "Вы также можете наследовать от ``CronService``, но помните, что регистрация " "обратного вызова должна выполняться до запуска" -#: ../../source/services.rst:205 +#: ../../source/services.rst:207 +msgid "" +"``GracefulService`` allows creation of tasks that will be either awaited " +"with an optional timeout or cancelled and awaited upon service stop." +msgstr "" +"``GracefulService`` позволяет запускать задачи, завершение которых " +"(либо отмену с завершением) сервис выждет при остановке." + +#: ../../source/services.rst:210 +msgid "" +"Optional service parameter ``graceful_wait_timeout`` (default ``None``) " +"specifies the allowed wait time in seconds for tasks created with " +"``create_graceful_task(coro, cancel=False)``." +msgstr "" +"Необязательный параметр сервиса ``graceful_wait_timeout`` " +"(по умолчанию ``None``) указывает время выжидания завершения (в секундах) " +"для задач, запущенных с ``create_graceful_task(coro, cancel=False)``." + +#: ../../source/services.rst:214 +msgid "" +"Optional service parameter ``cancel_on_timeout`` (default ``True``) " +"specifies whether to cancel tasks (without further waiting) that didn't " +"complete within ``graceful_wait_timeout``." +msgstr "" +"Необязательный параметр сервиса ``cancel_on_timeout`` (по умолчанию ``True``) " +"указывает отменять ли задачи (без последующего ожидания), которые не " +"успели завершиться в течение ``graceful_wait_timeout``." + +#: ../../source/services.rst:218 +msgid "" +"Tasks created with ``create_graceful_task(coro, cancel=True)`` are cancelled " +"and awaited when service stops." +msgstr "" +"При остановке сервиса, задачи, запущенные с помощью " +"``create_graceful_task(coro, cancel=True)`` отменяются, и производится " +"ожидание их завершения." + +#: ../../source/services.rst:247 +# ../../source/services.rst:425 ../../source/services.rst:472 +msgid "Output example:" +msgstr "Пример вывода:" + +#: ../../source/services.rst:254 msgid "Multiple services" msgstr "Несколько сервисов" -#: ../../source/services.rst:207 +#: ../../source/services.rst:257 msgid "" "Pass several service instances to the ``entrypoint`` to run all of them. " "After exiting the entrypoint service instances will be gracefully shut " @@ -197,11 +239,11 @@ msgstr "" "корректно закрыты вызовом метода ``stop()`` или через " "отмену метода ``start()``." -#: ../../source/services.rst:246 +#: ../../source/services.rst:295 msgid "Configuration" msgstr "Конфигурация" -#: ../../source/services.rst:248 +#: ../../source/services.rst:298 msgid "" "``Service`` metaclass accepts all kwargs and will set it to ``self`` as " "attributes." @@ -209,19 +251,19 @@ msgstr "" "Метакласс ``Service`` принимает все именованные аргументы в ``__init__`` " "и устанавливает из как атрибуты в ``self``." -#: ../../source/services.rst:286 +#: ../../source/services.rst:335 msgid "aiohttp service" msgstr "aiohttp сервис" -#: ../../source/services.rst:290 +#: ../../source/services.rst:340 msgid "requires installed aiohttp:" msgstr "требуется установленная библиоткеа aiohttp" -#: ../../source/services.rst:303 +#: ../../source/services.rst:353 msgid "aiohttp application can be started as a service:" msgstr "Приложение aiohttp может быть запущено как сервис:" -#: ../../source/services.rst:345 +#: ../../source/services.rst:395 msgid "" "Class ``AIOHTTPSSLService`` is similar to ``AIOHTTPService`` but creates " "HTTPS server. You must pass SSL-required options (see ``TLSServer`` " @@ -231,11 +273,11 @@ msgstr "" "но создает HTTPS сервер. Вы должны передать требуемые для " "SSL параметры (см. Класс TLSServer)." -#: ../../source/services.rst:349 +#: ../../source/services.rst:398 msgid "Memory Tracer" msgstr "Трассировщик памяти" -#: ../../source/services.rst:351 +#: ../../source/services.rst:401 msgid "" "Simple and useful service for logging large python objects allocated in " "memory." @@ -243,15 +285,11 @@ msgstr "" "Простой и полезный сервис для логирования больших " "объектов Python, размещенных в памяти." -#: ../../source/services.rst:375 ../../source/services.rst:422 -msgid "Output example:" -msgstr "Пример вывода:" - -#: ../../source/services.rst:397 +#: ../../source/services.rst:446 msgid "Profiler" msgstr "Профилировщик" -#: ../../source/services.rst:399 +#: ../../source/services.rst:449 msgid "" "Simple service for profiling. Optional `path` argument can be provided to" " dump complete profiling data, which can be later used by, for example, " diff --git a/docs/source/services.rst b/docs/source/services.rst index ab1a75bd..10550df1 100644 --- a/docs/source/services.rst +++ b/docs/source/services.rst @@ -204,17 +204,19 @@ should be proceeded before start GracefulService +++++++++++++++ -``GracefulService`` allows creation of tasks that will be either awaited with -optional timeout or cancelled and awaited upon service stop. +``GracefulService`` allows creation of tasks that will be either awaited +with an optional timeout or cancelled and awaited upon service stop. -Optional service parameter ``graceful_wait_timeout`` (default ``None``) specifies the allowed -wait time in seconds for tasks created with ``create_graceful_task(coro, cancel=False)``. +Optional service parameter ``graceful_wait_timeout`` (default ``None``) +specifies the allowed wait time in seconds for tasks created with +``create_graceful_task(coro, cancel=False)``. -Optional service parameter ``cancel_on_timeout`` (default ``True``) specifies whether -to cancel tasks (without further waiting) that didn't complete within ``graceful_wait_timeout``. +Optional service parameter ``cancel_on_timeout`` (default ``True``) +specifies whether to cancel tasks (without further waiting) that didn't +complete within ``graceful_wait_timeout``. Tasks created with ``create_graceful_task(coro, cancel=True)`` are cancelled -and awaited when service stops. +and awaited when the service stops. .. code-block:: python From 4736b45f40e5d9729541d77431514fdaba3d6aa9 Mon Sep 17 00:00:00 2001 From: Artyom Nikitin Date: Wed, 25 Nov 2020 00:22:51 +0300 Subject: [PATCH 12/12] test: remove defaults --- tests/test_graceful.py | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/tests/test_graceful.py b/tests/test_graceful.py index 4ee6e634..de785166 100644 --- a/tests/test_graceful.py +++ b/tests/test_graceful.py @@ -15,7 +15,7 @@ async def pho(): graceful = Graceful() - task = graceful.create_graceful_task(pho(), cancel=False) + task = graceful.create_graceful_task(pho()) assert isinstance(task, Task) await asyncio.sleep(0.1) @@ -40,7 +40,7 @@ async def pho(): graceful = Graceful() - task = graceful.create_graceful_task(pho(), cancel=False) + task = graceful.create_graceful_task(pho()) assert isinstance(task, Task) await graceful.graceful_shutdown(wait_timeout=timeout) @@ -92,9 +92,7 @@ class TestService(GracefulService): task_cancel = None async def start(self): - self.task_wait = self.create_graceful_task( - self.pho(), cancel=False, - ) + self.task_wait = self.create_graceful_task(self.pho()) self.task_cancel = self.create_graceful_task( self.pho(), cancel=True, ) @@ -125,9 +123,7 @@ class TestService(GracefulService): task_wait = None async def start(self): - self.task_wait = self.create_graceful_task( - self.pho(), cancel=False, - ) + self.task_wait = self.create_graceful_task(self.pho()) async def pho(self): await asyncio.sleep(0.2) @@ -150,9 +146,7 @@ class TestService(GracefulService): task_wait = None async def start(self): - self.task_wait = self.create_graceful_task( - self.pho(), cancel=False, - ) + self.task_wait = self.create_graceful_task(self.pho()) async def pho(self): await asyncio.sleep(0.2)