/aiomisc/pool.py

https://github.com/aiokitchen/aiomisc · Python · 225 lines · 169 code · 54 blank · 2 comment · 24 complexity · 213ef5b7877583c4d7307e79dbe5ca9e MD5 · raw file

  1. import asyncio
  2. import logging
  3. from abc import ABC, abstractmethod
  4. from collections import defaultdict
  5. from random import random
  6. from typing import (
  7. Any, Awaitable, Callable, DefaultDict, NoReturn, Set, TypeVar, Union,
  8. )
  9. from .utils import cancel_tasks
  10. T = TypeVar("T")
  11. Number = Union[int, float]
  12. try:
  13. from typing import AsyncContextManager
  14. except ImportError:
  15. # Failed on Python 3.5.2 reproducible on ubuntu 16.04 (xenial)
  16. class AsyncContextManager(ABC): # type: ignore
  17. @abstractmethod
  18. async def __aenter__(self) -> Any:
  19. raise NotImplementedError
  20. @abstractmethod
  21. async def __aexit__(
  22. self, exc_type: Any, exc_val: Any,
  23. exc_tb: Any,
  24. ) -> Any:
  25. raise NotImplementedError
  26. log = logging.getLogger(__name__)
  27. class ContextManager(AsyncContextManager):
  28. __slots__ = "__aenter", "__aexit", "__instance"
  29. sentinel = object()
  30. def __init__(
  31. self, aenter: Callable[..., Awaitable[T]],
  32. aexit: Callable[..., Awaitable[T]],
  33. ):
  34. self.__aenter = aenter
  35. self.__aexit = aexit
  36. self.__instance = self.sentinel
  37. async def __aenter__(self) -> T:
  38. if self.__instance is not self.sentinel:
  39. raise RuntimeError("Reuse of context manager is not acceptable")
  40. self.__instance = await self.__aenter()
  41. return self.__instance
  42. async def __aexit__(
  43. self, exc_type: Any, exc_value: Any,
  44. traceback: Any,
  45. ) -> Any:
  46. await self.__aexit(self.__instance)
  47. class PoolBase(ABC):
  48. __slots__ = (
  49. "_create_lock",
  50. "_instances",
  51. "_loop",
  52. "_recycle",
  53. "_recycle_bin",
  54. "_recycle_times",
  55. "_semaphore",
  56. "_tasks",
  57. "_len",
  58. "_used",
  59. )
  60. _tasks: Set[Any]
  61. _used: Set[Any]
  62. _instances: asyncio.Queue
  63. _recycle_bin: asyncio.Queue
  64. def __init__(self, maxsize: int = 10, recycle: int = None):
  65. assert (
  66. recycle is None or recycle > 0
  67. ), "recycle should be positive number or None"
  68. self._loop = asyncio.get_event_loop()
  69. self._instances = asyncio.Queue()
  70. self._recycle_bin = asyncio.Queue()
  71. self._semaphore = asyncio.Semaphore(maxsize)
  72. self._len = 0
  73. self._recycle = recycle
  74. self._tasks = set()
  75. self._used = set()
  76. self._create_lock = asyncio.Lock()
  77. self._recycle_times: DefaultDict[float, Any] = defaultdict(
  78. self._loop.time,
  79. )
  80. self.__create_task(self.__recycler())
  81. def __create_task(self, coro: Awaitable[T]) -> asyncio.Task:
  82. task = self._loop.create_task(coro)
  83. self._tasks.add(task)
  84. task.add_done_callback(self._tasks.remove)
  85. return task
  86. async def __recycler(self) -> NoReturn:
  87. while True:
  88. instance = await self._recycle_bin.get()
  89. try:
  90. await self._destroy_instance(instance)
  91. except Exception:
  92. log.exception("Error when recycle instance %r", instance)
  93. finally:
  94. self._recycle_bin.task_done()
  95. @abstractmethod
  96. async def _create_instance(self) -> T:
  97. pass
  98. @abstractmethod
  99. async def _destroy_instance(self, instance: Any) -> None:
  100. pass
  101. # noinspection PyMethodMayBeStatic,PyUnusedLocal
  102. @abstractmethod
  103. async def _check_instance(self, instance: Any) -> bool:
  104. return True
  105. def __len__(self) -> int:
  106. return self._len
  107. def __recycle_instance(self, instance: Any) -> None:
  108. self._len -= 1
  109. self._semaphore.release()
  110. if instance in self._recycle_times:
  111. self._recycle_times.pop(instance)
  112. if instance in self._used:
  113. self._used.remove(instance)
  114. self._recycle_bin.put_nowait(instance)
  115. async def __create_new_instance(self) -> None:
  116. await self._semaphore.acquire()
  117. instance: Any = await self._create_instance()
  118. self._len += 1
  119. if self._recycle:
  120. deadline = self._recycle * (1 + random())
  121. self._recycle_times[instance] += deadline
  122. await self._instances.put(instance)
  123. async def __acquire(self) -> Any:
  124. if not self._semaphore.locked():
  125. await self.__create_new_instance()
  126. instance = await self._instances.get()
  127. try:
  128. result = await self._check_instance(instance)
  129. except Exception:
  130. log.exception("Check instance %r failed", instance)
  131. self.__recycle_instance(instance)
  132. else:
  133. if not result:
  134. self.__recycle_instance(instance)
  135. return await self.__acquire()
  136. self._used.add(instance)
  137. return instance
  138. async def __release(self, instance: Any) -> None:
  139. self._used.remove(instance)
  140. if self._recycle and self._recycle_times[instance] < self._loop.time():
  141. self.__recycle_instance(instance)
  142. return
  143. self._instances.put_nowait(instance)
  144. def acquire(self) -> ContextManager:
  145. return ContextManager(self.__acquire, self.__release)
  146. async def close(self, timeout: Number = None) -> None:
  147. instances = list(self._used)
  148. self._used.clear()
  149. while self._instances.qsize():
  150. try:
  151. instances.append(self._instances.get_nowait())
  152. except asyncio.QueueEmpty:
  153. break
  154. async def log_exception(coro: Awaitable[Any]) -> None:
  155. try:
  156. await coro
  157. except Exception:
  158. log.exception("Exception when task execution")
  159. await asyncio.wait_for(
  160. asyncio.gather(
  161. *[
  162. self.__create_task(
  163. log_exception(self._destroy_instance(instance)),
  164. )
  165. for instance in instances
  166. ],
  167. return_exceptions=True
  168. ),
  169. timeout=timeout,
  170. )
  171. await cancel_tasks(self._tasks)