prefect-client 2.19.2__py3-none-any.whl → 3.0.0rc1__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.
Files changed (239) hide show
  1. prefect/__init__.py +8 -56
  2. prefect/_internal/compatibility/deprecated.py +6 -115
  3. prefect/_internal/compatibility/experimental.py +4 -79
  4. prefect/_internal/concurrency/api.py +0 -34
  5. prefect/_internal/concurrency/calls.py +0 -6
  6. prefect/_internal/concurrency/cancellation.py +0 -3
  7. prefect/_internal/concurrency/event_loop.py +0 -20
  8. prefect/_internal/concurrency/inspection.py +3 -3
  9. prefect/_internal/concurrency/threads.py +35 -0
  10. prefect/_internal/concurrency/waiters.py +0 -28
  11. prefect/_internal/pydantic/__init__.py +0 -45
  12. prefect/_internal/pydantic/v1_schema.py +21 -22
  13. prefect/_internal/pydantic/v2_schema.py +0 -2
  14. prefect/_internal/pydantic/v2_validated_func.py +18 -23
  15. prefect/_internal/schemas/bases.py +44 -177
  16. prefect/_internal/schemas/fields.py +1 -43
  17. prefect/_internal/schemas/validators.py +60 -158
  18. prefect/artifacts.py +161 -14
  19. prefect/automations.py +39 -4
  20. prefect/blocks/abstract.py +1 -1
  21. prefect/blocks/core.py +268 -148
  22. prefect/blocks/fields.py +2 -57
  23. prefect/blocks/kubernetes.py +8 -12
  24. prefect/blocks/notifications.py +40 -20
  25. prefect/blocks/system.py +22 -11
  26. prefect/blocks/webhook.py +2 -9
  27. prefect/client/base.py +4 -4
  28. prefect/client/cloud.py +8 -13
  29. prefect/client/orchestration.py +347 -341
  30. prefect/client/schemas/actions.py +92 -86
  31. prefect/client/schemas/filters.py +20 -40
  32. prefect/client/schemas/objects.py +151 -145
  33. prefect/client/schemas/responses.py +16 -24
  34. prefect/client/schemas/schedules.py +47 -35
  35. prefect/client/subscriptions.py +2 -2
  36. prefect/client/utilities.py +5 -2
  37. prefect/concurrency/asyncio.py +3 -1
  38. prefect/concurrency/events.py +1 -1
  39. prefect/concurrency/services.py +6 -3
  40. prefect/context.py +195 -27
  41. prefect/deployments/__init__.py +5 -6
  42. prefect/deployments/base.py +7 -5
  43. prefect/deployments/flow_runs.py +185 -0
  44. prefect/deployments/runner.py +50 -45
  45. prefect/deployments/schedules.py +28 -23
  46. prefect/deployments/steps/__init__.py +0 -1
  47. prefect/deployments/steps/core.py +1 -0
  48. prefect/deployments/steps/pull.py +7 -21
  49. prefect/engine.py +12 -2422
  50. prefect/events/actions.py +17 -23
  51. prefect/events/cli/automations.py +19 -6
  52. prefect/events/clients.py +14 -37
  53. prefect/events/filters.py +14 -18
  54. prefect/events/related.py +2 -2
  55. prefect/events/schemas/__init__.py +0 -5
  56. prefect/events/schemas/automations.py +55 -46
  57. prefect/events/schemas/deployment_triggers.py +7 -197
  58. prefect/events/schemas/events.py +34 -65
  59. prefect/events/schemas/labelling.py +10 -14
  60. prefect/events/utilities.py +2 -3
  61. prefect/events/worker.py +2 -3
  62. prefect/filesystems.py +6 -517
  63. prefect/{new_flow_engine.py → flow_engine.py} +313 -72
  64. prefect/flow_runs.py +377 -5
  65. prefect/flows.py +307 -166
  66. prefect/futures.py +186 -345
  67. prefect/infrastructure/__init__.py +0 -27
  68. prefect/infrastructure/provisioners/__init__.py +5 -3
  69. prefect/infrastructure/provisioners/cloud_run.py +11 -6
  70. prefect/infrastructure/provisioners/container_instance.py +11 -7
  71. prefect/infrastructure/provisioners/ecs.py +6 -4
  72. prefect/infrastructure/provisioners/modal.py +8 -5
  73. prefect/input/actions.py +2 -4
  74. prefect/input/run_input.py +5 -7
  75. prefect/logging/formatters.py +0 -2
  76. prefect/logging/handlers.py +3 -11
  77. prefect/logging/loggers.py +2 -2
  78. prefect/manifests.py +2 -1
  79. prefect/records/__init__.py +1 -0
  80. prefect/records/result_store.py +42 -0
  81. prefect/records/store.py +9 -0
  82. prefect/results.py +43 -39
  83. prefect/runner/runner.py +19 -15
  84. prefect/runner/server.py +6 -10
  85. prefect/runner/storage.py +3 -8
  86. prefect/runner/submit.py +2 -2
  87. prefect/runner/utils.py +2 -2
  88. prefect/serializers.py +24 -35
  89. prefect/server/api/collections_data/views/aggregate-worker-metadata.json +5 -14
  90. prefect/settings.py +70 -133
  91. prefect/states.py +17 -47
  92. prefect/task_engine.py +697 -58
  93. prefect/task_runners.py +269 -301
  94. prefect/task_server.py +53 -34
  95. prefect/tasks.py +327 -337
  96. prefect/transactions.py +220 -0
  97. prefect/types/__init__.py +61 -82
  98. prefect/utilities/asyncutils.py +195 -136
  99. prefect/utilities/callables.py +311 -43
  100. prefect/utilities/collections.py +23 -38
  101. prefect/utilities/dispatch.py +11 -3
  102. prefect/utilities/dockerutils.py +4 -0
  103. prefect/utilities/engine.py +140 -20
  104. prefect/utilities/importtools.py +97 -27
  105. prefect/utilities/pydantic.py +128 -38
  106. prefect/utilities/schema_tools/hydration.py +5 -1
  107. prefect/utilities/templating.py +12 -2
  108. prefect/variables.py +78 -61
  109. prefect/workers/__init__.py +0 -1
  110. prefect/workers/base.py +15 -17
  111. prefect/workers/process.py +3 -8
  112. prefect/workers/server.py +2 -2
  113. {prefect_client-2.19.2.dist-info → prefect_client-3.0.0rc1.dist-info}/METADATA +22 -21
  114. prefect_client-3.0.0rc1.dist-info/RECORD +176 -0
  115. prefect/_internal/pydantic/_base_model.py +0 -51
  116. prefect/_internal/pydantic/_compat.py +0 -82
  117. prefect/_internal/pydantic/_flags.py +0 -20
  118. prefect/_internal/pydantic/_types.py +0 -8
  119. prefect/_internal/pydantic/utilities/__init__.py +0 -0
  120. prefect/_internal/pydantic/utilities/config_dict.py +0 -72
  121. prefect/_internal/pydantic/utilities/field_validator.py +0 -150
  122. prefect/_internal/pydantic/utilities/model_construct.py +0 -56
  123. prefect/_internal/pydantic/utilities/model_copy.py +0 -55
  124. prefect/_internal/pydantic/utilities/model_dump.py +0 -136
  125. prefect/_internal/pydantic/utilities/model_dump_json.py +0 -112
  126. prefect/_internal/pydantic/utilities/model_fields.py +0 -50
  127. prefect/_internal/pydantic/utilities/model_fields_set.py +0 -29
  128. prefect/_internal/pydantic/utilities/model_json_schema.py +0 -82
  129. prefect/_internal/pydantic/utilities/model_rebuild.py +0 -80
  130. prefect/_internal/pydantic/utilities/model_validate.py +0 -75
  131. prefect/_internal/pydantic/utilities/model_validate_json.py +0 -68
  132. prefect/_internal/pydantic/utilities/model_validator.py +0 -87
  133. prefect/_internal/pydantic/utilities/type_adapter.py +0 -71
  134. prefect/_vendor/__init__.py +0 -0
  135. prefect/_vendor/fastapi/__init__.py +0 -25
  136. prefect/_vendor/fastapi/applications.py +0 -946
  137. prefect/_vendor/fastapi/background.py +0 -3
  138. prefect/_vendor/fastapi/concurrency.py +0 -44
  139. prefect/_vendor/fastapi/datastructures.py +0 -58
  140. prefect/_vendor/fastapi/dependencies/__init__.py +0 -0
  141. prefect/_vendor/fastapi/dependencies/models.py +0 -64
  142. prefect/_vendor/fastapi/dependencies/utils.py +0 -877
  143. prefect/_vendor/fastapi/encoders.py +0 -177
  144. prefect/_vendor/fastapi/exception_handlers.py +0 -40
  145. prefect/_vendor/fastapi/exceptions.py +0 -46
  146. prefect/_vendor/fastapi/logger.py +0 -3
  147. prefect/_vendor/fastapi/middleware/__init__.py +0 -1
  148. prefect/_vendor/fastapi/middleware/asyncexitstack.py +0 -25
  149. prefect/_vendor/fastapi/middleware/cors.py +0 -3
  150. prefect/_vendor/fastapi/middleware/gzip.py +0 -3
  151. prefect/_vendor/fastapi/middleware/httpsredirect.py +0 -3
  152. prefect/_vendor/fastapi/middleware/trustedhost.py +0 -3
  153. prefect/_vendor/fastapi/middleware/wsgi.py +0 -3
  154. prefect/_vendor/fastapi/openapi/__init__.py +0 -0
  155. prefect/_vendor/fastapi/openapi/constants.py +0 -2
  156. prefect/_vendor/fastapi/openapi/docs.py +0 -203
  157. prefect/_vendor/fastapi/openapi/models.py +0 -480
  158. prefect/_vendor/fastapi/openapi/utils.py +0 -485
  159. prefect/_vendor/fastapi/param_functions.py +0 -340
  160. prefect/_vendor/fastapi/params.py +0 -453
  161. prefect/_vendor/fastapi/requests.py +0 -4
  162. prefect/_vendor/fastapi/responses.py +0 -40
  163. prefect/_vendor/fastapi/routing.py +0 -1331
  164. prefect/_vendor/fastapi/security/__init__.py +0 -15
  165. prefect/_vendor/fastapi/security/api_key.py +0 -98
  166. prefect/_vendor/fastapi/security/base.py +0 -6
  167. prefect/_vendor/fastapi/security/http.py +0 -172
  168. prefect/_vendor/fastapi/security/oauth2.py +0 -227
  169. prefect/_vendor/fastapi/security/open_id_connect_url.py +0 -34
  170. prefect/_vendor/fastapi/security/utils.py +0 -10
  171. prefect/_vendor/fastapi/staticfiles.py +0 -1
  172. prefect/_vendor/fastapi/templating.py +0 -3
  173. prefect/_vendor/fastapi/testclient.py +0 -1
  174. prefect/_vendor/fastapi/types.py +0 -3
  175. prefect/_vendor/fastapi/utils.py +0 -235
  176. prefect/_vendor/fastapi/websockets.py +0 -7
  177. prefect/_vendor/starlette/__init__.py +0 -1
  178. prefect/_vendor/starlette/_compat.py +0 -28
  179. prefect/_vendor/starlette/_exception_handler.py +0 -80
  180. prefect/_vendor/starlette/_utils.py +0 -88
  181. prefect/_vendor/starlette/applications.py +0 -261
  182. prefect/_vendor/starlette/authentication.py +0 -159
  183. prefect/_vendor/starlette/background.py +0 -43
  184. prefect/_vendor/starlette/concurrency.py +0 -59
  185. prefect/_vendor/starlette/config.py +0 -151
  186. prefect/_vendor/starlette/convertors.py +0 -87
  187. prefect/_vendor/starlette/datastructures.py +0 -707
  188. prefect/_vendor/starlette/endpoints.py +0 -130
  189. prefect/_vendor/starlette/exceptions.py +0 -60
  190. prefect/_vendor/starlette/formparsers.py +0 -276
  191. prefect/_vendor/starlette/middleware/__init__.py +0 -17
  192. prefect/_vendor/starlette/middleware/authentication.py +0 -52
  193. prefect/_vendor/starlette/middleware/base.py +0 -220
  194. prefect/_vendor/starlette/middleware/cors.py +0 -176
  195. prefect/_vendor/starlette/middleware/errors.py +0 -265
  196. prefect/_vendor/starlette/middleware/exceptions.py +0 -74
  197. prefect/_vendor/starlette/middleware/gzip.py +0 -113
  198. prefect/_vendor/starlette/middleware/httpsredirect.py +0 -19
  199. prefect/_vendor/starlette/middleware/sessions.py +0 -82
  200. prefect/_vendor/starlette/middleware/trustedhost.py +0 -64
  201. prefect/_vendor/starlette/middleware/wsgi.py +0 -147
  202. prefect/_vendor/starlette/requests.py +0 -328
  203. prefect/_vendor/starlette/responses.py +0 -347
  204. prefect/_vendor/starlette/routing.py +0 -933
  205. prefect/_vendor/starlette/schemas.py +0 -154
  206. prefect/_vendor/starlette/staticfiles.py +0 -248
  207. prefect/_vendor/starlette/status.py +0 -199
  208. prefect/_vendor/starlette/templating.py +0 -231
  209. prefect/_vendor/starlette/testclient.py +0 -804
  210. prefect/_vendor/starlette/types.py +0 -30
  211. prefect/_vendor/starlette/websockets.py +0 -193
  212. prefect/agent.py +0 -698
  213. prefect/deployments/deployments.py +0 -1042
  214. prefect/deprecated/__init__.py +0 -0
  215. prefect/deprecated/data_documents.py +0 -350
  216. prefect/deprecated/packaging/__init__.py +0 -12
  217. prefect/deprecated/packaging/base.py +0 -96
  218. prefect/deprecated/packaging/docker.py +0 -146
  219. prefect/deprecated/packaging/file.py +0 -92
  220. prefect/deprecated/packaging/orion.py +0 -80
  221. prefect/deprecated/packaging/serializers.py +0 -171
  222. prefect/events/instrument.py +0 -135
  223. prefect/infrastructure/base.py +0 -323
  224. prefect/infrastructure/container.py +0 -818
  225. prefect/infrastructure/kubernetes.py +0 -920
  226. prefect/infrastructure/process.py +0 -289
  227. prefect/new_task_engine.py +0 -423
  228. prefect/pydantic/__init__.py +0 -76
  229. prefect/pydantic/main.py +0 -39
  230. prefect/software/__init__.py +0 -2
  231. prefect/software/base.py +0 -50
  232. prefect/software/conda.py +0 -199
  233. prefect/software/pip.py +0 -122
  234. prefect/software/python.py +0 -52
  235. prefect/workers/block.py +0 -218
  236. prefect_client-2.19.2.dist-info/RECORD +0 -292
  237. {prefect_client-2.19.2.dist-info → prefect_client-3.0.0rc1.dist-info}/LICENSE +0 -0
  238. {prefect_client-2.19.2.dist-info → prefect_client-3.0.0rc1.dist-info}/WHEEL +0 -0
  239. {prefect_client-2.19.2.dist-info → prefect_client-3.0.0rc1.dist-info}/top_level.txt +0 -0
