Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
_runner.py942 linesDownload Raw Back to pregel
1from __future__ import annotations2 3import asyncio4import concurrent.futures5import inspect6import threading7import time8import weakref9from collections.abc import (10    AsyncIterator,11    Awaitable,12    Callable,13    Collection,14    Iterable,15    Iterator,16    Mapping,17    Sequence,18)19from functools import partial20from typing import (21    Any,22    Generic,23    TypeVar,24    cast,25)26 27from langchain_core.callbacks import Callbacks28 29from langgraph._internal._constants import (30    CONF,31    CONFIG_KEY_CALL,32    CONFIG_KEY_SCRATCHPAD,33    ERROR,34    ERROR_SOURCE_NODE,35    INTERRUPT,36    NO_WRITES,37    RESUME,38    RETURN,39)40from langgraph._internal._future import chain_future, run_coroutine_threadsafe41from langgraph._internal._scratchpad import PregelScratchpad42from langgraph._internal._typing import MISSING43from langgraph.constants import TAG_HIDDEN44from langgraph.errors import GraphBubbleUp, GraphInterrupt45from langgraph.pregel._algo import Call46from langgraph.pregel._executor import Submit47from langgraph.pregel._retry import arun_with_retry, run_with_retry48from langgraph.types import (49    CachePolicy,50    PregelExecutableTask,51    RetryPolicy,52    TimeoutPolicy,53)54 55F = TypeVar("F", concurrent.futures.Future, asyncio.Future)56E = TypeVar("E", threading.Event, asyncio.Event)57 58# List of filenames to exclude from exception traceback59# Note: Frames will be removed if they are the last frame in traceback, recursively60EXCLUDED_FRAME_FNAMES = (61    "langgraph/pregel/retry.py",62    "langgraph/pregel/runner.py",63    "langgraph/pregel/executor.py",64    "langgraph/utils/runnable.py",65    "langchain_core/runnables/config.py",66    "concurrent/futures/thread.py",67    "concurrent/futures/_base.py",68)69 70SKIP_RERAISE_SET: weakref.WeakSet[concurrent.futures.Future | asyncio.Future] = (71    weakref.WeakSet()72)73 74 75class FuturesDict(Generic[F, E], dict[F, PregelExecutableTask | None]):76    event: E77    callback: weakref.ref[Callable[[PregelExecutableTask, BaseException | None], None]]78    # Stop condition is injected by PregelRunner instead of hard-coded here.79    # This lets the runner treat graph-error-handled exceptions as non-fatal80    # so `on_done` does not trigger an early stop for those futures.81    should_stop: Callable[[set[F]], bool]82    counter: int83    done: set[F]84    lock: threading.Lock85 86    def __init__(87        self,88        event: E,89        callback: weakref.ref[90            Callable[[PregelExecutableTask, BaseException | None], None]91        ],92        should_stop: Callable[[set[F]], bool],93        future_type: type[F],94        # used for generic typing, newer py supports FutureDict[...](...)95    ) -> None:96        super().__init__()97        self.lock = threading.Lock()98        self.event = event99        self.callback = callback100        self.should_stop = should_stop101        self.counter = 0102        self.done: set[F] = set()103 104    def __setitem__(105        self,106        key: F,107        value: PregelExecutableTask | None,108    ) -> None:109        super().__setitem__(key, value)  # type: ignore[index]110        if value is not None:111            with self.lock:112                self.event.clear()113                self.counter += 1114            key.add_done_callback(partial(self.on_done, value))115 116    def on_done(117        self,118        task: PregelExecutableTask,119        fut: F,120    ) -> None:121        # Called automatically by future.add_done_callback registered in __setitem__.122        try:123            if cb := self.callback():124                cb(task, _exception(fut))125        finally:126            with self.lock:127                self.done.add(fut)128                self.counter -= 1129                # Wake waiter when all tracked futures are done, or when runner-level130                # stop condition is met (for example, a non-handled fatal exception).131                if self.counter == 0 or self.should_stop(self.done):132                    self.event.set()133 134 135class PregelRunner:136    """Responsible for executing a set of Pregel tasks concurrently, committing137    their writes, yielding control to caller when there is output to emit, and138    interrupting other tasks if appropriate."""139 140    def __init__(141        self,142        *,143        submit: weakref.ref[Submit],144        put_writes: weakref.ref[Callable[[str, Sequence[tuple[str, Any]]], None]],145        use_astream: bool = False,146        node_finished: Callable[[str], None] | None = None,147        node_error_handler_map: Mapping[str, str] | None = None,148        schedule_error_handler: Callable[149            [PregelExecutableTask, BaseException], PregelExecutableTask | None150        ]151        | None = None,152        aschedule_error_handler: Callable[153            [PregelExecutableTask, BaseException],154            Awaitable[PregelExecutableTask | None],155        ]156        | None = None,157    ) -> None:158        self.submit = submit159        self.put_writes = put_writes160        self.use_astream = use_astream161        self.node_finished = node_finished162        self.node_error_handler_map = dict(node_error_handler_map or {})163        self.error_handler_nodes = set(self.node_error_handler_map.values())164        self.schedule_error_handler = schedule_error_handler165        self.aschedule_error_handler = aschedule_error_handler166        # Exception object ids that are already routed to graph-level error handler.167        # These ids are consulted by stop/panic checks to avoid re-raising handled168        # exceptions via the normal fatal path in the same run.169        self._handled_exception_ids: set[int] = set()170 171    def _should_route_to_error_handler(self, task: PregelExecutableTask) -> bool:172        if task.name in self.error_handler_nodes:173            return False174        return task.name in self.node_error_handler_map175 176    def tick(177        self,178        tasks: Iterable[PregelExecutableTask],179        *,180        reraise: bool = True,181        timeout: float | None = None,182        retry_policy: Sequence[RetryPolicy] | None = None,183        get_waiter: Callable[[], concurrent.futures.Future[None]] | None = None,184        schedule_task: Callable[185            [PregelExecutableTask, int, Call | None],186            PregelExecutableTask | None,187        ],188    ) -> Iterator[None]:189        tasks = tuple(tasks)190        futures = FuturesDict(191            callback=weakref.WeakMethod(self.commit),192            event=threading.Event(),193            should_stop=partial(194                _should_stop_others, handled_exception_ids=self._handled_exception_ids195            ),196            future_type=concurrent.futures.Future,197        )198        # give control back to the caller199        yield200        # fast path if single task with no timeout and no waiter201        if len(tasks) == 0:202            return203        elif len(tasks) == 1 and timeout is None and get_waiter is None:204            t = tasks[0]205            scheduled_error_handler = False206            try:207                run_with_retry(208                    t,209                    retry_policy,210                    configurable={211                        CONFIG_KEY_CALL: partial(212                            _call,213                            weakref.ref(t),214                            retry_policy=retry_policy,215                            futures=weakref.ref(futures),216                            schedule_task=schedule_task,217                            submit=self.submit,218                        ),219                    },220                )221                self.commit(t, None)222            except Exception as exc:223                self.commit(t, exc)224                if (225                    not isinstance(exc, GraphBubbleUp)226                    and self._should_route_to_error_handler(t)227                    and self.schedule_error_handler is not None228                ):229                    self._handled_exception_ids.add(id(exc))230                    if handler_task := self.schedule_error_handler(t, exc):231                        tasks = (handler_task,)232                        scheduled_error_handler = True233                        # Continue to the regular scheduling path for handler execution.234                if reraise and futures:235                    if id(exc) not in self._handled_exception_ids:236                        # will be re-raised after futures are done237                        fut: concurrent.futures.Future = concurrent.futures.Future()238                        fut.set_exception(exc)239                        futures.done.add(fut)240                elif reraise and id(exc) not in self._handled_exception_ids:241                    if tb := exc.__traceback__:242                        while tb.tb_next is not None and any(243                            tb.tb_frame.f_code.co_filename.endswith(name)244                            for name in EXCLUDED_FRAME_FNAMES245                        ):246                            tb = tb.tb_next247                        exc.__traceback__ = tb248                    raise249            if not futures and not scheduled_error_handler:250                # maybe `t` scheduled another task251                return252            else:253                if not scheduled_error_handler:254                    tasks = ()  # don't reschedule this task255        # add waiter task if requested256        if get_waiter is not None:257            futures[get_waiter()] = None258        # schedule tasks259        for t in tasks:260            fut = self.submit()(  # type: ignore[misc]261                run_with_retry,262                t,263                retry_policy,264                configurable={265                    CONFIG_KEY_CALL: partial(266                        _call,267                        weakref.ref(t),268                        retry_policy=retry_policy,269                        futures=weakref.ref(futures),270                        schedule_task=schedule_task,271                        submit=self.submit,272                    ),273                },274                __reraise_on_exit__=reraise,275            )276            futures[fut] = t277        # execute tasks, and wait for one to fail or all to finish.278        # each task is independent from all other concurrent tasks279        # yield updates/debug output as each task finishes280        end_time = timeout + time.monotonic() if timeout else None281        handled_futures: set[concurrent.futures.Future[Any]] = set()282        while len(futures) > (1 if get_waiter is not None else 0):283            done, inflight = concurrent.futures.wait(284                futures,285                return_when=concurrent.futures.FIRST_COMPLETED,286                timeout=(max(0, end_time - time.monotonic()) if end_time else None),287            )288            if not done:289                break  # timed out290            done_for_stop: set[concurrent.futures.Future[Any]] = set()291            for fut in done:292                task = futures.pop(fut)293                if task is None:294                    # waiter task finished, schedule another295                    if inflight and get_waiter is not None:296                        futures[get_waiter()] = None297                elif (298                    (task_exc := _exception(fut))299                    and self._should_route_to_error_handler(task)300                    and not isinstance(task_exc, GraphBubbleUp)301                ):302                    self._handled_exception_ids.add(id(task_exc))303                    SKIP_RERAISE_SET.add(fut)304                    handled_futures.add(fut)305                    if self.schedule_error_handler is not None:306                        if handler_task := self.schedule_error_handler(task, task_exc):307                            handler_fut = self.submit()(  # type: ignore[misc]308                                run_with_retry,309                                handler_task,310                                retry_policy,311                                configurable={312                                    CONFIG_KEY_CALL: partial(313                                        _call,314                                        weakref.ref(handler_task),315                                        retry_policy=retry_policy,316                                        futures=weakref.ref(futures),317                                        schedule_task=schedule_task,318                                        submit=self.submit,319                                    ),320                                },321                                __reraise_on_exit__=reraise,322                            )323                            futures[handler_fut] = handler_task324                else:325                    done_for_stop.add(fut)326            else:327                # remove references to loop vars328                del fut, task329            # maybe stop other tasks330            if _should_stop_others(331                done_for_stop, handled_exception_ids=self._handled_exception_ids332            ):333                break334            # give control back to the caller335            yield336        # wait for done callbacks337        futures.event.wait(338            timeout=(max(0, end_time - time.monotonic()) if end_time else None)339        )340        # give control back to the caller341        yield342        # panic on failure or timeout343        try:344            _panic_or_proceed(345                futures.done.union(f for f, t in futures.items() if t is not None),346                panic=reraise,347                handled_exception_ids=self._handled_exception_ids,348                handled_futures=handled_futures,349            )350        except Exception as exc:351            if tb := exc.__traceback__:352                while tb.tb_next is not None and any(353                    tb.tb_frame.f_code.co_filename.endswith(name)354                    for name in EXCLUDED_FRAME_FNAMES355                ):356                    tb = tb.tb_next357                exc.__traceback__ = tb358            raise359 360    async def atick(361        self,362        tasks: Iterable[PregelExecutableTask],363        *,364        reraise: bool = True,365        timeout: float | None = None,366        retry_policy: Sequence[RetryPolicy] | None = None,367        get_waiter: Callable[[], asyncio.Future[None]] | None = None,368        schedule_task: Callable[369            [PregelExecutableTask, int, Call | None],370            Awaitable[PregelExecutableTask | None],371        ],372    ) -> AsyncIterator[None]:373        try:374            loop = asyncio.get_event_loop()375        except RuntimeError:376            loop = asyncio.new_event_loop()377        asyncio.set_event_loop(loop)378        tasks = tuple(tasks)379        futures = FuturesDict(380            callback=weakref.WeakMethod(self.commit),381            event=asyncio.Event(),382            should_stop=partial(383                _should_stop_others, handled_exception_ids=self._handled_exception_ids384            ),385            future_type=asyncio.Future,386        )387        # give control back to the caller388        yield389        # fast path if single task with no waiter and no timeout390        if len(tasks) == 0:391            return392        elif len(tasks) == 1 and get_waiter is None and timeout is None:393            t = tasks[0]394            scheduled_error_handler = False395            try:396                await arun_with_retry(397                    t,398                    retry_policy,399                    stream=self.use_astream,400                    configurable={401                        CONFIG_KEY_CALL: partial(402                            _acall,403                            weakref.ref(t),404                            stream=self.use_astream,405                            retry_policy=retry_policy,406                            futures=weakref.ref(futures),407                            schedule_task=schedule_task,408                            submit=self.submit,409                            loop=loop,410                        ),411                    },412                )413                self.commit(t, None)414            except Exception as exc:415                self.commit(t, exc)416                if (417                    not isinstance(exc, GraphBubbleUp)418                    and self._should_route_to_error_handler(t)419                    and self.aschedule_error_handler is not None420                ):421                    self._handled_exception_ids.add(id(exc))422                    if handler_task := await self.aschedule_error_handler(t, exc):423                        tasks = (handler_task,)424                        scheduled_error_handler = True425                if reraise and futures:426                    if id(exc) not in self._handled_exception_ids:427                        # will be re-raised after futures are done428                        fut: asyncio.Future = loop.create_future()429                        fut.set_exception(exc)430                        futures.done.add(fut)431                elif reraise and id(exc) not in self._handled_exception_ids:432                    if tb := exc.__traceback__:433                        while tb.tb_next is not None and any(434                            tb.tb_frame.f_code.co_filename.endswith(name)435                            for name in EXCLUDED_FRAME_FNAMES436                        ):437                            tb = tb.tb_next438                        exc.__traceback__ = tb439                    raise440            if not futures and not scheduled_error_handler:441                # maybe `t` scheduled another task442                return443            else:444                if not scheduled_error_handler:445                    tasks = ()  # don't reschedule this task446        # add waiter task if requested447        if get_waiter is not None:448            futures[get_waiter()] = None449        # schedule tasks450        for t in tasks:451            fut = cast(452                asyncio.Future,453                self.submit()(  # type: ignore[misc]454                    arun_with_retry,455                    t,456                    retry_policy,457                    stream=self.use_astream,458                    configurable={459                        CONFIG_KEY_CALL: partial(460                            _acall,461                            weakref.ref(t),462                            retry_policy=retry_policy,463                            stream=self.use_astream,464                            futures=weakref.ref(futures),465                            schedule_task=schedule_task,466                            submit=self.submit,467                            loop=loop,468                        ),469                    },470                    __name__=t.name,471                    __cancel_on_exit__=True,472                    __reraise_on_exit__=reraise,473                ),474            )475            futures[fut] = t476        # execute tasks, and wait for one to fail or all to finish.477        # each task is independent from all other concurrent tasks478        # yield updates/debug output as each task finishes479        end_time = timeout + loop.time() if timeout else None480        handled_futures: set[asyncio.Future[Any]] = set()481        while len(futures) > (1 if get_waiter is not None else 0):482            done, inflight = await asyncio.wait(483                futures,484                return_when=asyncio.FIRST_COMPLETED,485                timeout=(max(0, end_time - loop.time()) if end_time else None),486            )487            if not done:488                break  # timed out489            done_for_stop: set[asyncio.Future[Any]] = set()490            for fut in done:491                task = futures.pop(fut)492                if task is None:493                    # waiter task finished, schedule another494                    if inflight and get_waiter is not None:495                        futures[get_waiter()] = None496                elif (497                    (task_exc := _exception(fut))498                    and self._should_route_to_error_handler(task)499                    and not isinstance(task_exc, GraphBubbleUp)500                ):501                    self._handled_exception_ids.add(id(task_exc))502                    SKIP_RERAISE_SET.add(fut)503                    handled_futures.add(fut)504                    if self.aschedule_error_handler is not None:505                        if handler_task := await self.aschedule_error_handler(506                            task, task_exc507                        ):508                            handler_fut = cast(509                                asyncio.Future,510                                self.submit()(  # type: ignore[misc]511                                    arun_with_retry,512                                    handler_task,513                                    retry_policy,514                                    stream=self.use_astream,515                                    configurable={516                                        CONFIG_KEY_CALL: partial(517                                            _acall,518                                            weakref.ref(handler_task),519                                            retry_policy=retry_policy,520                                            stream=self.use_astream,521                                            futures=weakref.ref(futures),522                                            schedule_task=schedule_task,523                                            submit=self.submit,524                                            loop=loop,525                                        ),526                                    },527                                    __name__=handler_task.name,528                                    __cancel_on_exit__=True,529                                    __reraise_on_exit__=reraise,530                                ),531                            )532                            futures[handler_fut] = handler_task533                else:534                    done_for_stop.add(fut)535            else:536                # remove references to loop vars537                del fut, task538            # maybe stop other tasks539            if _should_stop_others(540                done_for_stop, handled_exception_ids=self._handled_exception_ids541            ):542                break543            # give control back to the caller544            yield545        # wait for done callbacks546        await asyncio.wait_for(547            futures.event.wait(),548            timeout=(max(0, end_time - loop.time()) if end_time else None),549        )550        # give control back to the caller551        yield552        # cancel waiter task553        for fut in futures:554            fut.cancel()555        # panic on failure or timeout556        try:557            _panic_or_proceed(558                futures.done.union(f for f, t in futures.items() if t is not None),559                timeout_exc_cls=asyncio.TimeoutError,560                panic=reraise,561                handled_exception_ids=self._handled_exception_ids,562                handled_futures=handled_futures,563            )564        except Exception as exc:565            if tb := exc.__traceback__:566                while tb.tb_next is not None and any(567                    tb.tb_frame.f_code.co_filename.endswith(name)568                    for name in EXCLUDED_FRAME_FNAMES569                ):570                    tb = tb.tb_next571                exc.__traceback__ = tb572            raise573 574    def commit(575        self,576        task: PregelExecutableTask,577        exception: BaseException | None,578    ) -> None:579        if isinstance(exception, asyncio.CancelledError):580            # for cancelled tasks, also save error in task,581            # so loop can finish super-step582            task.writes.append((ERROR, exception))583            self.put_writes()(task.id, task.writes)  # type: ignore[misc]584        elif exception:585            if isinstance(exception, GraphInterrupt):586                # save interrupt to checkpointer587                if exception.args[0]:588                    writes = [(INTERRUPT, exception.args[0])]589                    if resumes := [w for w in task.writes if w[0] == RESUME]:590                        writes.extend(resumes)591                    self.put_writes()(task.id, writes)  # type: ignore[misc]592            elif isinstance(exception, GraphBubbleUp):593                # exception will be raised in _panic_or_proceed594                pass595            else:596                # save error to checkpointer597                task.writes.append((ERROR, exception))598                if self._should_route_to_error_handler(task) and not isinstance(599                    exception, GraphBubbleUp600                ):601                    task.writes.append((ERROR_SOURCE_NODE, task.name))602                    self._handled_exception_ids.add(id(exception))603                self.put_writes()(task.id, task.writes)  # type: ignore[misc]604        else:605            if self.node_finished and (606                task.config is None or TAG_HIDDEN not in task.config.get("tags", [])607            ):608                self.node_finished(task.name)609            if not task.writes:610                # add no writes marker611                task.writes.append((NO_WRITES, None))612            # save task writes to checkpointer613            self.put_writes()(task.id, task.writes)  # type: ignore[misc]614 615 616def _should_stop_others(617    done: set[F],618    *,619    handled_exception_ids: set[int] | None = None,620) -> bool:621    """Check if any task failed, if so, cancel all other tasks.622    GraphInterrupts are not considered failures."""623    for fut in done:624        if fut.cancelled():625            continue626        elif exc := fut.exception():627            if (628                id(exc) not in (handled_exception_ids or set())629                and not isinstance(exc, GraphBubbleUp)630                and fut not in SKIP_RERAISE_SET631            ):632                return True633 634    return False635 636 637def _exception(638    fut: concurrent.futures.Future[Any] | asyncio.Future[Any],639) -> BaseException | None:640    """Return the exception from a future, without raising CancelledError."""641    if fut.cancelled():642        if isinstance(fut, asyncio.Future):643            return asyncio.CancelledError()644        else:645            return concurrent.futures.CancelledError()646    else:647        return fut.exception()648 649 650def _panic_or_proceed(651    futs: set[concurrent.futures.Future] | set[asyncio.Future],652    *,653    timeout_exc_cls: type[Exception] = TimeoutError,654    panic: bool = True,655    handled_exception_ids: set[int] | None = None,656    handled_futures: Collection[concurrent.futures.Future[Any] | asyncio.Future[Any]]657    | None = None,658) -> None:659    """Cancel remaining tasks if any failed, re-raise exception if panic is True."""660    done: set[concurrent.futures.Future[Any] | asyncio.Future[Any]] = set()661    inflight: set[concurrent.futures.Future[Any] | asyncio.Future[Any]] = set()662    for fut in futs:663        if fut.cancelled():664            continue665        elif fut.done():666            done.add(fut)667        else:668            inflight.add(fut)669    interrupts: list[GraphInterrupt] = []670    while done:671        # if any task failed672        fut = done.pop()673        if exc := _exception(fut):674            if fut in (handled_futures or set()):675                continue676            if id(exc) in (handled_exception_ids or set()):677                continue678            # cancel all pending tasks679            while inflight:680                inflight.pop().cancel()681            # raise the exception682            if panic:683                if isinstance(exc, GraphInterrupt):684                    # collect interrupts685                    interrupts.append(exc)686                elif fut not in SKIP_RERAISE_SET:687                    raise exc688    # raise combined interrupts689    if interrupts:690        raise GraphInterrupt(tuple(i for exc in interrupts for i in exc.args[0]))691    if inflight:692        # if we got here means we timed out693        while inflight:694            # cancel all pending tasks695            inflight.pop().cancel()696        # raise timeout error697        raise timeout_exc_cls("Timed out")698 699 700def _call(701    task: weakref.ref[PregelExecutableTask],702    func: Callable[[Any], Awaitable[Any] | Any],703    input: Any,704    *,705    retry_policy: Sequence[RetryPolicy] | None = None,706    cache_policy: CachePolicy | None = None,707    timeout: TimeoutPolicy | None = None,708    callbacks: Callbacks = None,709    futures: weakref.ref[FuturesDict],710    schedule_task: Callable[711        [PregelExecutableTask, int, Call | None], PregelExecutableTask | None712    ],713    submit: weakref.ref[Submit],714) -> concurrent.futures.Future[Any]:715    if inspect.iscoroutinefunction(func):716        raise RuntimeError("In an sync context async tasks cannot be called")717 718    fut: concurrent.futures.Future | None = None719    # schedule PUSH tasks, collect futures720    scratchpad: PregelScratchpad = task().config[CONF][CONFIG_KEY_SCRATCHPAD]  # type: ignore[union-attr]721    # schedule the next task, if the callback returns one722    if next_task := schedule_task(723        task(),  # type: ignore[arg-type]724        scratchpad.call_counter(),725        Call(726            func,727            input,728            retry_policy=retry_policy,729            cache_policy=cache_policy,730            callbacks=callbacks,731            timeout=timeout,732        ),733    ):734        if fut := next(735            (736                f737                for f, t in list(futures().items())  # type: ignore[union-attr]738                if t is not None and t == next_task.id739            ),740            None,741        ):742            # if the parent task was retried,743            # the next task might already be running744            pass745        elif next_task.writes:746            # if it already ran, return the result747            fut = concurrent.futures.Future()748            ret = next((v for c, v in next_task.writes if c == RETURN), MISSING)749            if ret is not MISSING:750                fut.set_result(ret)751            elif exc := next((v for c, v in next_task.writes if c == ERROR), None):752                fut.set_exception(753                    exc if isinstance(exc, BaseException) else Exception(exc)754                )755            else:756                fut.set_result(None)757        else:758            # schedule the next task759            fut = submit()(  # type: ignore[misc]760                run_with_retry,761                next_task,762                retry_policy,763                configurable={764                    CONFIG_KEY_CALL: partial(765                        _call,766                        weakref.ref(next_task),767                        futures=futures,768                        retry_policy=retry_policy,769                        callbacks=callbacks,770                        schedule_task=schedule_task,771                        submit=submit,772                    ),773                },774                __reraise_on_exit__=False,775                # starting a new task in the next tick ensures776                # updates from this tick are committed/streamed first777                __next_tick__=True,778            )779            # exceptions for call() tasks are raised into the parent task780            # so we should not re-raise at the end of the tick781            SKIP_RERAISE_SET.add(fut)782            futures()[fut] = next_task  # type: ignore[index]783    fut = cast(asyncio.Future | concurrent.futures.Future, fut)784    # return a chained future to ensure commit() callback is called785    # before the returned future is resolved, to ensure stream order etc786    return chain_future(fut, concurrent.futures.Future())787 788 789def _acall(790    task: weakref.ref[PregelExecutableTask],791    func: Callable[[Any], Awaitable[Any] | Any],792    input: Any,793    *,794    retry_policy: Sequence[RetryPolicy] | None = None,795    cache_policy: CachePolicy | None = None,796    timeout: TimeoutPolicy | None = None,797    callbacks: Callbacks = None,798    # injected dependencies799    futures: weakref.ref[FuturesDict],800    schedule_task: Callable[801        [PregelExecutableTask, int, Call | None],802        Awaitable[PregelExecutableTask | None],803    ],804    submit: weakref.ref[Submit],805    loop: asyncio.AbstractEventLoop,806    stream: bool = False,807) -> asyncio.Future[Any] | concurrent.futures.Future[Any]:808    # return a chained future to ensure commit() callback is called809    # before the returned future is resolved, to ensure stream order etc810    try:811        in_async = asyncio.current_task() is not None812    except RuntimeError:813        in_async = False814    # if in async context return an async future, otherwise return a sync future815    if in_async:816        fut: asyncio.Future[Any] | concurrent.futures.Future[Any] = asyncio.Future(817            loop=loop818        )819    else:820        fut = concurrent.futures.Future()821    # schedule the next task822    run_coroutine_threadsafe(823        _acall_impl(824            fut,825            task,826            func,827            input,828            retry_policy=retry_policy,829            cache_policy=cache_policy,830            timeout=timeout,831            callbacks=callbacks,832            futures=futures,833            schedule_task=schedule_task,834            submit=submit,835            loop=loop,836            stream=stream,837        ),838        loop,839        lazy=False,840    )841    return fut842 843 844async def _acall_impl(845    destination: asyncio.Future[Any] | concurrent.futures.Future[Any],846    task: weakref.ref[PregelExecutableTask],847    func: Callable[[Any], Awaitable[Any] | Any],848    input: Any,849    *,850    retry_policy: Sequence[RetryPolicy] | None = None,851    cache_policy: CachePolicy | None = None,852    timeout: TimeoutPolicy | None = None,853    callbacks: Callbacks = None,854    # injected dependencies855    futures: weakref.ref[FuturesDict[asyncio.Future, asyncio.Event]],856    schedule_task: Callable[857        [PregelExecutableTask, int, Call | None],858        Awaitable[PregelExecutableTask | None],859    ],860    submit: weakref.ref[Submit],861    loop: asyncio.AbstractEventLoop,862    stream: bool = False,863) -> None:864    try:865        fut: asyncio.Future | None = None866        # schedule PUSH tasks, collect futures867        scratchpad: PregelScratchpad = task().config[CONF][CONFIG_KEY_SCRATCHPAD]  # type: ignore[union-attr]868        # schedule the next task, if the callback returns one869        if next_task := await schedule_task(870            task(),  # type: ignore[arg-type]871            scratchpad.call_counter(),872            Call(873                func,874                input,875                retry_policy=retry_policy,876                cache_policy=cache_policy,877                callbacks=callbacks,878                timeout=timeout,879            ),880        ):881            if fut := next(882                (883                    f884                    for f, t in list(futures().items())  # type: ignore[union-attr]885                    if t is not None and t == next_task.id886                ),887                None,888            ):889                # if the parent task was retried,890                # the next task might already be running891                pass892            elif next_task.writes:893                # if it already ran, return the result894                fut = asyncio.Future(loop=loop)895                ret = next((v for c, v in next_task.writes if c == RETURN), MISSING)896                if ret is not MISSING:897                    fut.set_result(ret)898                elif exc := next((v for c, v in next_task.writes if c == ERROR), None):899                    fut.set_exception(900                        exc if isinstance(exc, BaseException) else Exception(exc)901                    )902                else:903                    fut.set_result(None)904            else:905                # schedule the next task906                fut = cast(907                    asyncio.Future,908                    submit()(  # type: ignore[misc]909                        arun_with_retry,910                        next_task,911                        retry_policy,912                        stream=stream,913                        configurable={914                            CONFIG_KEY_CALL: partial(915                                _acall,916                                weakref.ref(next_task),917                                stream=stream,918                                futures=futures,919                                schedule_task=schedule_task,920                                submit=submit,921                                loop=loop,922                            ),923                        },924                        __name__=next_task.name,925                        __cancel_on_exit__=True,926                        __reraise_on_exit__=False,927                        # starting a new task in the next tick ensures928                        # updates from this tick are committed/streamed first929                        __next_tick__=True,930                    ),931                )932                # exceptions for call() tasks are raised into the parent task933                # so we should not re-raise at the end of the tick934                SKIP_RERAISE_SET.add(fut)935                futures()[fut] = next_task  # type: ignore[index]936        if fut is not None:937            chain_future(fut, destination)938        else:939            destination.set_exception(RuntimeError("Task not scheduled"))940    except Exception as exc:941        destination.set_exception(exc)942 
codekingpro/portable-devtools · Team Ai