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.
Files changed (242) hide show
  1. prefect/__init__.py +8 -56
  2. prefect/_internal/compatibility/deprecated.py +6 -115
  3. prefect/_internal/compatibility/experimental.py +4 -79
  4. prefect/_internal/concurrency/api.py +0 -34
  5. prefect/_internal/concurrency/calls.py +0 -6
  6. prefect/_internal/concurrency/cancellation.py +0 -3
  7. prefect/_internal/concurrency/event_loop.py +0 -20
  8. prefect/_internal/concurrency/inspection.py +3 -3
  9. prefect/_internal/concurrency/threads.py +35 -0
  10. prefect/_internal/concurrency/waiters.py +0 -28
  11. prefect/_internal/pydantic/__init__.py +0 -45
  12. prefect/_internal/pydantic/v1_schema.py +21 -22
  13. prefect/_internal/pydantic/v2_schema.py +0 -2
  14. prefect/_internal/pydantic/v2_validated_func.py +18 -23
  15. prefect/_internal/schemas/bases.py +44 -177
  16. prefect/_internal/schemas/fields.py +1 -43
  17. prefect/_internal/schemas/validators.py +60 -158
  18. prefect/artifacts.py +161 -14
  19. prefect/automations.py +39 -4
  20. prefect/blocks/abstract.py +1 -1
  21. prefect/blocks/core.py +268 -148
  22. prefect/blocks/fields.py +2 -57
  23. prefect/blocks/kubernetes.py +8 -12
  24. prefect/blocks/notifications.py +40 -20
  25. prefect/blocks/redis.py +168 -0
  26. prefect/blocks/system.py +22 -11
  27. prefect/blocks/webhook.py +2 -9
  28. prefect/client/base.py +4 -4
  29. prefect/client/cloud.py +8 -13
  30. prefect/client/orchestration.py +362 -340
  31. prefect/client/schemas/actions.py +92 -86
  32. prefect/client/schemas/filters.py +20 -40
  33. prefect/client/schemas/objects.py +158 -152
  34. prefect/client/schemas/responses.py +16 -24
  35. prefect/client/schemas/schedules.py +47 -35
  36. prefect/client/subscriptions.py +2 -2
  37. prefect/client/utilities.py +5 -2
  38. prefect/concurrency/asyncio.py +4 -2
  39. prefect/concurrency/events.py +1 -1
  40. prefect/concurrency/services.py +7 -4
  41. prefect/context.py +195 -27
  42. prefect/deployments/__init__.py +5 -6
  43. prefect/deployments/base.py +7 -5
  44. prefect/deployments/flow_runs.py +185 -0
  45. prefect/deployments/runner.py +50 -45
  46. prefect/deployments/schedules.py +28 -23
  47. prefect/deployments/steps/__init__.py +0 -1
  48. prefect/deployments/steps/core.py +1 -0
  49. prefect/deployments/steps/pull.py +7 -21
  50. prefect/engine.py +12 -2422
  51. prefect/events/actions.py +17 -23
  52. prefect/events/cli/automations.py +19 -6
  53. prefect/events/clients.py +14 -37
  54. prefect/events/filters.py +14 -18
  55. prefect/events/related.py +2 -2
  56. prefect/events/schemas/__init__.py +0 -5
  57. prefect/events/schemas/automations.py +55 -46
  58. prefect/events/schemas/deployment_triggers.py +7 -197
  59. prefect/events/schemas/events.py +36 -65
  60. prefect/events/schemas/labelling.py +10 -14
  61. prefect/events/utilities.py +2 -3
  62. prefect/events/worker.py +2 -3
  63. prefect/filesystems.py +6 -517
  64. prefect/{new_flow_engine.py → flow_engine.py} +315 -74
  65. prefect/flow_runs.py +379 -7
  66. prefect/flows.py +248 -165
  67. prefect/futures.py +187 -345
  68. prefect/infrastructure/__init__.py +0 -27
  69. prefect/infrastructure/provisioners/__init__.py +5 -3
  70. prefect/infrastructure/provisioners/cloud_run.py +11 -6
  71. prefect/infrastructure/provisioners/container_instance.py +11 -7
  72. prefect/infrastructure/provisioners/ecs.py +6 -4
  73. prefect/infrastructure/provisioners/modal.py +8 -5
  74. prefect/input/actions.py +2 -4
  75. prefect/input/run_input.py +9 -9
  76. prefect/logging/formatters.py +0 -2
  77. prefect/logging/handlers.py +3 -11
  78. prefect/logging/loggers.py +2 -2
  79. prefect/manifests.py +2 -1
  80. prefect/records/__init__.py +1 -0
  81. prefect/records/cache_policies.py +179 -0
  82. prefect/records/result_store.py +42 -0
  83. prefect/records/store.py +9 -0
  84. prefect/results.py +43 -39
  85. prefect/runner/runner.py +9 -9
  86. prefect/runner/server.py +6 -10
  87. prefect/runner/storage.py +3 -8
  88. prefect/runner/submit.py +2 -2
  89. prefect/runner/utils.py +2 -2
  90. prefect/serializers.py +24 -35
  91. prefect/server/api/collections_data/views/aggregate-worker-metadata.json +5 -14
  92. prefect/settings.py +76 -136
  93. prefect/states.py +22 -50
  94. prefect/task_engine.py +666 -56
  95. prefect/task_runners.py +272 -300
  96. prefect/task_runs.py +203 -0
  97. prefect/{task_server.py → task_worker.py} +89 -60
  98. prefect/tasks.py +358 -341
  99. prefect/transactions.py +224 -0
  100. prefect/types/__init__.py +61 -82
  101. prefect/utilities/asyncutils.py +195 -136
  102. prefect/utilities/callables.py +121 -41
  103. prefect/utilities/collections.py +23 -38
  104. prefect/utilities/dispatch.py +11 -3
  105. prefect/utilities/dockerutils.py +4 -0
  106. prefect/utilities/engine.py +140 -20
  107. prefect/utilities/importtools.py +26 -27
  108. prefect/utilities/pydantic.py +128 -38
  109. prefect/utilities/schema_tools/hydration.py +5 -1
  110. prefect/utilities/templating.py +12 -2
  111. prefect/variables.py +84 -62
  112. prefect/workers/__init__.py +0 -1
  113. prefect/workers/base.py +26 -18
  114. prefect/workers/process.py +3 -8
  115. prefect/workers/server.py +2 -2
  116. {prefect_client-2.19.4.dist-info → prefect_client-3.0.0rc2.dist-info}/METADATA +23 -21
  117. prefect_client-3.0.0rc2.dist-info/RECORD +179 -0
  118. prefect/_internal/pydantic/_base_model.py +0 -51
  119. prefect/_internal/pydantic/_compat.py +0 -82
  120. prefect/_internal/pydantic/_flags.py +0 -20
  121. prefect/_internal/pydantic/_types.py +0 -8
  122. prefect/_internal/pydantic/utilities/__init__.py +0 -0
  123. prefect/_internal/pydantic/utilities/config_dict.py +0 -72
  124. prefect/_internal/pydantic/utilities/field_validator.py +0 -150
  125. prefect/_internal/pydantic/utilities/model_construct.py +0 -56
  126. prefect/_internal/pydantic/utilities/model_copy.py +0 -55
  127. prefect/_internal/pydantic/utilities/model_dump.py +0 -136
  128. prefect/_internal/pydantic/utilities/model_dump_json.py +0 -112
  129. prefect/_internal/pydantic/utilities/model_fields.py +0 -50
  130. prefect/_internal/pydantic/utilities/model_fields_set.py +0 -29
  131. prefect/_internal/pydantic/utilities/model_json_schema.py +0 -82
  132. prefect/_internal/pydantic/utilities/model_rebuild.py +0 -80
  133. prefect/_internal/pydantic/utilities/model_validate.py +0 -75
  134. prefect/_internal/pydantic/utilities/model_validate_json.py +0 -68
  135. prefect/_internal/pydantic/utilities/model_validator.py +0 -87
  136. prefect/_internal/pydantic/utilities/type_adapter.py +0 -71
  137. prefect/_vendor/__init__.py +0 -0
  138. prefect/_vendor/fastapi/__init__.py +0 -25
  139. prefect/_vendor/fastapi/applications.py +0 -946
  140. prefect/_vendor/fastapi/background.py +0 -3
  141. prefect/_vendor/fastapi/concurrency.py +0 -44
  142. prefect/_vendor/fastapi/datastructures.py +0 -58
  143. prefect/_vendor/fastapi/dependencies/__init__.py +0 -0
  144. prefect/_vendor/fastapi/dependencies/models.py +0 -64
  145. prefect/_vendor/fastapi/dependencies/utils.py +0 -877
  146. prefect/_vendor/fastapi/encoders.py +0 -177
  147. prefect/_vendor/fastapi/exception_handlers.py +0 -40
  148. prefect/_vendor/fastapi/exceptions.py +0 -46
  149. prefect/_vendor/fastapi/logger.py +0 -3
  150. prefect/_vendor/fastapi/middleware/__init__.py +0 -1
  151. prefect/_vendor/fastapi/middleware/asyncexitstack.py +0 -25
  152. prefect/_vendor/fastapi/middleware/cors.py +0 -3
  153. prefect/_vendor/fastapi/middleware/gzip.py +0 -3
  154. prefect/_vendor/fastapi/middleware/httpsredirect.py +0 -3
  155. prefect/_vendor/fastapi/middleware/trustedhost.py +0 -3
  156. prefect/_vendor/fastapi/middleware/wsgi.py +0 -3
  157. prefect/_vendor/fastapi/openapi/__init__.py +0 -0
  158. prefect/_vendor/fastapi/openapi/constants.py +0 -2
  159. prefect/_vendor/fastapi/openapi/docs.py +0 -203
  160. prefect/_vendor/fastapi/openapi/models.py +0 -480
  161. prefect/_vendor/fastapi/openapi/utils.py +0 -485
  162. prefect/_vendor/fastapi/param_functions.py +0 -340
  163. prefect/_vendor/fastapi/params.py +0 -453
  164. prefect/_vendor/fastapi/requests.py +0 -4
  165. prefect/_vendor/fastapi/responses.py +0 -40
  166. prefect/_vendor/fastapi/routing.py +0 -1331
  167. prefect/_vendor/fastapi/security/__init__.py +0 -15
  168. prefect/_vendor/fastapi/security/api_key.py +0 -98
  169. prefect/_vendor/fastapi/security/base.py +0 -6
  170. prefect/_vendor/fastapi/security/http.py +0 -172
  171. prefect/_vendor/fastapi/security/oauth2.py +0 -227
  172. prefect/_vendor/fastapi/security/open_id_connect_url.py +0 -34
  173. prefect/_vendor/fastapi/security/utils.py +0 -10
  174. prefect/_vendor/fastapi/staticfiles.py +0 -1
  175. prefect/_vendor/fastapi/templating.py +0 -3
  176. prefect/_vendor/fastapi/testclient.py +0 -1
  177. prefect/_vendor/fastapi/types.py +0 -3
  178. prefect/_vendor/fastapi/utils.py +0 -235
  179. prefect/_vendor/fastapi/websockets.py +0 -7
  180. prefect/_vendor/starlette/__init__.py +0 -1
  181. prefect/_vendor/starlette/_compat.py +0 -28
  182. prefect/_vendor/starlette/_exception_handler.py +0 -80
  183. prefect/_vendor/starlette/_utils.py +0 -88
  184. prefect/_vendor/starlette/applications.py +0 -261
  185. prefect/_vendor/starlette/authentication.py +0 -159
  186. prefect/_vendor/starlette/background.py +0 -43
  187. prefect/_vendor/starlette/concurrency.py +0 -59
  188. prefect/_vendor/starlette/config.py +0 -151
  189. prefect/_vendor/starlette/convertors.py +0 -87
  190. prefect/_vendor/starlette/datastructures.py +0 -707
  191. prefect/_vendor/starlette/endpoints.py +0 -130
  192. prefect/_vendor/starlette/exceptions.py +0 -60
  193. prefect/_vendor/starlette/formparsers.py +0 -276
  194. prefect/_vendor/starlette/middleware/__init__.py +0 -17
  195. prefect/_vendor/starlette/middleware/authentication.py +0 -52
  196. prefect/_vendor/starlette/middleware/base.py +0 -220
  197. prefect/_vendor/starlette/middleware/cors.py +0 -176
  198. prefect/_vendor/starlette/middleware/errors.py +0 -265
  199. prefect/_vendor/starlette/middleware/exceptions.py +0 -74
  200. prefect/_vendor/starlette/middleware/gzip.py +0 -113
  201. prefect/_vendor/starlette/middleware/httpsredirect.py +0 -19
  202. prefect/_vendor/starlette/middleware/sessions.py +0 -82
  203. prefect/_vendor/starlette/middleware/trustedhost.py +0 -64
  204. prefect/_vendor/starlette/middleware/wsgi.py +0 -147
  205. prefect/_vendor/starlette/requests.py +0 -328
  206. prefect/_vendor/starlette/responses.py +0 -347
  207. prefect/_vendor/starlette/routing.py +0 -933
  208. prefect/_vendor/starlette/schemas.py +0 -154
  209. prefect/_vendor/starlette/staticfiles.py +0 -248
  210. prefect/_vendor/starlette/status.py +0 -199
  211. prefect/_vendor/starlette/templating.py +0 -231
  212. prefect/_vendor/starlette/testclient.py +0 -804
  213. prefect/_vendor/starlette/types.py +0 -30
  214. prefect/_vendor/starlette/websockets.py +0 -193
  215. prefect/agent.py +0 -698
  216. prefect/deployments/deployments.py +0 -1042
  217. prefect/deprecated/__init__.py +0 -0
  218. prefect/deprecated/data_documents.py +0 -350
  219. prefect/deprecated/packaging/__init__.py +0 -12
  220. prefect/deprecated/packaging/base.py +0 -96
  221. prefect/deprecated/packaging/docker.py +0 -146
  222. prefect/deprecated/packaging/file.py +0 -92
  223. prefect/deprecated/packaging/orion.py +0 -80
  224. prefect/deprecated/packaging/serializers.py +0 -171
  225. prefect/events/instrument.py +0 -135
  226. prefect/infrastructure/base.py +0 -323
  227. prefect/infrastructure/container.py +0 -818
  228. prefect/infrastructure/kubernetes.py +0 -920
  229. prefect/infrastructure/process.py +0 -289
  230. prefect/new_task_engine.py +0 -423
  231. prefect/pydantic/__init__.py +0 -76
  232. prefect/pydantic/main.py +0 -39
  233. prefect/software/__init__.py +0 -2
  234. prefect/software/base.py +0 -50
  235. prefect/software/conda.py +0 -199
  236. prefect/software/pip.py +0 -122
  237. prefect/software/python.py +0 -52
  238. prefect/workers/block.py +0 -218
  239. prefect_client-2.19.4.dist-info/RECORD +0 -292
  240. {prefect_client-2.19.4.dist-info → prefect_client-3.0.0rc2.dist-info}/LICENSE +0 -0
  241. {prefect_client-2.19.4.dist-info → prefect_client-3.0.0rc2.dist-info}/WHEEL +0 -0
  242. {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 functools import partial
9
- from typing import List, Optional, Type
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, get_client
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 submit_autonomous_task_run_to_engine
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("task_server")
35
+ logger = get_logger("task_worker")
37
36
 
38
37
 
39
- class StopTaskServer(Exception):
40
- """Raised when the task server is stopped."""
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 TaskServer:
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 server will open a websocket connection to a
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
- - task_runner: The task runner to use for executing the tasks. Defaults to
68
- `ConcurrentTaskRunner`.
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
- task_runner: Optional[Type[BaseTaskRunner]] = None,
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
- "TaskServer must be initialized within an async context."
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 server when a SIGTERM is received.
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 server, which runs the tasks provided in the constructor.
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 server...")
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 server's polling cycle."""
129
+ """Stops the task worker's polling cycle."""
130
130
  if not self.started:
131
131
  raise RuntimeError(
132
- "Task server has not yet started. Please start the task server by"
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 StopTaskServer
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
- await self._submit_scheduled_task_run(task_run)
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.value():
164
+ if PREFECT_TASK_SCHEDULING_DELETE_FAILED_SUBMISSIONS:
163
165
  logger.warning(
164
- f"Task {task_run.name!r} not found in task server registry."
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
- parameters = await factory.read_parameters(parameters_id)
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=Pending(),
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
- self._runs_task_group.start_soon(
229
- partial(
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
- task_runner=self.task_runner,
235
- client=self._client,
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 server."""
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 server...")
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.task_runner.start())
252
- await self._runs_task_group.__aenter__()
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 server...")
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, task_runner: Optional[Type[BaseTaskRunner]] = None):
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
- - task_runner: The task runner to use for executing the tasks. Defaults to
274
- `ConcurrentTaskRunner`.
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.task_server import serve
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
- if not PREFECT_EXPERIMENTAL_ENABLE_TASK_SCHEDULING.value():
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 task_server.start()
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 StopTaskServer:
305
- logger.info("Task server stopped.")
333
+ except StopTaskWorker:
334
+ logger.info("Task worker stopped.")
306
335
 
307
- except asyncio.CancelledError:
308
- logger.info("Task server interrupted, stopping...")
336
+ except (asyncio.CancelledError, KeyboardInterrupt):
337
+ logger.info("Task worker interrupted, stopping...")