prefect/engine.py CHANGED
@@ -1,2428 +1,21 @@
1
- """
2
- Client-side execution and orchestration of flows and tasks.
3
-
4
- ## Engine process overview
5
-
6
- ### Flows
7
-
8
- - **The flow is called by the user or an existing flow run is executed in a new process.**
9
-
10
- See `Flow.__call__` and `prefect.engine.__main__` (`python -m prefect.engine`)
11
-
12
- - **A synchronous function acts as an entrypoint to the engine.**
13
- The engine executes on a dedicated "global loop" thread. For asynchronous flow calls,
14
- we return a coroutine from the entrypoint so the user can enter the engine without
15
- blocking their event loop.
16
-
17
- See `enter_flow_run_engine_from_flow_call`, `enter_flow_run_engine_from_subprocess`
18
-
19
- - **The thread that calls the entrypoint waits until orchestration of the flow run completes.**
20
- This thread is referred to as the "user" thread and is usually the "main" thread.
21
- The thread is not blocked while waiting — it allows the engine to send work back to it.
22
- This allows us to send calls back to the user thread from the global loop thread.
23
-
24
- See `wait_for_call_in_loop_thread` and `call_soon_in_waiting_thread`
25
-
26
- - **The asynchronous engine branches depending on if the flow run exists already and if
27
- there is a parent flow run in the current context.**
28
-
29
- See `create_then_begin_flow_run`, `create_and_begin_subflow_run`, and `retrieve_flow_then_begin_flow_run`
30
-
31
- - **The asynchronous engine prepares for execution of the flow run.**
32
- This includes starting the task runner, preparing context, etc.
33
-
34
- See `begin_flow_run`
35
-
36
- - **The flow run is orchestrated through states, calling the user's function as necessary.**
37
- Generally the user's function is sent for execution on the user thread.
38
- If the flow function cannot be safely executed on the user thread, e.g. it is
39
- a synchronous child in an asynchronous parent it will be scheduled on a worker
40
- thread instead.
41
-
42
- See `orchestrate_flow_run`, `call_soon_in_waiting_thread`, `call_soon_in_new_thread`
43
-
44
- ### Tasks
45
-
46
- - **The task is called or submitted by the user.**
47
- We require that this is always within a flow.
48
-
49
- See `Task.__call__` and `Task.submit`
50
-
51
- - **A synchronous function acts as an entrypoint to the engine.**
52
- Unlike flow calls, this _will not_ block until completion if `submit` was used.
53
-
54
- See `enter_task_run_engine`
55
-
56
- - **A future is created for the task call.**
57
- Creation of the task run and submission to the task runner is scheduled as a
58
- background task so submission of many tasks can occur concurrently.
59
-
60
- See `create_task_run_future` and `create_task_run_then_submit`
61
-
62
- - **The engine branches depending on if a future, state, or result is requested.**
63
- If a future is requested, it is returned immediately to the user thread.
64
- Otherwise, the engine will wait for the task run to complete and return the final
65
- state or result.
66
-
67
- See `get_task_call_return_value`
68
-
69
- - **An engine function is submitted to the task runner.**
70
- The task runner will schedule this function for execution on a worker.
71
- When executed, it will prepare for orchestration and wait for completion of the run.
72
-
73
- See `create_task_run_then_submit` and `begin_task_run`
74
-
75
- - **The task run is orchestrated through states, calling the user's function as necessary.**
76
- The user's function is always executed in a worker thread for isolation.
77
-
78
- See `orchestrate_task_run`, `call_soon_in_new_thread`
79
-
80
- _Ideally, for local and sequential task runners we would send the task run to the
81
- user thread as we do for flows. See [#9855](https://github.com/PrefectHQ/prefect/pull/9855).
82
- """
83
-
84
- import asyncio
85
- import logging
86
1
  import os
87
- import random
88
2
  import sys
89
- import threading
90
- import time
91
- from contextlib import AsyncExitStack, asynccontextmanager
92
- from functools import partial
93
- from typing import (
94
- Any,
95
- Awaitable,
96
- Dict,
97
- Iterable,
98
- List,
99
- Optional,
100
- Set,
101
- Type,
102
- TypeVar,
103
- Union,
104
- overload,
105
- )
106
- from uuid import UUID, uuid4
107
-
108
- import anyio
109
- import pendulum
110
- from anyio import start_blocking_portal
111
- from typing_extensions import Literal
3
+ from uuid import UUID
112
4
 
113
- import prefect
114
- import prefect.context
115
- import prefect.plugins
116
- from prefect._internal.compatibility.deprecated import deprecated_parameter
117
- from prefect._internal.compatibility.experimental import experimental_parameter
118
- from prefect._internal.concurrency.api import create_call, from_async, from_sync
119
- from prefect._internal.concurrency.calls import get_current_call
120
- from prefect._internal.concurrency.cancellation import CancelledError
121
- from prefect._internal.concurrency.threads import wait_for_global_loop_exit
122
- from prefect.client.orchestration import PrefectClient, get_client
123
- from prefect.client.schemas import FlowRun, TaskRun
124
- from prefect.client.schemas.filters import FlowRunFilter
125
- from prefect.client.schemas.objects import (
126
- StateDetails,
127
- StateType,
128
- TaskRunInput,
129
- )
130
- from prefect.client.schemas.responses import SetStateStatus
131
- from prefect.client.schemas.sorting import FlowRunSort
132
- from prefect.client.utilities import inject_client
133
- from prefect.context import (
134
- FlowRunContext,
135
- PrefectObjectRegistry,
136
- TagsContext,
137
- TaskRunContext,
138
- )
139
- from prefect.deployments import load_flow_from_flow_run
140
5
  from prefect.exceptions import (
141
6
  Abort,
142
- FlowPauseTimeout,
143
- MappingLengthMismatch,
144
- MappingMissingIterable,
145
- NotPausedError,
146
7
  Pause,
147
- PausedRun,
148
- UpstreamTaskError,
149
8
  )
150
- from prefect.flows import Flow, load_flow_from_entrypoint
151
- from prefect.futures import PrefectFuture, call_repr, resolve_futures_to_states
152
- from prefect.input import keyset_from_paused_state
153
- from prefect.input.run_input import run_input_subclass_from_type
154
- from prefect.logging.configuration import setup_logging
155
- from prefect.logging.handlers import APILogHandler
156
9
  from prefect.logging.loggers import (
157
- flow_run_logger,
158
10
  get_logger,
159
- get_run_logger,
160
- patch_print,
161
- task_run_logger,
162
- )
163
- from prefect.results import ResultFactory, UnknownResult
164
- from prefect.settings import (
165
- PREFECT_DEBUG_MODE,
166
- PREFECT_EXPERIMENTAL_ENABLE_NEW_ENGINE,
167
- PREFECT_TASK_INTROSPECTION_WARN_THRESHOLD,
168
- PREFECT_TASKS_REFRESH_CACHE,
169
- PREFECT_UI_URL,
170
11
  )
171
- from prefect.states import (
172
- Completed,
173
- Paused,
174
- Pending,
175
- Running,
176
- Scheduled,
177
- State,
178
- Suspended,
179
- exception_to_crashed_state,
180
- exception_to_failed_state,
181
- return_value_to_state,
182
- )
183
- from prefect.task_runners import (
184
- CONCURRENCY_MESSAGES,
185
- BaseTaskRunner,
186
- TaskConcurrencyType,
187
- )
188
- from prefect.tasks import Task
189
- from prefect.utilities.annotations import allow_failure, quote, unmapped
190
12
  from prefect.utilities.asyncutils import (
191
- gather,
192
- is_async_fn,
193
- run_sync,
194
- sync_compatible,
195
- )
196
- from prefect.utilities.callables import (
197
- collapse_variadic_parameters,
198
- explode_variadic_parameter,
199
- get_parameter_defaults,
200
- parameters_to_args_kwargs,
201
- )
202
- from prefect.utilities.collections import isiterable
203
- from prefect.utilities.engine import (
204
- _dynamic_key_for_task_run,
205
- _get_hook_name,
206
- _observed_flow_pauses,
207
- _resolve_custom_flow_run_name,
208
- _resolve_custom_task_run_name,
209
- capture_sigterm,
210
- check_api_reachable,
211
- collect_task_run_inputs,
212
- emit_task_run_state_change_event,
213
- propose_state,
214
- resolve_inputs,
215
- should_log_prints,
216
- wait_for_task_runs_and_report_crashes,
13
+ run_coro_as_sync,
217
14
  )
218
15
 
219
- R = TypeVar("R")
220
- T = TypeVar("T")
221
- EngineReturnType = Literal["future", "state", "result"]
222
-
223
- NUM_CHARS_DYNAMIC_KEY = 8
224
-
225
16
  engine_logger = get_logger("engine")
226
17
 
227
18
 
