-
Notifications
You must be signed in to change notification settings - Fork 16
Add shard async loop #25
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
Merged
Merged
Changes from all commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
8f9a3a3
add shard async loop
Yuki-Imajuku 5c3a02e
update shared async loop
Yuki-Imajuku 8b915bc
bugfix KeyboardInterrupt
Yuki-Imajuku 0a30d9b
revert changes
Yuki-Imajuku 7738f8a
update shared_async_loop
Yuki-Imajuku bd6755d
update shard async loop
Yuki-Imajuku be64d79
apply ruff
Yuki-Imajuku 4705de3
update
Yuki-Imajuku be2b114
update
Yuki-Imajuku 3c00af2
update
Yuki-Imajuku File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,118 @@ | ||
| import asyncio | ||
| import atexit | ||
| import logging | ||
| import threading | ||
| from concurrent.futures import Future, TimeoutError as FutureTimeoutError | ||
| from typing import Coroutine, TypeVar | ||
|
|
||
| T = TypeVar("T") | ||
|
|
||
|
|
||
| class SharedAsyncLoop: | ||
| """Background event loop shared across threads for async-only providers. | ||
|
|
||
| This class is intended for use cases where synchronous code needs to execute asynchronous coroutines, | ||
| such as when interacting with async-only providers (e.g., Google GenAI) from synchronous contexts. | ||
| """ | ||
|
|
||
| SHUTDOWN_TIMEOUT = 5 | ||
|
|
||
| def __init__(self) -> None: | ||
| self._loop = asyncio.new_event_loop() | ||
| self._thread = threading.Thread(target=self._run_loop, daemon=True) | ||
| self._atexit_cb = self.shutdown | ||
| self._thread.start() | ||
| try: | ||
| atexit.register(self._atexit_cb) | ||
| except Exception: | ||
| # Ensure the background thread is cleaned up if registration fails. | ||
| self.shutdown() | ||
| raise | ||
|
|
||
| def _run_loop(self) -> None: | ||
| asyncio.set_event_loop(self._loop) | ||
| self._loop.run_forever() | ||
|
|
||
| async def _drain_pending(self) -> None: | ||
| tasks = [t for t in asyncio.all_tasks(self._loop) if t is not asyncio.current_task(self._loop)] | ||
| for task in tasks: | ||
| task.cancel() | ||
| if tasks: | ||
| await asyncio.gather(*tasks, return_exceptions=True) | ||
|
|
||
| def run(self, coroutine: Coroutine[object, object, T], timeout: float | None = None) -> T: | ||
Yuki-Imajuku marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| """Execute a coroutine on the shared event loop from any thread and return its result. | ||
|
|
||
| Args: | ||
| coroutine (Coroutine): The coroutine to execute. | ||
| timeout (float, optional): Maximum time in seconds to wait for the result. If None, wait indefinitely. | ||
| Returns: | ||
| T: The result returned by the coroutine. | ||
| Raises: | ||
| asyncio.TimeoutError: If the coroutine does not complete within the specified timeout. | ||
| Exception: Any exception raised by the coroutine will be propagated. | ||
| Note: | ||
| On exception, this method requests cancellation of the underlying coroutine via the returned Future. | ||
| If the coroutine ignores cancellation, it may continue running briefly. | ||
| """ | ||
| future: Future[T] = asyncio.run_coroutine_threadsafe(coroutine, self._loop) | ||
| try: | ||
| return future.result(timeout=timeout) | ||
| except FutureTimeoutError as exc: | ||
| future.cancel() | ||
| raise asyncio.TimeoutError(f"Timed out waiting for coroutine result after {timeout}s") from exc | ||
| except Exception: | ||
| future.cancel() | ||
| raise | ||
|
|
||
| def shutdown(self) -> None: | ||
| global SHARED_ASYNC_LOOP | ||
| with SHARED_ASYNC_LOOP_LOCK: | ||
| if self._loop.is_closed(): | ||
| if SHARED_ASYNC_LOOP is self: | ||
| SHARED_ASYNC_LOOP = None | ||
| return | ||
Yuki-Imajuku marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| if self._loop.is_running(): | ||
| drain_future = asyncio.run_coroutine_threadsafe(self._drain_pending(), self._loop) | ||
| try: | ||
| drain_future.result(timeout=self.SHUTDOWN_TIMEOUT) | ||
| except FutureTimeoutError: | ||
| logging.getLogger(__name__).warning("Timed out cancelling pending tasks on shared async loop") | ||
| self._loop.call_soon_threadsafe(self._loop.stop) | ||
| if threading.current_thread() is not self._thread and self._thread.is_alive(): | ||
| self._thread.join(timeout=self.SHUTDOWN_TIMEOUT) | ||
| if self._thread.is_alive(): | ||
| logging.getLogger(__name__).warning( | ||
| f"Shared async loop thread did not stop within {self.SHUTDOWN_TIMEOUT}s" | ||
| ) | ||
Yuki-Imajuku marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| if not self._loop.is_closed(): | ||
| self._loop.close() | ||
| try: | ||
| atexit.unregister(self._atexit_cb) | ||
| except Exception: | ||
| # During interpreter shutdown unregister may fail; ignore. | ||
| pass | ||
| if SHARED_ASYNC_LOOP is self: | ||
| SHARED_ASYNC_LOOP = None | ||
|
|
||
| def is_closed(self) -> bool: | ||
| return self._loop.is_closed() | ||
|
|
||
|
|
||
| SHARED_ASYNC_LOOP: SharedAsyncLoop | None = None | ||
| SHARED_ASYNC_LOOP_LOCK = threading.Lock() | ||
|
|
||
|
|
||
| def shared_async_loop() -> SharedAsyncLoop: | ||
Yuki-Imajuku marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| """Returns a singleton instance of SharedAsyncLoop, creating a new instance if None or the previous one is closed. | ||
|
|
||
| This function is thread-safe and ensures only one SharedAsyncLoop instance is active at a time. | ||
|
|
||
| Returns: | ||
| SharedAsyncLoop: The shared async event loop instance. | ||
| """ | ||
| global SHARED_ASYNC_LOOP | ||
| with SHARED_ASYNC_LOOP_LOCK: | ||
| if SHARED_ASYNC_LOOP is None or SHARED_ASYNC_LOOP.is_closed(): | ||
| SHARED_ASYNC_LOOP = SharedAsyncLoop() | ||
| return SHARED_ASYNC_LOOP | ||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.