44
55from asyncio import (
66 FIRST_COMPLETED ,
7+ Future ,
78 TimeoutError , # only needed for Python < 3.11 # noqa: A004
89 ensure_future ,
910 gather ,
@@ -217,6 +218,7 @@ class Executor(IncrementalPublisherContext):
217218 abort_signal : AbortSignal | None
218219 errors : list [GraphQLError ] | None
219220 cancellable_streams : set [CancellableStreamRecord ] | None
221+ pending_incremental_futures : set [Future [Any ]]
220222 middleware_manager : MiddlewareManager | None
221223 error_propagation : bool
222224
@@ -271,6 +273,7 @@ def __init__( # noqa: PLR0913
271273 self .is_async_iterable = is_async_iterable or default_is_async_iterable
272274 self .errors = None
273275 self .cancellable_streams = None
276+ self .pending_incremental_futures = set ()
274277 self ._relevant_sub_fields : dict [tuple , CollectedFields ] = {}
275278 self ._stream_usages : RefMap [FieldDetailsList , StreamUsage ] = RefMap ()
276279 self ._execution_plans : RefMap [GroupedFieldSet , ExecutionPlan ] = RefMap ()
@@ -469,6 +472,11 @@ async def await_result() -> (
469472 resolved = await self .with_abort_signal (graphql_wrapped_result )
470473 except GraphQLError as error :
471474 return ExecutionResult (None , with_error (self .errors , error ))
475+ except Exception :
476+ # cancel incremental work started early and close the
477+ # stream sources before re-raising, e.g. the abort reason
478+ await self .cancel_incremental_work ()
479+ raise
472480 return self .build_data_response (
473481 resolved .result , resolved .increments
474482 )
@@ -975,6 +983,50 @@ def abort_error(self) -> Exception:
975983 msg = f"Unexpected error value: { inspect (reason )} "
976984 return TypeError (msg )
977985
986+ def box_incremental_result (
987+ self , result : AwaitableOrValue [T ]
988+ ) -> BoxedAwaitableOrValue [T ]:
989+ """Box a possibly awaitable incremental result.
990+
991+ A pending result is registered so that it can be cancelled when the
992+ incremental execution is stopped before it has settled.
993+ """
994+ boxed = BoxedAwaitableOrValue (result )
995+ future = boxed .pending_future
996+ if future is not None :
997+ futures = self .pending_incremental_futures
998+ futures .add (future )
999+ future .add_done_callback (futures .discard )
1000+ return boxed
1001+
1002+ async def cancel_incremental_work (self ) -> None :
1003+ """Cancel all pending incremental work and close the stream sources.
1004+
1005+ Cancels the still pending incremental execution tasks first and waits for
1006+ their cancellation to settle, so that no early execution continues and no
1007+ iteration is pending on the stream sources any more, then triggers and
1008+ awaits the early return of all remaining cancellable streams.
1009+ """
1010+ futures = self .pending_incremental_futures
1011+ if futures :
1012+ pending = list (futures )
1013+ for future in pending :
1014+ future .cancel ()
1015+ await gather (* pending , return_exceptions = True )
1016+ cancellable_streams = self .cancellable_streams
1017+ if cancellable_streams :
1018+ early_returns = [
1019+ early_return
1020+ for early_return in (
1021+ stream_record .early_return ()
1022+ for stream_record in cancellable_streams
1023+ )
1024+ if default_is_awaitable (early_return )
1025+ ]
1026+ cancellable_streams .clear ()
1027+ if early_returns :
1028+ await gather (* early_returns , return_exceptions = True )
1029+
9781030 def cancellable_iterable (self , iterable : AsyncIterable [T ]) -> AsyncIterable [T ]:
9791031 """Wrap an async iterable so pending iteration is cancelled on abort.
9801032
@@ -1135,7 +1187,7 @@ async def complete_async_iterator_value(
11351187 )
11361188 else :
11371189 stream_record = CancellableStreamRecord (
1138- early_return () ,
1190+ early_return ,
11391191 stream_item_queue ,
11401192 path ,
11411193 stream_usage .label ,
@@ -1840,17 +1892,17 @@ async def execute_async(
18401892 return await result
18411893 return result # type: ignore
18421894
1843- pending_group .result = BoxedAwaitableOrValue (execute_async ())
1895+ pending_group .result = self . box_incremental_result (execute_async ())
18441896 else :
1845- pending_group .result = BoxedAwaitableOrValue (executor ())
1897+ pending_group .result = self . box_incremental_result (executor ())
18461898 else :
18471899
18481900 def execute_sync (
18491901 executor : Callable [
18501902 [], AwaitableOrValue [CompletedExecutionGroup ]
18511903 ] = executor ,
18521904 ) -> BoxedAwaitableOrValue [CompletedExecutionGroup ]:
1853- return BoxedAwaitableOrValue (executor ())
1905+ return self . box_incremental_result (executor ())
18541906
18551907 pending_group .result = execute_sync
18561908
@@ -1934,7 +1986,7 @@ def first_executor() -> StreamItemResult:
19341986 initial_path = stream_path .add_key (initial_index )
19351987
19361988 first_stream_item : BoxedAwaitableOrValue [StreamItemResult ] = (
1937- BoxedAwaitableOrValue (
1989+ self . box_incremental_result (
19381990 complete_stream_item (
19391991 initial_path ,
19401992 initial_item ,
@@ -1972,9 +2024,9 @@ def current_executor(
19722024 )
19732025
19742026 current_stream_item = (
1975- BoxedAwaitableOrValue (current_executor ())
2027+ self . box_incremental_result (current_executor ())
19762028 if enable_early_execution
1977- else lambda executor = current_executor : BoxedAwaitableOrValue (
2029+ else lambda executor = current_executor : self . box_incremental_result (
19782030 executor ()
19792031 )
19802032 )
@@ -1992,9 +2044,9 @@ def current_executor(
19922044 async def await_first_stream_item () -> StreamItemResult :
19932045 return first_executor ()
19942046
1995- append_stream_item (BoxedAwaitableOrValue (await_first_stream_item ()))
2047+ append_stream_item (self . box_incremental_result (await_first_stream_item ()))
19962048 else :
1997- append_stream_item (lambda : BoxedAwaitableOrValue (first_executor ()))
2049+ append_stream_item (lambda : self . box_incremental_result (first_executor ()))
19982050
19992051 return stream_item_queue
20002052
@@ -2022,9 +2074,9 @@ def executor() -> AwaitableOrValue[StreamItemResult]:
20222074
20232075 stream_item_queue : list [StreamItemRecord ] = []
20242076 stream_item_queue .append (
2025- BoxedAwaitableOrValue (executor ())
2077+ self . box_incremental_result (executor ())
20262078 if self .enable_early_execution
2027- else lambda : BoxedAwaitableOrValue (executor ())
2079+ else lambda : self . box_incremental_result (executor ())
20282080 )
20292081
20302082 return stream_item_queue
@@ -2076,9 +2128,9 @@ def executor() -> AwaitableOrValue[StreamItemResult]:
20762128 )
20772129
20782130 stream_item_queue .append (
2079- BoxedAwaitableOrValue (executor ())
2131+ self . box_incremental_result (executor ())
20802132 if self .enable_early_execution
2081- else lambda : BoxedAwaitableOrValue (executor ())
2133+ else lambda : self . box_incremental_result (executor ())
20822134 )
20832135
20842136 if self .is_awaitable (result ):
0 commit comments