prefect-client 3.1.3__py3-none-any.whl → 3.1.5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- prefect/_version.py +3 -3
- prefect/client/cloud.py +1 -1
- prefect/client/orchestration.py +32 -12
- prefect/client/schemas/actions.py +17 -12
- prefect/client/schemas/filters.py +5 -0
- prefect/client/schemas/objects.py +5 -31
- prefect/client/schemas/responses.py +3 -0
- prefect/concurrency/services.py +3 -0
- prefect/deployments/runner.py +2 -1
- prefect/flow_engine.py +707 -130
- prefect/settings/sources.py +9 -2
- prefect/types/__init__.py +40 -3
- prefect/workers/base.py +3 -2
- {prefect_client-3.1.3.dist-info → prefect_client-3.1.5.dist-info}/METADATA +2 -4
- {prefect_client-3.1.3.dist-info → prefect_client-3.1.5.dist-info}/RECORD +18 -18
- {prefect_client-3.1.3.dist-info → prefect_client-3.1.5.dist-info}/WHEEL +1 -1
- {prefect_client-3.1.3.dist-info → prefect_client-3.1.5.dist-info}/LICENSE +0 -0
- {prefect_client-3.1.3.dist-info → prefect_client-3.1.5.dist-info}/top_level.txt +0 -0
prefect/flow_engine.py
CHANGED
@@ -2,7 +2,7 @@ import asyncio
|
|
2
2
|
import logging
|
3
3
|
import os
|
4
4
|
import time
|
5
|
-
from contextlib import ExitStack, contextmanager
|
5
|
+
from contextlib import ExitStack, asynccontextmanager, contextmanager
|
6
6
|
from dataclasses import dataclass, field
|
7
7
|
from typing import (
|
8
8
|
Any,
|
@@ -22,19 +22,25 @@ from typing import (
|
|
22
22
|
)
|
23
23
|
from uuid import UUID
|
24
24
|
|
25
|
+
from anyio import CancelScope
|
25
26
|
from opentelemetry import trace
|
26
27
|
from opentelemetry.trace import Tracer, get_tracer
|
27
28
|
from typing_extensions import ParamSpec
|
28
29
|
|
29
30
|
import prefect
|
30
31
|
from prefect import Task
|
31
|
-
from prefect.client.orchestration import SyncPrefectClient, get_client
|
32
|
+
from prefect.client.orchestration import PrefectClient, SyncPrefectClient, get_client
|
32
33
|
from prefect.client.schemas import FlowRun, TaskRun
|
33
34
|
from prefect.client.schemas.filters import FlowRunFilter
|
34
35
|
from prefect.client.schemas.sorting import FlowRunSort
|
35
36
|
from prefect.concurrency.context import ConcurrencyContext
|
36
37
|
from prefect.concurrency.v1.context import ConcurrencyContext as ConcurrencyContextV1
|
37
|
-
from prefect.context import
|
38
|
+
from prefect.context import (
|
39
|
+
AsyncClientContext,
|
40
|
+
FlowRunContext,
|
41
|
+
SyncClientContext,
|
42
|
+
TagsContext,
|
43
|
+
)
|
38
44
|
from prefect.exceptions import (
|
39
45
|
Abort,
|
40
46
|
Pause,
|
@@ -79,6 +85,7 @@ from prefect.utilities.engine import (
|
|
79
85
|
_resolve_custom_flow_run_name,
|
80
86
|
capture_sigterm,
|
81
87
|
link_state_to_result,
|
88
|
+
propose_state,
|
82
89
|
propose_state_sync,
|
83
90
|
resolve_to_final_result,
|
84
91
|
)
|
@@ -112,7 +119,7 @@ def load_flow_and_flow_run(flow_run_id: UUID) -> Tuple[FlowRun, Flow]:
|
|
112
119
|
|
113
120
|
|
114
121
|
@dataclass
|
115
|
-
class
|
122
|
+
class BaseFlowRunEngine(Generic[P, R]):
|
116
123
|
flow: Union[Flow[P, R], Flow[P, Coroutine[Any, Any, R]]]
|
117
124
|
parameters: Optional[Dict[str, Any]] = None
|
118
125
|
flow_run: Optional[FlowRun] = None
|
@@ -124,7 +131,6 @@ class FlowRunEngine(Generic[P, R]):
|
|
124
131
|
# holds the exception raised by the user code, if any
|
125
132
|
_raised: Union[Exception, Type[NotSet]] = NotSet
|
126
133
|
_is_started: bool = False
|
127
|
-
_client: Optional[SyncPrefectClient] = None
|
128
134
|
short_circuit: bool = False
|
129
135
|
_flow_run_name_set: bool = False
|
130
136
|
_tracer: Tracer = field(
|
@@ -139,16 +145,50 @@ class FlowRunEngine(Generic[P, R]):
|
|
139
145
|
if self.parameters is None:
|
140
146
|
self.parameters = {}
|
141
147
|
|
148
|
+
@property
|
149
|
+
def state(self) -> State:
|
150
|
+
return self.flow_run.state # type: ignore
|
151
|
+
|
152
|
+
def _end_span_on_success(self):
|
153
|
+
if not self._span:
|
154
|
+
return
|
155
|
+
self._span.set_status(trace.Status(trace.StatusCode.OK))
|
156
|
+
self._span.end(time.time_ns())
|
157
|
+
self._span = None
|
158
|
+
|
159
|
+
def _end_span_on_error(self, exc: BaseException, description: Optional[str]):
|
160
|
+
if not self._span:
|
161
|
+
return
|
162
|
+
self._span.record_exception(exc)
|
163
|
+
self._span.set_status(trace.Status(trace.StatusCode.ERROR, description))
|
164
|
+
self._span.end(time.time_ns())
|
165
|
+
self._span = None
|
166
|
+
|
167
|
+
def is_running(self) -> bool:
|
168
|
+
if getattr(self, "flow_run", None) is None:
|
169
|
+
return False
|
170
|
+
return getattr(self, "flow_run").state.is_running()
|
171
|
+
|
172
|
+
def is_pending(self) -> bool:
|
173
|
+
if getattr(self, "flow_run", None) is None:
|
174
|
+
return False # TODO: handle this differently?
|
175
|
+
return getattr(self, "flow_run").state.is_pending()
|
176
|
+
|
177
|
+
def cancel_all_tasks(self):
|
178
|
+
if hasattr(self.flow.task_runner, "cancel_all"):
|
179
|
+
self.flow.task_runner.cancel_all() # type: ignore
|
180
|
+
|
181
|
+
|
182
|
+
@dataclass
|
183
|
+
class FlowRunEngine(BaseFlowRunEngine[P, R]):
|
184
|
+
_client: Optional[SyncPrefectClient] = None
|
185
|
+
|
142
186
|
@property
|
143
187
|
def client(self) -> SyncPrefectClient:
|
144
188
|
if not self._is_started or self._client is None:
|
145
189
|
raise RuntimeError("Engine has not started.")
|
146
190
|
return self._client
|
147
191
|
|
148
|
-
@property
|
149
|
-
def state(self) -> State:
|
150
|
-
return self.flow_run.state # type: ignore
|
151
|
-
|
152
192
|
def _resolve_parameters(self):
|
153
193
|
if not self.parameters:
|
154
194
|
return {}
|
@@ -364,21 +404,6 @@ class FlowRunEngine(Generic[P, R]):
|
|
364
404
|
|
365
405
|
self._end_span_on_error(exc, state.message)
|
366
406
|
|
367
|
-
def _end_span_on_success(self):
|
368
|
-
if not self._span:
|
369
|
-
return
|
370
|
-
self._span.set_status(trace.Status(trace.StatusCode.OK))
|
371
|
-
self._span.end(time.time_ns())
|
372
|
-
self._span = None
|
373
|
-
|
374
|
-
def _end_span_on_error(self, exc: BaseException, description: Optional[str]):
|
375
|
-
if not self._span:
|
376
|
-
return
|
377
|
-
self._span.record_exception(exc)
|
378
|
-
self._span.set_status(trace.Status(trace.StatusCode.ERROR, description))
|
379
|
-
self._span.end(time.time_ns())
|
380
|
-
self._span = None
|
381
|
-
|
382
407
|
def load_subflow_run(
|
383
408
|
self,
|
384
409
|
parent_task_run: TaskRun,
|
@@ -665,20 +690,6 @@ class FlowRunEngine(Generic[P, R]):
|
|
665
690
|
self._is_started = False
|
666
691
|
self._client = None
|
667
692
|
|
668
|
-
def is_running(self) -> bool:
|
669
|
-
if getattr(self, "flow_run", None) is None:
|
670
|
-
return False
|
671
|
-
return getattr(self, "flow_run").state.is_running()
|
672
|
-
|
673
|
-
def is_pending(self) -> bool:
|
674
|
-
if getattr(self, "flow_run", None) is None:
|
675
|
-
return False # TODO: handle this differently?
|
676
|
-
return getattr(self, "flow_run").state.is_pending()
|
677
|
-
|
678
|
-
def cancel_all_tasks(self):
|
679
|
-
if hasattr(self.flow.task_runner, "cancel_all"):
|
680
|
-
self.flow.task_runner.cancel_all() # type: ignore
|
681
|
-
|
682
693
|
# --------------------------
|
683
694
|
#
|
684
695
|
# The following methods compose the main task run loop
|
@@ -734,119 +745,685 @@ class FlowRunEngine(Generic[P, R]):
|
|
734
745
|
self.handle_success(result)
|
735
746
|
|
736
747
|
|
737
|
-
|
738
|
-
|
739
|
-
|
740
|
-
|
741
|
-
wait_for: Optional[Iterable[PrefectFuture]] = None,
|
742
|
-
return_type: Literal["state", "result"] = "result",
|
743
|
-
) -> Union[R, State, None]:
|
744
|
-
engine = FlowRunEngine[P, R](
|
745
|
-
flow=flow,
|
746
|
-
parameters=parameters,
|
747
|
-
flow_run=flow_run,
|
748
|
-
wait_for=wait_for,
|
749
|
-
)
|
748
|
+
@dataclass
|
749
|
+
class AsyncFlowRunEngine(BaseFlowRunEngine[P, R]):
|
750
|
+
"""
|
751
|
+
Async version of the flow run engine.
|
750
752
|
|
751
|
-
|
752
|
-
|
753
|
-
|
754
|
-
engine.call_flow_fn()
|
753
|
+
NOTE: This has not been fully asyncified yet which may lead to async flows
|
754
|
+
not being fully asyncified.
|
755
|
+
"""
|
755
756
|
|
756
|
-
|
757
|
+
_client: Optional[PrefectClient] = None
|
757
758
|
|
759
|
+
@property
|
760
|
+
def client(self) -> PrefectClient:
|
761
|
+
if not self._is_started or self._client is None:
|
762
|
+
raise RuntimeError("Engine has not started.")
|
763
|
+
return self._client
|
758
764
|
|
759
|
-
|
760
|
-
|
761
|
-
|
762
|
-
parameters: Optional[Dict[str, Any]] = None,
|
763
|
-
wait_for: Optional[Iterable[PrefectFuture]] = None,
|
764
|
-
return_type: Literal["state", "result"] = "result",
|
765
|
-
) -> Union[R, State, None]:
|
766
|
-
engine = FlowRunEngine[P, R](
|
767
|
-
flow=flow, parameters=parameters, flow_run=flow_run, wait_for=wait_for
|
768
|
-
)
|
765
|
+
def _resolve_parameters(self):
|
766
|
+
if not self.parameters:
|
767
|
+
return {}
|
769
768
|
|
770
|
-
|
771
|
-
|
772
|
-
|
773
|
-
|
769
|
+
resolved_parameters = {}
|
770
|
+
for parameter, value in self.parameters.items():
|
771
|
+
try:
|
772
|
+
resolved_parameters[parameter] = visit_collection(
|
773
|
+
value,
|
774
|
+
visit_fn=resolve_to_final_result,
|
775
|
+
return_data=True,
|
776
|
+
max_depth=-1,
|
777
|
+
remove_annotations=True,
|
778
|
+
context={},
|
779
|
+
)
|
780
|
+
except UpstreamTaskError:
|
781
|
+
raise
|
782
|
+
except Exception as exc:
|
783
|
+
raise PrefectException(
|
784
|
+
f"Failed to resolve inputs in parameter {parameter!r}. If your"
|
785
|
+
" parameter type is not supported, consider using the `quote`"
|
786
|
+
" annotation to skip resolution of inputs."
|
787
|
+
) from exc
|
774
788
|
|
775
|
-
|
789
|
+
self.parameters = resolved_parameters
|
776
790
|
|
791
|
+
def _wait_for_dependencies(self):
|
792
|
+
if not self.wait_for:
|
793
|
+
return
|
777
794
|
|
778
|
-
|
779
|
-
|
780
|
-
|
781
|
-
|
782
|
-
|
783
|
-
|
784
|
-
|
785
|
-
|
786
|
-
raise ValueError("The return_type for a generator flow must be 'result'")
|
795
|
+
visit_collection(
|
796
|
+
self.wait_for,
|
797
|
+
visit_fn=resolve_to_final_result,
|
798
|
+
return_data=False,
|
799
|
+
max_depth=-1,
|
800
|
+
remove_annotations=True,
|
801
|
+
context={},
|
802
|
+
)
|
787
803
|
|
788
|
-
|
789
|
-
|
790
|
-
|
804
|
+
async def begin_run(self) -> State:
|
805
|
+
try:
|
806
|
+
self._resolve_parameters()
|
807
|
+
self._wait_for_dependencies()
|
808
|
+
except UpstreamTaskError as upstream_exc:
|
809
|
+
state = await self.set_state(
|
810
|
+
Pending(
|
811
|
+
name="NotReady",
|
812
|
+
message=str(upstream_exc),
|
813
|
+
),
|
814
|
+
# if orchestrating a run already in a pending state, force orchestration to
|
815
|
+
# update the state name
|
816
|
+
force=self.state.is_pending(),
|
817
|
+
)
|
818
|
+
return state
|
791
819
|
|
792
|
-
|
793
|
-
|
794
|
-
|
795
|
-
|
796
|
-
|
820
|
+
# validate prior to context so that context receives validated params
|
821
|
+
if self.flow.should_validate_parameters:
|
822
|
+
try:
|
823
|
+
self.parameters = self.flow.validate_parameters(self.parameters or {})
|
824
|
+
except Exception as exc:
|
825
|
+
message = "Validation of flow parameters failed with error:"
|
826
|
+
self.logger.error("%s %s", message, exc)
|
827
|
+
await self.handle_exception(
|
828
|
+
exc,
|
829
|
+
msg=message,
|
830
|
+
result_store=get_result_store().update_for_flow(
|
831
|
+
self.flow, _sync=True
|
832
|
+
),
|
797
833
|
)
|
798
|
-
|
799
|
-
|
800
|
-
while True:
|
801
|
-
gen_result = next(gen)
|
802
|
-
# link the current state to the result for dependency tracking
|
803
|
-
link_state_to_result(engine.state, gen_result)
|
804
|
-
yield gen_result
|
805
|
-
except StopIteration as exc:
|
806
|
-
engine.handle_success(exc.value)
|
807
|
-
except GeneratorExit as exc:
|
808
|
-
engine.handle_success(None)
|
809
|
-
gen.throw(exc)
|
834
|
+
self.short_circuit = True
|
835
|
+
await self.call_hooks()
|
810
836
|
|
811
|
-
|
837
|
+
new_state = Running()
|
838
|
+
state = await self.set_state(new_state)
|
839
|
+
while state.is_pending():
|
840
|
+
await asyncio.sleep(0.2)
|
841
|
+
state = await self.set_state(new_state)
|
842
|
+
return state
|
812
843
|
|
844
|
+
async def set_state(self, state: State, force: bool = False) -> State:
|
845
|
+
""" """
|
846
|
+
# prevents any state-setting activity
|
847
|
+
if self.short_circuit:
|
848
|
+
return self.state
|
813
849
|
|
814
|
-
|
815
|
-
|
816
|
-
|
817
|
-
|
818
|
-
|
819
|
-
|
820
|
-
) -> AsyncGenerator[R, None]:
|
821
|
-
if return_type != "result":
|
822
|
-
raise ValueError("The return_type for a generator flow must be 'result'")
|
850
|
+
state = await propose_state(
|
851
|
+
self.client, state, flow_run_id=self.flow_run.id, force=force
|
852
|
+
) # type: ignore
|
853
|
+
self.flow_run.state = state # type: ignore
|
854
|
+
self.flow_run.state_name = state.name # type: ignore
|
855
|
+
self.flow_run.state_type = state.type # type: ignore
|
823
856
|
|
824
|
-
|
825
|
-
|
826
|
-
|
857
|
+
if self._span:
|
858
|
+
self._span.add_event(
|
859
|
+
state.name,
|
860
|
+
{
|
861
|
+
"prefect.state.message": state.message or "",
|
862
|
+
"prefect.state.type": state.type,
|
863
|
+
"prefect.state.name": state.name or state.type,
|
864
|
+
"prefect.state.id": str(state.id),
|
865
|
+
},
|
866
|
+
)
|
867
|
+
return state
|
827
868
|
|
828
|
-
|
829
|
-
|
830
|
-
|
831
|
-
|
832
|
-
|
833
|
-
)
|
834
|
-
|
835
|
-
|
836
|
-
|
837
|
-
|
838
|
-
|
839
|
-
|
840
|
-
|
841
|
-
|
842
|
-
|
843
|
-
|
869
|
+
async def result(self, raise_on_failure: bool = True) -> "Union[R, State, None]":
|
870
|
+
if self._return_value is not NotSet and not isinstance(
|
871
|
+
self._return_value, State
|
872
|
+
):
|
873
|
+
if isinstance(self._return_value, BaseResult):
|
874
|
+
_result = self._return_value.get()
|
875
|
+
else:
|
876
|
+
_result = self._return_value
|
877
|
+
|
878
|
+
if asyncio.iscoroutine(_result):
|
879
|
+
# getting the value for a BaseResult may return an awaitable
|
880
|
+
# depending on whether the parent frame is sync or not
|
881
|
+
_result = await _result
|
882
|
+
return _result
|
883
|
+
|
884
|
+
if self._raised is not NotSet:
|
885
|
+
if raise_on_failure:
|
886
|
+
raise self._raised
|
887
|
+
return self._raised
|
888
|
+
|
889
|
+
# This is a fall through case which leans on the existing state result mechanics to get the
|
890
|
+
# return value. This is necessary because we currently will return a State object if the
|
891
|
+
# the State was Prefect-created.
|
892
|
+
# TODO: Remove the need to get the result from a State except in cases where the return value
|
893
|
+
# is a State object.
|
894
|
+
_result = self.state.result(raise_on_failure=raise_on_failure, fetch=True) # type: ignore
|
895
|
+
# state.result is a `sync_compatible` function that may or may not return an awaitable
|
896
|
+
# depending on whether the parent frame is sync or not
|
897
|
+
if asyncio.iscoroutine(_result):
|
898
|
+
_result = await _result
|
899
|
+
return _result
|
900
|
+
|
901
|
+
async def handle_success(self, result: R) -> R:
|
902
|
+
result_store = getattr(FlowRunContext.get(), "result_store", None)
|
903
|
+
if result_store is None:
|
904
|
+
raise ValueError("Result store is not set")
|
905
|
+
resolved_result = resolve_futures_to_states(result)
|
906
|
+
terminal_state = await return_value_to_state(
|
907
|
+
resolved_result,
|
908
|
+
result_store=result_store,
|
909
|
+
write_result=should_persist_result(),
|
910
|
+
)
|
911
|
+
await self.set_state(terminal_state)
|
912
|
+
self._return_value = resolved_result
|
913
|
+
|
914
|
+
self._end_span_on_success()
|
915
|
+
|
916
|
+
return result
|
917
|
+
|
918
|
+
async def handle_exception(
|
919
|
+
self,
|
920
|
+
exc: Exception,
|
921
|
+
msg: Optional[str] = None,
|
922
|
+
result_store: Optional[ResultStore] = None,
|
923
|
+
) -> State:
|
924
|
+
context = FlowRunContext.get()
|
925
|
+
terminal_state = cast(
|
926
|
+
State,
|
927
|
+
await exception_to_failed_state(
|
928
|
+
exc,
|
929
|
+
message=msg or "Flow run encountered an exception:",
|
930
|
+
result_store=result_store or getattr(context, "result_store", None),
|
931
|
+
write_result=True,
|
932
|
+
),
|
933
|
+
)
|
934
|
+
state = await self.set_state(terminal_state)
|
935
|
+
if self.state.is_scheduled():
|
936
|
+
self.logger.info(
|
937
|
+
(
|
938
|
+
f"Received non-final state {state.name!r} when proposing final"
|
939
|
+
f" state {terminal_state.name!r} and will attempt to run again..."
|
940
|
+
),
|
941
|
+
)
|
942
|
+
state = await self.set_state(Running())
|
943
|
+
self._raised = exc
|
944
|
+
|
945
|
+
self._end_span_on_error(exc, state.message)
|
946
|
+
|
947
|
+
return state
|
948
|
+
|
949
|
+
async def handle_timeout(self, exc: TimeoutError) -> None:
|
950
|
+
if isinstance(exc, FlowRunTimeoutError):
|
951
|
+
message = (
|
952
|
+
f"Flow run exceeded timeout of {self.flow.timeout_seconds} second(s)"
|
953
|
+
)
|
954
|
+
else:
|
955
|
+
message = f"Flow run failed due to timeout: {exc!r}"
|
956
|
+
self.logger.error(message)
|
957
|
+
state = Failed(
|
958
|
+
data=exc,
|
959
|
+
message=message,
|
960
|
+
name="TimedOut",
|
961
|
+
)
|
962
|
+
await self.set_state(state)
|
963
|
+
self._raised = exc
|
964
|
+
|
965
|
+
self._end_span_on_error(exc, message)
|
966
|
+
|
967
|
+
async def handle_crash(self, exc: BaseException) -> None:
|
968
|
+
# need to shield from asyncio cancellation to ensure we update the state
|
969
|
+
# on the server before exiting
|
970
|
+
with CancelScope(shield=True):
|
971
|
+
state = await exception_to_crashed_state(exc)
|
972
|
+
self.logger.error(f"Crash detected! {state.message}")
|
973
|
+
self.logger.debug("Crash details:", exc_info=exc)
|
974
|
+
await self.set_state(state, force=True)
|
975
|
+
self._raised = exc
|
976
|
+
|
977
|
+
self._end_span_on_error(exc, state.message)
|
978
|
+
|
979
|
+
async def load_subflow_run(
|
980
|
+
self,
|
981
|
+
parent_task_run: TaskRun,
|
982
|
+
client: PrefectClient,
|
983
|
+
context: FlowRunContext,
|
984
|
+
) -> Union[FlowRun, None]:
|
985
|
+
"""
|
986
|
+
This method attempts to load an existing flow run for a subflow task
|
987
|
+
run, if appropriate.
|
988
|
+
|
989
|
+
If the parent task run is in a final but not COMPLETED state, and not
|
990
|
+
being rerun, then we attempt to load an existing flow run instead of
|
991
|
+
creating a new one. This will prevent the engine from running the
|
992
|
+
subflow again.
|
993
|
+
|
994
|
+
If no existing flow run is found, or if the subflow should be rerun,
|
995
|
+
then no flow run is returned.
|
996
|
+
"""
|
997
|
+
|
998
|
+
# check if the parent flow run is rerunning
|
999
|
+
rerunning = (
|
1000
|
+
context.flow_run.run_count > 1
|
1001
|
+
if getattr(context, "flow_run", None)
|
1002
|
+
and isinstance(context.flow_run, FlowRun)
|
1003
|
+
else False
|
1004
|
+
)
|
1005
|
+
|
1006
|
+
# if the parent task run is in a final but not completed state, and
|
1007
|
+
# not rerunning, then retrieve the most recent flow run instead of
|
1008
|
+
# creating a new one. This effectively loads a cached flow run for
|
1009
|
+
# situations where we are confident the flow should not be run
|
1010
|
+
# again.
|
1011
|
+
assert isinstance(parent_task_run.state, State)
|
1012
|
+
if parent_task_run.state.is_final() and not (
|
1013
|
+
rerunning and not parent_task_run.state.is_completed()
|
1014
|
+
):
|
1015
|
+
# return the most recent flow run, if it exists
|
1016
|
+
flow_runs = await client.read_flow_runs(
|
1017
|
+
flow_run_filter=FlowRunFilter(
|
1018
|
+
parent_task_run_id={"any_": [parent_task_run.id]}
|
1019
|
+
),
|
1020
|
+
sort=FlowRunSort.EXPECTED_START_TIME_ASC,
|
1021
|
+
limit=1,
|
1022
|
+
)
|
1023
|
+
if flow_runs:
|
1024
|
+
loaded_flow_run = flow_runs[-1]
|
1025
|
+
self._return_value = loaded_flow_run.state
|
1026
|
+
return loaded_flow_run
|
1027
|
+
|
1028
|
+
async def create_flow_run(self, client: PrefectClient) -> FlowRun:
|
1029
|
+
flow_run_ctx = FlowRunContext.get()
|
1030
|
+
parameters = self.parameters or {}
|
1031
|
+
|
1032
|
+
parent_task_run = None
|
1033
|
+
|
1034
|
+
# this is a subflow run
|
1035
|
+
if flow_run_ctx:
|
1036
|
+
# add a task to a parent flow run that represents the execution of a subflow run
|
1037
|
+
parent_task = Task(
|
1038
|
+
name=self.flow.name, fn=self.flow.fn, version=self.flow.version
|
1039
|
+
)
|
1040
|
+
|
1041
|
+
parent_task_run = await parent_task.create_run(
|
1042
|
+
flow_run_context=flow_run_ctx,
|
1043
|
+
parameters=self.parameters,
|
1044
|
+
wait_for=self.wait_for,
|
1045
|
+
)
|
1046
|
+
|
1047
|
+
# check if there is already a flow run for this subflow
|
1048
|
+
if subflow_run := await self.load_subflow_run(
|
1049
|
+
parent_task_run=parent_task_run, client=client, context=flow_run_ctx
|
1050
|
+
):
|
1051
|
+
return subflow_run
|
1052
|
+
|
1053
|
+
flow_run = await client.create_flow_run(
|
1054
|
+
flow=self.flow,
|
1055
|
+
parameters=self.flow.serialize_parameters(parameters),
|
1056
|
+
state=Pending(),
|
1057
|
+
parent_task_run_id=getattr(parent_task_run, "id", None),
|
1058
|
+
tags=TagsContext.get().current_tags,
|
1059
|
+
)
|
1060
|
+
if flow_run_ctx:
|
1061
|
+
parent_logger = get_run_logger(flow_run_ctx)
|
1062
|
+
parent_logger.info(
|
1063
|
+
f"Created subflow run {flow_run.name!r} for flow {self.flow.name!r}"
|
1064
|
+
)
|
1065
|
+
else:
|
1066
|
+
self.logger.info(
|
1067
|
+
f"Created flow run {flow_run.name!r} for flow {self.flow.name!r}"
|
1068
|
+
)
|
1069
|
+
|
1070
|
+
return flow_run
|
1071
|
+
|
1072
|
+
async def call_hooks(self, state: Optional[State] = None):
|
1073
|
+
if state is None:
|
1074
|
+
state = self.state
|
1075
|
+
flow = self.flow
|
1076
|
+
flow_run = self.flow_run
|
1077
|
+
|
1078
|
+
if not flow_run:
|
1079
|
+
raise ValueError("Flow run is not set")
|
1080
|
+
|
1081
|
+
enable_cancellation_and_crashed_hooks = (
|
1082
|
+
os.environ.get(
|
1083
|
+
"PREFECT__ENABLE_CANCELLATION_AND_CRASHED_HOOKS", "true"
|
1084
|
+
).lower()
|
1085
|
+
== "true"
|
1086
|
+
)
|
1087
|
+
|
1088
|
+
if state.is_failed() and flow.on_failure_hooks:
|
1089
|
+
hooks = flow.on_failure_hooks
|
1090
|
+
elif state.is_completed() and flow.on_completion_hooks:
|
1091
|
+
hooks = flow.on_completion_hooks
|
1092
|
+
elif (
|
1093
|
+
enable_cancellation_and_crashed_hooks
|
1094
|
+
and state.is_cancelling()
|
1095
|
+
and flow.on_cancellation_hooks
|
1096
|
+
):
|
1097
|
+
hooks = flow.on_cancellation_hooks
|
1098
|
+
elif (
|
1099
|
+
enable_cancellation_and_crashed_hooks
|
1100
|
+
and state.is_crashed()
|
1101
|
+
and flow.on_crashed_hooks
|
1102
|
+
):
|
1103
|
+
hooks = flow.on_crashed_hooks
|
1104
|
+
elif state.is_running() and flow.on_running_hooks:
|
1105
|
+
hooks = flow.on_running_hooks
|
1106
|
+
else:
|
1107
|
+
hooks = None
|
1108
|
+
|
1109
|
+
for hook in hooks or []:
|
1110
|
+
hook_name = _get_hook_name(hook)
|
1111
|
+
|
1112
|
+
try:
|
1113
|
+
self.logger.info(
|
1114
|
+
f"Running hook {hook_name!r} in response to entering state"
|
1115
|
+
f" {state.name!r}"
|
1116
|
+
)
|
1117
|
+
result = hook(flow, flow_run, state)
|
1118
|
+
if asyncio.iscoroutine(result):
|
1119
|
+
await result
|
1120
|
+
except Exception:
|
1121
|
+
self.logger.error(
|
1122
|
+
f"An error was encountered while running hook {hook_name!r}",
|
1123
|
+
exc_info=True,
|
1124
|
+
)
|
1125
|
+
else:
|
1126
|
+
self.logger.info(f"Hook {hook_name!r} finished running successfully")
|
1127
|
+
|
1128
|
+
@asynccontextmanager
|
1129
|
+
async def setup_run_context(self, client: Optional[PrefectClient] = None):
|
1130
|
+
from prefect.utilities.engine import (
|
1131
|
+
should_log_prints,
|
1132
|
+
)
|
1133
|
+
|
1134
|
+
if client is None:
|
1135
|
+
client = self.client
|
1136
|
+
if not self.flow_run:
|
1137
|
+
raise ValueError("Flow run not set")
|
1138
|
+
|
1139
|
+
self.flow_run = await client.read_flow_run(self.flow_run.id)
|
1140
|
+
log_prints = should_log_prints(self.flow)
|
1141
|
+
|
1142
|
+
with ExitStack() as stack:
|
1143
|
+
# TODO: Explore closing task runner before completing the flow to
|
1144
|
+
# wait for futures to complete
|
1145
|
+
stack.enter_context(capture_sigterm())
|
1146
|
+
if log_prints:
|
1147
|
+
stack.enter_context(patch_print())
|
1148
|
+
task_runner = stack.enter_context(self.flow.task_runner.duplicate())
|
1149
|
+
stack.enter_context(
|
1150
|
+
FlowRunContext(
|
1151
|
+
flow=self.flow,
|
1152
|
+
log_prints=log_prints,
|
1153
|
+
flow_run=self.flow_run,
|
1154
|
+
parameters=self.parameters,
|
1155
|
+
client=client,
|
1156
|
+
result_store=get_result_store().update_for_flow(
|
1157
|
+
self.flow, _sync=True
|
1158
|
+
),
|
1159
|
+
task_runner=task_runner,
|
1160
|
+
persist_result=self.flow.persist_result
|
1161
|
+
if self.flow.persist_result is not None
|
1162
|
+
else should_persist_result(),
|
1163
|
+
)
|
1164
|
+
)
|
1165
|
+
stack.enter_context(ConcurrencyContextV1())
|
1166
|
+
stack.enter_context(ConcurrencyContext())
|
1167
|
+
|
1168
|
+
# set the logger to the flow run logger
|
1169
|
+
self.logger = flow_run_logger(flow_run=self.flow_run, flow=self.flow)
|
1170
|
+
|
1171
|
+
# update the flow run name if necessary
|
1172
|
+
if not self._flow_run_name_set and self.flow.flow_run_name:
|
1173
|
+
flow_run_name = _resolve_custom_flow_run_name(
|
1174
|
+
flow=self.flow, parameters=self.parameters
|
1175
|
+
)
|
1176
|
+
await self.client.set_flow_run_name(
|
1177
|
+
flow_run_id=self.flow_run.id, name=flow_run_name
|
1178
|
+
)
|
1179
|
+
self.logger.extra["flow_run_name"] = flow_run_name
|
1180
|
+
self.logger.debug(
|
1181
|
+
f"Renamed flow run {self.flow_run.name!r} to {flow_run_name!r}"
|
1182
|
+
)
|
1183
|
+
self.flow_run.name = flow_run_name
|
1184
|
+
self._flow_run_name_set = True
|
1185
|
+
yield
|
1186
|
+
|
1187
|
+
@asynccontextmanager
|
1188
|
+
async def initialize_run(self):
|
1189
|
+
"""
|
1190
|
+
Enters a client context and creates a flow run if needed.
|
1191
|
+
"""
|
1192
|
+
async with AsyncClientContext.get_or_create() as client_ctx:
|
1193
|
+
self._client = client_ctx.client
|
1194
|
+
self._is_started = True
|
1195
|
+
|
1196
|
+
if not self.flow_run:
|
1197
|
+
self.flow_run = await self.create_flow_run(self.client)
|
1198
|
+
flow_run_url = url_for(self.flow_run)
|
1199
|
+
|
1200
|
+
if flow_run_url:
|
1201
|
+
self.logger.info(
|
1202
|
+
f"View at {flow_run_url}", extra={"send_to_api": False}
|
1203
|
+
)
|
1204
|
+
else:
|
1205
|
+
# Update the empirical policy to match the flow if it is not set
|
1206
|
+
if self.flow_run.empirical_policy.retry_delay is None:
|
1207
|
+
self.flow_run.empirical_policy.retry_delay = (
|
1208
|
+
self.flow.retry_delay_seconds
|
1209
|
+
)
|
1210
|
+
|
1211
|
+
if self.flow_run.empirical_policy.retries is None:
|
1212
|
+
self.flow_run.empirical_policy.retries = self.flow.retries
|
1213
|
+
|
1214
|
+
await self.client.update_flow_run(
|
1215
|
+
flow_run_id=self.flow_run.id,
|
1216
|
+
flow_version=self.flow.version,
|
1217
|
+
empirical_policy=self.flow_run.empirical_policy,
|
1218
|
+
)
|
1219
|
+
|
1220
|
+
self._span = self._tracer.start_span(
|
1221
|
+
name=self.flow_run.name,
|
1222
|
+
attributes={
|
1223
|
+
**self.flow_run.labels,
|
1224
|
+
"prefect.run.type": "flow",
|
1225
|
+
"prefect.run.id": str(self.flow_run.id),
|
1226
|
+
"prefect.tags": self.flow_run.tags,
|
1227
|
+
"prefect.flow.name": self.flow.name,
|
1228
|
+
},
|
1229
|
+
)
|
1230
|
+
|
1231
|
+
try:
|
1232
|
+
yield self
|
1233
|
+
|
1234
|
+
except TerminationSignal as exc:
|
1235
|
+
self.cancel_all_tasks()
|
1236
|
+
await self.handle_crash(exc)
|
1237
|
+
raise
|
1238
|
+
except Exception:
|
1239
|
+
# regular exceptions are caught and re-raised to the user
|
1240
|
+
raise
|
1241
|
+
except (Abort, Pause):
|
1242
|
+
raise
|
1243
|
+
except GeneratorExit:
|
1244
|
+
# Do not capture generator exits as crashes
|
1245
|
+
raise
|
1246
|
+
except BaseException as exc:
|
1247
|
+
# BaseExceptions are caught and handled as crashes
|
1248
|
+
await self.handle_crash(exc)
|
1249
|
+
raise
|
1250
|
+
finally:
|
1251
|
+
# If debugging, use the more complete `repr` than the usual `str` description
|
1252
|
+
display_state = (
|
1253
|
+
repr(self.state) if PREFECT_DEBUG_MODE else str(self.state)
|
1254
|
+
)
|
1255
|
+
self.logger.log(
|
1256
|
+
level=logging.INFO if self.state.is_completed() else logging.ERROR,
|
1257
|
+
msg=f"Finished in state {display_state}",
|
1258
|
+
)
|
1259
|
+
|
1260
|
+
self._is_started = False
|
1261
|
+
self._client = None
|
1262
|
+
|
1263
|
+
# --------------------------
|
1264
|
+
#
|
1265
|
+
# The following methods compose the main task run loop
|
1266
|
+
#
|
1267
|
+
# --------------------------
|
1268
|
+
|
1269
|
+
@asynccontextmanager
|
1270
|
+
async def start(self) -> AsyncGenerator[None, None]:
|
1271
|
+
async with self.initialize_run():
|
1272
|
+
with trace.use_span(self._span):
|
1273
|
+
await self.begin_run()
|
1274
|
+
|
1275
|
+
if self.state.is_running():
|
1276
|
+
await self.call_hooks()
|
1277
|
+
yield
|
1278
|
+
|
1279
|
+
@asynccontextmanager
|
1280
|
+
async def run_context(self):
|
1281
|
+
timeout_context = timeout_async if self.flow.isasync else timeout
|
1282
|
+
# reenter the run context to ensure it is up to date for every run
|
1283
|
+
async with self.setup_run_context():
|
1284
|
+
try:
|
1285
|
+
with timeout_context(
|
1286
|
+
seconds=self.flow.timeout_seconds,
|
1287
|
+
timeout_exc_type=FlowRunTimeoutError,
|
1288
|
+
):
|
1289
|
+
self.logger.debug(
|
1290
|
+
f"Executing flow {self.flow.name!r} for flow run {self.flow_run.name!r}..."
|
1291
|
+
)
|
1292
|
+
yield self
|
1293
|
+
except TimeoutError as exc:
|
1294
|
+
await self.handle_timeout(exc)
|
1295
|
+
except Exception as exc:
|
1296
|
+
self.logger.exception("Encountered exception during execution: %r", exc)
|
1297
|
+
await self.handle_exception(exc)
|
1298
|
+
finally:
|
1299
|
+
if self.state.is_final() or self.state.is_cancelling():
|
1300
|
+
await self.call_hooks()
|
1301
|
+
|
1302
|
+
async def call_flow_fn(self) -> Coroutine[Any, Any, R]:
|
1303
|
+
"""
|
1304
|
+
Convenience method to call the flow function. Returns a coroutine if the
|
1305
|
+
flow is async.
|
1306
|
+
"""
|
1307
|
+
assert self.flow.isasync, "Flow must be async to be run with AsyncFlowRunEngine"
|
1308
|
+
|
1309
|
+
result = await call_with_parameters(self.flow.fn, self.parameters)
|
1310
|
+
await self.handle_success(result)
|
1311
|
+
return result
|
1312
|
+
|
1313
|
+
|
1314
|
+
def run_flow_sync(
|
1315
|
+
flow: Flow[P, R],
|
1316
|
+
flow_run: Optional[FlowRun] = None,
|
1317
|
+
parameters: Optional[Dict[str, Any]] = None,
|
1318
|
+
wait_for: Optional[Iterable[PrefectFuture]] = None,
|
1319
|
+
return_type: Literal["state", "result"] = "result",
|
1320
|
+
) -> Union[R, State, None]:
|
1321
|
+
engine = FlowRunEngine[P, R](
|
1322
|
+
flow=flow,
|
1323
|
+
parameters=parameters,
|
1324
|
+
flow_run=flow_run,
|
1325
|
+
wait_for=wait_for,
|
1326
|
+
)
|
1327
|
+
|
1328
|
+
with engine.start():
|
1329
|
+
while engine.is_running():
|
1330
|
+
with engine.run_context():
|
1331
|
+
engine.call_flow_fn()
|
1332
|
+
|
1333
|
+
return engine.state if return_type == "state" else engine.result()
|
1334
|
+
|
1335
|
+
|
1336
|
+
async def run_flow_async(
|
1337
|
+
flow: Flow[P, R],
|
1338
|
+
flow_run: Optional[FlowRun] = None,
|
1339
|
+
parameters: Optional[Dict[str, Any]] = None,
|
1340
|
+
wait_for: Optional[Iterable[PrefectFuture]] = None,
|
1341
|
+
return_type: Literal["state", "result"] = "result",
|
1342
|
+
) -> Union[R, State, None]:
|
1343
|
+
engine = AsyncFlowRunEngine[P, R](
|
1344
|
+
flow=flow, parameters=parameters, flow_run=flow_run, wait_for=wait_for
|
1345
|
+
)
|
1346
|
+
|
1347
|
+
async with engine.start():
|
1348
|
+
while engine.is_running():
|
1349
|
+
async with engine.run_context():
|
1350
|
+
await engine.call_flow_fn()
|
1351
|
+
|
1352
|
+
return engine.state if return_type == "state" else await engine.result()
|
1353
|
+
|
1354
|
+
|
1355
|
+
def run_generator_flow_sync(
|
1356
|
+
flow: Flow[P, R],
|
1357
|
+
flow_run: Optional[FlowRun] = None,
|
1358
|
+
parameters: Optional[Dict[str, Any]] = None,
|
1359
|
+
wait_for: Optional[Iterable[PrefectFuture]] = None,
|
1360
|
+
return_type: Literal["state", "result"] = "result",
|
1361
|
+
) -> Generator[R, None, None]:
|
1362
|
+
if return_type != "result":
|
1363
|
+
raise ValueError("The return_type for a generator flow must be 'result'")
|
1364
|
+
|
1365
|
+
engine = FlowRunEngine[P, R](
|
1366
|
+
flow=flow, parameters=parameters, flow_run=flow_run, wait_for=wait_for
|
1367
|
+
)
|
1368
|
+
|
1369
|
+
with engine.start():
|
1370
|
+
while engine.is_running():
|
1371
|
+
with engine.run_context():
|
1372
|
+
call_args, call_kwargs = parameters_to_args_kwargs(
|
1373
|
+
flow.fn, engine.parameters or {}
|
1374
|
+
)
|
1375
|
+
gen = flow.fn(*call_args, **call_kwargs)
|
1376
|
+
try:
|
1377
|
+
while True:
|
1378
|
+
gen_result = next(gen)
|
1379
|
+
# link the current state to the result for dependency tracking
|
1380
|
+
link_state_to_result(engine.state, gen_result)
|
1381
|
+
yield gen_result
|
1382
|
+
except StopIteration as exc:
|
1383
|
+
engine.handle_success(exc.value)
|
1384
|
+
except GeneratorExit as exc:
|
1385
|
+
engine.handle_success(None)
|
1386
|
+
gen.throw(exc)
|
1387
|
+
|
1388
|
+
return engine.result()
|
1389
|
+
|
1390
|
+
|
1391
|
+
async def run_generator_flow_async(
|
1392
|
+
flow: Flow[P, R],
|
1393
|
+
flow_run: Optional[FlowRun] = None,
|
1394
|
+
parameters: Optional[Dict[str, Any]] = None,
|
1395
|
+
wait_for: Optional[Iterable[PrefectFuture]] = None,
|
1396
|
+
return_type: Literal["state", "result"] = "result",
|
1397
|
+
) -> AsyncGenerator[R, None]:
|
1398
|
+
if return_type != "result":
|
1399
|
+
raise ValueError("The return_type for a generator flow must be 'result'")
|
1400
|
+
|
1401
|
+
engine = AsyncFlowRunEngine[P, R](
|
1402
|
+
flow=flow, parameters=parameters, flow_run=flow_run, wait_for=wait_for
|
1403
|
+
)
|
1404
|
+
|
1405
|
+
async with engine.start():
|
1406
|
+
while engine.is_running():
|
1407
|
+
async with engine.run_context():
|
1408
|
+
call_args, call_kwargs = parameters_to_args_kwargs(
|
1409
|
+
flow.fn, engine.parameters or {}
|
1410
|
+
)
|
1411
|
+
gen = flow.fn(*call_args, **call_kwargs)
|
1412
|
+
try:
|
1413
|
+
while True:
|
1414
|
+
# can't use anext in Python < 3.10
|
1415
|
+
gen_result = await gen.__anext__()
|
1416
|
+
# link the current state to the result for dependency tracking
|
1417
|
+
link_state_to_result(engine.state, gen_result)
|
1418
|
+
yield gen_result
|
1419
|
+
except (StopAsyncIteration, GeneratorExit) as exc:
|
1420
|
+
await engine.handle_success(None)
|
844
1421
|
if isinstance(exc, GeneratorExit):
|
845
1422
|
gen.throw(exc)
|
846
1423
|
|
847
1424
|
# async generators can't return, but we can raise failures here
|
848
1425
|
if engine.state.is_failed():
|
849
|
-
engine.result()
|
1426
|
+
await engine.result()
|
850
1427
|
|
851
1428
|
|
852
1429
|
def run_flow(
|