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