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.
- prefect/__init__.py +74 -110
- prefect/_internal/compatibility/deprecated.py +6 -115
- prefect/_internal/compatibility/experimental.py +4 -79
- prefect/_internal/compatibility/migration.py +166 -0
- prefect/_internal/concurrency/__init__.py +2 -2
- prefect/_internal/concurrency/api.py +1 -35
- prefect/_internal/concurrency/calls.py +0 -6
- prefect/_internal/concurrency/cancellation.py +0 -3
- prefect/_internal/concurrency/event_loop.py +0 -20
- prefect/_internal/concurrency/inspection.py +3 -3
- prefect/_internal/concurrency/primitives.py +1 -0
- prefect/_internal/concurrency/services.py +23 -0
- prefect/_internal/concurrency/threads.py +35 -0
- prefect/_internal/concurrency/waiters.py +0 -28
- prefect/_internal/integrations.py +7 -0
- prefect/_internal/pydantic/__init__.py +0 -45
- prefect/_internal/pydantic/annotations/pendulum.py +2 -2
- prefect/_internal/pydantic/v1_schema.py +21 -22
- prefect/_internal/pydantic/v2_schema.py +0 -2
- prefect/_internal/pydantic/v2_validated_func.py +18 -23
- prefect/_internal/pytz.py +1 -1
- prefect/_internal/retries.py +61 -0
- prefect/_internal/schemas/bases.py +45 -177
- prefect/_internal/schemas/fields.py +1 -43
- prefect/_internal/schemas/validators.py +47 -233
- prefect/agent.py +3 -695
- prefect/artifacts.py +173 -14
- prefect/automations.py +39 -4
- prefect/blocks/abstract.py +1 -1
- prefect/blocks/core.py +405 -153
- prefect/blocks/fields.py +2 -57
- prefect/blocks/notifications.py +43 -28
- prefect/blocks/redis.py +168 -0
- prefect/blocks/system.py +67 -20
- prefect/blocks/webhook.py +2 -9
- prefect/cache_policies.py +239 -0
- prefect/client/__init__.py +4 -0
- prefect/client/base.py +33 -27
- prefect/client/cloud.py +65 -20
- prefect/client/collections.py +1 -1
- prefect/client/orchestration.py +650 -442
- prefect/client/schemas/actions.py +115 -100
- prefect/client/schemas/filters.py +46 -52
- prefect/client/schemas/objects.py +228 -178
- prefect/client/schemas/responses.py +18 -36
- prefect/client/schemas/schedules.py +55 -36
- prefect/client/schemas/sorting.py +2 -0
- prefect/client/subscriptions.py +8 -7
- prefect/client/types/flexible_schedule_list.py +11 -0
- prefect/client/utilities.py +9 -6
- prefect/concurrency/asyncio.py +60 -11
- prefect/concurrency/context.py +24 -0
- prefect/concurrency/events.py +2 -2
- prefect/concurrency/services.py +46 -16
- prefect/concurrency/sync.py +51 -7
- prefect/concurrency/v1/asyncio.py +143 -0
- prefect/concurrency/v1/context.py +27 -0
- prefect/concurrency/v1/events.py +61 -0
- prefect/concurrency/v1/services.py +116 -0
- prefect/concurrency/v1/sync.py +92 -0
- prefect/context.py +246 -149
- prefect/deployments/__init__.py +33 -18
- prefect/deployments/base.py +10 -15
- prefect/deployments/deployments.py +2 -1048
- prefect/deployments/flow_runs.py +178 -0
- prefect/deployments/runner.py +72 -173
- prefect/deployments/schedules.py +31 -25
- prefect/deployments/steps/__init__.py +0 -1
- prefect/deployments/steps/core.py +7 -0
- prefect/deployments/steps/pull.py +15 -21
- prefect/deployments/steps/utility.py +2 -1
- prefect/docker/__init__.py +20 -0
- prefect/docker/docker_image.py +82 -0
- prefect/engine.py +15 -2475
- prefect/events/actions.py +17 -23
- prefect/events/cli/automations.py +20 -7
- prefect/events/clients.py +142 -80
- prefect/events/filters.py +14 -18
- prefect/events/related.py +74 -75
- prefect/events/schemas/__init__.py +0 -5
- prefect/events/schemas/automations.py +55 -46
- prefect/events/schemas/deployment_triggers.py +7 -197
- prefect/events/schemas/events.py +46 -65
- prefect/events/schemas/labelling.py +10 -14
- prefect/events/utilities.py +4 -5
- prefect/events/worker.py +23 -8
- prefect/exceptions.py +15 -0
- prefect/filesystems.py +30 -529
- prefect/flow_engine.py +827 -0
- prefect/flow_runs.py +379 -7
- prefect/flows.py +470 -360
- prefect/futures.py +382 -331
- prefect/infrastructure/__init__.py +5 -26
- prefect/infrastructure/base.py +3 -320
- prefect/infrastructure/provisioners/__init__.py +5 -3
- prefect/infrastructure/provisioners/cloud_run.py +13 -8
- prefect/infrastructure/provisioners/container_instance.py +14 -9
- prefect/infrastructure/provisioners/ecs.py +10 -8
- prefect/infrastructure/provisioners/modal.py +8 -5
- prefect/input/__init__.py +4 -0
- prefect/input/actions.py +2 -4
- prefect/input/run_input.py +9 -9
- prefect/logging/formatters.py +2 -4
- prefect/logging/handlers.py +9 -14
- prefect/logging/loggers.py +5 -5
- prefect/main.py +72 -0
- prefect/plugins.py +2 -64
- prefect/profiles.toml +16 -2
- prefect/records/__init__.py +1 -0
- prefect/records/base.py +223 -0
- prefect/records/filesystem.py +207 -0
- prefect/records/memory.py +178 -0
- prefect/records/result_store.py +64 -0
- prefect/results.py +577 -504
- prefect/runner/runner.py +117 -47
- prefect/runner/server.py +32 -34
- prefect/runner/storage.py +3 -12
- prefect/runner/submit.py +2 -10
- prefect/runner/utils.py +2 -2
- prefect/runtime/__init__.py +1 -0
- prefect/runtime/deployment.py +1 -0
- prefect/runtime/flow_run.py +40 -5
- prefect/runtime/task_run.py +1 -0
- prefect/serializers.py +28 -39
- prefect/server/api/collections_data/views/aggregate-worker-metadata.json +5 -14
- prefect/settings.py +209 -332
- prefect/states.py +160 -63
- prefect/task_engine.py +1478 -57
- prefect/task_runners.py +383 -287
- prefect/task_runs.py +240 -0
- prefect/task_worker.py +463 -0
- prefect/tasks.py +684 -374
- prefect/transactions.py +410 -0
- prefect/types/__init__.py +72 -86
- prefect/types/entrypoint.py +13 -0
- prefect/utilities/annotations.py +4 -3
- prefect/utilities/asyncutils.py +227 -148
- prefect/utilities/callables.py +137 -45
- prefect/utilities/collections.py +134 -86
- prefect/utilities/dispatch.py +27 -14
- prefect/utilities/dockerutils.py +11 -4
- prefect/utilities/engine.py +186 -32
- prefect/utilities/filesystem.py +4 -5
- prefect/utilities/importtools.py +26 -27
- prefect/utilities/pydantic.py +128 -38
- prefect/utilities/schema_tools/hydration.py +18 -1
- prefect/utilities/schema_tools/validation.py +30 -0
- prefect/utilities/services.py +35 -9
- prefect/utilities/templating.py +12 -2
- prefect/utilities/timeout.py +20 -5
- prefect/utilities/urls.py +195 -0
- prefect/utilities/visualization.py +1 -0
- prefect/variables.py +78 -59
- prefect/workers/__init__.py +0 -1
- prefect/workers/base.py +237 -244
- prefect/workers/block.py +5 -226
- prefect/workers/cloud.py +6 -0
- prefect/workers/process.py +265 -12
- prefect/workers/server.py +29 -11
- {prefect_client-2.20.4.dist-info → prefect_client-3.0.0.dist-info}/METADATA +28 -24
- prefect_client-3.0.0.dist-info/RECORD +201 -0
- {prefect_client-2.20.4.dist-info → prefect_client-3.0.0.dist-info}/WHEEL +1 -1
- prefect/_internal/pydantic/_base_model.py +0 -51
- prefect/_internal/pydantic/_compat.py +0 -82
- prefect/_internal/pydantic/_flags.py +0 -20
- prefect/_internal/pydantic/_types.py +0 -8
- prefect/_internal/pydantic/utilities/config_dict.py +0 -72
- prefect/_internal/pydantic/utilities/field_validator.py +0 -150
- prefect/_internal/pydantic/utilities/model_construct.py +0 -56
- prefect/_internal/pydantic/utilities/model_copy.py +0 -55
- prefect/_internal/pydantic/utilities/model_dump.py +0 -136
- prefect/_internal/pydantic/utilities/model_dump_json.py +0 -112
- prefect/_internal/pydantic/utilities/model_fields.py +0 -50
- prefect/_internal/pydantic/utilities/model_fields_set.py +0 -29
- prefect/_internal/pydantic/utilities/model_json_schema.py +0 -82
- prefect/_internal/pydantic/utilities/model_rebuild.py +0 -80
- prefect/_internal/pydantic/utilities/model_validate.py +0 -75
- prefect/_internal/pydantic/utilities/model_validate_json.py +0 -68
- prefect/_internal/pydantic/utilities/model_validator.py +0 -87
- prefect/_internal/pydantic/utilities/type_adapter.py +0 -71
- prefect/_vendor/fastapi/__init__.py +0 -25
- prefect/_vendor/fastapi/applications.py +0 -946
- prefect/_vendor/fastapi/background.py +0 -3
- prefect/_vendor/fastapi/concurrency.py +0 -44
- prefect/_vendor/fastapi/datastructures.py +0 -58
- prefect/_vendor/fastapi/dependencies/__init__.py +0 -0
- prefect/_vendor/fastapi/dependencies/models.py +0 -64
- prefect/_vendor/fastapi/dependencies/utils.py +0 -877
- prefect/_vendor/fastapi/encoders.py +0 -177
- prefect/_vendor/fastapi/exception_handlers.py +0 -40
- prefect/_vendor/fastapi/exceptions.py +0 -46
- prefect/_vendor/fastapi/logger.py +0 -3
- prefect/_vendor/fastapi/middleware/__init__.py +0 -1
- prefect/_vendor/fastapi/middleware/asyncexitstack.py +0 -25
- prefect/_vendor/fastapi/middleware/cors.py +0 -3
- prefect/_vendor/fastapi/middleware/gzip.py +0 -3
- prefect/_vendor/fastapi/middleware/httpsredirect.py +0 -3
- prefect/_vendor/fastapi/middleware/trustedhost.py +0 -3
- prefect/_vendor/fastapi/middleware/wsgi.py +0 -3
- prefect/_vendor/fastapi/openapi/__init__.py +0 -0
- prefect/_vendor/fastapi/openapi/constants.py +0 -2
- prefect/_vendor/fastapi/openapi/docs.py +0 -203
- prefect/_vendor/fastapi/openapi/models.py +0 -480
- prefect/_vendor/fastapi/openapi/utils.py +0 -485
- prefect/_vendor/fastapi/param_functions.py +0 -340
- prefect/_vendor/fastapi/params.py +0 -453
- prefect/_vendor/fastapi/py.typed +0 -0
- prefect/_vendor/fastapi/requests.py +0 -4
- prefect/_vendor/fastapi/responses.py +0 -40
- prefect/_vendor/fastapi/routing.py +0 -1331
- prefect/_vendor/fastapi/security/__init__.py +0 -15
- prefect/_vendor/fastapi/security/api_key.py +0 -98
- prefect/_vendor/fastapi/security/base.py +0 -6
- prefect/_vendor/fastapi/security/http.py +0 -172
- prefect/_vendor/fastapi/security/oauth2.py +0 -227
- prefect/_vendor/fastapi/security/open_id_connect_url.py +0 -34
- prefect/_vendor/fastapi/security/utils.py +0 -10
- prefect/_vendor/fastapi/staticfiles.py +0 -1
- prefect/_vendor/fastapi/templating.py +0 -3
- prefect/_vendor/fastapi/testclient.py +0 -1
- prefect/_vendor/fastapi/types.py +0 -3
- prefect/_vendor/fastapi/utils.py +0 -235
- prefect/_vendor/fastapi/websockets.py +0 -7
- prefect/_vendor/starlette/__init__.py +0 -1
- prefect/_vendor/starlette/_compat.py +0 -28
- prefect/_vendor/starlette/_exception_handler.py +0 -80
- prefect/_vendor/starlette/_utils.py +0 -88
- prefect/_vendor/starlette/applications.py +0 -261
- prefect/_vendor/starlette/authentication.py +0 -159
- prefect/_vendor/starlette/background.py +0 -43
- prefect/_vendor/starlette/concurrency.py +0 -59
- prefect/_vendor/starlette/config.py +0 -151
- prefect/_vendor/starlette/convertors.py +0 -87
- prefect/_vendor/starlette/datastructures.py +0 -707
- prefect/_vendor/starlette/endpoints.py +0 -130
- prefect/_vendor/starlette/exceptions.py +0 -60
- prefect/_vendor/starlette/formparsers.py +0 -276
- prefect/_vendor/starlette/middleware/__init__.py +0 -17
- prefect/_vendor/starlette/middleware/authentication.py +0 -52
- prefect/_vendor/starlette/middleware/base.py +0 -220
- prefect/_vendor/starlette/middleware/cors.py +0 -176
- prefect/_vendor/starlette/middleware/errors.py +0 -265
- prefect/_vendor/starlette/middleware/exceptions.py +0 -74
- prefect/_vendor/starlette/middleware/gzip.py +0 -113
- prefect/_vendor/starlette/middleware/httpsredirect.py +0 -19
- prefect/_vendor/starlette/middleware/sessions.py +0 -82
- prefect/_vendor/starlette/middleware/trustedhost.py +0 -64
- prefect/_vendor/starlette/middleware/wsgi.py +0 -147
- prefect/_vendor/starlette/py.typed +0 -0
- prefect/_vendor/starlette/requests.py +0 -328
- prefect/_vendor/starlette/responses.py +0 -347
- prefect/_vendor/starlette/routing.py +0 -933
- prefect/_vendor/starlette/schemas.py +0 -154
- prefect/_vendor/starlette/staticfiles.py +0 -248
- prefect/_vendor/starlette/status.py +0 -199
- prefect/_vendor/starlette/templating.py +0 -231
- prefect/_vendor/starlette/testclient.py +0 -804
- prefect/_vendor/starlette/types.py +0 -30
- prefect/_vendor/starlette/websockets.py +0 -193
- prefect/blocks/kubernetes.py +0 -119
- prefect/deprecated/__init__.py +0 -0
- prefect/deprecated/data_documents.py +0 -350
- prefect/deprecated/packaging/__init__.py +0 -12
- prefect/deprecated/packaging/base.py +0 -96
- prefect/deprecated/packaging/docker.py +0 -146
- prefect/deprecated/packaging/file.py +0 -92
- prefect/deprecated/packaging/orion.py +0 -80
- prefect/deprecated/packaging/serializers.py +0 -171
- prefect/events/instrument.py +0 -135
- prefect/infrastructure/container.py +0 -824
- prefect/infrastructure/kubernetes.py +0 -920
- prefect/infrastructure/process.py +0 -289
- prefect/manifests.py +0 -20
- prefect/new_flow_engine.py +0 -449
- prefect/new_task_engine.py +0 -423
- prefect/pydantic/__init__.py +0 -76
- prefect/pydantic/main.py +0 -39
- prefect/software/__init__.py +0 -2
- prefect/software/base.py +0 -50
- prefect/software/conda.py +0 -199
- prefect/software/pip.py +0 -122
- prefect/software/python.py +0 -52
- prefect/task_server.py +0 -322
- prefect_client-2.20.4.dist-info/RECORD +0 -294
- /prefect/{_internal/pydantic/utilities → client/types}/__init__.py +0 -0
- /prefect/{_vendor → concurrency/v1}/__init__.py +0 -0
- {prefect_client-2.20.4.dist-info → prefect_client-3.0.0.dist-info}/LICENSE +0 -0
- {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
|
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
|
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
|
-
|
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
|
-
|
2492
|
-
|
2493
|
-
|
2494
|
-
|
2495
|
-
run_flow_sync,
|
2496
|
-
)
|
32
|
+
from prefect.flow_engine import (
|
33
|
+
load_flow_and_flow_run,
|
34
|
+
run_flow,
|
35
|
+
)
|
2497
36
|
|
2498
|
-
|
2499
|
-
|
2500
|
-
|
2501
|
-
|
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
|
-
|
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__)
|