228
- def enter_flow_run_engine_from_flow_call(
229
- flow: Flow,
230
- parameters: Dict[str, Any],
231
- wait_for: Optional[Iterable[PrefectFuture]],
232
- return_type: EngineReturnType,
233
- ) -> Union[State, Awaitable[State]]:
234
- """
235
- Sync entrypoint for flow calls.
236
-
237
- This function does the heavy lifting of ensuring we can get into an async context
238
- for flow run execution with minimal overhead.
239
- """
240
- setup_logging()
241
-
242
- registry = PrefectObjectRegistry.get()
243
- if registry and registry.block_code_execution:
244
- engine_logger.warning(
245
- f"Script loading is in progress, flow {flow.name!r} will not be executed."
246
- " Consider updating the script to only call the flow if executed"
247
- f' directly:\n\n\tif __name__ == "__main__":\n\t\t{flow.fn.__name__}()'
248
- )
249
- return None
250
-
251
- parent_flow_run_context = FlowRunContext.get()
252
- is_subflow_run = parent_flow_run_context is not None
253
-
254
- if wait_for is not None and not is_subflow_run:
255
- raise ValueError("Only flows run as subflows can wait for dependencies.")
256
-
257
- begin_run = create_call(
258
- create_and_begin_subflow_run if is_subflow_run else create_then_begin_flow_run,
259
- flow=flow,
260
- parameters=parameters,
261
- wait_for=wait_for,
262
- return_type=return_type,
263
- client=parent_flow_run_context.client if is_subflow_run else None,
264
- user_thread=threading.current_thread(),
265
- )
266
-
267
- # On completion of root flows, wait for the global thread to ensure that
268
- # any work there is complete
269
- done_callbacks = (
270
- [create_call(wait_for_global_loop_exit)] if not is_subflow_run else None
271
- )
272
-
273
- # WARNING: You must define any context managers here to pass to our concurrency
274
- # api instead of entering them in here in the engine entrypoint. Otherwise, async
275
- # flows will not use the context as this function _exits_ to return an awaitable to
276
- # the user. Generally, you should enter contexts _within_ the async `begin_run`
277
- # instead but if you need to enter a context from the main thread you'll need to do
278
- # it here.
279
- contexts = [capture_sigterm()]
280
-
281
- if flow.isasync and (
282
- not is_subflow_run or (is_subflow_run and parent_flow_run_context.flow.isasync)
283
- ):
284
- # return a coro for the user to await if the flow is async
285
- # unless it is an async subflow called in a sync flow
286
- retval = from_async.wait_for_call_in_loop_thread(
287
- begin_run,
288
- done_callbacks=done_callbacks,
289
- contexts=contexts,
290
- )
291
-
292
- else:
293
- retval = from_sync.wait_for_call_in_loop_thread(
294
- begin_run,
295
- done_callbacks=done_callbacks,
296
- contexts=contexts,
297
- )
298
-
299
- return retval
300
-
301
-
302
- def enter_flow_run_engine_from_subprocess(flow_run_id: UUID) -> State:
303
- """
304
- Sync entrypoint for flow runs that have been submitted for execution by an agent
305
-
306
- Differs from `enter_flow_run_engine_from_flow_call` in that we have a flow run id
307
- but not a flow object. The flow must be retrieved before execution can begin.
308
- Additionally, this assumes that the caller is always in a context without an event
309
- loop as this should be called from a fresh process.
310
- """
311
-
312
- # Ensure collections are imported and have the opportunity to register types before
313
- # loading the user code from the deployment
314
- prefect.plugins.load_prefect_collections()
315
-
316
- setup_logging()
317
-
318
- state = from_sync.wait_for_call_in_loop_thread(
319
- create_call(
320
- retrieve_flow_then_begin_flow_run,
321
- flow_run_id,
322
- user_thread=threading.current_thread(),
323
- ),
324
- contexts=[capture_sigterm()],
325
- )
326
-
327
- APILogHandler.flush()
328
- return state
329
-
330
-
331
- @inject_client
332
- async def create_then_begin_flow_run(
333
- flow: Flow,
334
- parameters: Dict[str, Any],
335
- wait_for: Optional[Iterable[PrefectFuture]],
336
- return_type: EngineReturnType,
337
- client: PrefectClient,
338
- user_thread: threading.Thread,
339
- ) -> Any:
340
- """
341
- Async entrypoint for flow calls
342
-
343
- Creates the flow run in the backend, then enters the main flow run engine.
344
- """
345
- # TODO: Returns a `State` depending on `return_type` and we can add an overload to
346
- # the function signature to clarify this eventually.
347
-
348
- await check_api_reachable(client, "Cannot create flow run")
349
-
350
- state = Pending()
351
- if flow.should_validate_parameters:
352
- try:
353
- parameters = flow.validate_parameters(parameters)
354
- except Exception:
355
- state = await exception_to_failed_state(
356
- message="Validation of flow parameters failed with error:"
357
- )
358
-
359
- flow_run = await client.create_flow_run(
360
- flow,
361
- # Send serialized parameters to the backend
362
- parameters=flow.serialize_parameters(parameters),
363
- state=state,
364
- tags=TagsContext.get().current_tags,
365
- )
366
-
367
- engine_logger.info(f"Created flow run {flow_run.name!r} for flow {flow.name!r}")
368
-
369
- logger = flow_run_logger(flow_run, flow)
370
-
371
- ui_url = PREFECT_UI_URL.value()
372
- if ui_url:
373
- logger.info(
374
- f"View at {ui_url}/flow-runs/flow-run/{flow_run.id}",
375
- extra={"send_to_api": False},
376
- )
377
-
378
- if state.is_failed():
379
- logger.error(state.message)
380
- engine_logger.info(
381
- f"Flow run {flow_run.name!r} received invalid parameters and is marked as"
382
- " failed."
383
- )
384
- else:
385
- state = await begin_flow_run(
386
- flow=flow,
387
- flow_run=flow_run,
388
- parameters=parameters,
389
- client=client,
390
- user_thread=user_thread,
391
- )
392
-
393
- if return_type == "state":
394
- return state
395
- elif return_type == "result":
396
- return await state.result(fetch=True)
397
- else:
398
- raise ValueError(f"Invalid return type for flow engine {return_type!r}.")
399
-
400
-
401
- @inject_client
402
- async def retrieve_flow_then_begin_flow_run(
403
- flow_run_id: UUID,
404
- client: PrefectClient,
405
- user_thread: threading.Thread,
406
- ) -> State:
407
- """
408
- Async entrypoint for flow runs that have been submitted for execution by an agent
409
-
410
- - Retrieves the deployment information
411
- - Loads the flow object using deployment information
412
- - Updates the flow run version
413
- """
414
- flow_run = await client.read_flow_run(flow_run_id)
415
-
416
- entrypoint = os.environ.get("PREFECT__FLOW_ENTRYPOINT")
417
-
418
- try:
419
- flow = (
420
- load_flow_from_entrypoint(entrypoint)
421
- if entrypoint
422
- else await load_flow_from_flow_run(flow_run, client=client)
423
- )
424
- except Exception:
425
- message = (
426
- "Flow could not be retrieved from"
427
- f" {'entrypoint' if entrypoint else 'deployment'}."
428
- )
429
- flow_run_logger(flow_run).exception(message)
430
- state = await exception_to_failed_state(message=message)
431
- await client.set_flow_run_state(
432
- state=state, flow_run_id=flow_run_id, force=True
433
- )
434
- return state
435
-
436
- # Update the flow run policy defaults to match settings on the flow
437
- # Note: Mutating the flow run object prevents us from performing another read
438
- # operation if these properties are used by the client downstream
439
- if flow_run.empirical_policy.retry_delay is None:
440
- flow_run.empirical_policy.retry_delay = flow.retry_delay_seconds
441
-
442
- if flow_run.empirical_policy.retries is None:
443
- flow_run.empirical_policy.retries = flow.retries
444
-
445
- await client.update_flow_run(
446
- flow_run_id=flow_run_id,
447
- flow_version=flow.version,
448
- empirical_policy=flow_run.empirical_policy,
449
- )
450
-
451
- if flow.should_validate_parameters:
452
- failed_state = None
453
- try:
454
- parameters = flow.validate_parameters(flow_run.parameters)
455
- except Exception:
456
- message = "Validation of flow parameters failed with error: "
457
- flow_run_logger(flow_run).exception(message)
458
- failed_state = await exception_to_failed_state(message=message)
459
-
460
- if failed_state is not None:
461
- await propose_state(
462
- client,
463
- state=failed_state,
464
- flow_run_id=flow_run_id,
465
- )
466
- return failed_state
467
- else:
468
- parameters = flow_run.parameters
469
-
470
- # Ensure default values are populated
471
- parameters = {**get_parameter_defaults(flow.fn), **parameters}
472
-
473
- return await begin_flow_run(
474
- flow=flow,
475
- flow_run=flow_run,
476
- parameters=parameters,
477
- client=client,
478
- user_thread=user_thread,
479
- )
480
-
481
-
482
- async def begin_flow_run(
483
- flow: Flow,
484
- flow_run: FlowRun,
485
- parameters: Dict[str, Any],
486
- client: PrefectClient,
487
- user_thread: threading.Thread,
488
- ) -> State:
489
- """
490
- Begins execution of a flow run; blocks until completion of the flow run
491
-
492
- - Starts a task runner
493
- - Determines the result storage block to use
494
- - Orchestrates the flow run (runs the user-function and generates tasks)
495
- - Waits for tasks to complete / shutsdown the task runner
496
- - Sets a terminal state for the flow run
497
-
498
- Note that the `flow_run` contains a `parameters` attribute which is the serialized
499
- parameters sent to the backend while the `parameters` argument here should be the
500
- deserialized and validated dictionary of python objects.
501
-
502
- Returns:
503
- The final state of the run
504
- """
505
- logger = flow_run_logger(flow_run, flow)
506
-
507
- log_prints = should_log_prints(flow)
508
- flow_run_context = FlowRunContext.construct(log_prints=log_prints)
509
-
510
- async with AsyncExitStack() as stack:
511
- await stack.enter_async_context(
512
- report_flow_run_crashes(flow_run=flow_run, client=client, flow=flow)
513
- )
514
-
515
- # Create a task group for background tasks
516
- flow_run_context.background_tasks = await stack.enter_async_context(
517
- anyio.create_task_group()
518
- )
519
-
520
- # If the flow is async, we need to provide a portal so sync tasks can run
521
- flow_run_context.sync_portal = (
522
- stack.enter_context(start_blocking_portal()) if flow.isasync else None
523
- )
524
-
525
- task_runner = flow.task_runner.duplicate()
526
- if task_runner is NotImplemented:
527
- # Backwards compatibility; will not support concurrent flow runs
528
- task_runner = flow.task_runner
529
- logger.warning(
530
- f"Task runner {type(task_runner).__name__!r} does not implement the"
531
- " `duplicate` method and will fail if used for concurrent execution of"
532
- " the same flow."
533
- )
534
-
535
- logger.debug(
536
- f"Starting {type(flow.task_runner).__name__!r}; submitted tasks "
537
- f"will be run {CONCURRENCY_MESSAGES[flow.task_runner.concurrency_type]}..."
538
- )
539
-
540
- flow_run_context.task_runner = await stack.enter_async_context(
541
- task_runner.start()
542
- )
543
-
544
- flow_run_context.result_factory = await ResultFactory.from_flow(
545
- flow, client=client
546
- )
547
-
548
- if log_prints:
549
- stack.enter_context(patch_print())
550
-
551
- terminal_or_paused_state = await orchestrate_flow_run(
552
- flow,
553
- flow_run=flow_run,
554
- parameters=parameters,
555
- wait_for=None,
556
- client=client,
557
- partial_flow_run_context=flow_run_context,
558
- # Orchestration needs to be interruptible if it has a timeout
559
- interruptible=flow.timeout_seconds is not None,
560
- user_thread=user_thread,
561
- )
562
-
563
- if terminal_or_paused_state.is_paused():
564
- timeout = terminal_or_paused_state.state_details.pause_timeout
565
- msg = "Currently paused and suspending execution."
566
- if timeout:
567
- msg += f" Resume before {timeout.to_rfc3339_string()} to finish execution."
568
- logger.log(level=logging.INFO, msg=msg)
569
- await APILogHandler.aflush()
570
-
571
- return terminal_or_paused_state
572
- else:
573
- terminal_state = terminal_or_paused_state
574
-
575
- # If debugging, use the more complete `repr` than the usual `str` description
576
- display_state = repr(terminal_state) if PREFECT_DEBUG_MODE else str(terminal_state)
577
-
578
- logger.log(
579
- level=logging.INFO if terminal_state.is_completed() else logging.ERROR,
580
- msg=f"Finished in state {display_state}",
581
- )
582
-
583
- # When a "root" flow run finishes, flush logs so we do not have to rely on handling
584
- # during interpreter shutdown
585
- await APILogHandler.aflush()
586
-
587
- return terminal_state
588
-
589
-
590
- @inject_client
591
- async def create_and_begin_subflow_run(
592
- flow: Flow,
593
- parameters: Dict[str, Any],
594
- wait_for: Optional[Iterable[PrefectFuture]],
595
- return_type: EngineReturnType,
596
- client: PrefectClient,
597
- user_thread: threading.Thread,
598
- ) -> Any:
599
- """
600
- Async entrypoint for flows calls within a flow run
601
-
602
- Subflows differ from parent flows in that they
603
- - Resolve futures in passed parameters into values
604
- - Create a dummy task for representation in the parent flow
605
- - Retrieve default result storage from the parent flow rather than the server
606
-
607
- Returns:
608
- The final state of the run
609
- """
610
- parent_flow_run_context = FlowRunContext.get()
611
- parent_logger = get_run_logger(parent_flow_run_context)
612
- log_prints = should_log_prints(flow)
613
- terminal_state = None
614
-
615
- parent_logger.debug(f"Resolving inputs to {flow.name!r}")
616
- task_inputs = {k: await collect_task_run_inputs(v) for k, v in parameters.items()}
617
-
618
- if wait_for:
619
- task_inputs["wait_for"] = await collect_task_run_inputs(wait_for)
620
-
621
- rerunning = (
622
- parent_flow_run_context.flow_run.run_count > 1
623
- if getattr(parent_flow_run_context, "flow_run", None)
624
- else False
625
- )
626
-
627
- # Generate a task in the parent flow run to represent the result of the subflow run
628
- dummy_task = Task(name=flow.name, fn=flow.fn, version=flow.version)
629
- parent_task_run = await client.create_task_run(
630
- task=dummy_task,
631
- flow_run_id=(
632
- parent_flow_run_context.flow_run.id
633
- if getattr(parent_flow_run_context, "flow_run", None)
634
- else None
635
- ),
636
- dynamic_key=_dynamic_key_for_task_run(parent_flow_run_context, dummy_task),
637
- task_inputs=task_inputs,
638
- state=Pending(),
639
- )
640
-
641
- # Resolve any task futures in the input
642
- parameters = await resolve_inputs(parameters)
643
-
644
- if parent_task_run.state.is_final() and not (
645
- rerunning and not parent_task_run.state.is_completed()
646
- ):
647
- # Retrieve the most recent flow run from the database
648
- flow_runs = await client.read_flow_runs(
649
- flow_run_filter=FlowRunFilter(
650
- parent_task_run_id={"any_": [parent_task_run.id]}
651
- ),
652
- sort=FlowRunSort.EXPECTED_START_TIME_ASC,
653
- )
654
- flow_run = flow_runs[-1]
655
-
656
- # Set up variables required downstream
657
- terminal_state = flow_run.state
658
- logger = flow_run_logger(flow_run, flow)
659
-
660
- else:
661
- flow_run = await client.create_flow_run(
662
- flow,
663
- parameters=flow.serialize_parameters(parameters),
664
- parent_task_run_id=parent_task_run.id,
665
- state=parent_task_run.state if not rerunning else Pending(),
666
- tags=TagsContext.get().current_tags,
667
- )
668
-
669
- parent_logger.info(
670
- f"Created subflow run {flow_run.name!r} for flow {flow.name!r}"
671
- )
672
-
673
- logger = flow_run_logger(flow_run, flow)
674
- ui_url = PREFECT_UI_URL.value()
675
- if ui_url:
676
- logger.info(
677
- f"View at {ui_url}/flow-runs/flow-run/{flow_run.id}",
678
- extra={"send_to_api": False},
679
- )
680
-
681
- result_factory = await ResultFactory.from_flow(
682
- flow, client=parent_flow_run_context.client
683
- )
684
-
685
- if flow.should_validate_parameters:
686
- try:
687
- parameters = flow.validate_parameters(parameters)
688
- except Exception:
689
- message = "Validation of flow parameters failed with error:"
690
- logger.exception(message)
691
- terminal_state = await propose_state(
692
- client,
693
- state=await exception_to_failed_state(
694
- message=message, result_factory=result_factory
695
- ),
696
- flow_run_id=flow_run.id,
697
- )
698
-
699
- if terminal_state is None or not terminal_state.is_final():
700
- async with AsyncExitStack() as stack:
701
- await stack.enter_async_context(
702
- report_flow_run_crashes(flow_run=flow_run, client=client, flow=flow)
703
- )
704
-
705
- task_runner = flow.task_runner.duplicate()
706
- if task_runner is NotImplemented:
707
- # Backwards compatibility; will not support concurrent flow runs
708
- task_runner = flow.task_runner
709
- logger.warning(
710
- f"Task runner {type(task_runner).__name__!r} does not implement"
711
- " the `duplicate` method and will fail if used for concurrent"
712
- " execution of the same flow."
713
- )
714
-
715
- await stack.enter_async_context(task_runner.start())
716
-
717
- if log_prints:
718
- stack.enter_context(patch_print())
719
-
720
- terminal_state = await orchestrate_flow_run(
721
- flow,
722
- flow_run=flow_run,
723
- parameters=parameters,
724
- wait_for=wait_for,
725
- # If the parent flow run has a timeout, then this one needs to be
726
- # interruptible as well
727
- interruptible=parent_flow_run_context.timeout_scope is not None,
728
- client=client,
729
- partial_flow_run_context=FlowRunContext.construct(
730
- sync_portal=parent_flow_run_context.sync_portal,
731
- task_runner=task_runner,
732
- background_tasks=parent_flow_run_context.background_tasks,
733
- result_factory=result_factory,
734
- log_prints=log_prints,
735
- ),
736
- user_thread=user_thread,
737
- )
738
-
739
- # Display the full state (including the result) if debugging
740
- display_state = repr(terminal_state) if PREFECT_DEBUG_MODE else str(terminal_state)
741
- logger.log(
742
- level=logging.INFO if terminal_state.is_completed() else logging.ERROR,
743
- msg=f"Finished in state {display_state}",
744
- )
745
-
746
- # Track the subflow state so the parent flow can use it to determine its final state
747
- parent_flow_run_context.flow_run_states.append(terminal_state)
748
-
749
- if return_type == "state":
750
- return terminal_state
751
- elif return_type == "result":
752
- return await terminal_state.result(fetch=True)
753
- else:
754
- raise ValueError(f"Invalid return type for flow engine {return_type!r}.")
755
-
756
-
757
- async def orchestrate_flow_run(
758
- flow: Flow,
759
- flow_run: FlowRun,
760
- parameters: Dict[str, Any],
761
- wait_for: Optional[Iterable[PrefectFuture]],
762
- interruptible: bool,
763
- client: PrefectClient,
764
- partial_flow_run_context: FlowRunContext,
765
- user_thread: threading.Thread,
766
- ) -> State:
767
- """
768
- Executes a flow run.
769
-
770
- Note on flow timeouts:
771
- Since async flows are run directly in the main event loop, timeout behavior will
772
- match that described by anyio. If the flow is awaiting something, it will
773
- immediately return; otherwise, the next time it awaits it will exit. Sync flows
774
- are being task runner in a worker thread, which cannot be interrupted. The worker
775
- thread will exit at the next task call. The worker thread also has access to the
776
- status of the cancellation scope at `FlowRunContext.timeout_scope.cancel_called`
777
- which allows it to raise a `TimeoutError` to respect the timeout.
778
-
779
- Returns:
780
- The final state of the run
781
- """
782
-
783
- logger = flow_run_logger(flow_run, flow)
784
-
785
- flow_run_context = None
786
- parent_flow_run_context = FlowRunContext.get()
787
-
788
- try:
789
- # Resolve futures in any non-data dependencies to ensure they are ready
790
- if wait_for is not None:
791
- await resolve_inputs({"wait_for": wait_for}, return_data=False)
792
- except UpstreamTaskError as upstream_exc:
793
- return await propose_state(
794
- client,
795
- Pending(name="NotReady", message=str(upstream_exc)),
796
- flow_run_id=flow_run.id,
797
- # if orchestrating a run already in a pending state, force orchestration to
798
- # update the state name
799
- force=flow_run.state.is_pending(),
800
- )
801
-
802
- state = await propose_state(client, Running(), flow_run_id=flow_run.id)
803
-
804
- # flag to ensure we only update the flow run name once
805
- run_name_set = False
806
-
807
- await _run_flow_hooks(flow=flow, flow_run=flow_run, state=state)
808
-
809
- while state.is_running():
810
- waited_for_task_runs = False
811
-
812
- # Update the flow run to the latest data
813
- flow_run = await client.read_flow_run(flow_run.id)
814
- try:
815
- with FlowRunContext(
816
- **{
817
- **partial_flow_run_context.dict(),
818
- **{
819
- "flow_run": flow_run,
820
- "flow": flow,
821
- "client": client,
822
- "parameters": parameters,
823
- },
824
- }
825
- ) as flow_run_context:
826
- # update flow run name
827
- if not run_name_set and flow.flow_run_name:
828
- flow_run_name = _resolve_custom_flow_run_name(
829
- flow=flow, parameters=parameters
830
- )
831
-
832
- await client.update_flow_run(
833
- flow_run_id=flow_run.id, name=flow_run_name
834
- )
835
- logger.extra["flow_run_name"] = flow_run_name
836
- logger.debug(
837
- f"Renamed flow run {flow_run.name!r} to {flow_run_name!r}"
838
- )
839
- flow_run.name = flow_run_name
840
- run_name_set = True
841
-
842
- args, kwargs = parameters_to_args_kwargs(flow.fn, parameters)
843
- logger.debug(
844
- f"Executing flow {flow.name!r} for flow run {flow_run.name!r}..."
845
- )
846
-
847
- if PREFECT_DEBUG_MODE:
848
- logger.debug(f"Executing {call_repr(flow.fn, *args, **kwargs)}")
849
- else:
850
- logger.debug(
851
- "Beginning execution...", extra={"state_message": True}
852
- )
853
-
854
- flow_call = create_call(flow.fn, *args, **kwargs)
855
-
856
- # This check for a parent call is needed for cases where the engine
857
- # was entered directly during testing
858
- parent_call = get_current_call()
859
-
860
- if parent_call and (
861
- not parent_flow_run_context
862
- or (
863
- getattr(parent_flow_run_context, "flow", None)
864
- and parent_flow_run_context.flow.isasync == flow.isasync
865
- )
866
- ):
867
- from_async.call_soon_in_waiting_thread(
868
- flow_call,
869
- thread=user_thread,
870
- timeout=flow.timeout_seconds,
871
- )
872
- else:
873
- from_async.call_soon_in_new_thread(
874
- flow_call, timeout=flow.timeout_seconds
875
- )
876
-
877
- result = await flow_call.aresult()
878
-
879
- waited_for_task_runs = await wait_for_task_runs_and_report_crashes(
880
- flow_run_context.task_run_futures, client=client
881
- )
882
- except PausedRun as exc:
883
- # could get raised either via utility or by returning Paused from a task run
884
- # if a task run pauses, we set its state as the flow's state
885
- # to preserve reschedule and timeout behavior
886
- paused_flow_run = await client.read_flow_run(flow_run.id)
887
- if paused_flow_run.state.is_running():
888
- state = await propose_state(
889
- client,
890
- state=exc.state,
891
- flow_run_id=flow_run.id,
892
- )
893
-
894
- return state
895
- paused_flow_run_state = paused_flow_run.state
896
- return paused_flow_run_state
897
- except CancelledError as exc:
898
- if not flow_call.timedout():
899
- # If the flow call was not cancelled by us; this is a crash
900
- raise
901
- # Construct a new exception as `TimeoutError`
902
- original = exc
903
- exc = TimeoutError()
904
- exc.__cause__ = original
905
- logger.exception("Encountered exception during execution:")
906
- terminal_state = await exception_to_failed_state(
907
- exc,
908
- message=f"Flow run exceeded timeout of {flow.timeout_seconds} seconds",
909
- result_factory=flow_run_context.result_factory,
910
- name="TimedOut",
911
- )
912
- except Exception:
913
- # Generic exception in user code
914
- logger.exception("Encountered exception during execution:")
915
- terminal_state = await exception_to_failed_state(
916
- message="Flow run encountered an exception.",
917
- result_factory=flow_run_context.result_factory,
918
- )
919
- else:
920
- if result is None:
921
- # All tasks and subflows are reference tasks if there is no return value
922
- # If there are no tasks, use `None` instead of an empty iterable
923
- result = (
924
- flow_run_context.task_run_futures
925
- + flow_run_context.task_run_states
926
- + flow_run_context.flow_run_states
927
- ) or None
928
-
929
- terminal_state = await return_value_to_state(
930
- await resolve_futures_to_states(result),
931
- result_factory=flow_run_context.result_factory,
932
- )
933
-
934
- if not waited_for_task_runs:
935
- # An exception occurred that prevented us from waiting for task runs to
936
- # complete. Ensure that we wait for them before proposing a final state
937
- # for the flow run.
938
- await wait_for_task_runs_and_report_crashes(
939
- flow_run_context.task_run_futures, client=client
940
- )
941
-
942
- # Before setting the flow run state, store state.data using
943
- # block storage and send the resulting data document to the Prefect API instead.
944
- # This prevents the pickled return value of flow runs
945
- # from being sent to the Prefect API and stored in the Prefect database.
946
- # state.data is left as is, otherwise we would have to load
947
- # the data from block storage again after storing.
948
- state = await propose_state(
949
- client,
950
- state=terminal_state,
951
- flow_run_id=flow_run.id,
952
- )
953
-
954
- await _run_flow_hooks(flow=flow, flow_run=flow_run, state=state)
955
-
956
- if state.type != terminal_state.type and PREFECT_DEBUG_MODE:
957
- logger.debug(
958
- (
959
- f"Received new state {state} when proposing final state"
960
- f" {terminal_state}"
961
- ),
962
- extra={"send_to_api": False},
963
- )
964
-
965
- if not state.is_final() and not state.is_paused():
966
- logger.info(
967
- (
968
- f"Received non-final state {state.name!r} when proposing final"
969
- f" state {terminal_state.name!r} and will attempt to run again..."
970
- ),
971
- )
972
- # Attempt to enter a running state again
973
- state = await propose_state(client, Running(), flow_run_id=flow_run.id)
974
-
975
- return state
976
-
977
-
978
- @overload
979
- async def pause_flow_run(
980
- wait_for_input: None = None,
981
- flow_run_id: UUID = None,
982
- timeout: int = 3600,
983
- poll_interval: int = 10,
984
- reschedule: bool = False,
985
- key: str = None,
986
- ) -> None:
987
- ...
988
-
989
-
990
- @overload
991
- async def pause_flow_run(
992
- wait_for_input: Type[T],
993
- flow_run_id: UUID = None,
994
- timeout: int = 3600,
995
- poll_interval: int = 10,
996
- reschedule: bool = False,
997
- key: str = None,
998
- ) -> T:
999
- ...
1000
-
1001
-
1002
- @sync_compatible
1003
- @deprecated_parameter(
1004
- "flow_run_id", start_date="Dec 2023", help="Use `suspend_flow_run` instead."
1005
- )
1006
- @deprecated_parameter(
1007
- "reschedule",
1008
- start_date="Dec 2023",
1009
- when=lambda p: p is True,
1010
- help="Use `suspend_flow_run` instead.",
1011
- )
1012
- @experimental_parameter(
1013
- "wait_for_input", group="flow_run_input", when=lambda y: y is not None
1014
- )
1015
- async def pause_flow_run(
1016
- wait_for_input: Optional[Type[T]] = None,
1017
- flow_run_id: UUID = None,
1018
- timeout: int = 3600,
1019
- poll_interval: int = 10,
1020
- reschedule: bool = False,
1021
- key: str = None,
1022
- ) -> Optional[T]:
1023
- """
1024
- Pauses the current flow run by blocking execution until resumed.
1025
-
1026
- When called within a flow run, execution will block and no downstream tasks will
1027
- run until the flow is resumed. Task runs that have already started will continue
1028
- running. A timeout parameter can be passed that will fail the flow run if it has not
1029
- been resumed within the specified time.
1030
-
1031
- Args:
1032
- flow_run_id: a flow run id. If supplied, this function will attempt to pause
1033
- the specified flow run outside of the flow run process. When paused, the
1034
- flow run will continue execution until the NEXT task is orchestrated, at
1035
- which point the flow will exit. Any tasks that have already started will
1036
- run until completion. When resumed, the flow run will be rescheduled to
1037
- finish execution. In order pause a flow run in this way, the flow needs to
1038
- have an associated deployment and results need to be configured with the
1039
- `persist_results` option.
1040
- timeout: the number of seconds to wait for the flow to be resumed before
1041
- failing. Defaults to 1 hour (3600 seconds). If the pause timeout exceeds
1042
- any configured flow-level timeout, the flow might fail even after resuming.
1043
- poll_interval: The number of seconds between checking whether the flow has been
1044
- resumed. Defaults to 10 seconds.
1045
- reschedule: Flag that will reschedule the flow run if resumed. Instead of
1046
- blocking execution, the flow will gracefully exit (with no result returned)
1047
- instead. To use this flag, a flow needs to have an associated deployment and
1048
- results need to be configured with the `persist_results` option.
1049
- key: An optional key to prevent calling pauses more than once. This defaults to
1050
- the number of pauses observed by the flow so far, and prevents pauses that
1051
- use the "reschedule" option from running the same pause twice. A custom key
1052
- can be supplied for custom pausing behavior.
1053
- wait_for_input: a subclass of `RunInput` or any type supported by
1054
- Pydantic. If provided when the flow pauses, the flow will wait for the
1055
- input to be provided before resuming. If the flow is resumed without
1056
- providing the input, the flow will fail. If the flow is resumed with the
1057
- input, the flow will resume and the input will be loaded and returned
1058
- from this function.
1059
-
1060
- Example:
1061
- ```python
1062
- @task
1063
- def task_one():
1064
- for i in range(3):
1065
- sleep(1)
1066
-
1067
- @flow
1068
- def my_flow():
1069
- terminal_state = task_one.submit(return_state=True)
1070
- if terminal_state.type == StateType.COMPLETED:
1071
- print("Task one succeeded! Pausing flow run..")
1072
- pause_flow_run(timeout=2)
1073
- else:
1074
- print("Task one failed. Skipping pause flow run..")
1075
- ```
1076
-
1077
- """
1078
- if flow_run_id:
1079
- if wait_for_input is not None:
1080
- raise RuntimeError("Cannot wait for input when pausing out of process.")
1081
-
1082
- return await _out_of_process_pause(
1083
- flow_run_id=flow_run_id,
1084
- timeout=timeout,
1085
- reschedule=reschedule,
1086
- key=key,
1087
- )
1088
- else:
1089
- return await _in_process_pause(
1090
- timeout=timeout,
1091
- poll_interval=poll_interval,
1092
- reschedule=reschedule,
1093
- key=key,
1094
- wait_for_input=wait_for_input,
1095
- )
1096
-
1097
-
1098
- @inject_client
1099
- async def _in_process_pause(
1100
- timeout: int = 3600,
1101
- poll_interval: int = 10,
1102
- reschedule=False,
1103
- key: str = None,
1104
- client=None,
1105
- wait_for_input: Optional[T] = None,
1106
- ) -> Optional[T]:
1107
- if TaskRunContext.get():
1108
- raise RuntimeError("Cannot pause task runs.")
1109
-
1110
- context = FlowRunContext.get()
1111
- if not context:
1112
- raise RuntimeError("Flow runs can only be paused from within a flow run.")
1113
-
1114
- logger = get_run_logger(context=context)
1115
-
1116
- pause_counter = _observed_flow_pauses(context)
1117
- pause_key = key or str(pause_counter)
1118
-
1119
- logger.info("Pausing flow, execution will continue when this flow run is resumed.")
1120
-
1121
- proposed_state = Paused(
1122
- timeout_seconds=timeout, reschedule=reschedule, pause_key=pause_key
1123
- )
1124
-
1125
- if wait_for_input:
1126
- wait_for_input = run_input_subclass_from_type(wait_for_input)
1127
- run_input_keyset = keyset_from_paused_state(proposed_state)
1128
- proposed_state.state_details.run_input_keyset = run_input_keyset
1129
-
1130
- try:
1131
- state = await propose_state(
1132
- client=client,
1133
- state=proposed_state,
1134
- flow_run_id=context.flow_run.id,
1135
- )
1136
- except Abort as exc:
1137
- # Aborted pause requests mean the pause is not allowed
1138
- raise RuntimeError(f"Flow run cannot be paused: {exc}")
1139
-
1140
- if state.is_running():
1141
- # The orchestrator rejected the paused state which means that this
1142
- # pause has happened before (via reschedule) and the flow run has
1143
- # been resumed.
1144
- if wait_for_input:
1145
- # The flow run wanted input, so we need to load it and return it
1146
- # to the user.
1147
- await wait_for_input.load(run_input_keyset)
1148
-
1149
- return
1150
-
1151
- if not state.is_paused():
1152
- # If we receive anything but a PAUSED state, we are unable to continue
1153
- raise RuntimeError(
1154
- f"Flow run cannot be paused. Received non-paused state from API: {state}"
1155
- )
1156
-
1157
- if wait_for_input:
1158
- # We're now in a paused state and the flow run is waiting for input.
1159
- # Save the schema of the users `RunInput` subclass, stored in
1160
- # `wait_for_input`, so the UI can display the form and we can validate
1161
- # the input when the flow is resumed.
1162
- await wait_for_input.save(run_input_keyset)
1163
-
1164
- if reschedule:
1165
- # If a rescheduled pause, exit this process so the run can be resubmitted later
1166
- raise Pause(state=state)
1167
-
1168
- # Otherwise, block and check for completion on an interval
1169
- with anyio.move_on_after(timeout):
1170
- # attempt to check if a flow has resumed at least once
1171
- initial_sleep = min(timeout / 2, poll_interval)
1172
- await anyio.sleep(initial_sleep)
1173
- while True:
1174
- flow_run = await client.read_flow_run(context.flow_run.id)
1175
- if flow_run.state.is_running():
1176
- logger.info("Resuming flow run execution!")
1177
- if wait_for_input:
1178
- return await wait_for_input.load(run_input_keyset)
1179
- return
1180
- await anyio.sleep(poll_interval)
1181
-
1182
- # check one last time before failing the flow
1183
- flow_run = await client.read_flow_run(context.flow_run.id)
1184
- if flow_run.state.is_running():
1185
- logger.info("Resuming flow run execution!")
1186
- if wait_for_input:
1187
- return await wait_for_input.load(run_input_keyset)
1188
- return
1189
-
1190
- raise FlowPauseTimeout("Flow run was paused and never resumed.")
1191
-
1192
-
1193
- @inject_client
1194
- async def _out_of_process_pause(
1195
- flow_run_id: UUID,
1196
- timeout: int = 3600,
1197
- reschedule: bool = True,
1198
- key: str = None,
1199
- client=None,
1200
- ):
1201
- if reschedule:
1202
- raise RuntimeError(
1203
- "Pausing a flow run out of process requires the `reschedule` option set to"
1204
- " True."
1205
- )
1206
-
1207
- response = await client.set_flow_run_state(
1208
- flow_run_id,
1209
- Paused(timeout_seconds=timeout, reschedule=True, pause_key=key),
1210
- )
1211
- if response.status != SetStateStatus.ACCEPT:
1212
- raise RuntimeError(response.details.reason)
1213
-
1214
-
1215
- @overload
1216
- async def suspend_flow_run(
1217
- wait_for_input: None = None,
1218
- flow_run_id: Optional[UUID] = None,
1219
- timeout: Optional[int] = 3600,
1220
- key: Optional[str] = None,
1221
- client: PrefectClient = None,
1222
- ) -> None:
1223
- ...
1224
-
1225
-
1226
- @overload
1227
- async def suspend_flow_run(
1228
- wait_for_input: Type[T],
1229
- flow_run_id: Optional[UUID] = None,
1230
- timeout: Optional[int] = 3600,
1231
- key: Optional[str] = None,
1232
- client: PrefectClient = None,
1233
- ) -> T:
1234
- ...
1235
-
1236
-
1237
- @sync_compatible
1238
- @inject_client
1239
- @experimental_parameter(
1240
- "wait_for_input", group="flow_run_input", when=lambda y: y is not None
1241
- )
1242
- async def suspend_flow_run(
1243
- wait_for_input: Optional[Type[T]] = None,
1244
- flow_run_id: Optional[UUID] = None,
1245
- timeout: Optional[int] = 3600,
1246
- key: Optional[str] = None,
1247
- client: PrefectClient = None,
1248
- ) -> Optional[T]:
1249
- """
1250
- Suspends a flow run by stopping code execution until resumed.
1251
-
1252
- When suspended, the flow run will continue execution until the NEXT task is
1253
- orchestrated, at which point the flow will exit. Any tasks that have
1254
- already started will run until completion. When resumed, the flow run will
1255
- be rescheduled to finish execution. In order suspend a flow run in this
1256
- way, the flow needs to have an associated deployment and results need to be
1257
- configured with the `persist_results` option.
1258
-
1259
- Args:
1260
- flow_run_id: a flow run id. If supplied, this function will attempt to
1261
- suspend the specified flow run. If not supplied will attempt to
1262
- suspend the current flow run.
1263
- timeout: the number of seconds to wait for the flow to be resumed before
1264
- failing. Defaults to 1 hour (3600 seconds). If the pause timeout
1265
- exceeds any configured flow-level timeout, the flow might fail even
1266
- after resuming.
1267
- key: An optional key to prevent calling suspend more than once. This
1268
- defaults to a random string and prevents suspends from running the
1269
- same suspend twice. A custom key can be supplied for custom
1270
- suspending behavior.
1271
- wait_for_input: a subclass of `RunInput` or any type supported by
1272
- Pydantic. If provided when the flow suspends, the flow will remain
1273
- suspended until receiving the input before resuming. If the flow is
1274
- resumed without providing the input, the flow will fail. If the flow is
1275
- resumed with the input, the flow will resume and the input will be
1276
- loaded and returned from this function.
1277
- """
1278
- context = FlowRunContext.get()
1279
-
1280
- if flow_run_id is None:
1281
- if TaskRunContext.get():
1282
- raise RuntimeError("Cannot suspend task runs.")
1283
-
1284
- if context is None or context.flow_run is None:
1285
- raise RuntimeError(
1286
- "Flow runs can only be suspended from within a flow run."
1287
- )
1288
-
1289
- logger = get_run_logger(context=context)
1290
- logger.info(
1291
- "Suspending flow run, execution will be rescheduled when this flow run is"
1292
- " resumed."
1293
- )
1294
- flow_run_id = context.flow_run.id
1295
- suspending_current_flow_run = True
1296
- pause_counter = _observed_flow_pauses(context)
1297
- pause_key = key or str(pause_counter)
1298
- else:
1299
- # Since we're suspending another flow run we need to generate a pause
1300
- # key that won't conflict with whatever suspends/pauses that flow may
1301
- # have. Since this method won't be called during that flow run it's
1302
- # okay that this is non-deterministic.
1303
- suspending_current_flow_run = False
1304
- pause_key = key or str(uuid4())
1305
-
1306
- proposed_state = Suspended(timeout_seconds=timeout, pause_key=pause_key)
1307
-
1308
- if wait_for_input:
1309
- wait_for_input = run_input_subclass_from_type(wait_for_input)
1310
- run_input_keyset = keyset_from_paused_state(proposed_state)
1311
- proposed_state.state_details.run_input_keyset = run_input_keyset
1312
-
1313
- try:
1314
- state = await propose_state(
1315
- client=client,
1316
- state=proposed_state,
1317
- flow_run_id=flow_run_id,
1318
- )
1319
- except Abort as exc:
1320
- # Aborted requests mean the suspension is not allowed
1321
- raise RuntimeError(f"Flow run cannot be suspended: {exc}")
1322
-
1323
- if state.is_running():
1324
- # The orchestrator rejected the suspended state which means that this
1325
- # suspend has happened before and the flow run has been resumed.
1326
- if wait_for_input:
1327
- # The flow run wanted input, so we need to load it and return it
1328
- # to the user.
1329
- return await wait_for_input.load(run_input_keyset)
1330
- return
1331
-
1332
- if not state.is_paused():
1333
- # If we receive anything but a PAUSED state, we are unable to continue
1334
- raise RuntimeError(
1335
- f"Flow run cannot be suspended. Received unexpected state from API: {state}"
1336
- )
1337
-
1338
- if wait_for_input:
1339
- await wait_for_input.save(run_input_keyset)
1340
-
1341
- if suspending_current_flow_run:
1342
- # Exit this process so the run can be resubmitted later
1343
- raise Pause()
1344
-
1345
-
1346
- @sync_compatible
1347
- async def resume_flow_run(flow_run_id, run_input: Optional[Dict] = None):
1348
- """
1349
- Resumes a paused flow.
1350
-
1351
- Args:
1352
- flow_run_id: the flow_run_id to resume
1353
- run_input: a dictionary of inputs to provide to the flow run.
1354
- """
1355
- client = get_client()
1356
- async with client:
1357
- flow_run = await client.read_flow_run(flow_run_id)
1358
-
1359
- if not flow_run.state.is_paused():
1360
- raise NotPausedError("Cannot resume a run that isn't paused!")
1361
-
1362
- response = await client.resume_flow_run(flow_run_id, run_input=run_input)
1363
-
1364
- if response.status == SetStateStatus.REJECT:
1365
- if response.state.type == StateType.FAILED:
1366
- raise FlowPauseTimeout("Flow run can no longer be resumed.")
1367
- else:
1368
- raise RuntimeError(f"Cannot resume this run: {response.details.reason}")
1369
-
1370
-
1371
- def enter_task_run_engine(
1372
- task: Task,
1373
- parameters: Dict[str, Any],
1374
- wait_for: Optional[Iterable[PrefectFuture]],
1375
- return_type: EngineReturnType,
1376
- task_runner: Optional[BaseTaskRunner],
1377
- mapped: bool,
1378
- ) -> Union[PrefectFuture, Awaitable[PrefectFuture], TaskRun]:
1379
- """Sync entrypoint for task calls"""
1380
-
1381
- flow_run_context = FlowRunContext.get()
1382
-
1383
- if not flow_run_context:
1384
- if return_type == "future" or mapped:
1385
- raise RuntimeError(
1386
- " If you meant to submit a background task, you need to set"
1387
- " `prefect config set PREFECT_EXPERIMENTAL_ENABLE_TASK_SCHEDULING=true`"
1388
- " and use `your_task.submit()` instead of `your_task()`."
1389
- )
1390
- from prefect.task_engine import submit_autonomous_task_run_to_engine
1391
-
1392
- return submit_autonomous_task_run_to_engine(
1393
- task=task,
1394
- task_run=None,
1395
- parameters=parameters,
1396
- task_runner=task_runner,
1397
- wait_for=wait_for,
1398
- return_type=return_type,
1399
- client=get_client(),
1400
- )
1401
-
1402
- if flow_run_context.timeout_scope and flow_run_context.timeout_scope.cancel_called:
1403
- raise TimeoutError("Flow run timed out")
1404
-
1405
- begin_run = create_call(
1406
- begin_task_map if mapped else get_task_call_return_value,
1407
- task=task,
1408
- flow_run_context=flow_run_context,
1409
- parameters=parameters,
1410
- wait_for=wait_for,
1411
- return_type=return_type,
1412
- task_runner=task_runner,
1413
- )
1414
-
1415
- if task.isasync and (
1416
- flow_run_context.flow is None or flow_run_context.flow.isasync
1417
- ):
1418
- # return a coro for the user to await if an async task in an async flow
1419
- return from_async.wait_for_call_in_loop_thread(begin_run)
1420
- else:
1421
- return from_sync.wait_for_call_in_loop_thread(begin_run)
1422
-
1423
-
1424
- async def begin_task_map(
1425
- task: Task,
1426
- flow_run_context: Optional[FlowRunContext],
1427
- parameters: Dict[str, Any],
1428
- wait_for: Optional[Iterable[PrefectFuture]],
1429
- return_type: EngineReturnType,
1430
- task_runner: Optional[BaseTaskRunner],
1431
- autonomous: bool = False,
1432
- ) -> List[Union[PrefectFuture, Awaitable[PrefectFuture], TaskRun]]:
1433
- """Async entrypoint for task mapping"""
1434
- # We need to resolve some futures to map over their data, collect the upstream
1435
- # links beforehand to retain relationship tracking.
1436
- task_inputs = {
1437
- k: await collect_task_run_inputs(v, max_depth=0) for k, v in parameters.items()
1438
- }
1439
-
1440
- # Resolve the top-level parameters in order to get mappable data of a known length.
1441
- # Nested parameters will be resolved in each mapped child where their relationships
1442
- # will also be tracked.
1443
- parameters = await resolve_inputs(parameters, max_depth=1)
1444
-
1445
- # Ensure that any parameters in kwargs are expanded before this check
1446
- parameters = explode_variadic_parameter(task.fn, parameters)
1447
-
1448
- iterable_parameters = {}
1449
- static_parameters = {}
1450
- annotated_parameters = {}
1451
- for key, val in parameters.items():
1452
- if isinstance(val, (allow_failure, quote)):
1453
- # Unwrap annotated parameters to determine if they are iterable
1454
- annotated_parameters[key] = val
1455
- val = val.unwrap()
1456
-
1457
- if isinstance(val, unmapped):
1458
- static_parameters[key] = val.value
1459
- elif isiterable(val):
1460
- iterable_parameters[key] = list(val)
1461
- else:
1462
- static_parameters[key] = val
1463
-
1464
- if not len(iterable_parameters):
1465
- raise MappingMissingIterable(
1466
- "No iterable parameters were received. Parameters for map must "
1467
- f"include at least one iterable. Parameters: {parameters}"
1468
- )
1469
-
1470
- iterable_parameter_lengths = {
1471
- key: len(val) for key, val in iterable_parameters.items()
1472
- }
1473
- lengths = set(iterable_parameter_lengths.values())
1474
- if len(lengths) > 1:
1475
- raise MappingLengthMismatch(
1476
- "Received iterable parameters with different lengths. Parameters for map"
1477
- f" must all be the same length. Got lengths: {iterable_parameter_lengths}"
1478
- )
1479
-
1480
- map_length = list(lengths)[0]
1481
-
1482
- task_runs = []
1483
- for i in range(map_length):
1484
- call_parameters = {key: value[i] for key, value in iterable_parameters.items()}
1485
- call_parameters.update({key: value for key, value in static_parameters.items()})
1486
-
1487
- # Add default values for parameters; these are skipped earlier since they should
1488
- # not be mapped over
1489
- for key, value in get_parameter_defaults(task.fn).items():
1490
- call_parameters.setdefault(key, value)
1491
-
1492
- # Re-apply annotations to each key again
1493
- for key, annotation in annotated_parameters.items():
1494
- call_parameters[key] = annotation.rewrap(call_parameters[key])
1495
-
1496
- # Collapse any previously exploded kwargs
1497
- call_parameters = collapse_variadic_parameters(task.fn, call_parameters)
1498
-
1499
- if autonomous:
1500
- task_runs.append(
1501
- await create_autonomous_task_run(
1502
- task=task,
1503
- parameters=call_parameters,
1504
- )
1505
- )
1506
- else:
1507
- task_runs.append(
1508
- partial(
1509
- get_task_call_return_value,
1510
- task=task,
1511
- flow_run_context=flow_run_context,
1512
- parameters=call_parameters,
1513
- wait_for=wait_for,
1514
- return_type=return_type,
1515
- task_runner=task_runner,
1516
- extra_task_inputs=task_inputs,
1517
- )
1518
- )
1519
-
1520
- if autonomous:
1521
- return task_runs
1522
-
1523
- # Maintain the order of the task runs when using the sequential task runner
1524
- runner = task_runner if task_runner else flow_run_context.task_runner
1525
- if runner.concurrency_type == TaskConcurrencyType.SEQUENTIAL:
1526
- return [await task_run() for task_run in task_runs]
1527
-
1528
- return await gather(*task_runs)
1529
-
1530
-
1531
- async def get_task_call_return_value(
1532
- task: Task,
1533
- flow_run_context: FlowRunContext,
1534
- parameters: Dict[str, Any],
1535
- wait_for: Optional[Iterable[PrefectFuture]],
1536
- return_type: EngineReturnType,
1537
- task_runner: Optional[BaseTaskRunner],
1538
- extra_task_inputs: Optional[Dict[str, Set[TaskRunInput]]] = None,
1539
- ):
1540
- extra_task_inputs = extra_task_inputs or {}
1541
-
1542
- future = await create_task_run_future(
1543
- task=task,
1544
- flow_run_context=flow_run_context,
1545
- parameters=parameters,
1546
- wait_for=wait_for,
1547
- task_runner=task_runner,
1548
- extra_task_inputs=extra_task_inputs,
1549
- )
1550
- if return_type == "future":
1551
- return future
1552
- elif return_type == "state":
1553
- return await future._wait()
1554
- elif return_type == "result":
1555
- return await future._result()
1556
- else:
1557
- raise ValueError(f"Invalid return type for task engine {return_type!r}.")
1558
-
1559
-
1560
- async def create_task_run_future(
1561
- task: Task,
1562
- flow_run_context: FlowRunContext,
1563
- parameters: Dict[str, Any],
1564
- wait_for: Optional[Iterable[PrefectFuture]],
1565
- task_runner: Optional[BaseTaskRunner],
1566
- extra_task_inputs: Dict[str, Set[TaskRunInput]],
1567
- ) -> PrefectFuture:
1568
- # Default to the flow run's task runner
1569
- task_runner = task_runner or flow_run_context.task_runner
1570
-
1571
- # Generate a name for the future
1572
- dynamic_key = _dynamic_key_for_task_run(flow_run_context, task)
1573
- task_run_name = (
1574
- f"{task.name}-{dynamic_key}"
1575
- if flow_run_context and flow_run_context.flow_run
1576
- else f"{task.name}-{dynamic_key[:NUM_CHARS_DYNAMIC_KEY]}" # autonomous task run
1577
- )
1578
-
1579
- # Generate a future
1580
- future = PrefectFuture(
1581
- name=task_run_name,
1582
- key=uuid4(),
1583
- task_runner=task_runner,
1584
- asynchronous=(
1585
- task.isasync and flow_run_context.flow.isasync
1586
- if flow_run_context and flow_run_context.flow
1587
- else task.isasync
1588
- ),
1589
- )
1590
-
1591
- # Create and submit the task run in the background
1592
- flow_run_context.background_tasks.start_soon(
1593
- partial(
1594
- create_task_run_then_submit,
1595
- task=task,
1596
- task_run_name=task_run_name,
1597
- task_run_dynamic_key=dynamic_key,
1598
- future=future,
1599
- flow_run_context=flow_run_context,
1600
- parameters=parameters,
1601
- wait_for=wait_for,
1602
- task_runner=task_runner,
1603
- extra_task_inputs=extra_task_inputs,
1604
- )
1605
- )
1606
-
1607
- # Track the task run future in the flow run context
1608
- flow_run_context.task_run_futures.append(future)
1609
-
1610
- if task_runner.concurrency_type == TaskConcurrencyType.SEQUENTIAL:
1611
- await future._wait()
1612
-
1613
- # Return the future without waiting for task run creation or submission
1614
- return future
1615
-
1616
-
1617
- async def create_task_run_then_submit(
1618
- task: Task,
1619
- task_run_name: str,
1620
- task_run_dynamic_key: str,
1621
- future: PrefectFuture,
1622
- flow_run_context: FlowRunContext,
1623
- parameters: Dict[str, Any],
1624
- wait_for: Optional[Iterable[PrefectFuture]],
1625
- task_runner: BaseTaskRunner,
1626
- extra_task_inputs: Dict[str, Set[TaskRunInput]],
1627
- ) -> None:
1628
- task_run = (
1629
- await create_task_run(
1630
- task=task,
1631
- name=task_run_name,
1632
- flow_run_context=flow_run_context,
1633
- parameters=parameters,
1634
- dynamic_key=task_run_dynamic_key,
1635
- wait_for=wait_for,
1636
- extra_task_inputs=extra_task_inputs,
1637
- )
1638
- if not flow_run_context.autonomous_task_run
1639
- else flow_run_context.autonomous_task_run
1640
- )
1641
-
1642
- # Attach the task run to the future to support `get_state` operations
1643
- future.task_run = task_run
1644
-
1645
- await submit_task_run(
1646
- task=task,
1647
- future=future,
1648
- flow_run_context=flow_run_context,
1649
- parameters=parameters,
1650
- task_run=task_run,
1651
- wait_for=wait_for,
1652
- task_runner=task_runner,
1653
- )
1654
-
1655
- future._submitted.set()
1656
-
1657
-
1658
- async def create_task_run(
1659
- task: Task,
1660
- name: str,
1661
- flow_run_context: FlowRunContext,
1662
- parameters: Dict[str, Any],
1663
- dynamic_key: str,
1664
- wait_for: Optional[Iterable[PrefectFuture]],
1665
- extra_task_inputs: Dict[str, Set[TaskRunInput]],
1666
- ) -> TaskRun:
1667
- task_inputs = {k: await collect_task_run_inputs(v) for k, v in parameters.items()}
1668
- if wait_for:
1669
- task_inputs["wait_for"] = await collect_task_run_inputs(wait_for)
1670
-
1671
- # Join extra task inputs
1672
- for k, extras in extra_task_inputs.items():
1673
- task_inputs[k] = task_inputs[k].union(extras)
1674
-
1675
- logger = get_run_logger(flow_run_context)
1676
-
1677
- task_run = await flow_run_context.client.create_task_run(
1678
- task=task,
1679
- name=name,
1680
- flow_run_id=flow_run_context.flow_run.id if flow_run_context.flow_run else None,
1681
- dynamic_key=dynamic_key,
1682
- state=Pending(),
1683
- extra_tags=TagsContext.get().current_tags,
1684
- task_inputs=task_inputs,
1685
- )
1686
-
1687
- if flow_run_context.flow_run:
1688
- logger.info(f"Created task run {task_run.name!r} for task {task.name!r}")
1689
- else:
1690
- engine_logger.info(f"Created task run {task_run.name!r} for task {task.name!r}")
1691
-
1692
- return task_run
1693
-
1694
-
1695
- async def submit_task_run(
1696
- task: Task,
1697
- future: PrefectFuture,
1698
- flow_run_context: FlowRunContext,
1699
- parameters: Dict[str, Any],
1700
- task_run: TaskRun,
1701
- wait_for: Optional[Iterable[PrefectFuture]],
1702
- task_runner: BaseTaskRunner,
1703
- ) -> PrefectFuture:
1704
- logger = get_run_logger(flow_run_context)
1705
-
1706
- if (
1707
- task_runner.concurrency_type == TaskConcurrencyType.SEQUENTIAL
1708
- and flow_run_context.flow_run
1709
- ):
1710
- logger.info(f"Executing {task_run.name!r} immediately...")
1711
-
1712
- future = await task_runner.submit(
1713
- key=future.key,
1714
- call=partial(
1715
- begin_task_run,
1716
- task=task,
1717
- task_run=task_run,
1718
- parameters=parameters,
1719
- wait_for=wait_for,
1720
- result_factory=await ResultFactory.from_task(
1721
- task, client=flow_run_context.client
1722
- ),
1723
- log_prints=should_log_prints(task),
1724
- settings=prefect.context.SettingsContext.get().copy(),
1725
- ),
1726
- )
1727
-
1728
- if (
1729
- task_runner.concurrency_type != TaskConcurrencyType.SEQUENTIAL
1730
- and not flow_run_context.autonomous_task_run
1731
- ):
1732
- logger.info(f"Submitted task run {task_run.name!r} for execution.")
1733
-
1734
- return future
1735
-
1736
-
1737
- async def begin_task_run(
1738
- task: Task,
1739
- task_run: TaskRun,
1740
- parameters: Dict[str, Any],
1741
- wait_for: Optional[Iterable[PrefectFuture]],
1742
- result_factory: ResultFactory,
1743
- log_prints: bool,
1744
- settings: prefect.context.SettingsContext,
1745
- ):
1746
- """
1747
- Entrypoint for task run execution.
1748
-
1749
- This function is intended for submission to the task runner.
1750
-
1751
- This method may be called from a worker so we ensure the settings context has been
1752
- entered. For example, with a runner that is executing tasks in the same event loop,
1753
- we will likely not enter the context again because the current context already
1754
- matches:
1755
-
1756
- main thread:
1757
- --> Flow called with settings A
1758
- --> `begin_task_run` executes same event loop
1759
- --> Profile A matches and is not entered again
1760
-
1761
- However, with execution on a remote environment, we are going to need to ensure the
1762
- settings for the task run are respected by entering the context:
1763
-
1764
- main thread:
1765
- --> Flow called with settings A
1766
- --> `begin_task_run` is scheduled on a remote worker, settings A is serialized
1767
- remote worker:
1768
- --> Remote worker imports Prefect (may not occur)
1769
- --> Global settings is loaded with default settings
1770
- --> `begin_task_run` executes on a different event loop than the flow
1771
- --> Current settings is not set or does not match, settings A is entered
1772
- """
1773
- maybe_flow_run_context = prefect.context.FlowRunContext.get()
1774
-
1775
- async with AsyncExitStack() as stack:
1776
- # The settings context may be null on a remote worker so we use the safe `.get`
1777
- # method and compare it to the settings required for this task run
1778
- if prefect.context.SettingsContext.get() != settings:
1779
- stack.enter_context(settings)
1780
- setup_logging()
1781
-
1782
- if maybe_flow_run_context:
1783
- # Accessible if on a worker that is running in the same thread as the flow
1784
- client = maybe_flow_run_context.client
1785
- # Only run the task in an interruptible thread if it in the same thread as
1786
- # the flow _and_ the flow run has a timeout attached. If the task is on a
1787
- # worker, the flow run timeout will not be raised in the worker process.
1788
- interruptible = maybe_flow_run_context.timeout_scope is not None
1789
- else:
1790
- # Otherwise, retrieve a new clien`t
1791
- client = await stack.enter_async_context(get_client())
1792
- interruptible = False
1793
- await stack.enter_async_context(anyio.create_task_group())
1794
-
1795
- await stack.enter_async_context(report_task_run_crashes(task_run, client))
1796
-
1797
- # TODO: Use the background tasks group to manage logging for this task
1798
-
1799
- if log_prints:
1800
- stack.enter_context(patch_print())
1801
-
1802
- await check_api_reachable(
1803
- client, f"Cannot orchestrate task run '{task_run.id}'"
1804
- )
1805
- try:
1806
- state = await orchestrate_task_run(
1807
- task=task,
1808
- task_run=task_run,
1809
- parameters=parameters,
1810
- wait_for=wait_for,
1811
- result_factory=result_factory,
1812
- log_prints=log_prints,
1813
- interruptible=interruptible,
1814
- client=client,
1815
- )
1816
-
1817
- if not maybe_flow_run_context:
1818
- # When a a task run finishes on a remote worker flush logs to prevent
1819
- # loss if the process exits
1820
- await APILogHandler.aflush()
1821
-
1822
- except Abort as abort:
1823
- # Task run probably already completed, fetch its state
1824
- task_run = await client.read_task_run(task_run.id)
1825
-
1826
- if task_run.state.is_final():
1827
- task_run_logger(task_run).info(
1828
- f"Task run '{task_run.id}' already finished."
1829
- )
1830
- else:
1831
- # TODO: This is a concerning case; we should determine when this occurs
1832
- # 1. This can occur when the flow run is not in a running state
1833
- task_run_logger(task_run).warning(
1834
- f"Task run '{task_run.id}' received abort during orchestration: "
1835
- f"{abort} Task run is in {task_run.state.type.value} state."
1836
- )
1837
- state = task_run.state
1838
-
1839
- except Pause:
1840
- # A pause signal here should mean the flow run suspended, so we
1841
- # should do the same. We'll look up the flow run's pause state to
1842
- # try and reuse it, so we capture any data like timeouts.
1843
- flow_run = await client.read_flow_run(task_run.flow_run_id)
1844
- if flow_run.state and flow_run.state.is_paused():
1845
- state = flow_run.state
1846
- else:
1847
- state = Suspended()
1848
-
1849
- task_run_logger(task_run).info(
1850
- "Task run encountered a pause signal during orchestration."
1851
- )
1852
-
1853
- return state
1854
-
1855
-
1856
- async def orchestrate_task_run(
1857
- task: Task,
1858
- task_run: TaskRun,
1859
- parameters: Dict[str, Any],
1860
- wait_for: Optional[Iterable[PrefectFuture]],
1861
- result_factory: ResultFactory,
1862
- log_prints: bool,
1863
- interruptible: bool,
1864
- client: PrefectClient,
1865
- ) -> State:
1866
- """
1867
- Execute a task run
1868
-
1869
- This function should be submitted to a task runner. We must construct the context
1870
- here instead of receiving it already populated since we may be in a new environment.
1871
-
1872
- Proposes a RUNNING state, then
1873
- - if accepted, the task user function will be run
1874
- - if rejected, the received state will be returned
1875
-
1876
- When the user function is run, the result will be used to determine a final state
1877
- - if an exception is encountered, it is trapped and stored in a FAILED state
1878
- - otherwise, `return_value_to_state` is used to determine the state
1879
-
1880
- If the final state is COMPLETED, we generate a cache key as specified by the task
1881
-
1882
- The final state is then proposed
1883
- - if accepted, this is the final state and will be returned
1884
- - if rejected and a new final state is provided, it will be returned
1885
- - if rejected and a non-final state is provided, we will attempt to enter a RUNNING
1886
- state again
1887
-
1888
- Returns:
1889
- The final state of the run
1890
- """
1891
- flow_run_context = prefect.context.FlowRunContext.get()
1892
- if flow_run_context:
1893
- flow_run = flow_run_context.flow_run
1894
- else:
1895
- flow_run = await client.read_flow_run(task_run.flow_run_id)
1896
- logger = task_run_logger(task_run, task=task, flow_run=flow_run)
1897
-
1898
- partial_task_run_context = TaskRunContext.construct(
1899
- task_run=task_run,
1900
- task=task,
1901
- client=client,
1902
- result_factory=result_factory,
1903
- log_prints=log_prints,
1904
- )
1905
- task_introspection_start_time = time.perf_counter()
1906
- try:
1907
- # Resolve futures in parameters into data
1908
- resolved_parameters = await resolve_inputs(parameters)
1909
- # Resolve futures in any non-data dependencies to ensure they are ready
1910
- await resolve_inputs({"wait_for": wait_for}, return_data=False)
1911
- except UpstreamTaskError as upstream_exc:
1912
- return await propose_state(
1913
- client,
1914
- Pending(name="NotReady", message=str(upstream_exc)),
1915
- task_run_id=task_run.id,
1916
- # if orchestrating a run already in a pending state, force orchestration to
1917
- # update the state name
1918
- force=task_run.state.is_pending(),
1919
- )
1920
- task_introspection_end_time = time.perf_counter()
1921
-
1922
- introspection_time = round(
1923
- task_introspection_end_time - task_introspection_start_time, 3
1924
- )
1925
- threshold = PREFECT_TASK_INTROSPECTION_WARN_THRESHOLD.value()
1926
- if threshold and introspection_time > threshold:
1927
- logger.warning(
1928
- f"Task parameter introspection took {introspection_time} seconds "
1929
- f", exceeding `PREFECT_TASK_INTROSPECTION_WARN_THRESHOLD` of {threshold}. "
1930
- "Try wrapping large task parameters with "
1931
- "`prefect.utilities.annotations.quote` for increased performance, "
1932
- "e.g. `my_task(quote(param))`. To disable this message set "
1933
- "`PREFECT_TASK_INTROSPECTION_WARN_THRESHOLD=0`."
1934
- )
1935
-
1936
- # Generate the cache key to attach to proposed states
1937
- # The cache key uses a TaskRunContext that does not include a `timeout_context``
1938
-
1939
- task_run_context = TaskRunContext(
1940
- **partial_task_run_context.dict(), parameters=resolved_parameters
1941
- )
1942
-
1943
- cache_key = (
1944
- task.cache_key_fn(
1945
- task_run_context,
1946
- resolved_parameters,
1947
- )
1948
- if task.cache_key_fn
1949
- else None
1950
- )
1951
-
1952
- # Ignore the cached results for a cache key, default = false
1953
- # Setting on task level overrules the Prefect setting (env var)
1954
- refresh_cache = (
1955
- task.refresh_cache
1956
- if task.refresh_cache is not None
1957
- else PREFECT_TASKS_REFRESH_CACHE.value()
1958
- )
1959
-
1960
- # Emit an event to capture that the task run was in the `PENDING` state.
1961
- last_event = emit_task_run_state_change_event(
1962
- task_run=task_run, initial_state=None, validated_state=task_run.state
1963
- )
1964
- last_state = (
1965
- Pending()
1966
- if flow_run_context and flow_run_context.autonomous_task_run
1967
- else task_run.state
1968
- )
1969
-
1970
- # Completed states with persisted results should have result data. If it's missing,
1971
- # this could be a manual state transition, so we should use the Unknown result type
1972
- # to represent that we know we don't know the result.
1973
- if (
1974
- last_state
1975
- and last_state.is_completed()
1976
- and result_factory.persist_result
1977
- and not last_state.data
1978
- ):
1979
- state = await propose_state(
1980
- client,
1981
- state=Completed(data=await UnknownResult.create()),
1982
- task_run_id=task_run.id,
1983
- force=True,
1984
- )
1985
-
1986
- # Transition from `PENDING` -> `RUNNING`
1987
- try:
1988
- state = await propose_state(
1989
- client,
1990
- Running(
1991
- state_details=StateDetails(
1992
- cache_key=cache_key, refresh_cache=refresh_cache
1993
- )
1994
- ),
1995
- task_run_id=task_run.id,
1996
- )
1997
- except Pause as exc:
1998
- # We shouldn't get a pause signal without a state, but if this happens,
1999
- # just use a Paused state to assume an in-process pause.
2000
- state = exc.state if exc.state else Paused()
2001
-
2002
- # If a flow submits tasks and then pauses, we may reach this point due
2003
- # to concurrency timing because the tasks will try to transition after
2004
- # the flow run has paused. Orchestration will send back a Paused state
2005
- # for the task runs.
2006
- if state.state_details.pause_reschedule:
2007
- # If we're being asked to pause and reschedule, we should exit the
2008
- # task and expect to be resumed later.
2009
- raise
2010
-
2011
- if state.is_paused():
2012
- BACKOFF_MAX = 10 # Seconds
2013
- backoff_count = 0
2014
-
2015
- async def tick():
2016
- nonlocal backoff_count
2017
- if backoff_count < BACKOFF_MAX:
2018
- backoff_count += 1
2019
- interval = 1 + backoff_count + random.random() * backoff_count
2020
- await anyio.sleep(interval)
2021
-
2022
- # Enter a loop to wait for the task run to be resumed, i.e.
2023
- # become Pending, and then propose a Running state again.
2024
- while True:
2025
- await tick()
2026
-
2027
- # Propose a Running state again. We do this instead of reading the
2028
- # task run because if the flow run times out, this lets
2029
- # orchestration fail the task run.
2030
- try:
2031
- state = await propose_state(
2032
- client,
2033
- Running(
2034
- state_details=StateDetails(
2035
- cache_key=cache_key, refresh_cache=refresh_cache
2036
- )
2037
- ),
2038
- task_run_id=task_run.id,
2039
- )
2040
- except Pause as exc:
2041
- if not exc.state:
2042
- continue
2043
-
2044
- if exc.state.state_details.pause_reschedule:
2045
- # If the pause state includes pause_reschedule, we should exit the
2046
- # task and expect to be resumed later. We've already checked for this
2047
- # above, but we check again here in case the state changed; e.g. the
2048
- # flow run suspended.
2049
- raise
2050
- else:
2051
- # Propose a Running state again.
2052
- continue
2053
- else:
2054
- break
2055
-
2056
- # Emit an event to capture the result of proposing a `RUNNING` state.
2057
- last_event = emit_task_run_state_change_event(
2058
- task_run=task_run,
2059
- initial_state=last_state,
2060
- validated_state=state,
2061
- follows=last_event,
2062
- )
2063
- last_state = state
2064
-
2065
- # flag to ensure we only update the task run name once
2066
- run_name_set = False
2067
-
2068
- # Only run the task if we enter a `RUNNING` state
2069
- while state.is_running():
2070
- # Retrieve the latest metadata for the task run context
2071
- task_run = await client.read_task_run(task_run.id)
2072
-
2073
- with task_run_context.copy(
2074
- update={"task_run": task_run, "start_time": pendulum.now("UTC")}
2075
- ):
2076
- try:
2077
- args, kwargs = parameters_to_args_kwargs(task.fn, resolved_parameters)
2078
- # update task run name
2079
- if not run_name_set and task.task_run_name:
2080
- task_run_name = _resolve_custom_task_run_name(
2081
- task=task, parameters=resolved_parameters
2082
- )
2083
- await client.set_task_run_name(
2084
- task_run_id=task_run.id, name=task_run_name
2085
- )
2086
- logger.extra["task_run_name"] = task_run_name
2087
- logger.debug(
2088
- f"Renamed task run {task_run.name!r} to {task_run_name!r}"
2089
- )
2090
- task_run.name = task_run_name
2091
- run_name_set = True
2092
-
2093
- if PREFECT_DEBUG_MODE.value():
2094
- logger.debug(f"Executing {call_repr(task.fn, *args, **kwargs)}")
2095
- else:
2096
- logger.debug(
2097
- "Beginning execution...", extra={"state_message": True}
2098
- )
2099
-
2100
- call = from_async.call_soon_in_new_thread(
2101
- create_call(task.fn, *args, **kwargs), timeout=task.timeout_seconds
2102
- )
2103
- result = await call.aresult()
2104
-
2105
- except (CancelledError, asyncio.CancelledError) as exc:
2106
- if not call.timedout():
2107
- # If the task call was not cancelled by us; this is a crash
2108
- raise
2109
- # Construct a new exception as `TimeoutError`
2110
- original = exc
2111
- exc = TimeoutError()
2112
- exc.__cause__ = original
2113
- logger.exception("Encountered exception during execution:")
2114
- terminal_state = await exception_to_failed_state(
2115
- exc,
2116
- message=(
2117
- f"Task run exceeded timeout of {task.timeout_seconds} seconds"
2118
- ),
2119
- result_factory=task_run_context.result_factory,
2120
- name="TimedOut",
2121
- )
2122
- except Exception as exc:
2123
- logger.exception("Encountered exception during execution:")
2124
- terminal_state = await exception_to_failed_state(
2125
- exc,
2126
- message="Task run encountered an exception",
2127
- result_factory=task_run_context.result_factory,
2128
- )
2129
- else:
2130
- terminal_state = await return_value_to_state(
2131
- result,
2132
- result_factory=task_run_context.result_factory,
2133
- )
2134
-
2135
- # for COMPLETED tasks, add the cache key and expiration
2136
- if terminal_state.is_completed():
2137
- terminal_state.state_details.cache_expiration = (
2138
- (pendulum.now("utc") + task.cache_expiration)
2139
- if task.cache_expiration
2140
- else None
2141
- )
2142
- terminal_state.state_details.cache_key = cache_key
2143
-
2144
- if terminal_state.is_failed():
2145
- # Defer to user to decide whether failure is retriable
2146
- terminal_state.state_details.retriable = (
2147
- await _check_task_failure_retriable(task, task_run, terminal_state)
2148
- )
2149
- state = await propose_state(client, terminal_state, task_run_id=task_run.id)
2150
- last_event = emit_task_run_state_change_event(
2151
- task_run=task_run,
2152
- initial_state=last_state,
2153
- validated_state=state,
2154
- follows=last_event,
2155
- )
2156
- last_state = state
2157
-
2158
- await _run_task_hooks(
2159
- task=task,
2160
- task_run=task_run,
2161
- state=state,
2162
- )
2163
-
2164
- if state.type != terminal_state.type and PREFECT_DEBUG_MODE:
2165
- logger.debug(
2166
- (
2167
- f"Received new state {state} when proposing final state"
2168
- f" {terminal_state}"
2169
- ),
2170
- extra={"send_to_api": False},
2171
- )
2172
-
2173
- if not state.is_final() and not state.is_paused():
2174
- logger.info(
2175
- (
2176
- f"Received non-final state {state.name!r} when proposing final"
2177
- f" state {terminal_state.name!r} and will attempt to run"
2178
- " again..."
2179
- ),
2180
- )
2181
- # Attempt to enter a running state again
2182
- state = await propose_state(client, Running(), task_run_id=task_run.id)
2183
- last_event = emit_task_run_state_change_event(
2184
- task_run=task_run,
2185
- initial_state=last_state,
2186
- validated_state=state,
2187
- follows=last_event,
2188
- )
2189
- last_state = state
2190
-
2191
- # If debugging, use the more complete `repr` than the usual `str` description
2192
- display_state = repr(state) if PREFECT_DEBUG_MODE else str(state)
2193
-
2194
- logger.log(
2195
- level=logging.INFO if state.is_completed() else logging.ERROR,
2196
- msg=f"Finished in state {display_state}",
2197
- )
2198
- return state
2199
-
2200
-
2201
- @asynccontextmanager
2202
- async def report_flow_run_crashes(flow_run: FlowRun, client: PrefectClient, flow: Flow):
2203
- """
2204
- Detect flow run crashes during this context and update the run to a proper final
2205
- state.
2206
-
2207
- This context _must_ reraise the exception to properly exit the run.
2208
- """
2209
-
2210
- try:
2211
- yield
2212
- except (Abort, Pause):
2213
- # Do not capture internal signals as crashes
2214
- raise
2215
- except BaseException as exc:
2216
- state = await exception_to_crashed_state(exc)
2217
- logger = flow_run_logger(flow_run)
2218
- with anyio.CancelScope(shield=True):
2219
- logger.error(f"Crash detected! {state.message}")
2220
- logger.debug("Crash details:", exc_info=exc)
2221
- flow_run_state = await propose_state(client, state, flow_run_id=flow_run.id)
2222
- engine_logger.debug(
2223
- f"Reported crashed flow run {flow_run.name!r} successfully!"
2224
- )
2225
-
2226
- # Only `on_crashed` and `on_cancellation` flow run state change hooks can be called here.
2227
- # We call the hooks after the state change proposal to `CRASHED` is validated
2228
- # or rejected (if it is in a `CANCELLING` state).
2229
- await _run_flow_hooks(
2230
- flow=flow,
2231
- flow_run=flow_run,
2232
- state=flow_run_state,
2233
- )
2234
-
2235
- # Reraise the exception
2236
- raise
2237
-
2238
-
2239
- @asynccontextmanager
2240
- async def report_task_run_crashes(task_run: TaskRun, client: PrefectClient):
2241
- """
2242
- Detect task run crashes during this context and update the run to a proper final
2243
- state.
2244
-
2245
- This context _must_ reraise the exception to properly exit the run.
2246
- """
2247
- try:
2248
- yield
2249
- except (Abort, Pause):
2250
- # Do not capture internal signals as crashes
2251
- raise
2252
- except BaseException as exc:
2253
- state = await exception_to_crashed_state(exc)
2254
- logger = task_run_logger(task_run)
2255
- with anyio.CancelScope(shield=True):
2256
- logger.error(f"Crash detected! {state.message}")
2257
- logger.debug("Crash details:", exc_info=exc)
2258
- await client.set_task_run_state(
2259
- state=state,
2260
- task_run_id=task_run.id,
2261
- force=True,
2262
- )
2263
- engine_logger.debug(
2264
- f"Reported crashed task run {task_run.name!r} successfully!"
2265
- )
2266
-
2267
- # Reraise the exception
2268
- raise
2269
-
2270
-
2271
- async def _run_task_hooks(task: Task, task_run: TaskRun, state: State) -> None:
2272
- """Run the on_failure and on_completion hooks for a task, making sure to
2273
- catch and log any errors that occur.
2274
- """
2275
- hooks = None
2276
- if state.is_failed() and task.on_failure:
2277
- hooks = task.on_failure
2278
- elif state.is_completed() and task.on_completion:
2279
- hooks = task.on_completion
2280
-
2281
- if hooks:
2282
- logger = task_run_logger(task_run)
2283
- for hook in hooks:
2284
- hook_name = _get_hook_name(hook)
2285
- try:
2286
- logger.info(
2287
- f"Running hook {hook_name!r} in response to entering state"
2288
- f" {state.name!r}"
2289
- )
2290
- if is_async_fn(hook):
2291
- await hook(task=task, task_run=task_run, state=state)
2292
- else:
2293
- await from_async.call_in_new_thread(
2294
- create_call(hook, task=task, task_run=task_run, state=state)
2295
- )
2296
- except Exception:
2297
- logger.error(
2298
- f"An error was encountered while running hook {hook_name!r}",
2299
- exc_info=True,
2300
- )
2301
- else:
2302
- logger.info(f"Hook {hook_name!r} finished running successfully")
2303
-
2304
-
2305
- async def _check_task_failure_retriable(
2306
- task: Task, task_run: TaskRun, state: State
2307
- ) -> bool:
2308
- """Run the `retry_condition_fn` callable for a task, making sure to catch and log any errors
2309
- that occur. If None, return True. If not callable, logs an error and returns False.
2310
- """
2311
- if task.retry_condition_fn is None:
2312
- return True
2313
-
2314
- logger = task_run_logger(task_run)
2315
-
2316
- try:
2317
- logger.debug(
2318
- f"Running `retry_condition_fn` check {task.retry_condition_fn!r} for task"
2319
- f" {task.name!r}"
2320
- )
2321
- if is_async_fn(task.retry_condition_fn):
2322
- return bool(
2323
- await task.retry_condition_fn(task=task, task_run=task_run, state=state)
2324
- )
2325
- else:
2326
- return bool(
2327
- await from_async.call_in_new_thread(
2328
- create_call(
2329
- task.retry_condition_fn,
2330
- task=task,
2331
- task_run=task_run,
2332
- state=state,
2333
- )
2334
- )
2335
- )
2336
- except Exception:
2337
- logger.error(
2338
- (
2339
- "An error was encountered while running `retry_condition_fn` check"
2340
- f" '{task.retry_condition_fn!r}' for task {task.name!r}"
2341
- ),
2342
- exc_info=True,
2343
- )
2344
- return False
2345
-
2346
-
2347
- async def _run_flow_hooks(flow: Flow, flow_run: FlowRun, state: State) -> None:
2348
- """Run the on_failure, on_completion, on_cancellation, and on_crashed hooks for a flow, making sure to
2349
- catch and log any errors that occur.
2350
- """
2351
- hooks = None
2352
- enable_cancellation_and_crashed_hooks = (
2353
- os.environ.get("PREFECT__ENABLE_CANCELLATION_AND_CRASHED_HOOKS", "true").lower()
2354
- == "true"
2355
- )
2356
-
2357
- if state.is_running() and flow.on_running:
2358
- hooks = flow.on_running
2359
- elif state.is_failed() and flow.on_failure:
2360
- hooks = flow.on_failure
2361
- elif state.is_completed() and flow.on_completion:
2362
- hooks = flow.on_completion
2363
- elif (
2364
- enable_cancellation_and_crashed_hooks
2365
- and state.is_cancelling()
2366
- and flow.on_cancellation
2367
- ):
2368
- hooks = flow.on_cancellation
2369
- elif (
2370
- enable_cancellation_and_crashed_hooks and state.is_crashed() and flow.on_crashed
2371
- ):
2372
- hooks = flow.on_crashed
2373
-
2374
- if hooks:
2375
- logger = flow_run_logger(flow_run)
2376
- for hook in hooks:
2377
- hook_name = _get_hook_name(hook)
2378
- try:
2379
- logger.info(
2380
- f"Running hook {hook_name!r} in response to entering state"
2381
- f" {state.name!r}"
2382
- )
2383
- if is_async_fn(hook):
2384
- await hook(flow=flow, flow_run=flow_run, state=state)
2385
- else:
2386
- await from_async.call_in_new_thread(
2387
- create_call(hook, flow=flow, flow_run=flow_run, state=state)
2388
- )
2389
- except Exception:
2390
- logger.error(
2391
- f"An error was encountered while running hook {hook_name!r}",
2392
- exc_info=True,
2393
- )
2394
- else:
2395
- logger.info(f"Hook {hook_name!r} finished running successfully")
2396
-
2397
-
2398
- async def create_autonomous_task_run(task: Task, parameters: Dict[str, Any]) -> TaskRun:
2399
- """Create a task run in the API for an autonomous task submission and store
2400
- the provided parameters using the existing result storage mechanism.
2401
- """
2402
- async with get_client() as client:
2403
- state = Scheduled()
2404
- if parameters:
2405
- parameters_id = uuid4()
2406
- state.state_details.task_parameters_id = parameters_id
2407
-
2408
- # TODO: Improve use of result storage for parameter storage / reference
2409
- task.persist_result = True
2410
-
2411
- factory = await ResultFactory.from_autonomous_task(task, client=client)
2412
- await factory.store_parameters(parameters_id, parameters)
2413
-
2414
- task_run = await client.create_task_run(
2415
- task=task,
2416
- flow_run_id=None,
2417
- dynamic_key=f"{task.task_key}-{str(uuid4())[:NUM_CHARS_DYNAMIC_KEY]}",
2418
- state=state,
2419
- )
2420
-
2421
- engine_logger.debug(f"Submitted run of task {task.name!r} for execution")
2422
-
2423
- return task_run
2424
-
2425
-
2426
19
  if __name__ == "__main__":
