From 1c5a354a7dd967040bc38f292a1e836d2e840cdf Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Thu, 27 Mar 2025 11:00:37 -0700 Subject: [PATCH 1/2] In Python 3.12 or above, use asyncio eager task factory - This is a performance improvement when calling async functions that do not use await, as they are run immediately and never scheduled in the loop --- libs/langgraph/langgraph/utils/future.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/libs/langgraph/langgraph/utils/future.py b/libs/langgraph/langgraph/utils/future.py index e61afec91..1a4f838c0 100644 --- a/libs/langgraph/langgraph/utils/future.py +++ b/libs/langgraph/langgraph/utils/future.py @@ -10,6 +10,7 @@ T = TypeVar("T") AnyFuture = Union[asyncio.Future, concurrent.futures.Future] CONTEXT_NOT_SUPPORTED = sys.version_info < (3, 11) +EAGER_NOT_SUPPORTED = sys.version_info < (3, 12) def _get_loop(fut: asyncio.Future) -> asyncio.AbstractEventLoop: @@ -159,8 +160,12 @@ def _ensure_future( try: if CONTEXT_NOT_SUPPORTED: return loop.create_task(coro_or_future, name=name) - else: + elif EAGER_NOT_SUPPORTED: return loop.create_task(coro_or_future, name=name, context=context) + else: + return asyncio.eager_task_factory( + loop, coro_or_future, name=name, context=context + ) except RuntimeError: if not called_wrap_awaitable: coro_or_future.close() From ef71656f0510bd43902ac8832c7f8d092f06b46f Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Thu, 27 Mar 2025 13:14:39 -0700 Subject: [PATCH 2/2] Fix --- libs/langgraph/langgraph/pregel/executor.py | 10 ++++-- libs/langgraph/langgraph/utils/future.py | 36 +++++++++++++-------- 2 files changed, 30 insertions(+), 16 deletions(-) diff --git a/libs/langgraph/langgraph/pregel/executor.py b/libs/langgraph/langgraph/pregel/executor.py index 64bb6c90e..0a117651d 100644 --- a/libs/langgraph/langgraph/pregel/executor.py +++ b/libs/langgraph/langgraph/pregel/executor.py @@ -156,10 +156,16 @@ class AsyncBackgroundExecutor(AsyncContextManager): if self.semaphore: coro = gated(self.semaphore, coro) if CONTEXT_NOT_SUPPORTED: - task = run_coroutine_threadsafe(coro, self.loop, name=__name__) + task = run_coroutine_threadsafe( + coro, self.loop, name=__name__, lazy=__next_tick__ + ) else: task = run_coroutine_threadsafe( - coro, self.loop, name=__name__, context=copy_context() + coro, + self.loop, + name=__name__, + context=copy_context(), + lazy=__next_tick__, ) self.tasks[task] = (__cancel_on_exit__, __reraise_on_exit__) task.add_done_callback(self.done) diff --git a/libs/langgraph/langgraph/utils/future.py b/libs/langgraph/langgraph/utils/future.py index 1a4f838c0..a311133df 100644 --- a/libs/langgraph/langgraph/utils/future.py +++ b/libs/langgraph/langgraph/utils/future.py @@ -143,6 +143,7 @@ def _ensure_future( loop: asyncio.AbstractEventLoop, name: Optional[str] = None, context: Optional[contextvars.Context] = None, + lazy: bool = True, ) -> asyncio.Task[T]: called_wrap_awaitable = False if not asyncio.iscoroutine(coro_or_future): @@ -160,7 +161,7 @@ def _ensure_future( try: if CONTEXT_NOT_SUPPORTED: return loop.create_task(coro_or_future, name=name) - elif EAGER_NOT_SUPPORTED: + elif EAGER_NOT_SUPPORTED or lazy: return loop.create_task(coro_or_future, name=name, context=context) else: return asyncio.eager_task_factory( @@ -185,6 +186,8 @@ def _wrap_awaitable(awaitable: Awaitable[T]) -> Generator[None, None, T]: def run_coroutine_threadsafe( coro: Coroutine[None, None, T], loop: asyncio.AbstractEventLoop, + *, + lazy: bool, name: Optional[str] = None, context: Optional[contextvars.Context] = None, ) -> asyncio.Future[T]: @@ -192,18 +195,23 @@ def run_coroutine_threadsafe( Return a asyncio.Future to access the result. """ - future: asyncio.Future[T] = asyncio.Future(loop=loop) - def callback() -> None: - try: - chain_future( - _ensure_future(coro, loop=loop, name=name, context=context), future - ) - except (SystemExit, KeyboardInterrupt): - raise - except BaseException as exc: - future.set_exception(exc) - raise + if asyncio._get_running_loop() is loop: + return _ensure_future(coro, loop=loop, name=name, context=context, lazy=lazy) + else: + future: asyncio.Future[T] = asyncio.Future(loop=loop) - loop.call_soon_threadsafe(callback, context=context) - return future + def callback() -> None: + try: + chain_future( + _ensure_future(coro, loop=loop, name=name, context=context), + future, + ) + except (SystemExit, KeyboardInterrupt): + raise + except BaseException as exc: + future.set_exception(exc) + raise + + loop.call_soon_threadsafe(callback, context=context) + return future