-
Notifications
You must be signed in to change notification settings - Fork 253
Implemented sharing the same event loop with AsyncSingleThreadContext #542
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,6 +8,7 @@ | |
| import threading | ||
| import warnings | ||
| import weakref | ||
| from asyncio import constants | ||
| from concurrent.futures import Future, ThreadPoolExecutor | ||
| from typing import ( | ||
| TYPE_CHECKING, | ||
|
|
@@ -32,10 +33,16 @@ | |
| else: | ||
| from typing_extensions import ParamSpec | ||
|
|
||
| THREAD_JOIN_TIMEOUT = getattr(constants, "THREAD_JOIN_TIMEOUT", 0) | ||
|
|
||
| if TYPE_CHECKING: | ||
| # This is not available to import at runtime | ||
| from _typeshed import OptExcInfo | ||
|
|
||
| AsyncSingleThreadContextMapType = weakref.WeakKeyDictionary[ | ||
| "AsyncSingleThreadContext", tuple[ThreadPoolExecutor, asyncio.AbstractEventLoop] | ||
| ] | ||
|
|
||
| _F = TypeVar("_F", bound=Callable[..., Any]) | ||
| _P = ParamSpec("_P") | ||
| _R = TypeVar("_R") | ||
|
|
@@ -97,11 +104,48 @@ def __enter__(self): | |
|
|
||
| return self | ||
|
|
||
| def _cancel_all_tasks(self, loop): | ||
| to_cancel = asyncio.all_tasks(loop) | ||
| if not to_cancel: | ||
| return | ||
|
|
||
| for task in to_cancel: | ||
| task.cancel() | ||
|
|
||
| loop.run_until_complete(asyncio.gather(*to_cancel, return_exceptions=True)) | ||
|
|
||
| for task in to_cancel: | ||
| if task.cancelled(): | ||
| continue | ||
| if task.exception() is not None: | ||
| loop.call_exception_handler( | ||
| { | ||
| "message": "unhandled exception during AsyncSingleThreadContext shutdown", | ||
| "exception": task.exception(), | ||
| "task": task, | ||
| } | ||
| ) | ||
|
|
||
| def __exit__(self, exc, value, tb): | ||
| if not self.token: | ||
| return | ||
|
|
||
| executor = AsyncToSync.context_to_thread_executor.pop(self, None) | ||
| executor, loop = AsyncToSync.async_single_thread_context_map.pop( | ||
| self, (None, None) | ||
| ) | ||
| if loop: | ||
| try: | ||
| self._cancel_all_tasks(loop) | ||
| loop.run_until_complete(loop.shutdown_asyncgens()) | ||
| if THREAD_JOIN_TIMEOUT: | ||
| loop.run_until_complete( | ||
| loop.shutdown_default_executor(THREAD_JOIN_TIMEOUT) | ||
| ) | ||
| else: | ||
| loop.run_until_complete(loop.shutdown_default_executor()) | ||
| finally: | ||
| loop.close() | ||
|
|
||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Instead of using Internally, class Runner:
def close(self):
try:
_cancel_all_tasks(self._loop)
self._loop.run_until_complete(self._loop.shutdown_asyncgens())
self._loop.run_until_complete(
self._loop.shutdown_default_executor(constants.THREAD_JOIN_TIMEOUT))
finally:
if self._set_event_loop:
events.set_event_loop(None)
loop.close()
def run(self, coro, *, context=None):
self._loop = events.new_event_loop()
events.set_event_loop(self._loop)
task = self._loop.create_task(coro)
return self._loop.run_until_complete(task)I added everything except the |
||
| if executor: | ||
| executor.shutdown() | ||
|
|
||
|
|
@@ -174,7 +218,7 @@ class AsyncToSync(Generic[_P, _R]): | |
| contextvars.ContextVar("async_single_thread_context") | ||
| ) | ||
|
|
||
| context_to_thread_executor: "weakref.WeakKeyDictionary[AsyncSingleThreadContext, ThreadPoolExecutor]" = ( | ||
| async_single_thread_context_map: "AsyncSingleThreadContextMapType" = ( | ||
| weakref.WeakKeyDictionary() | ||
| ) | ||
|
|
||
|
|
@@ -293,25 +337,29 @@ async def new_loop_wrap() -> None: | |
| running_in_main_event_loop = False | ||
|
|
||
| if not running_in_main_event_loop: | ||
| loop_executor = None | ||
|
|
||
| if self.async_single_thread_context.get(None): | ||
| single_thread_context = self.async_single_thread_context.get() | ||
|
|
||
| if single_thread_context in self.context_to_thread_executor: | ||
| loop_executor = self.context_to_thread_executor[ | ||
| single_thread_context | ||
| ] | ||
| if single_thread_context in self.async_single_thread_context_map: | ||
| ( | ||
| loop_executor, | ||
| context_loop, | ||
| ) = self.async_single_thread_context_map[single_thread_context] | ||
| else: | ||
| context_loop = asyncio.new_event_loop() | ||
| loop_executor = ThreadPoolExecutor(max_workers=1) | ||
| self.context_to_thread_executor[ | ||
| single_thread_context | ||
| ] = loop_executor | ||
| self.async_single_thread_context_map[single_thread_context] = ( | ||
| loop_executor, | ||
| context_loop, | ||
| ) | ||
|
|
||
| loop_future = loop_executor.submit( | ||
| context_loop.run_until_complete, new_loop_wrap() | ||
| ) | ||
| else: | ||
| # Make our own event loop - in a new thread - and run inside that. | ||
| loop_executor = ThreadPoolExecutor(max_workers=1) | ||
|
|
||
| loop_future = loop_executor.submit(asyncio.run, new_loop_wrap()) | ||
| loop_future = loop_executor.submit(asyncio.run, new_loop_wrap()) | ||
| # Run the CurrentThreadExecutor until the future is done. | ||
| current_executor.run_until_future(loop_future) | ||
| # Wait for future and/or allow for exception propagation | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I ran into a failing test that caused an endless loop, so I added a timeout to automatically cancel such pipelines.