2427
20
  try:
2428
21
  flow_run_id = UUID(
@@ -2435,21 +28,18 @@ if __name__ == "__main__":
2435
28
  exit(1)
2436
29
 
2437
30
  try:
2438
- if PREFECT_EXPERIMENTAL_ENABLE_NEW_ENGINE.value():
2439
- from prefect.new_flow_engine import (
2440
- load_flow_and_flow_run,
2441
- run_flow_async,
2442
- run_flow_sync,
2443
- )
31
+ from prefect.flow_engine import (
32
+ load_flow_and_flow_run,
33
+ run_flow_async,
34
+ run_flow_sync,
35
+ )
2444
36
 
2445
- flow_run, flow = run_sync(load_flow_and_flow_run)
2446
- # run the flow
2447
- if flow.isasync:
2448
- run_sync(run_flow_async(flow, flow_run=flow_run))
2449
- else:
2450
- run_flow_sync(flow, flow_run=flow_run)
37
+ flow_run, flow = load_flow_and_flow_run(flow_run_id=flow_run_id)
38
+ # run the flow
39
+ if flow.isasync:
40
+ run_coro_as_sync(run_flow_async(flow, flow_run=flow_run))
2451
41
  else:
2452
- enter_flow_run_engine_from_subprocess(flow_run_id)
42
+ run_flow_sync(flow, flow_run=flow_run)
2453
43
  except Abort as exc:
2454
44
  engine_logger.info(
2455
45
  f"Engine execution of flow run '{flow_run_id}' aborted by orchestrator:"