prefect-client 3.2.7__py3-none-any.whl → 3.2.9__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/_build_info.py +3 -3
- prefect/_experimental/bundles.py +79 -0
- prefect/_waiters.py +254 -0
- prefect/client/subscriptions.py +2 -1
- prefect/events/clients.py +19 -17
- prefect/flow_runs.py +67 -35
- prefect/flows.py +3 -1
- prefect/futures.py +192 -22
- prefect/runner/runner.py +106 -39
- prefect/server/api/artifacts.py +5 -0
- prefect/server/api/automations.py +5 -0
- prefect/server/api/block_capabilities.py +5 -0
- prefect/server/api/block_documents.py +2 -0
- prefect/server/api/block_schemas.py +5 -0
- prefect/server/api/block_types.py +3 -1
- prefect/server/api/concurrency_limits.py +5 -0
- prefect/server/api/concurrency_limits_v2.py +5 -0
- prefect/server/api/deployments.py +2 -0
- prefect/server/api/events.py +5 -1
- prefect/server/api/flow_run_notification_policies.py +2 -0
- prefect/server/api/flow_run_states.py +2 -0
- prefect/server/api/flow_runs.py +2 -0
- prefect/server/api/flows.py +2 -0
- prefect/server/api/logs.py +5 -1
- prefect/server/api/task_run_states.py +2 -0
- prefect/server/api/task_runs.py +2 -0
- prefect/server/api/task_workers.py +5 -1
- prefect/server/api/variables.py +5 -0
- prefect/server/api/work_queues.py +2 -0
- prefect/server/api/workers.py +4 -0
- prefect/settings/profiles.py +6 -5
- prefect/task_worker.py +3 -3
- prefect/telemetry/instrumentation.py +2 -2
- prefect/utilities/templating.py +50 -11
- prefect/workers/base.py +3 -3
- prefect/workers/process.py +22 -319
- {prefect_client-3.2.7.dist-info → prefect_client-3.2.9.dist-info}/METADATA +2 -2
- {prefect_client-3.2.7.dist-info → prefect_client-3.2.9.dist-info}/RECORD +40 -39
- {prefect_client-3.2.7.dist-info → prefect_client-3.2.9.dist-info}/WHEEL +0 -0
- {prefect_client-3.2.7.dist-info → prefect_client-3.2.9.dist-info}/licenses/LICENSE +0 -0
prefect/utilities/templating.py
CHANGED
@@ -15,10 +15,13 @@ from typing import (
|
|
15
15
|
)
|
16
16
|
|
17
17
|
from prefect.client.utilities import inject_client
|
18
|
+
from prefect.logging.loggers import get_logger
|
18
19
|
from prefect.utilities.annotations import NotSet
|
19
20
|
from prefect.utilities.collections import get_from_dict
|
20
21
|
|
21
22
|
if TYPE_CHECKING:
|
23
|
+
import logging
|
24
|
+
|
22
25
|
from prefect.client.orchestration import PrefectClient
|
23
26
|
|
24
27
|
|
@@ -30,6 +33,9 @@ VARIABLE_PLACEHOLDER_PREFIX = "prefect.variables."
|
|
30
33
|
ENV_VAR_PLACEHOLDER_PREFIX = "$"
|
31
34
|
|
32
35
|
|
36
|
+
logger: "logging.Logger" = get_logger("utilities.templating")
|
37
|
+
|
38
|
+
|
33
39
|
class PlaceholderType(enum.Enum):
|
34
40
|
STANDARD = "standard"
|
35
41
|
BLOCK_DOCUMENT = "block_document"
|
@@ -92,24 +98,36 @@ def find_placeholders(template: T) -> set[Placeholder]:
|
|
92
98
|
|
93
99
|
@overload
|
94
100
|
def apply_values(
|
95
|
-
template: T,
|
101
|
+
template: T,
|
102
|
+
values: dict[str, Any],
|
103
|
+
remove_notset: Literal[True] = True,
|
104
|
+
warn_on_notset: bool = False,
|
96
105
|
) -> T: ...
|
97
106
|
|
98
107
|
|
99
108
|
@overload
|
100
109
|
def apply_values(
|
101
|
-
template: T,
|
110
|
+
template: T,
|
111
|
+
values: dict[str, Any],
|
112
|
+
remove_notset: Literal[False] = False,
|
113
|
+
warn_on_notset: bool = False,
|
102
114
|
) -> Union[T, type[NotSet]]: ...
|
103
115
|
|
104
116
|
|
105
117
|
@overload
|
106
118
|
def apply_values(
|
107
|
-
template: T,
|
119
|
+
template: T,
|
120
|
+
values: dict[str, Any],
|
121
|
+
remove_notset: bool = False,
|
122
|
+
warn_on_notset: bool = False,
|
108
123
|
) -> Union[T, type[NotSet]]: ...
|
109
124
|
|
110
125
|
|
111
126
|
def apply_values(
|
112
|
-
template: T,
|
127
|
+
template: T,
|
128
|
+
values: dict[str, Any],
|
129
|
+
remove_notset: bool = True,
|
130
|
+
warn_on_notset: bool = False,
|
113
131
|
) -> Union[T, type[NotSet]]:
|
114
132
|
"""
|
115
133
|
Replaces placeholders in a template with values from a supplied dictionary.
|
@@ -134,6 +152,7 @@ def apply_values(
|
|
134
152
|
template: template to discover and replace values in
|
135
153
|
values: The values to apply to placeholders in the template
|
136
154
|
remove_notset: If True, remove keys with an unset value
|
155
|
+
warn_on_notset: If True, warn when a placeholder is not found in `values`
|
137
156
|
|
138
157
|
Returns:
|
139
158
|
The template with the values applied
|
@@ -153,7 +172,13 @@ def apply_values(
|
|
153
172
|
# If there is only one variable with no surrounding text,
|
154
173
|
# we can replace it. If there is no variable value, we
|
155
174
|
# return NotSet to indicate that the value should not be included.
|
156
|
-
|
175
|
+
value = get_from_dict(values, list(placeholders)[0].name, NotSet)
|
176
|
+
if value is NotSet and warn_on_notset:
|
177
|
+
logger.warning(
|
178
|
+
f"Value for placeholder {list(placeholders)[0].name!r} not found in provided values. Please ensure that "
|
179
|
+
"the placeholder is spelled correctly and that the corresponding value is provided.",
|
180
|
+
)
|
181
|
+
return value
|
157
182
|
else:
|
158
183
|
for full_match, name, placeholder_type in placeholders:
|
159
184
|
if placeholder_type is PlaceholderType.STANDARD:
|
@@ -164,10 +189,14 @@ def apply_values(
|
|
164
189
|
else:
|
165
190
|
continue
|
166
191
|
|
167
|
-
if value is NotSet
|
168
|
-
|
169
|
-
|
170
|
-
|
192
|
+
if value is NotSet:
|
193
|
+
if warn_on_notset:
|
194
|
+
logger.warning(
|
195
|
+
f"Value for placeholder {full_match!r} not found in provided values. Please ensure that "
|
196
|
+
"the placeholder is spelled correctly and that the corresponding value is provided.",
|
197
|
+
)
|
198
|
+
if remove_notset:
|
199
|
+
template = template.replace(full_match, "")
|
171
200
|
else:
|
172
201
|
template = template.replace(full_match, str(value))
|
173
202
|
|
@@ -175,7 +204,12 @@ def apply_values(
|
|
175
204
|
elif isinstance(template, dict):
|
176
205
|
updated_template: dict[str, Any] = {}
|
177
206
|
for key, value in template.items():
|
178
|
-
updated_value = apply_values(
|
207
|
+
updated_value = apply_values(
|
208
|
+
value,
|
209
|
+
values,
|
210
|
+
remove_notset=remove_notset,
|
211
|
+
warn_on_notset=warn_on_notset,
|
212
|
+
)
|
179
213
|
if updated_value is not NotSet:
|
180
214
|
updated_template[key] = updated_value
|
181
215
|
elif not remove_notset:
|
@@ -185,7 +219,12 @@ def apply_values(
|
|
185
219
|
elif isinstance(template, list):
|
186
220
|
updated_list: list[Any] = []
|
187
221
|
for value in template:
|
188
|
-
updated_value = apply_values(
|
222
|
+
updated_value = apply_values(
|
223
|
+
value,
|
224
|
+
values,
|
225
|
+
remove_notset=remove_notset,
|
226
|
+
warn_on_notset=warn_on_notset,
|
227
|
+
)
|
189
228
|
if updated_value is not NotSet:
|
190
229
|
updated_list.append(updated_value)
|
191
230
|
return cast(T, updated_list)
|
prefect/workers/base.py
CHANGED
@@ -566,8 +566,6 @@ class BaseWorker(abc.ABC, Generic[C, V, R]):
|
|
566
566
|
healthcheck_thread = None
|
567
567
|
try:
|
568
568
|
async with self as worker:
|
569
|
-
# wait for an initial heartbeat to configure the worker
|
570
|
-
await worker.sync_with_backend()
|
571
569
|
# schedule the scheduled flow run polling loop
|
572
570
|
async with anyio.create_task_group() as loops_task_group:
|
573
571
|
loops_task_group.start_soon(
|
@@ -655,6 +653,8 @@ class BaseWorker(abc.ABC, Generic[C, V, R]):
|
|
655
653
|
await self._exit_stack.enter_async_context(self._client)
|
656
654
|
await self._exit_stack.enter_async_context(self._runs_task_group)
|
657
655
|
|
656
|
+
await self.sync_with_backend()
|
657
|
+
|
658
658
|
self.is_setup = True
|
659
659
|
|
660
660
|
async def teardown(self, *exc_info: Any) -> None:
|
@@ -1085,7 +1085,7 @@ class BaseWorker(abc.ABC, Generic[C, V, R]):
|
|
1085
1085
|
self,
|
1086
1086
|
flow_run: "FlowRun",
|
1087
1087
|
deployment: Optional["DeploymentResponse"] = None,
|
1088
|
-
) ->
|
1088
|
+
) -> C:
|
1089
1089
|
deployment = (
|
1090
1090
|
deployment
|
1091
1091
|
if deployment
|
prefect/workers/process.py
CHANGED
@@ -16,17 +16,11 @@ checkout out the [Prefect docs](/concepts/work-pools/).
|
|
16
16
|
|
17
17
|
from __future__ import annotations
|
18
18
|
|
19
|
-
import contextlib
|
20
19
|
import os
|
21
|
-
import signal
|
22
|
-
import socket
|
23
|
-
import subprocess
|
24
|
-
import sys
|
25
|
-
import tempfile
|
26
20
|
import threading
|
27
21
|
from functools import partial
|
28
22
|
from pathlib import Path
|
29
|
-
from typing import TYPE_CHECKING,
|
23
|
+
from typing import TYPE_CHECKING, Any, Callable, Optional
|
30
24
|
|
31
25
|
import anyio
|
32
26
|
import anyio.abc
|
@@ -34,26 +28,9 @@ from pydantic import Field, field_validator
|
|
34
28
|
|
35
29
|
from prefect._internal.schemas.validators import validate_working_dir
|
36
30
|
from prefect.client.schemas import FlowRun
|
37
|
-
from prefect.
|
38
|
-
FlowRunFilter,
|
39
|
-
FlowRunFilterId,
|
40
|
-
FlowRunFilterState,
|
41
|
-
FlowRunFilterStateName,
|
42
|
-
FlowRunFilterStateType,
|
43
|
-
WorkPoolFilter,
|
44
|
-
WorkPoolFilterName,
|
45
|
-
WorkQueueFilter,
|
46
|
-
WorkQueueFilterName,
|
47
|
-
)
|
48
|
-
from prefect.client.schemas.objects import StateType
|
49
|
-
from prefect.events.utilities import emit_event
|
50
|
-
from prefect.exceptions import (
|
51
|
-
InfrastructureNotAvailable,
|
52
|
-
InfrastructureNotFound,
|
53
|
-
ObjectNotFound,
|
54
|
-
)
|
31
|
+
from prefect.runner.runner import Runner
|
55
32
|
from prefect.settings import PREFECT_WORKER_QUERY_SECONDS
|
56
|
-
from prefect.utilities.processutils import get_sys_executable
|
33
|
+
from prefect.utilities.processutils import get_sys_executable
|
57
34
|
from prefect.utilities.services import critical_service_loop
|
58
35
|
from prefect.workers.base import (
|
59
36
|
BaseJobConfiguration,
|
@@ -66,20 +43,6 @@ if TYPE_CHECKING:
|
|
66
43
|
from prefect.client.schemas.objects import Flow
|
67
44
|
from prefect.client.schemas.responses import DeploymentResponse
|
68
45
|
|
69
|
-
if sys.platform == "win32":
|
70
|
-
# exit code indicating that the process was terminated by Ctrl+C or Ctrl+Break
|
71
|
-
STATUS_CONTROL_C_EXIT = 0xC000013A
|
72
|
-
|
73
|
-
|
74
|
-
def _infrastructure_pid_from_process(process: anyio.abc.Process) -> str:
|
75
|
-
hostname = socket.gethostname()
|
76
|
-
return f"{hostname}:{process.pid}"
|
77
|
-
|
78
|
-
|
79
|
-
def _parse_infrastructure_pid(infrastructure_pid: str) -> Tuple[str, int]:
|
80
|
-
hostname, pid = infrastructure_pid.split(":")
|
81
|
-
return hostname, int(pid)
|
82
|
-
|
83
46
|
|
84
47
|
class ProcessJobConfiguration(BaseJobConfiguration):
|
85
48
|
stream_output: bool = Field(default=True)
|
@@ -107,7 +70,8 @@ class ProcessJobConfiguration(BaseJobConfiguration):
|
|
107
70
|
else self.command
|
108
71
|
)
|
109
72
|
|
110
|
-
|
73
|
+
@staticmethod
|
74
|
+
def _base_flow_run_command() -> str:
|
111
75
|
"""
|
112
76
|
Override the base flow run command because enhanced cancellation doesn't
|
113
77
|
work with the process worker.
|
@@ -205,16 +169,6 @@ class ProcessWorker(
|
|
205
169
|
backoff=4,
|
206
170
|
)
|
207
171
|
)
|
208
|
-
loops_task_group.start_soon(
|
209
|
-
partial(
|
210
|
-
critical_service_loop,
|
211
|
-
workload=self.check_for_cancelled_flow_runs,
|
212
|
-
interval=PREFECT_WORKER_QUERY_SECONDS.value() * 2,
|
213
|
-
run_once=run_once,
|
214
|
-
jitter_range=0.3,
|
215
|
-
backoff=4,
|
216
|
-
)
|
217
|
-
)
|
218
172
|
|
219
173
|
self._started_event = await self._emit_worker_started_event()
|
220
174
|
|
@@ -249,279 +203,28 @@ class ProcessWorker(
|
|
249
203
|
configuration: ProcessJobConfiguration,
|
250
204
|
task_status: Optional[anyio.abc.TaskStatus[int]] = None,
|
251
205
|
) -> ProcessWorkerResult:
|
252
|
-
|
253
|
-
|
254
|
-
command
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
# parameter on Windows, because the presence of creationflags causes
|
260
|
-
# errors on Unix even if set to None
|
261
|
-
kwargs: Dict[str, object] = {}
|
262
|
-
if sys.platform == "win32":
|
263
|
-
kwargs["creationflags"] = subprocess.CREATE_NEW_PROCESS_GROUP
|
264
|
-
|
265
|
-
flow_run_logger.info("Opening process...")
|
266
|
-
|
267
|
-
working_dir_ctx = (
|
268
|
-
tempfile.TemporaryDirectory(suffix="prefect")
|
269
|
-
if not configuration.working_dir
|
270
|
-
else contextlib.nullcontext(configuration.working_dir)
|
206
|
+
process = await self._runner.execute_flow_run(
|
207
|
+
flow_run_id=flow_run.id,
|
208
|
+
command=configuration.command,
|
209
|
+
cwd=configuration.working_dir,
|
210
|
+
env=configuration.env,
|
211
|
+
stream_output=configuration.stream_output,
|
212
|
+
task_status=task_status,
|
271
213
|
)
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
)
|
276
|
-
process = await run_process(
|
277
|
-
command.split(" "),
|
278
|
-
stream_output=configuration.stream_output,
|
279
|
-
task_status=task_status,
|
280
|
-
task_status_handler=_infrastructure_pid_from_process,
|
281
|
-
cwd=working_dir,
|
282
|
-
env=configuration.env,
|
283
|
-
**kwargs,
|
284
|
-
)
|
285
|
-
|
286
|
-
# Use the pid for display if no name was given
|
287
|
-
display_name = f" {process.pid}"
|
288
|
-
|
289
|
-
if process.returncode:
|
290
|
-
help_message = None
|
291
|
-
if process.returncode == -9:
|
292
|
-
help_message = (
|
293
|
-
"This indicates that the process exited due to a SIGKILL signal. "
|
294
|
-
"Typically, this is either caused by manual cancellation or "
|
295
|
-
"high memory usage causing the operating system to "
|
296
|
-
"terminate the process."
|
297
|
-
)
|
298
|
-
if process.returncode == -15:
|
299
|
-
help_message = (
|
300
|
-
"This indicates that the process exited due to a SIGTERM signal. "
|
301
|
-
"Typically, this is caused by manual cancellation."
|
302
|
-
)
|
303
|
-
elif process.returncode == 247:
|
304
|
-
help_message = (
|
305
|
-
"This indicates that the process was terminated due to high "
|
306
|
-
"memory usage."
|
307
|
-
)
|
308
|
-
elif (
|
309
|
-
sys.platform == "win32" and process.returncode == STATUS_CONTROL_C_EXIT
|
310
|
-
):
|
311
|
-
help_message = (
|
312
|
-
"Process was terminated due to a Ctrl+C or Ctrl+Break signal. "
|
313
|
-
"Typically, this is caused by manual cancellation."
|
314
|
-
)
|
315
|
-
|
316
|
-
flow_run_logger.error(
|
317
|
-
f"Process{display_name} exited with status code: {process.returncode}"
|
318
|
-
+ (f"; {help_message}" if help_message else "")
|
319
|
-
)
|
320
|
-
else:
|
321
|
-
flow_run_logger.info(f"Process{display_name} exited cleanly.")
|
214
|
+
|
215
|
+
if process is None or process.returncode is None:
|
216
|
+
raise RuntimeError("Failed to start flow run process.")
|
322
217
|
|
323
218
|
return ProcessWorkerResult(
|
324
219
|
status_code=process.returncode, identifier=str(process.pid)
|
325
220
|
)
|
326
221
|
|
327
|
-
async def
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
) -> None:
|
332
|
-
hostname, pid = _parse_infrastructure_pid(infrastructure_pid)
|
333
|
-
|
334
|
-
if hostname != socket.gethostname():
|
335
|
-
raise InfrastructureNotAvailable(
|
336
|
-
f"Unable to kill process {pid!r}: The process is running on a different"
|
337
|
-
f" host {hostname!r}."
|
338
|
-
)
|
339
|
-
|
340
|
-
# In a non-windows environment first send a SIGTERM, then, after
|
341
|
-
# `grace_seconds` seconds have passed subsequent send SIGKILL. In
|
342
|
-
# Windows we use CTRL_BREAK_EVENT as SIGTERM is useless:
|
343
|
-
# https://bugs.python.org/issue26350
|
344
|
-
if sys.platform == "win32":
|
345
|
-
try:
|
346
|
-
os.kill(pid, signal.CTRL_BREAK_EVENT)
|
347
|
-
except (ProcessLookupError, WindowsError):
|
348
|
-
raise InfrastructureNotFound(
|
349
|
-
f"Unable to kill process {pid!r}: The process was not found."
|
350
|
-
)
|
351
|
-
else:
|
352
|
-
try:
|
353
|
-
os.kill(pid, signal.SIGTERM)
|
354
|
-
except ProcessLookupError:
|
355
|
-
raise InfrastructureNotFound(
|
356
|
-
f"Unable to kill process {pid!r}: The process was not found."
|
357
|
-
)
|
358
|
-
|
359
|
-
# Throttle how often we check if the process is still alive to keep
|
360
|
-
# from making too many system calls in a short period of time.
|
361
|
-
check_interval = max(grace_seconds / 10, 1)
|
362
|
-
|
363
|
-
with anyio.move_on_after(grace_seconds):
|
364
|
-
while True:
|
365
|
-
await anyio.sleep(check_interval)
|
366
|
-
|
367
|
-
# Detect if the process is still alive. If not do an early
|
368
|
-
# return as the process respected the SIGTERM from above.
|
369
|
-
try:
|
370
|
-
os.kill(pid, 0)
|
371
|
-
except ProcessLookupError:
|
372
|
-
return
|
373
|
-
|
374
|
-
try:
|
375
|
-
os.kill(pid, signal.SIGKILL)
|
376
|
-
except OSError:
|
377
|
-
# We shouldn't ever end up here, but it's possible that the
|
378
|
-
# process ended right after the check above.
|
379
|
-
return
|
380
|
-
|
381
|
-
async def check_for_cancelled_flow_runs(self) -> list["FlowRun"]:
|
382
|
-
if not self.is_setup:
|
383
|
-
raise RuntimeError(
|
384
|
-
"Worker is not set up. Please make sure you are running this worker "
|
385
|
-
"as an async context manager."
|
386
|
-
)
|
387
|
-
|
388
|
-
self._logger.debug("Checking for cancelled flow runs...")
|
389
|
-
|
390
|
-
work_queue_filter = (
|
391
|
-
WorkQueueFilter(name=WorkQueueFilterName(any_=list(self._work_queues)))
|
392
|
-
if self._work_queues
|
393
|
-
else None
|
394
|
-
)
|
395
|
-
|
396
|
-
named_cancelling_flow_runs = await self._client.read_flow_runs(
|
397
|
-
flow_run_filter=FlowRunFilter(
|
398
|
-
state=FlowRunFilterState(
|
399
|
-
type=FlowRunFilterStateType(any_=[StateType.CANCELLED]),
|
400
|
-
name=FlowRunFilterStateName(any_=["Cancelling"]),
|
401
|
-
),
|
402
|
-
# Avoid duplicate cancellation calls
|
403
|
-
id=FlowRunFilterId(not_any_=list(self._cancelling_flow_run_ids)),
|
404
|
-
),
|
405
|
-
work_pool_filter=WorkPoolFilter(
|
406
|
-
name=WorkPoolFilterName(any_=[self._work_pool_name])
|
407
|
-
),
|
408
|
-
work_queue_filter=work_queue_filter,
|
222
|
+
async def __aenter__(self) -> ProcessWorker:
|
223
|
+
await super().__aenter__()
|
224
|
+
self._runner = await self._exit_stack.enter_async_context(
|
225
|
+
Runner(pause_on_shutdown=False, limit=None)
|
409
226
|
)
|
227
|
+
return self
|
410
228
|
|
411
|
-
|
412
|
-
|
413
|
-
state=FlowRunFilterState(
|
414
|
-
type=FlowRunFilterStateType(any_=[StateType.CANCELLING]),
|
415
|
-
),
|
416
|
-
# Avoid duplicate cancellation calls
|
417
|
-
id=FlowRunFilterId(not_any_=list(self._cancelling_flow_run_ids)),
|
418
|
-
),
|
419
|
-
work_pool_filter=WorkPoolFilter(
|
420
|
-
name=WorkPoolFilterName(any_=[self._work_pool_name])
|
421
|
-
),
|
422
|
-
work_queue_filter=work_queue_filter,
|
423
|
-
)
|
424
|
-
|
425
|
-
cancelling_flow_runs = named_cancelling_flow_runs + typed_cancelling_flow_runs
|
426
|
-
|
427
|
-
if cancelling_flow_runs:
|
428
|
-
self._logger.info(
|
429
|
-
f"Found {len(cancelling_flow_runs)} flow runs awaiting cancellation."
|
430
|
-
)
|
431
|
-
|
432
|
-
for flow_run in cancelling_flow_runs:
|
433
|
-
self._cancelling_flow_run_ids.add(flow_run.id)
|
434
|
-
self._runs_task_group.start_soon(self.cancel_run, flow_run)
|
435
|
-
|
436
|
-
return cancelling_flow_runs
|
437
|
-
|
438
|
-
async def cancel_run(self, flow_run: "FlowRun") -> None:
|
439
|
-
run_logger = self.get_flow_run_logger(flow_run)
|
440
|
-
|
441
|
-
try:
|
442
|
-
configuration = await self._get_configuration(flow_run)
|
443
|
-
except ObjectNotFound:
|
444
|
-
self._logger.warning(
|
445
|
-
f"Flow run {flow_run.id!r} cannot be cancelled by this worker:"
|
446
|
-
f" associated deployment {flow_run.deployment_id!r} does not exist."
|
447
|
-
)
|
448
|
-
await self._mark_flow_run_as_cancelled(
|
449
|
-
flow_run,
|
450
|
-
state_updates={
|
451
|
-
"message": (
|
452
|
-
"This flow run is missing infrastructure configuration information"
|
453
|
-
" and cancellation cannot be guaranteed."
|
454
|
-
)
|
455
|
-
},
|
456
|
-
)
|
457
|
-
return
|
458
|
-
else:
|
459
|
-
if configuration.is_using_a_runner:
|
460
|
-
self._logger.info(
|
461
|
-
f"Skipping cancellation because flow run {str(flow_run.id)!r} is"
|
462
|
-
" using enhanced cancellation. A dedicated runner will handle"
|
463
|
-
" cancellation."
|
464
|
-
)
|
465
|
-
return
|
466
|
-
|
467
|
-
if not flow_run.infrastructure_pid:
|
468
|
-
run_logger.error(
|
469
|
-
f"Flow run '{flow_run.id}' does not have an infrastructure pid"
|
470
|
-
" attached. Cancellation cannot be guaranteed."
|
471
|
-
)
|
472
|
-
await self._mark_flow_run_as_cancelled(
|
473
|
-
flow_run,
|
474
|
-
state_updates={
|
475
|
-
"message": (
|
476
|
-
"This flow run is missing infrastructure tracking information"
|
477
|
-
" and cancellation cannot be guaranteed."
|
478
|
-
)
|
479
|
-
},
|
480
|
-
)
|
481
|
-
return
|
482
|
-
|
483
|
-
try:
|
484
|
-
await self.kill_process(
|
485
|
-
infrastructure_pid=flow_run.infrastructure_pid,
|
486
|
-
)
|
487
|
-
except NotImplementedError:
|
488
|
-
self._logger.error(
|
489
|
-
f"Worker type {self.type!r} does not support killing created "
|
490
|
-
"infrastructure. Cancellation cannot be guaranteed."
|
491
|
-
)
|
492
|
-
except InfrastructureNotFound as exc:
|
493
|
-
self._logger.warning(f"{exc} Marking flow run as cancelled.")
|
494
|
-
await self._mark_flow_run_as_cancelled(flow_run)
|
495
|
-
except InfrastructureNotAvailable as exc:
|
496
|
-
self._logger.warning(f"{exc} Flow run cannot be cancelled by this worker.")
|
497
|
-
except Exception:
|
498
|
-
run_logger.exception(
|
499
|
-
"Encountered exception while killing infrastructure for flow run "
|
500
|
-
f"'{flow_run.id}'. Flow run may not be cancelled."
|
501
|
-
)
|
502
|
-
# We will try again on generic exceptions
|
503
|
-
self._cancelling_flow_run_ids.remove(flow_run.id)
|
504
|
-
return
|
505
|
-
else:
|
506
|
-
self._emit_flow_run_cancelled_event(
|
507
|
-
flow_run=flow_run, configuration=configuration
|
508
|
-
)
|
509
|
-
await self._mark_flow_run_as_cancelled(flow_run)
|
510
|
-
run_logger.info(f"Cancelled flow run '{flow_run.id}'!")
|
511
|
-
|
512
|
-
def _emit_flow_run_cancelled_event(
|
513
|
-
self, flow_run: "FlowRun", configuration: BaseJobConfiguration
|
514
|
-
):
|
515
|
-
related = self._event_related_resources(configuration=configuration)
|
516
|
-
|
517
|
-
for resource in related:
|
518
|
-
if resource.role == "flow-run":
|
519
|
-
resource["prefect.infrastructure.identifier"] = str(
|
520
|
-
flow_run.infrastructure_pid
|
521
|
-
)
|
522
|
-
|
523
|
-
emit_event(
|
524
|
-
event="prefect.worker.cancelled-flow-run",
|
525
|
-
resource=self._event_resource(),
|
526
|
-
related=related,
|
527
|
-
)
|
229
|
+
async def __aexit__(self, *exc_info: Any) -> None:
|
230
|
+
await super().__aexit__(*exc_info)
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: prefect-client
|
3
|
-
Version: 3.2.
|
3
|
+
Version: 3.2.9
|
4
4
|
Summary: Workflow orchestration and management.
|
5
5
|
Project-URL: Changelog, https://github.com/PrefectHQ/prefect/releases
|
6
6
|
Project-URL: Documentation, https://docs.prefect.io
|
@@ -57,7 +57,7 @@ Requires-Dist: toml>=0.10.0
|
|
57
57
|
Requires-Dist: typing-extensions<5.0.0,>=4.5.0
|
58
58
|
Requires-Dist: ujson<6.0.0,>=5.8.0
|
59
59
|
Requires-Dist: uvicorn!=0.29.0,>=0.14.0
|
60
|
-
Requires-Dist: websockets<
|
60
|
+
Requires-Dist: websockets<16.0,>=13.0
|
61
61
|
Provides-Extra: notifications
|
62
62
|
Requires-Dist: apprise<2.0.0,>=1.1.0; extra == 'notifications'
|
63
63
|
Description-Content-Type: text/markdown
|