_concurrency_py3k.py 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270
  1. # util/_concurrency_py3k.py
  2. # Copyright (C) 2005-2024 the SQLAlchemy authors and contributors
  3. # <see AUTHORS file>
  4. #
  5. # This module is part of SQLAlchemy and is released under
  6. # the MIT License: https://www.opensource.org/licenses/mit-license.php
  7. # mypy: allow-untyped-defs, allow-untyped-calls
  8. from __future__ import annotations
  9. import asyncio
  10. from contextvars import Context
  11. import sys
  12. import typing
  13. from typing import Any
  14. from typing import Awaitable
  15. from typing import Callable
  16. from typing import Coroutine
  17. from typing import Optional
  18. from typing import TYPE_CHECKING
  19. from typing import TypeVar
  20. from .langhelpers import memoized_property
  21. from .. import exc
  22. from ..util.typing import Protocol
  23. from ..util.typing import TypeGuard
  24. _T = TypeVar("_T")
  25. if typing.TYPE_CHECKING:
  26. class greenlet(Protocol):
  27. dead: bool
  28. gr_context: Optional[Context]
  29. def __init__(self, fn: Callable[..., Any], driver: greenlet):
  30. ...
  31. def throw(self, *arg: Any) -> Any:
  32. return None
  33. def switch(self, value: Any) -> Any:
  34. return None
  35. def getcurrent() -> greenlet:
  36. ...
  37. else:
  38. from greenlet import getcurrent
  39. from greenlet import greenlet
  40. # If greenlet.gr_context is present in current version of greenlet,
  41. # it will be set with the current context on creation.
  42. # Refs: https://github.com/python-greenlet/greenlet/pull/198
  43. _has_gr_context = hasattr(getcurrent(), "gr_context")
  44. def is_exit_exception(e: BaseException) -> bool:
  45. # note asyncio.CancelledError is already BaseException
  46. # so was an exit exception in any case
  47. return not isinstance(e, Exception) or isinstance(
  48. e, (asyncio.TimeoutError, asyncio.CancelledError)
  49. )
  50. # implementation based on snaury gist at
  51. # https://gist.github.com/snaury/202bf4f22c41ca34e56297bae5f33fef
  52. # Issue for context: https://github.com/python-greenlet/greenlet/issues/173
  53. class _AsyncIoGreenlet(greenlet):
  54. dead: bool
  55. def __init__(self, fn: Callable[..., Any], driver: greenlet):
  56. greenlet.__init__(self, fn, driver)
  57. self.driver = driver
  58. if _has_gr_context:
  59. self.gr_context = driver.gr_context
  60. _T_co = TypeVar("_T_co", covariant=True)
  61. if TYPE_CHECKING:
  62. def iscoroutine(
  63. awaitable: Awaitable[_T_co],
  64. ) -> TypeGuard[Coroutine[Any, Any, _T_co]]:
  65. ...
  66. else:
  67. iscoroutine = asyncio.iscoroutine
  68. def _safe_cancel_awaitable(awaitable: Awaitable[Any]) -> None:
  69. # https://docs.python.org/3/reference/datamodel.html#coroutine.close
  70. if iscoroutine(awaitable):
  71. awaitable.close()
  72. def in_greenlet() -> bool:
  73. current = getcurrent()
  74. return isinstance(current, _AsyncIoGreenlet)
  75. def await_only(awaitable: Awaitable[_T]) -> _T:
  76. """Awaits an async function in a sync method.
  77. The sync method must be inside a :func:`greenlet_spawn` context.
  78. :func:`await_only` calls cannot be nested.
  79. :param awaitable: The coroutine to call.
  80. """
  81. # this is called in the context greenlet while running fn
  82. current = getcurrent()
  83. if not isinstance(current, _AsyncIoGreenlet):
  84. _safe_cancel_awaitable(awaitable)
  85. raise exc.MissingGreenlet(
  86. "greenlet_spawn has not been called; can't call await_only() "
  87. "here. Was IO attempted in an unexpected place?"
  88. )
  89. # returns the control to the driver greenlet passing it
  90. # a coroutine to run. Once the awaitable is done, the driver greenlet
  91. # switches back to this greenlet with the result of awaitable that is
  92. # then returned to the caller (or raised as error)
  93. return current.driver.switch(awaitable) # type: ignore[no-any-return]
  94. def await_fallback(awaitable: Awaitable[_T]) -> _T:
  95. """Awaits an async function in a sync method.
  96. The sync method must be inside a :func:`greenlet_spawn` context.
  97. :func:`await_fallback` calls cannot be nested.
  98. :param awaitable: The coroutine to call.
  99. .. deprecated:: 2.0.24 The ``await_fallback()`` function will be removed
  100. in SQLAlchemy 2.1. Use :func:`_util.await_only` instead, running the
  101. function / program / etc. within a top-level greenlet that is set up
  102. using :func:`_util.greenlet_spawn`.
  103. """
  104. # this is called in the context greenlet while running fn
  105. current = getcurrent()
  106. if not isinstance(current, _AsyncIoGreenlet):
  107. loop = get_event_loop()
  108. if loop.is_running():
  109. _safe_cancel_awaitable(awaitable)
  110. raise exc.MissingGreenlet(
  111. "greenlet_spawn has not been called and asyncio event "
  112. "loop is already running; can't call await_fallback() here. "
  113. "Was IO attempted in an unexpected place?"
  114. )
  115. return loop.run_until_complete(awaitable)
  116. return current.driver.switch(awaitable) # type: ignore[no-any-return]
  117. async def greenlet_spawn(
  118. fn: Callable[..., _T],
  119. *args: Any,
  120. _require_await: bool = False,
  121. **kwargs: Any,
  122. ) -> _T:
  123. """Runs a sync function ``fn`` in a new greenlet.
  124. The sync function can then use :func:`await_only` to wait for async
  125. functions.
  126. :param fn: The sync callable to call.
  127. :param \\*args: Positional arguments to pass to the ``fn`` callable.
  128. :param \\*\\*kwargs: Keyword arguments to pass to the ``fn`` callable.
  129. """
  130. result: Any
  131. context = _AsyncIoGreenlet(fn, getcurrent())
  132. # runs the function synchronously in gl greenlet. If the execution
  133. # is interrupted by await_only, context is not dead and result is a
  134. # coroutine to wait. If the context is dead the function has
  135. # returned, and its result can be returned.
  136. switch_occurred = False
  137. try:
  138. result = context.switch(*args, **kwargs)
  139. while not context.dead:
  140. switch_occurred = True
  141. try:
  142. # wait for a coroutine from await_only and then return its
  143. # result back to it.
  144. value = await result
  145. except BaseException:
  146. # this allows an exception to be raised within
  147. # the moderated greenlet so that it can continue
  148. # its expected flow.
  149. result = context.throw(*sys.exc_info())
  150. else:
  151. result = context.switch(value)
  152. finally:
  153. # clean up to avoid cycle resolution by gc
  154. del context.driver
  155. if _require_await and not switch_occurred:
  156. raise exc.AwaitRequired(
  157. "The current operation required an async execution but none was "
  158. "detected. This will usually happen when using a non compatible "
  159. "DBAPI driver. Please ensure that an async DBAPI is used."
  160. )
  161. return result # type: ignore[no-any-return]
  162. class AsyncAdaptedLock:
  163. @memoized_property
  164. def mutex(self) -> asyncio.Lock:
  165. # there should not be a race here for coroutines creating the
  166. # new lock as we are not using await, so therefore no concurrency
  167. return asyncio.Lock()
  168. def __enter__(self) -> bool:
  169. # await is used to acquire the lock only after the first calling
  170. # coroutine has created the mutex.
  171. return await_fallback(self.mutex.acquire())
  172. def __exit__(self, *arg: Any, **kw: Any) -> None:
  173. self.mutex.release()
  174. def _util_async_run_coroutine_function(
  175. fn: Callable[..., Coroutine[Any, Any, Any]], *args: Any, **kwargs: Any
  176. ) -> Any:
  177. """for test suite/ util only"""
  178. loop = get_event_loop()
  179. if loop.is_running():
  180. raise Exception(
  181. "for async run coroutine we expect that no greenlet or event "
  182. "loop is running when we start out"
  183. )
  184. return loop.run_until_complete(fn(*args, **kwargs))
  185. def _util_async_run(
  186. fn: Callable[..., Coroutine[Any, Any, Any]], *args: Any, **kwargs: Any
  187. ) -> Any:
  188. """for test suite/ util only"""
  189. loop = get_event_loop()
  190. if not loop.is_running():
  191. return loop.run_until_complete(greenlet_spawn(fn, *args, **kwargs))
  192. else:
  193. # allow for a wrapped test function to call another
  194. assert isinstance(getcurrent(), _AsyncIoGreenlet)
  195. return fn(*args, **kwargs)
  196. def get_event_loop() -> asyncio.AbstractEventLoop:
  197. """vendor asyncio.get_event_loop() for python 3.7 and above.
  198. Python 3.10 deprecates get_event_loop() as a standalone.
  199. """
  200. try:
  201. return asyncio.get_running_loop()
  202. except RuntimeError:
  203. # avoid "During handling of the above exception, another exception..."
  204. pass
  205. return asyncio.get_event_loop_policy().get_event_loop()