prefect-client 2.19.4__py3-none-any.whl → 3.0.0rc2__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 +8 -56
- prefect/_internal/compatibility/deprecated.py +6 -115
- prefect/_internal/compatibility/experimental.py +4 -79
- prefect/_internal/concurrency/api.py +0 -34
- 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/threads.py +35 -0
- prefect/_internal/concurrency/waiters.py +0 -28
- prefect/_internal/pydantic/__init__.py +0 -45
- 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/schemas/bases.py +44 -177
- prefect/_internal/schemas/fields.py +1 -43
- prefect/_internal/schemas/validators.py +60 -158
- prefect/artifacts.py +161 -14
- prefect/automations.py +39 -4
- prefect/blocks/abstract.py +1 -1
- prefect/blocks/core.py +268 -148
- prefect/blocks/fields.py +2 -57
- prefect/blocks/kubernetes.py +8 -12
- prefect/blocks/notifications.py +40 -20
- prefect/blocks/redis.py +168 -0
- prefect/blocks/system.py +22 -11
- prefect/blocks/webhook.py +2 -9
- prefect/client/base.py +4 -4
- prefect/client/cloud.py +8 -13
- prefect/client/orchestration.py +362 -340
- prefect/client/schemas/actions.py +92 -86
- prefect/client/schemas/filters.py +20 -40
- prefect/client/schemas/objects.py +158 -152
- prefect/client/schemas/responses.py +16 -24
- prefect/client/schemas/schedules.py +47 -35
- prefect/client/subscriptions.py +2 -2
- prefect/client/utilities.py +5 -2
- prefect/concurrency/asyncio.py +4 -2
- prefect/concurrency/events.py +1 -1
- prefect/concurrency/services.py +7 -4
- prefect/context.py +195 -27
- prefect/deployments/__init__.py +5 -6
- prefect/deployments/base.py +7 -5
- prefect/deployments/flow_runs.py +185 -0
- prefect/deployments/runner.py +50 -45
- prefect/deployments/schedules.py +28 -23
- prefect/deployments/steps/__init__.py +0 -1
- prefect/deployments/steps/core.py +1 -0
- prefect/deployments/steps/pull.py +7 -21
- prefect/engine.py +12 -2422
- prefect/events/actions.py +17 -23
- prefect/events/cli/automations.py +19 -6
- prefect/events/clients.py +14 -37
- prefect/events/filters.py +14 -18
- prefect/events/related.py +2 -2
- 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 +36 -65
- prefect/events/schemas/labelling.py +10 -14
- prefect/events/utilities.py +2 -3
- prefect/events/worker.py +2 -3
- prefect/filesystems.py +6 -517
- prefect/{new_flow_engine.py → flow_engine.py} +315 -74
- prefect/flow_runs.py +379 -7
- prefect/flows.py +248 -165
- prefect/futures.py +187 -345
- prefect/infrastructure/__init__.py +0 -27
- prefect/infrastructure/provisioners/__init__.py +5 -3
- prefect/infrastructure/provisioners/cloud_run.py +11 -6
- prefect/infrastructure/provisioners/container_instance.py +11 -7
- prefect/infrastructure/provisioners/ecs.py +6 -4
- prefect/infrastructure/provisioners/modal.py +8 -5
- prefect/input/actions.py +2 -4
- prefect/input/run_input.py +9 -9
- prefect/logging/formatters.py +0 -2
- prefect/logging/handlers.py +3 -11
- prefect/logging/loggers.py +2 -2
- prefect/manifests.py +2 -1
- prefect/records/__init__.py +1 -0
- prefect/records/cache_policies.py +179 -0
- prefect/records/result_store.py +42 -0
- prefect/records/store.py +9 -0
- prefect/results.py +43 -39
- prefect/runner/runner.py +9 -9
- prefect/runner/server.py +6 -10
- prefect/runner/storage.py +3 -8
- prefect/runner/submit.py +2 -2
- prefect/runner/utils.py +2 -2
- prefect/serializers.py +24 -35
- prefect/server/api/collections_data/views/aggregate-worker-metadata.json +5 -14
- prefect/settings.py +76 -136
- prefect/states.py +22 -50
- prefect/task_engine.py +666 -56
- prefect/task_runners.py +272 -300
- prefect/task_runs.py +203 -0
- prefect/{task_server.py → task_worker.py} +89 -60
- prefect/tasks.py +358 -341
- prefect/transactions.py +224 -0
- prefect/types/__init__.py +61 -82
- prefect/utilities/asyncutils.py +195 -136
- prefect/utilities/callables.py +121 -41
- prefect/utilities/collections.py +23 -38
- prefect/utilities/dispatch.py +11 -3
- prefect/utilities/dockerutils.py +4 -0
- prefect/utilities/engine.py +140 -20
- prefect/utilities/importtools.py +26 -27
- prefect/utilities/pydantic.py +128 -38
- prefect/utilities/schema_tools/hydration.py +5 -1
- prefect/utilities/templating.py +12 -2
- prefect/variables.py +84 -62
- prefect/workers/__init__.py +0 -1
- prefect/workers/base.py +26 -18
- prefect/workers/process.py +3 -8
- prefect/workers/server.py +2 -2
- {prefect_client-2.19.4.dist-info → prefect_client-3.0.0rc2.dist-info}/METADATA +23 -21
- prefect_client-3.0.0rc2.dist-info/RECORD +179 -0
- 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/__init__.py +0 -0
- 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/__init__.py +0 -0
- 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/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/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/agent.py +0 -698
- prefect/deployments/deployments.py +0 -1042
- 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/base.py +0 -323
- prefect/infrastructure/container.py +0 -818
- prefect/infrastructure/kubernetes.py +0 -920
- prefect/infrastructure/process.py +0 -289
- 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/workers/block.py +0 -218
- prefect_client-2.19.4.dist-info/RECORD +0 -292
- {prefect_client-2.19.4.dist-info → prefect_client-3.0.0rc2.dist-info}/LICENSE +0 -0
- {prefect_client-2.19.4.dist-info → prefect_client-3.0.0rc2.dist-info}/WHEEL +0 -0
- {prefect_client-2.19.4.dist-info → prefect_client-3.0.0rc2.dist-info}/top_level.txt +0 -0
prefect/task_runs.py
ADDED
@@ -0,0 +1,203 @@
|
|
1
|
+
import asyncio
|
2
|
+
import atexit
|
3
|
+
import threading
|
4
|
+
import uuid
|
5
|
+
from typing import Dict, Optional
|
6
|
+
|
7
|
+
import anyio
|
8
|
+
from cachetools import TTLCache
|
9
|
+
from typing_extensions import Self
|
10
|
+
|
11
|
+
from prefect._internal.concurrency.api import create_call, from_async, from_sync
|
12
|
+
from prefect._internal.concurrency.threads import get_global_loop
|
13
|
+
from prefect.client.schemas.objects import TERMINAL_STATES
|
14
|
+
from prefect.events.clients import get_events_subscriber
|
15
|
+
from prefect.events.filters import EventFilter, EventNameFilter
|
16
|
+
from prefect.logging.loggers import get_logger
|
17
|
+
|
18
|
+
|
19
|
+
class TaskRunWaiter:
|
20
|
+
"""
|
21
|
+
A service used for waiting for a task run to finish.
|
22
|
+
|
23
|
+
This service listens for task run events and provides a way to wait for a specific
|
24
|
+
task run to finish. This is useful for waiting for a task run to finish before
|
25
|
+
continuing execution.
|
26
|
+
|
27
|
+
The service is a singleton and must be started before use. The service will
|
28
|
+
automatically start when the first instance is created. A single websocket
|
29
|
+
connection is used to listen for task run events.
|
30
|
+
|
31
|
+
The service can be used to wait for a task run to finish by calling
|
32
|
+
`TaskRunWaiter.wait_for_task_run` with the task run ID to wait for. The method
|
33
|
+
will return when the task run has finished or the timeout has elapsed.
|
34
|
+
|
35
|
+
The service will automatically stop when the Python process exits or when the
|
36
|
+
global loop thread is stopped.
|
37
|
+
|
38
|
+
Example:
|
39
|
+
```python
|
40
|
+
import asyncio
|
41
|
+
from uuid import uuid4
|
42
|
+
|
43
|
+
from prefect import task
|
44
|
+
from prefect.task_engine import run_task_async
|
45
|
+
from prefect.task_runs import TaskRunWaiter
|
46
|
+
|
47
|
+
|
48
|
+
@task
|
49
|
+
async def test_task():
|
50
|
+
await asyncio.sleep(5)
|
51
|
+
print("Done!")
|
52
|
+
|
53
|
+
|
54
|
+
async def main():
|
55
|
+
task_run_id = uuid4()
|
56
|
+
asyncio.create_task(run_task_async(task=test_task, task_run_id=task_run_id))
|
57
|
+
|
58
|
+
await TaskRunWaiter.wait_for_task_run(task_run_id)
|
59
|
+
print("Task run finished")
|
60
|
+
|
61
|
+
|
62
|
+
if __name__ == "__main__":
|
63
|
+
asyncio.run(main())
|
64
|
+
```
|
65
|
+
"""
|
66
|
+
|
67
|
+
_instance: Optional[Self] = None
|
68
|
+
_instance_lock = threading.Lock()
|
69
|
+
|
70
|
+
def __init__(self):
|
71
|
+
self.logger = get_logger("TaskRunWaiter")
|
72
|
+
self._consumer_task: Optional[asyncio.Task] = None
|
73
|
+
self._observed_completed_task_runs: TTLCache[uuid.UUID, bool] = TTLCache(
|
74
|
+
maxsize=100, ttl=60
|
75
|
+
)
|
76
|
+
self._completion_events: Dict[uuid.UUID, asyncio.Event] = {}
|
77
|
+
self._loop: Optional[asyncio.AbstractEventLoop] = None
|
78
|
+
self._observed_completed_task_runs_lock = threading.Lock()
|
79
|
+
self._completion_events_lock = threading.Lock()
|
80
|
+
self._started = False
|
81
|
+
|
82
|
+
def start(self):
|
83
|
+
"""
|
84
|
+
Start the TaskRunWaiter service.
|
85
|
+
"""
|
86
|
+
if self._started:
|
87
|
+
return
|
88
|
+
self.logger.info("Starting TaskRunWaiter")
|
89
|
+
loop_thread = get_global_loop()
|
90
|
+
|
91
|
+
if not asyncio.get_running_loop() == loop_thread._loop:
|
92
|
+
raise RuntimeError("TaskRunWaiter must run on the global loop thread.")
|
93
|
+
|
94
|
+
self._loop = loop_thread._loop
|
95
|
+
self._consumer_task = self._loop.create_task(self._consume_events())
|
96
|
+
|
97
|
+
loop_thread.add_shutdown_call(create_call(self.stop))
|
98
|
+
atexit.register(self.stop)
|
99
|
+
self._started = True
|
100
|
+
|
101
|
+
async def _consume_events(self):
|
102
|
+
async with get_events_subscriber(
|
103
|
+
filter=EventFilter(
|
104
|
+
event=EventNameFilter(
|
105
|
+
name=[
|
106
|
+
f"prefect.task-run.{state.name.title()}"
|
107
|
+
for state in TERMINAL_STATES
|
108
|
+
],
|
109
|
+
)
|
110
|
+
)
|
111
|
+
) as subscriber:
|
112
|
+
async for event in subscriber:
|
113
|
+
try:
|
114
|
+
self.logger.info(
|
115
|
+
f"Received event: {event.resource['prefect.resource.id']}"
|
116
|
+
)
|
117
|
+
task_run_id = uuid.UUID(
|
118
|
+
event.resource["prefect.resource.id"].replace(
|
119
|
+
"prefect.task-run.", ""
|
120
|
+
)
|
121
|
+
)
|
122
|
+
with self._observed_completed_task_runs_lock:
|
123
|
+
# Cache the task run ID for a short period of time to avoid
|
124
|
+
# unnecessary waits
|
125
|
+
self._observed_completed_task_runs[task_run_id] = True
|
126
|
+
with self._completion_events_lock:
|
127
|
+
# Set the event for the task run ID if it is in the cache
|
128
|
+
# so the waiter can wake up the waiting coroutine
|
129
|
+
if task_run_id in self._completion_events:
|
130
|
+
self._completion_events[task_run_id].set()
|
131
|
+
except Exception as exc:
|
132
|
+
self.logger.error(f"Error processing event: {exc}")
|
133
|
+
|
134
|
+
def stop(self):
|
135
|
+
"""
|
136
|
+
Stop the TaskRunWaiter service.
|
137
|
+
"""
|
138
|
+
self.logger.debug("Stopping TaskRunWaiter")
|
139
|
+
if self._consumer_task:
|
140
|
+
self._consumer_task.cancel()
|
141
|
+
self._consumer_task = None
|
142
|
+
self.__class__._instance = None
|
143
|
+
self._started = False
|
144
|
+
|
145
|
+
@classmethod
|
146
|
+
async def wait_for_task_run(
|
147
|
+
cls, task_run_id: uuid.UUID, timeout: Optional[float] = None
|
148
|
+
):
|
149
|
+
"""
|
150
|
+
Wait for a task run to finish.
|
151
|
+
|
152
|
+
Note this relies on a websocket connection to receive events from the server
|
153
|
+
and will not work with an ephemeral server.
|
154
|
+
|
155
|
+
Args:
|
156
|
+
task_run_id: The ID of the task run to wait for.
|
157
|
+
timeout: The maximum time to wait for the task run to
|
158
|
+
finish. Defaults to None.
|
159
|
+
"""
|
160
|
+
instance = cls.instance()
|
161
|
+
with instance._observed_completed_task_runs_lock:
|
162
|
+
if task_run_id in instance._observed_completed_task_runs:
|
163
|
+
return
|
164
|
+
|
165
|
+
# Need to create event in loop thread to ensure it can be set
|
166
|
+
# from the loop thread
|
167
|
+
finished_event = await from_async.wait_for_call_in_loop_thread(
|
168
|
+
create_call(asyncio.Event)
|
169
|
+
)
|
170
|
+
with instance._completion_events_lock:
|
171
|
+
# Cache the event for the task run ID so the consumer can set it
|
172
|
+
# when the event is received
|
173
|
+
instance._completion_events[task_run_id] = finished_event
|
174
|
+
|
175
|
+
with anyio.move_on_after(delay=timeout):
|
176
|
+
await from_async.wait_for_call_in_loop_thread(
|
177
|
+
create_call(finished_event.wait)
|
178
|
+
)
|
179
|
+
|
180
|
+
with instance._completion_events_lock:
|
181
|
+
# Remove the event from the cache after it has been waited on
|
182
|
+
instance._completion_events.pop(task_run_id, None)
|
183
|
+
|
184
|
+
@classmethod
|
185
|
+
def instance(cls):
|
186
|
+
"""
|
187
|
+
Get the singleton instance of TaskRunWaiter.
|
188
|
+
"""
|
189
|
+
with cls._instance_lock:
|
190
|
+
if cls._instance is None:
|
191
|
+
cls._instance = cls._new_instance()
|
192
|
+
return cls._instance
|
193
|
+
|
194
|
+
@classmethod
|
195
|
+
def _new_instance(cls):
|
196
|
+
instance = cls()
|
197
|
+
|
198
|
+
if threading.get_ident() == get_global_loop().thread.ident:
|
199
|
+
instance.start()
|
200
|
+
else:
|
201
|
+
from_sync.call_soon_in_loop_thread(create_call(instance.start)).result()
|
202
|
+
|
203
|
+
return instance
|
@@ -4,40 +4,39 @@ import os
|
|
4
4
|
import signal
|
5
5
|
import socket
|
6
6
|
import sys
|
7
|
+
from concurrent.futures import ThreadPoolExecutor
|
7
8
|
from contextlib import AsyncExitStack
|
8
|
-
from
|
9
|
-
from typing import List, Optional
|
9
|
+
from contextvars import copy_context
|
10
|
+
from typing import List, Optional
|
10
11
|
|
11
12
|
import anyio
|
13
|
+
import anyio.abc
|
14
|
+
from exceptiongroup import BaseExceptionGroup # novermin
|
12
15
|
from websockets.exceptions import InvalidStatusCode
|
13
16
|
|
14
|
-
from prefect import Task
|
17
|
+
from prefect import Task
|
15
18
|
from prefect._internal.concurrency.api import create_call, from_sync
|
19
|
+
from prefect.client.orchestration import get_client
|
16
20
|
from prefect.client.schemas.objects import TaskRun
|
17
21
|
from prefect.client.subscriptions import Subscription
|
18
|
-
from prefect.engine import emit_task_run_state_change_event, propose_state
|
19
22
|
from prefect.exceptions import Abort, PrefectHTTPStatusError
|
20
23
|
from prefect.logging.loggers import get_logger
|
21
24
|
from prefect.results import ResultFactory
|
22
25
|
from prefect.settings import (
|
23
26
|
PREFECT_API_URL,
|
24
|
-
PREFECT_EXPERIMENTAL_ENABLE_TASK_SCHEDULING,
|
25
27
|
PREFECT_TASK_SCHEDULING_DELETE_FAILED_SUBMISSIONS,
|
26
28
|
)
|
27
29
|
from prefect.states import Pending
|
28
|
-
from prefect.task_engine import
|
29
|
-
from prefect.task_runners import (
|
30
|
-
BaseTaskRunner,
|
31
|
-
ConcurrentTaskRunner,
|
32
|
-
)
|
30
|
+
from prefect.task_engine import run_task_async, run_task_sync
|
33
31
|
from prefect.utilities.asyncutils import asyncnullcontext, sync_compatible
|
32
|
+
from prefect.utilities.engine import emit_task_run_state_change_event, propose_state
|
34
33
|
from prefect.utilities.processutils import _register_signal
|
35
34
|
|
36
|
-
logger = get_logger("
|
35
|
+
logger = get_logger("task_worker")
|
37
36
|
|
38
37
|
|
39
|
-
class
|
40
|
-
"""Raised when the task
|
38
|
+
class StopTaskWorker(Exception):
|
39
|
+
"""Raised when the task worker is stopped."""
|
41
40
|
|
42
41
|
pass
|
43
42
|
|
@@ -52,11 +51,11 @@ def should_try_to_read_parameters(task: Task, task_run: TaskRun) -> bool:
|
|
52
51
|
return new_enough_state_details and task_accepts_parameters
|
53
52
|
|
54
53
|
|
55
|
-
class
|
54
|
+
class TaskWorker:
|
56
55
|
"""This class is responsible for serving tasks that may be executed in the background
|
57
56
|
by a task runner via the traditional engine machinery.
|
58
57
|
|
59
|
-
When `start()` is called, the task
|
58
|
+
When `start()` is called, the task worker will open a websocket connection to a
|
60
59
|
server-side queue of scheduled task runs. When a scheduled task run is found, the
|
61
60
|
scheduled task run is submitted to the engine for execution with a minimal `EngineContext`
|
62
61
|
so that the task run can be governed by orchestration rules.
|
@@ -64,18 +63,17 @@ class TaskServer:
|
|
64
63
|
Args:
|
65
64
|
- tasks: A list of tasks to serve. These tasks will be submitted to the engine
|
66
65
|
when a scheduled task run is found.
|
67
|
-
-
|
68
|
-
`
|
66
|
+
- limit: The maximum number of tasks that can be run concurrently. Defaults to 10.
|
67
|
+
Pass `None` to remove the limit.
|
69
68
|
"""
|
70
69
|
|
71
70
|
def __init__(
|
72
71
|
self,
|
73
72
|
*tasks: Task,
|
74
|
-
|
73
|
+
limit: Optional[int] = 10,
|
75
74
|
):
|
76
|
-
self.tasks: List[Task] = tasks
|
75
|
+
self.tasks: List[Task] = list(tasks)
|
77
76
|
|
78
|
-
self.task_runner: BaseTaskRunner = task_runner or ConcurrentTaskRunner()
|
79
77
|
self.started: bool = False
|
80
78
|
self.stopping: bool = False
|
81
79
|
|
@@ -84,10 +82,12 @@ class TaskServer:
|
|
84
82
|
|
85
83
|
if not asyncio.get_event_loop().is_running():
|
86
84
|
raise RuntimeError(
|
87
|
-
"
|
85
|
+
"TaskWorker must be initialized within an async context."
|
88
86
|
)
|
89
87
|
|
90
88
|
self._runs_task_group: anyio.abc.TaskGroup = anyio.create_task_group()
|
89
|
+
self._executor = ThreadPoolExecutor()
|
90
|
+
self._limiter = anyio.CapacityLimiter(limit) if limit else None
|
91
91
|
|
92
92
|
@property
|
93
93
|
def _client_id(self) -> str:
|
@@ -95,7 +95,7 @@ class TaskServer:
|
|
95
95
|
|
96
96
|
def handle_sigterm(self, signum, frame):
|
97
97
|
"""
|
98
|
-
Shuts down the task
|
98
|
+
Shuts down the task worker when a SIGTERM is received.
|
99
99
|
"""
|
100
100
|
logger.info("SIGTERM received, initiating graceful shutdown...")
|
101
101
|
from_sync.call_in_loop_thread(create_call(self.stop))
|
@@ -105,12 +105,12 @@ class TaskServer:
|
|
105
105
|
@sync_compatible
|
106
106
|
async def start(self) -> None:
|
107
107
|
"""
|
108
|
-
Starts a task
|
108
|
+
Starts a task worker, which runs the tasks provided in the constructor.
|
109
109
|
"""
|
110
110
|
_register_signal(signal.SIGTERM, self.handle_sigterm)
|
111
111
|
|
112
112
|
async with asyncnullcontext() if self.started else self:
|
113
|
-
logger.info("Starting task
|
113
|
+
logger.info("Starting task worker...")
|
114
114
|
try:
|
115
115
|
await self._subscribe_to_task_scheduling()
|
116
116
|
except InvalidStatusCode as exc:
|
@@ -126,17 +126,17 @@ class TaskServer:
|
|
126
126
|
|
127
127
|
@sync_compatible
|
128
128
|
async def stop(self):
|
129
|
-
"""Stops the task
|
129
|
+
"""Stops the task worker's polling cycle."""
|
130
130
|
if not self.started:
|
131
131
|
raise RuntimeError(
|
132
|
-
"Task
|
132
|
+
"Task worker has not yet started. Please start the task worker by"
|
133
133
|
" calling .start()"
|
134
134
|
)
|
135
135
|
|
136
136
|
self.started = False
|
137
137
|
self.stopping = True
|
138
138
|
|
139
|
-
raise
|
139
|
+
raise StopTaskWorker
|
140
140
|
|
141
141
|
async def _subscribe_to_task_scheduling(self):
|
142
142
|
logger.info(
|
@@ -148,8 +148,10 @@ class TaskServer:
|
|
148
148
|
keys=[task.task_key for task in self.tasks],
|
149
149
|
client_id=self._client_id,
|
150
150
|
):
|
151
|
+
if self._limiter:
|
152
|
+
await self._limiter.acquire_on_behalf_of(task_run.id)
|
151
153
|
logger.info(f"Received task run: {task_run.id} - {task_run.name}")
|
152
|
-
|
154
|
+
self._runs_task_group.start_soon(self._submit_scheduled_task_run, task_run)
|
153
155
|
|
154
156
|
async def _submit_scheduled_task_run(self, task_run: TaskRun):
|
155
157
|
logger.debug(
|
@@ -159,11 +161,11 @@ class TaskServer:
|
|
159
161
|
task = next((t for t in self.tasks if t.task_key == task_run.task_key), None)
|
160
162
|
|
161
163
|
if not task:
|
162
|
-
if PREFECT_TASK_SCHEDULING_DELETE_FAILED_SUBMISSIONS
|
164
|
+
if PREFECT_TASK_SCHEDULING_DELETE_FAILED_SUBMISSIONS:
|
163
165
|
logger.warning(
|
164
|
-
f"Task {task_run.name!r} not found in task
|
166
|
+
f"Task {task_run.name!r} not found in task worker registry."
|
165
167
|
)
|
166
|
-
await self._client._client.delete(f"/task_runs/{task_run.id}")
|
168
|
+
await self._client._client.delete(f"/task_runs/{task_run.id}") # type: ignore
|
167
169
|
|
168
170
|
return
|
169
171
|
|
@@ -171,12 +173,17 @@ class TaskServer:
|
|
171
173
|
# state_details. If there is no parameters_id, then the task was created
|
172
174
|
# without parameters.
|
173
175
|
parameters = {}
|
176
|
+
wait_for = []
|
177
|
+
run_context = None
|
174
178
|
if should_try_to_read_parameters(task, task_run):
|
175
179
|
parameters_id = task_run.state.state_details.task_parameters_id
|
176
180
|
task.persist_result = True
|
177
181
|
factory = await ResultFactory.from_autonomous_task(task)
|
178
182
|
try:
|
179
|
-
|
183
|
+
run_data = await factory.read_parameters(parameters_id)
|
184
|
+
parameters = run_data.get("parameters", {})
|
185
|
+
wait_for = run_data.get("wait_for", [])
|
186
|
+
run_context = run_data.get("context", None)
|
180
187
|
except Exception as exc:
|
181
188
|
logger.exception(
|
182
189
|
f"Failed to read parameters for task run {task_run.id!r}",
|
@@ -194,9 +201,11 @@ class TaskServer:
|
|
194
201
|
)
|
195
202
|
|
196
203
|
try:
|
204
|
+
new_state = Pending()
|
205
|
+
new_state.state_details.deferred = True
|
197
206
|
state = await propose_state(
|
198
207
|
client=get_client(), # TODO prove that we cannot use self._client here
|
199
|
-
state=
|
208
|
+
state=new_state,
|
200
209
|
task_run_id=task_run.id,
|
201
210
|
)
|
202
211
|
except Abort as exc:
|
@@ -225,44 +234,61 @@ class TaskServer:
|
|
225
234
|
validated_state=state,
|
226
235
|
)
|
227
236
|
|
228
|
-
|
229
|
-
|
230
|
-
submit_autonomous_task_run_to_engine,
|
237
|
+
if task.isasync:
|
238
|
+
await run_task_async(
|
231
239
|
task=task,
|
240
|
+
task_run_id=task_run.id,
|
232
241
|
task_run=task_run,
|
233
242
|
parameters=parameters,
|
234
|
-
|
235
|
-
|
243
|
+
wait_for=wait_for,
|
244
|
+
return_type="state",
|
245
|
+
context=run_context,
|
236
246
|
)
|
237
|
-
|
247
|
+
else:
|
248
|
+
context = copy_context()
|
249
|
+
future = self._executor.submit(
|
250
|
+
context.run,
|
251
|
+
run_task_sync,
|
252
|
+
task=task,
|
253
|
+
task_run_id=task_run.id,
|
254
|
+
task_run=task_run,
|
255
|
+
parameters=parameters,
|
256
|
+
wait_for=wait_for,
|
257
|
+
return_type="state",
|
258
|
+
context=run_context,
|
259
|
+
)
|
260
|
+
await asyncio.wrap_future(future)
|
261
|
+
if self._limiter:
|
262
|
+
self._limiter.release_on_behalf_of(task_run.id)
|
238
263
|
|
239
264
|
async def execute_task_run(self, task_run: TaskRun):
|
240
|
-
"""Execute a task run in the task
|
265
|
+
"""Execute a task run in the task worker."""
|
241
266
|
async with self if not self.started else asyncnullcontext():
|
267
|
+
if self._limiter:
|
268
|
+
await self._limiter.acquire_on_behalf_of(task_run.id)
|
242
269
|
await self._submit_scheduled_task_run(task_run)
|
243
270
|
|
244
271
|
async def __aenter__(self):
|
245
|
-
logger.debug("Starting task
|
272
|
+
logger.debug("Starting task worker...")
|
246
273
|
|
247
274
|
if self._client._closed:
|
248
275
|
self._client = get_client()
|
249
276
|
|
250
277
|
await self._exit_stack.enter_async_context(self._client)
|
251
|
-
await self._exit_stack.enter_async_context(self.
|
252
|
-
|
278
|
+
await self._exit_stack.enter_async_context(self._runs_task_group)
|
279
|
+
self._exit_stack.enter_context(self._executor)
|
253
280
|
|
254
281
|
self.started = True
|
255
282
|
return self
|
256
283
|
|
257
284
|
async def __aexit__(self, *exc_info):
|
258
|
-
logger.debug("Stopping task
|
285
|
+
logger.debug("Stopping task worker...")
|
259
286
|
self.started = False
|
260
|
-
await self._runs_task_group.__aexit__(*exc_info)
|
261
287
|
await self._exit_stack.__aexit__(*exc_info)
|
262
288
|
|
263
289
|
|
264
290
|
@sync_compatible
|
265
|
-
async def serve(*tasks: Task,
|
291
|
+
async def serve(*tasks: Task, limit: Optional[int] = 10):
|
266
292
|
"""Serve the provided tasks so that their runs may be submitted to and executed.
|
267
293
|
in the engine. Tasks do not need to be within a flow run context to be submitted.
|
268
294
|
You must `.submit` the same task object that you pass to `serve`.
|
@@ -270,13 +296,13 @@ async def serve(*tasks: Task, task_runner: Optional[Type[BaseTaskRunner]] = None
|
|
270
296
|
Args:
|
271
297
|
- tasks: A list of tasks to serve. When a scheduled task run is found for a
|
272
298
|
given task, the task run will be submitted to the engine for execution.
|
273
|
-
-
|
274
|
-
`
|
299
|
+
- limit: The maximum number of tasks that can be run concurrently. Defaults to 10.
|
300
|
+
Pass `None` to remove the limit.
|
275
301
|
|
276
302
|
Example:
|
277
303
|
```python
|
278
304
|
from prefect import task
|
279
|
-
from prefect.
|
305
|
+
from prefect.task_worker import serve
|
280
306
|
|
281
307
|
@task(log_prints=True)
|
282
308
|
def say(message: str):
|
@@ -291,18 +317,21 @@ async def serve(*tasks: Task, task_runner: Optional[Type[BaseTaskRunner]] = None
|
|
291
317
|
serve(say, yell)
|
292
318
|
```
|
293
319
|
"""
|
294
|
-
|
295
|
-
raise RuntimeError(
|
296
|
-
"To enable task scheduling, set PREFECT_EXPERIMENTAL_ENABLE_TASK_SCHEDULING"
|
297
|
-
" to True."
|
298
|
-
)
|
320
|
+
task_worker = TaskWorker(*tasks, limit=limit)
|
299
321
|
|
300
|
-
task_server = TaskServer(*tasks, task_runner=task_runner)
|
301
322
|
try:
|
302
|
-
await
|
323
|
+
await task_worker.start()
|
324
|
+
|
325
|
+
except BaseExceptionGroup as exc: # novermin
|
326
|
+
exceptions = exc.exceptions
|
327
|
+
n_exceptions = len(exceptions)
|
328
|
+
logger.error(
|
329
|
+
f"Task worker stopped with {n_exceptions} exception{'s' if n_exceptions != 1 else ''}:"
|
330
|
+
f"\n" + "\n".join(str(e) for e in exceptions)
|
331
|
+
)
|
303
332
|
|
304
|
-
except
|
305
|
-
logger.info("Task
|
333
|
+
except StopTaskWorker:
|
334
|
+
logger.info("Task worker stopped.")
|
306
335
|
|
307
|
-
except asyncio.CancelledError:
|
308
|
-
logger.info("Task
|
336
|
+
except (asyncio.CancelledError, KeyboardInterrupt):
|
337
|
+
logger.info("Task worker interrupted, stopping...")
|