prefect-client 3.7.2.dev2__py3-none-any.whl → 3.7.2.dev4__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 +5 -0
- prefect/_build_info.py +3 -3
- prefect/automations.py +2 -0
- prefect/bundles/_zip_builder.py +32 -25
- prefect/events/__init__.py +2 -0
- prefect/events/actions.py +7 -0
- prefect/runner/runner.py +12 -12
- prefect/server/api/clients.py +3 -0
- prefect/server/api/work_queues.py +18 -1
- prefect/server/api/workers.py +366 -17
- prefect/settings/sources.py +7 -1
- prefect/utilities/templating/__init__.py +25 -4
- prefect/workers/_worker_channel/_protocol.py +4 -0
- prefect/workers/_worker_channel/_sync.py +15 -8
- prefect/workers/base.py +1 -3
- {prefect_client-3.7.2.dev2.dist-info → prefect_client-3.7.2.dev4.dist-info}/METADATA +1 -1
- {prefect_client-3.7.2.dev2.dist-info → prefect_client-3.7.2.dev4.dist-info}/RECORD +19 -19
- {prefect_client-3.7.2.dev2.dist-info → prefect_client-3.7.2.dev4.dist-info}/WHEEL +0 -0
- {prefect_client-3.7.2.dev2.dist-info → prefect_client-3.7.2.dev4.dist-info}/licenses/LICENSE +0 -0
prefect/__init__.py
CHANGED
|
@@ -113,6 +113,11 @@ def _initialize_plugins() -> None:
|
|
|
113
113
|
# Re-raise SystemExit from strict mode
|
|
114
114
|
raise
|
|
115
115
|
except Exception as e:
|
|
116
|
+
from pydantic_settings.exceptions import SettingsError
|
|
117
|
+
|
|
118
|
+
if isinstance(e, SettingsError):
|
|
119
|
+
return
|
|
120
|
+
|
|
116
121
|
# Log but don't crash on plugin errors
|
|
117
122
|
try:
|
|
118
123
|
from prefect.logging import get_logger
|
prefect/_build_info.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# Generated by versioningit
|
|
2
|
-
__version__ = "3.7.2.
|
|
3
|
-
__build_date__ = "2026-05-
|
|
4
|
-
__git_commit__ = "
|
|
2
|
+
__version__ = "3.7.2.dev4"
|
|
3
|
+
__build_date__ = "2026-05-21 09:15:36.144441+00:00"
|
|
4
|
+
__git_commit__ = "354623c1258b2e75de62230b8e3f0400f2722cf1"
|
|
5
5
|
__dirty__ = False
|
prefect/automations.py
CHANGED
|
@@ -11,6 +11,7 @@ from prefect.events.actions import (
|
|
|
11
11
|
CancelFlowRun,
|
|
12
12
|
ChangeFlowRunState,
|
|
13
13
|
DeclareIncident,
|
|
14
|
+
DeleteFlowRun,
|
|
14
15
|
DoNothing,
|
|
15
16
|
PauseAutomation,
|
|
16
17
|
PauseDeployment,
|
|
@@ -61,6 +62,7 @@ __all__ = [
|
|
|
61
62
|
"PauseDeployment",
|
|
62
63
|
"ResumeDeployment",
|
|
63
64
|
"CancelFlowRun",
|
|
65
|
+
"DeleteFlowRun",
|
|
64
66
|
"ChangeFlowRunState",
|
|
65
67
|
"PauseWorkQueue",
|
|
66
68
|
"ResumeWorkQueue",
|
prefect/bundles/_zip_builder.py
CHANGED
|
@@ -11,6 +11,7 @@ from __future__ import annotations
|
|
|
11
11
|
import hashlib
|
|
12
12
|
import logging
|
|
13
13
|
import shutil
|
|
14
|
+
import struct
|
|
14
15
|
import tempfile
|
|
15
16
|
import zipfile
|
|
16
17
|
from dataclasses import dataclass
|
|
@@ -32,7 +33,8 @@ class ZipResult:
|
|
|
32
33
|
|
|
33
34
|
Attributes:
|
|
34
35
|
zip_path: Path to the temporary zip file.
|
|
35
|
-
sha256_hash:
|
|
36
|
+
sha256_hash: SHA256 hex digest of the canonical bundled file contents
|
|
37
|
+
(paths, modes, and bytes), not the raw zip archive bytes.
|
|
36
38
|
storage_key: Content-addressed storage key in format "files/{hash}.zip".
|
|
37
39
|
size_bytes: Size of the zip file in bytes.
|
|
38
40
|
"""
|
|
@@ -95,16 +97,40 @@ class ZipBuilder:
|
|
|
95
97
|
self._temp_dir = tempfile.mkdtemp(prefix="prefect-zip-")
|
|
96
98
|
zip_path = Path(self._temp_dir) / "files.zip"
|
|
97
99
|
|
|
98
|
-
# Build the zip
|
|
100
|
+
# Build the zip and compute the content hash in a single pass so
|
|
101
|
+
# that every file is read exactly once, avoiding race conditions
|
|
102
|
+
# where a source file could change between the zip write and the
|
|
103
|
+
# hash computation.
|
|
104
|
+
hasher = hashlib.sha256()
|
|
105
|
+
hasher.update(b"prefect-bundle-files-v1\0")
|
|
106
|
+
|
|
99
107
|
with zipfile.ZipFile(zip_path, "w", compression=zipfile.ZIP_DEFLATED) as zf:
|
|
100
108
|
for file_path in sorted_files:
|
|
101
|
-
# Compute relative path with forward slashes
|
|
102
109
|
rel_path = file_path.relative_to(self.base_dir)
|
|
103
110
|
arcname = str(rel_path).replace("\\", "/")
|
|
104
|
-
zf.write(file_path, arcname)
|
|
105
111
|
|
|
106
|
-
|
|
107
|
-
|
|
112
|
+
file_stat = file_path.stat()
|
|
113
|
+
|
|
114
|
+
arcname_bytes = arcname.encode("utf-8")
|
|
115
|
+
hasher.update(struct.pack(">I", len(arcname_bytes)))
|
|
116
|
+
hasher.update(arcname_bytes)
|
|
117
|
+
hasher.update(struct.pack(">I", file_stat.st_mode & 0xFFFF))
|
|
118
|
+
hasher.update(struct.pack(">Q", file_stat.st_size))
|
|
119
|
+
|
|
120
|
+
zip_info = zipfile.ZipInfo.from_file(file_path, arcname)
|
|
121
|
+
zip_info.compress_type = zipfile.ZIP_DEFLATED
|
|
122
|
+
with (
|
|
123
|
+
file_path.open("rb") as src,
|
|
124
|
+
zf.open(zip_info, "w") as dest,
|
|
125
|
+
):
|
|
126
|
+
while True:
|
|
127
|
+
chunk = src.read(HASH_CHUNK_SIZE)
|
|
128
|
+
if not chunk:
|
|
129
|
+
break
|
|
130
|
+
dest.write(chunk)
|
|
131
|
+
hasher.update(chunk)
|
|
132
|
+
|
|
133
|
+
sha256_hash = hasher.hexdigest()
|
|
108
134
|
|
|
109
135
|
# Get file size
|
|
110
136
|
size_bytes = zip_path.stat().st_size
|
|
@@ -123,25 +149,6 @@ class ZipBuilder:
|
|
|
123
149
|
size_bytes=size_bytes,
|
|
124
150
|
)
|
|
125
151
|
|
|
126
|
-
def _compute_hash(self, zip_path: Path) -> str:
|
|
127
|
-
"""
|
|
128
|
-
Compute SHA256 hash of a file using chunked reading.
|
|
129
|
-
|
|
130
|
-
Args:
|
|
131
|
-
zip_path: Path to the file to hash.
|
|
132
|
-
|
|
133
|
-
Returns:
|
|
134
|
-
Lowercase hex digest of the SHA256 hash.
|
|
135
|
-
"""
|
|
136
|
-
hasher = hashlib.sha256()
|
|
137
|
-
with open(zip_path, "rb") as f:
|
|
138
|
-
while True:
|
|
139
|
-
chunk = f.read(HASH_CHUNK_SIZE)
|
|
140
|
-
if not chunk:
|
|
141
|
-
break
|
|
142
|
-
hasher.update(chunk)
|
|
143
|
-
return hasher.hexdigest()
|
|
144
|
-
|
|
145
152
|
def _emit_size_warning(
|
|
146
153
|
self, zip_path: Path, files: list[Path], size_bytes: int
|
|
147
154
|
) -> None:
|
prefect/events/__init__.py
CHANGED
|
@@ -31,6 +31,7 @@ from .actions import (
|
|
|
31
31
|
ResumeDeployment,
|
|
32
32
|
ChangeFlowRunState,
|
|
33
33
|
CancelFlowRun,
|
|
34
|
+
DeleteFlowRun,
|
|
34
35
|
SuspendFlowRun,
|
|
35
36
|
CallWebhook,
|
|
36
37
|
SendNotification,
|
|
@@ -78,6 +79,7 @@ __all__ = [
|
|
|
78
79
|
"ResumeDeployment",
|
|
79
80
|
"ChangeFlowRunState",
|
|
80
81
|
"CancelFlowRun",
|
|
82
|
+
"DeleteFlowRun",
|
|
81
83
|
"SuspendFlowRun",
|
|
82
84
|
"CallWebhook",
|
|
83
85
|
"SendNotification",
|
prefect/events/actions.py
CHANGED
|
@@ -126,6 +126,12 @@ class CancelFlowRun(Action):
|
|
|
126
126
|
type: Literal["cancel-flow-run"] = "cancel-flow-run"
|
|
127
127
|
|
|
128
128
|
|
|
129
|
+
class DeleteFlowRun(Action):
|
|
130
|
+
"""Deletes a flow run associated with the trigger"""
|
|
131
|
+
|
|
132
|
+
type: Literal["delete-flow-run"] = "delete-flow-run"
|
|
133
|
+
|
|
134
|
+
|
|
129
135
|
class ResumeFlowRun(Action):
|
|
130
136
|
"""Resumes a flow run associated with the trigger"""
|
|
131
137
|
|
|
@@ -294,6 +300,7 @@ ActionTypes: TypeAlias = Union[
|
|
|
294
300
|
ResumeDeployment,
|
|
295
301
|
ResumeFlowRun,
|
|
296
302
|
CancelFlowRun,
|
|
303
|
+
DeleteFlowRun,
|
|
297
304
|
ChangeFlowRunState,
|
|
298
305
|
PauseWorkQueue,
|
|
299
306
|
ResumeWorkQueue,
|
prefect/runner/runner.py
CHANGED
|
@@ -215,7 +215,7 @@ class Runner:
|
|
|
215
215
|
def goodbye_flow(name):
|
|
216
216
|
print(f"goodbye {name}")
|
|
217
217
|
|
|
218
|
-
if __name__ == "__main__"
|
|
218
|
+
if __name__ == "__main__":
|
|
219
219
|
runner = Runner(name="my-runner")
|
|
220
220
|
|
|
221
221
|
# Will be runnable via the API
|
|
@@ -614,17 +614,17 @@ class Runner:
|
|
|
614
614
|
def goodbye_flow(name):
|
|
615
615
|
print(f"goodbye {name}")
|
|
616
616
|
|
|
617
|
-
|
|
618
|
-
|
|
617
|
+
if __name__ == "__main__":
|
|
618
|
+
runner = Runner(name="my-runner")
|
|
619
619
|
|
|
620
|
-
|
|
621
|
-
|
|
620
|
+
# Will be runnable via the API
|
|
621
|
+
runner.add_flow(hello_flow)
|
|
622
622
|
|
|
623
|
-
|
|
624
|
-
|
|
623
|
+
# Run on a cron schedule
|
|
624
|
+
runner.add_flow(goodbye_flow, schedule={"cron": "0 * * * *"})
|
|
625
625
|
|
|
626
|
-
|
|
627
|
-
|
|
626
|
+
asyncio.run(runner.start())
|
|
627
|
+
```
|
|
628
628
|
"""
|
|
629
629
|
from prefect.runner.server import start_webserver
|
|
630
630
|
|
|
@@ -781,9 +781,9 @@ class Runner:
|
|
|
781
781
|
# The process may be a multiprocessing.context.SpawnProcess, in which case it will have an `exitcode` attribute
|
|
782
782
|
# but no `returncode` attribute
|
|
783
783
|
if (
|
|
784
|
-
getattr(process, "returncode", None)
|
|
785
|
-
|
|
786
|
-
)
|
|
784
|
+
getattr(process, "returncode", None) is None
|
|
785
|
+
and getattr(process, "exitcode", None) is None
|
|
786
|
+
):
|
|
787
787
|
await self._add_flow_run_process_map_entry(
|
|
788
788
|
flow_run.id, ProcessMapEntry(pid=process.pid, flow_run=flow_run)
|
|
789
789
|
)
|
prefect/server/api/clients.py
CHANGED
|
@@ -93,6 +93,9 @@ class OrchestrationClient(BaseClient):
|
|
|
93
93
|
async def read_flow_run_raw(self, flow_run_id: UUID) -> Response:
|
|
94
94
|
return await self._http_client.get(f"/flow_runs/{flow_run_id}")
|
|
95
95
|
|
|
96
|
+
async def delete_flow_run(self, flow_run_id: UUID) -> Response:
|
|
97
|
+
return await self._http_client.delete(f"/flow_runs/{flow_run_id}")
|
|
98
|
+
|
|
96
99
|
async def read_task_run_raw(self, task_run_id: UUID) -> Response:
|
|
97
100
|
return await self._http_client.get(f"/task_runs/{task_run_id}")
|
|
98
101
|
|
|
@@ -78,6 +78,15 @@ async def update_work_queue(
|
|
|
78
78
|
Updates an existing work queue.
|
|
79
79
|
"""
|
|
80
80
|
async with db.session_context(begin_transaction=True) as session:
|
|
81
|
+
existing_work_queue = await models.work_queues.read_work_queue(
|
|
82
|
+
session=session, work_queue_id=work_queue_id
|
|
83
|
+
)
|
|
84
|
+
if existing_work_queue is None:
|
|
85
|
+
raise HTTPException(
|
|
86
|
+
status_code=status.HTTP_404_NOT_FOUND,
|
|
87
|
+
detail=f"Work Queue {work_queue_id} not found",
|
|
88
|
+
)
|
|
89
|
+
|
|
81
90
|
result = await models.work_queues.update_work_queue(
|
|
82
91
|
session=session,
|
|
83
92
|
work_queue_id=work_queue_id,
|
|
@@ -86,7 +95,8 @@ async def update_work_queue(
|
|
|
86
95
|
)
|
|
87
96
|
if not result:
|
|
88
97
|
raise HTTPException(
|
|
89
|
-
status_code=status.HTTP_404_NOT_FOUND,
|
|
98
|
+
status_code=status.HTTP_404_NOT_FOUND,
|
|
99
|
+
detail=f"Work Queue {work_queue_id} not found",
|
|
90
100
|
)
|
|
91
101
|
|
|
92
102
|
|
|
@@ -239,6 +249,13 @@ async def delete_work_queue(
|
|
|
239
249
|
Delete a work queue by id.
|
|
240
250
|
"""
|
|
241
251
|
async with db.session_context(begin_transaction=True) as session:
|
|
252
|
+
existing_work_queue = await models.work_queues.read_work_queue(
|
|
253
|
+
session=session, work_queue_id=work_queue_id
|
|
254
|
+
)
|
|
255
|
+
if existing_work_queue is None:
|
|
256
|
+
raise HTTPException(
|
|
257
|
+
status_code=status.HTTP_404_NOT_FOUND, detail="work queue not found"
|
|
258
|
+
)
|
|
242
259
|
result = await models.work_queues.delete_work_queue(
|
|
243
260
|
session=session, work_queue_id=work_queue_id
|
|
244
261
|
)
|
prefect/server/api/workers.py
CHANGED
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
Routes for interacting with work queue objects.
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
|
-
from
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from logging import Logger
|
|
7
|
+
from typing import TYPE_CHECKING, Any, List, Optional
|
|
6
8
|
from uuid import UUID
|
|
7
9
|
|
|
8
10
|
import sqlalchemy as sa
|
|
@@ -11,15 +13,29 @@ from fastapi import (
|
|
|
11
13
|
Depends,
|
|
12
14
|
HTTPException,
|
|
13
15
|
Path,
|
|
16
|
+
WebSocket,
|
|
14
17
|
status,
|
|
15
18
|
)
|
|
16
19
|
from packaging.version import Version
|
|
20
|
+
from pydantic import ValidationError
|
|
17
21
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
18
22
|
|
|
19
23
|
import prefect.server.api.dependencies as dependencies
|
|
20
24
|
import prefect.server.models as models
|
|
21
25
|
import prefect.server.schemas as schemas
|
|
22
26
|
from prefect._internal.uuid7 import uuid7
|
|
27
|
+
from prefect.client.schemas.worker_channel import (
|
|
28
|
+
WORK_POOL_SNAPSHOT_CAPABILITY,
|
|
29
|
+
WORKER_HEARTBEAT_CAPABILITY,
|
|
30
|
+
WorkerChannelCloseReason,
|
|
31
|
+
WorkerChannelProtocolError,
|
|
32
|
+
WorkerHelloFrame,
|
|
33
|
+
WorkerReadyFrame,
|
|
34
|
+
WorkPoolSnapshotPayload,
|
|
35
|
+
select_worker_channel_version,
|
|
36
|
+
validate_worker_channel_frame,
|
|
37
|
+
)
|
|
38
|
+
from prefect.logging import get_logger
|
|
23
39
|
from prefect.server.api.validation import validate_job_variable_defaults_for_work_pool
|
|
24
40
|
from prefect.server.database import PrefectDBInterface, provide_database_interface
|
|
25
41
|
from prefect.server.models.deployments import mark_deployments_ready
|
|
@@ -29,17 +45,26 @@ from prefect.server.models.work_queues import (
|
|
|
29
45
|
)
|
|
30
46
|
from prefect.server.models.workers import emit_work_pool_status_event
|
|
31
47
|
from prefect.server.schemas.statuses import WorkQueueStatus
|
|
48
|
+
from prefect.server.utilities import subscriptions
|
|
49
|
+
from prefect.server.utilities import worker_channel as worker_channel_utils
|
|
32
50
|
from prefect.server.utilities.server import PrefectRouter
|
|
33
51
|
from prefect.types import DateTime
|
|
34
52
|
from prefect.types._datetime import now
|
|
35
53
|
|
|
36
54
|
if TYPE_CHECKING:
|
|
37
|
-
from prefect.server.database.orm_models import
|
|
55
|
+
from prefect.server.database.orm_models import WorkPool as ORMWorkPool
|
|
56
|
+
from prefect.server.database.orm_models import WorkQueue as ORMWorkQueue
|
|
38
57
|
|
|
39
58
|
router: PrefectRouter = PrefectRouter(
|
|
40
59
|
prefix="/work_pools",
|
|
41
60
|
tags=["Work Pools"],
|
|
42
61
|
)
|
|
62
|
+
logger: Logger = get_logger("prefect.server.api.workers")
|
|
63
|
+
|
|
64
|
+
_OSS_WORKER_CHANNEL_ACCEPTED_CAPABILITIES = [
|
|
65
|
+
WORKER_HEARTBEAT_CAPABILITY,
|
|
66
|
+
WORK_POOL_SNAPSHOT_CAPABILITY,
|
|
67
|
+
]
|
|
43
68
|
|
|
44
69
|
|
|
45
70
|
# -----------------------------------------------------
|
|
@@ -145,6 +170,241 @@ class WorkerLookups:
|
|
|
145
170
|
return queue.id
|
|
146
171
|
|
|
147
172
|
|
|
173
|
+
class WorkerChannelSetupError(Exception):
|
|
174
|
+
def __init__(self, close_reason: WorkerChannelCloseReason, detail: str):
|
|
175
|
+
super().__init__(detail)
|
|
176
|
+
self.close_reason = close_reason
|
|
177
|
+
self.detail = detail
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
@dataclass(frozen=True)
|
|
181
|
+
class WorkerChannelWorkPoolUpdateEvent:
|
|
182
|
+
work_pool_id: UUID
|
|
183
|
+
changed_fields: dict[str, dict[str, Any]]
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
async def _receive_worker_hello(websocket: WebSocket) -> WorkerHelloFrame:
|
|
187
|
+
try:
|
|
188
|
+
message = await websocket.receive_json()
|
|
189
|
+
frame = validate_worker_channel_frame(message)
|
|
190
|
+
except ValidationError as exc:
|
|
191
|
+
raise WorkerChannelSetupError(
|
|
192
|
+
WorkerChannelCloseReason.PROTOCOL_ERROR,
|
|
193
|
+
"Worker channel received a malformed hello frame",
|
|
194
|
+
) from exc
|
|
195
|
+
except ValueError as exc:
|
|
196
|
+
raise WorkerChannelSetupError(
|
|
197
|
+
WorkerChannelCloseReason.PROTOCOL_ERROR,
|
|
198
|
+
"Worker channel received invalid JSON during setup",
|
|
199
|
+
) from exc
|
|
200
|
+
|
|
201
|
+
if not isinstance(frame, WorkerHelloFrame):
|
|
202
|
+
raise WorkerChannelSetupError(
|
|
203
|
+
WorkerChannelCloseReason.PROTOCOL_ERROR,
|
|
204
|
+
"Expected worker.hello.v1 during worker channel setup",
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
return frame
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
async def _resolve_worker_channel_work_pool(
|
|
211
|
+
session: AsyncSession,
|
|
212
|
+
work_pool_name: str,
|
|
213
|
+
hello: WorkerHelloFrame,
|
|
214
|
+
) -> "ORMWorkPool":
|
|
215
|
+
work_pool = await models.workers.read_work_pool_by_name(
|
|
216
|
+
session=session,
|
|
217
|
+
work_pool_name=work_pool_name,
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
default_base_job_template = hello.payload.default_base_job_template
|
|
221
|
+
if work_pool is None:
|
|
222
|
+
if not hello.payload.create_pool_if_not_found:
|
|
223
|
+
raise WorkerChannelSetupError(
|
|
224
|
+
WorkerChannelCloseReason.AUTHORIZATION_FAILED,
|
|
225
|
+
"work_pool_not_found",
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
if work_pool_name.lower().startswith("prefect"):
|
|
229
|
+
raise WorkerChannelSetupError(
|
|
230
|
+
WorkerChannelCloseReason.AUTHORIZATION_FAILED,
|
|
231
|
+
"work_pool_creation_unauthorized",
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
await validate_job_variable_defaults_for_work_pool(
|
|
235
|
+
session, work_pool_name, default_base_job_template
|
|
236
|
+
)
|
|
237
|
+
try:
|
|
238
|
+
async with session.begin_nested():
|
|
239
|
+
work_pool = await models.workers.create_work_pool(
|
|
240
|
+
session=session,
|
|
241
|
+
work_pool=schemas.actions.WorkPoolCreate(
|
|
242
|
+
name=work_pool_name,
|
|
243
|
+
type=hello.payload.worker_type,
|
|
244
|
+
base_job_template=default_base_job_template,
|
|
245
|
+
),
|
|
246
|
+
)
|
|
247
|
+
except sa.exc.IntegrityError:
|
|
248
|
+
work_pool = await models.workers.read_work_pool_by_name(
|
|
249
|
+
session=session,
|
|
250
|
+
work_pool_name=work_pool_name,
|
|
251
|
+
)
|
|
252
|
+
if work_pool is None:
|
|
253
|
+
raise
|
|
254
|
+
return work_pool
|
|
255
|
+
|
|
256
|
+
return work_pool
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
async def _resolve_worker_channel_work_queues(
|
|
260
|
+
session: AsyncSession,
|
|
261
|
+
work_pool_id: UUID,
|
|
262
|
+
work_pool_name: str,
|
|
263
|
+
work_queue_names: list[str],
|
|
264
|
+
) -> list["ORMWorkQueue"]:
|
|
265
|
+
if not work_queue_names:
|
|
266
|
+
return list(
|
|
267
|
+
await models.workers.read_work_queues(
|
|
268
|
+
session=session, work_pool_id=work_pool_id
|
|
269
|
+
)
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
work_queues = []
|
|
273
|
+
for work_queue_name in dict.fromkeys(work_queue_names):
|
|
274
|
+
work_queue = await models.workers.read_work_queue_by_name(
|
|
275
|
+
session=session,
|
|
276
|
+
work_pool_name=work_pool_name,
|
|
277
|
+
work_queue_name=work_queue_name,
|
|
278
|
+
)
|
|
279
|
+
if work_queue is None:
|
|
280
|
+
raise WorkerChannelSetupError(
|
|
281
|
+
WorkerChannelCloseReason.AUTHORIZATION_FAILED,
|
|
282
|
+
"work_queue_not_found",
|
|
283
|
+
)
|
|
284
|
+
work_queues.append(work_queue)
|
|
285
|
+
|
|
286
|
+
return work_queues
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
async def _build_worker_ready_frame(
|
|
290
|
+
session: AsyncSession,
|
|
291
|
+
work_pool_name: str,
|
|
292
|
+
hello: WorkerHelloFrame,
|
|
293
|
+
) -> tuple[WorkerReadyFrame, WorkerChannelWorkPoolUpdateEvent | None]:
|
|
294
|
+
try:
|
|
295
|
+
selected_channel_version = select_worker_channel_version(
|
|
296
|
+
hello.payload.supported_channel_versions
|
|
297
|
+
)
|
|
298
|
+
except WorkerChannelProtocolError as exc:
|
|
299
|
+
raise WorkerChannelSetupError(exc.close_reason, str(exc)) from exc
|
|
300
|
+
|
|
301
|
+
work_pool = await _resolve_worker_channel_work_pool(
|
|
302
|
+
session=session,
|
|
303
|
+
work_pool_name=work_pool_name,
|
|
304
|
+
hello=hello,
|
|
305
|
+
)
|
|
306
|
+
work_queues = await _resolve_worker_channel_work_queues(
|
|
307
|
+
session=session,
|
|
308
|
+
work_pool_id=work_pool.id,
|
|
309
|
+
work_pool_name=work_pool_name,
|
|
310
|
+
work_queue_names=hello.payload.work_queue_names,
|
|
311
|
+
)
|
|
312
|
+
default_base_job_template = hello.payload.default_base_job_template
|
|
313
|
+
work_pool_update_event = None
|
|
314
|
+
if not work_pool.base_job_template and default_base_job_template:
|
|
315
|
+
previous_base_job_template = work_pool.base_job_template
|
|
316
|
+
await validate_job_variable_defaults_for_work_pool(
|
|
317
|
+
session, work_pool_name, default_base_job_template
|
|
318
|
+
)
|
|
319
|
+
updated = await models.workers.update_work_pool(
|
|
320
|
+
session=session,
|
|
321
|
+
work_pool_id=work_pool.id,
|
|
322
|
+
work_pool=schemas.actions.WorkPoolUpdate(
|
|
323
|
+
base_job_template=default_base_job_template
|
|
324
|
+
),
|
|
325
|
+
emit_update_event=False,
|
|
326
|
+
emit_status_change=emit_work_pool_status_event,
|
|
327
|
+
)
|
|
328
|
+
if updated:
|
|
329
|
+
work_pool_update_event = WorkerChannelWorkPoolUpdateEvent(
|
|
330
|
+
work_pool_id=work_pool.id,
|
|
331
|
+
changed_fields={
|
|
332
|
+
"base_job_template": {
|
|
333
|
+
"from": previous_base_job_template,
|
|
334
|
+
"to": default_base_job_template,
|
|
335
|
+
}
|
|
336
|
+
},
|
|
337
|
+
)
|
|
338
|
+
refreshed = await models.workers.read_work_pool(
|
|
339
|
+
session=session, work_pool_id=work_pool.id
|
|
340
|
+
)
|
|
341
|
+
assert refreshed is not None
|
|
342
|
+
work_pool = refreshed
|
|
343
|
+
|
|
344
|
+
try:
|
|
345
|
+
worker = await models.workers.record_worker_heartbeat(
|
|
346
|
+
session=session,
|
|
347
|
+
work_pool=work_pool,
|
|
348
|
+
worker_name=hello.payload.worker_name,
|
|
349
|
+
heartbeat_interval_seconds=hello.payload.heartbeat_interval_seconds,
|
|
350
|
+
emit_status_change=emit_work_pool_status_event,
|
|
351
|
+
return_worker=True,
|
|
352
|
+
)
|
|
353
|
+
except Exception as exc:
|
|
354
|
+
raise WorkerChannelSetupError(
|
|
355
|
+
WorkerChannelCloseReason.HEARTBEAT_PERSISTENCE_FAILED,
|
|
356
|
+
"worker_channel_initial_heartbeat_failed",
|
|
357
|
+
) from exc
|
|
358
|
+
assert worker is not None
|
|
359
|
+
|
|
360
|
+
refreshed_work_pool = await models.workers.read_work_pool(
|
|
361
|
+
session=session, work_pool_id=work_pool.id
|
|
362
|
+
)
|
|
363
|
+
assert refreshed_work_pool is not None
|
|
364
|
+
initial_snapshot = WorkPoolSnapshotPayload(
|
|
365
|
+
snapshot_sequence=1,
|
|
366
|
+
reason="initial",
|
|
367
|
+
work_pool=await worker_channel_utils.build_worker_channel_work_pool_snapshot(
|
|
368
|
+
session=session,
|
|
369
|
+
work_pool=refreshed_work_pool,
|
|
370
|
+
),
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
requested_capabilities = list(dict.fromkeys(hello.payload.requested_capabilities))
|
|
374
|
+
accepted = _OSS_WORKER_CHANNEL_ACCEPTED_CAPABILITIES
|
|
375
|
+
accepted_set = set(accepted)
|
|
376
|
+
rejected = [
|
|
377
|
+
capability
|
|
378
|
+
for capability in requested_capabilities
|
|
379
|
+
if capability not in accepted_set
|
|
380
|
+
]
|
|
381
|
+
|
|
382
|
+
return (
|
|
383
|
+
WorkerReadyFrame(
|
|
384
|
+
type="worker.ready.v1",
|
|
385
|
+
id=uuid7(),
|
|
386
|
+
sent_at=now("UTC"),
|
|
387
|
+
payload={
|
|
388
|
+
"consumer_id": hello.payload.consumer_id,
|
|
389
|
+
"worker_id": worker.id,
|
|
390
|
+
"selected_channel_version": selected_channel_version,
|
|
391
|
+
"effective_heartbeat_interval_seconds": (
|
|
392
|
+
hello.payload.heartbeat_interval_seconds
|
|
393
|
+
),
|
|
394
|
+
"accepted_capabilities": accepted,
|
|
395
|
+
"rejected_capabilities": rejected,
|
|
396
|
+
"effective_max_cleanup_concurrency": 0,
|
|
397
|
+
"resolved_work_queues": [
|
|
398
|
+
{"id": work_queue.id, "name": work_queue.name}
|
|
399
|
+
for work_queue in work_queues
|
|
400
|
+
],
|
|
401
|
+
"initial_snapshot": initial_snapshot,
|
|
402
|
+
},
|
|
403
|
+
),
|
|
404
|
+
work_pool_update_event,
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
|
|
148
408
|
# -----------------------------------------------------
|
|
149
409
|
# --
|
|
150
410
|
# --
|
|
@@ -333,13 +593,23 @@ async def update_work_pool(
|
|
|
333
593
|
work_pool_id = await worker_lookups._get_work_pool_id_from_name(
|
|
334
594
|
session=session, work_pool_name=work_pool_name
|
|
335
595
|
)
|
|
336
|
-
await models.workers.update_work_pool(
|
|
596
|
+
updated = await models.workers.update_work_pool(
|
|
337
597
|
session=session,
|
|
338
598
|
work_pool_id=work_pool_id,
|
|
339
599
|
work_pool=work_pool,
|
|
340
600
|
emit_status_change=emit_work_pool_status_event,
|
|
341
601
|
)
|
|
342
602
|
|
|
603
|
+
if updated and worker_channel_utils.work_pool_update_triggers_snapshot(
|
|
604
|
+
update_values
|
|
605
|
+
):
|
|
606
|
+
await worker_channel_utils.publish_snapshot_invalidation(
|
|
607
|
+
worker_channel_utils.WorkerChannelSnapshotInvalidation(
|
|
608
|
+
work_pool_id=work_pool_id,
|
|
609
|
+
reason="work_pool_updated",
|
|
610
|
+
)
|
|
611
|
+
)
|
|
612
|
+
|
|
343
613
|
|
|
344
614
|
@router.delete("/{name}", status_code=status.HTTP_204_NO_CONTENT)
|
|
345
615
|
async def delete_work_pool(
|
|
@@ -365,10 +635,19 @@ async def delete_work_pool(
|
|
|
365
635
|
session=session, work_pool_name=work_pool_name
|
|
366
636
|
)
|
|
367
637
|
|
|
368
|
-
await models.workers.delete_work_pool(
|
|
638
|
+
deleted = await models.workers.delete_work_pool(
|
|
369
639
|
session=session, work_pool_id=work_pool_id
|
|
370
640
|
)
|
|
371
641
|
|
|
642
|
+
if deleted:
|
|
643
|
+
await worker_channel_utils.publish_snapshot_invalidation(
|
|
644
|
+
worker_channel_utils.WorkerChannelSnapshotInvalidation(
|
|
645
|
+
work_pool_id=work_pool_id,
|
|
646
|
+
reason="work_pool_deleted",
|
|
647
|
+
work_pool_deleted=True,
|
|
648
|
+
)
|
|
649
|
+
)
|
|
650
|
+
|
|
372
651
|
|
|
373
652
|
@router.post("/{name}/concurrency_status")
|
|
374
653
|
async def read_work_pool_concurrency_status(
|
|
@@ -704,8 +983,11 @@ async def update_work_queue(
|
|
|
704
983
|
"""
|
|
705
984
|
Update a work pool queue
|
|
706
985
|
"""
|
|
707
|
-
|
|
708
986
|
async with db.session_context(begin_transaction=True) as session:
|
|
987
|
+
await worker_lookups._get_work_pool_id_from_name(
|
|
988
|
+
session=session,
|
|
989
|
+
work_pool_name=work_pool_name,
|
|
990
|
+
)
|
|
709
991
|
work_queue_id = await worker_lookups._get_work_queue_id_from_name(
|
|
710
992
|
work_pool_name=work_pool_name,
|
|
711
993
|
work_queue_name=work_queue_name,
|
|
@@ -736,6 +1018,10 @@ async def delete_work_queue(
|
|
|
736
1018
|
"""
|
|
737
1019
|
|
|
738
1020
|
async with db.session_context(begin_transaction=True) as session:
|
|
1021
|
+
await worker_lookups._get_work_pool_id_from_name(
|
|
1022
|
+
session=session,
|
|
1023
|
+
work_pool_name=work_pool_name,
|
|
1024
|
+
)
|
|
739
1025
|
work_queue_id = await worker_lookups._get_work_queue_id_from_name(
|
|
740
1026
|
session=session,
|
|
741
1027
|
work_pool_name=work_pool_name,
|
|
@@ -756,6 +1042,78 @@ async def delete_work_queue(
|
|
|
756
1042
|
# -----------------------------------------------------
|
|
757
1043
|
|
|
758
1044
|
|
|
1045
|
+
@router.websocket("/{work_pool_name}/workers/connect")
|
|
1046
|
+
async def worker_channel_connect(
|
|
1047
|
+
websocket: WebSocket,
|
|
1048
|
+
work_pool_name: str = Path(..., description="The work pool name"),
|
|
1049
|
+
db: PrefectDBInterface = Depends(provide_database_interface),
|
|
1050
|
+
) -> None:
|
|
1051
|
+
websocket = await subscriptions.accept_prefect_socket(
|
|
1052
|
+
websocket,
|
|
1053
|
+
require_prefect_subprotocol=True,
|
|
1054
|
+
authentication_failed_reason=WorkerChannelCloseReason.AUTHENTICATION_FAILED.value,
|
|
1055
|
+
)
|
|
1056
|
+
if not websocket:
|
|
1057
|
+
return
|
|
1058
|
+
|
|
1059
|
+
try:
|
|
1060
|
+
hello = await _receive_worker_hello(websocket)
|
|
1061
|
+
async with worker_channel_utils.messaging.ephemeral_subscription(
|
|
1062
|
+
worker_channel_utils.WORKER_CHANNEL_SNAPSHOT_TOPIC,
|
|
1063
|
+
) as consumer_kwargs:
|
|
1064
|
+
async with db.session_context(begin_transaction=True) as session:
|
|
1065
|
+
ready, work_pool_update_event = await _build_worker_ready_frame(
|
|
1066
|
+
session=session,
|
|
1067
|
+
work_pool_name=work_pool_name,
|
|
1068
|
+
hello=hello,
|
|
1069
|
+
)
|
|
1070
|
+
|
|
1071
|
+
if work_pool_update_event is not None:
|
|
1072
|
+
async with db.session_context() as session:
|
|
1073
|
+
work_pool = await models.workers.read_work_pool(
|
|
1074
|
+
session=session,
|
|
1075
|
+
work_pool_id=work_pool_update_event.work_pool_id,
|
|
1076
|
+
)
|
|
1077
|
+
assert work_pool is not None
|
|
1078
|
+
await models.workers.emit_work_pool_updated_event(
|
|
1079
|
+
session=session,
|
|
1080
|
+
work_pool=work_pool,
|
|
1081
|
+
changed_fields=work_pool_update_event.changed_fields,
|
|
1082
|
+
)
|
|
1083
|
+
await worker_channel_utils.publish_snapshot_invalidation(
|
|
1084
|
+
worker_channel_utils.WorkerChannelSnapshotInvalidation(
|
|
1085
|
+
work_pool_id=work_pool_update_event.work_pool_id,
|
|
1086
|
+
reason="work_pool_updated",
|
|
1087
|
+
)
|
|
1088
|
+
)
|
|
1089
|
+
|
|
1090
|
+
connection = worker_channel_utils.WorkerChannelConnection(
|
|
1091
|
+
websocket=websocket,
|
|
1092
|
+
db=db,
|
|
1093
|
+
work_pool_name=work_pool_name,
|
|
1094
|
+
work_pool_id=ready.payload.initial_snapshot.work_pool.id,
|
|
1095
|
+
consumer_id=hello.payload.consumer_id,
|
|
1096
|
+
worker_name=hello.payload.worker_name,
|
|
1097
|
+
)
|
|
1098
|
+
await connection.run(ready, consumer_kwargs)
|
|
1099
|
+
|
|
1100
|
+
except WorkerChannelSetupError as exc:
|
|
1101
|
+
logger.info("Worker channel setup failed: %s", exc.detail)
|
|
1102
|
+
await worker_channel_utils.close_worker_channel(websocket, exc.close_reason)
|
|
1103
|
+
except HTTPException as exc:
|
|
1104
|
+
logger.info("Worker channel setup failed HTTP validation: %s", exc.detail)
|
|
1105
|
+
await worker_channel_utils.close_worker_channel(
|
|
1106
|
+
websocket, WorkerChannelCloseReason.PROTOCOL_ERROR
|
|
1107
|
+
)
|
|
1108
|
+
except subscriptions.NORMAL_DISCONNECT_EXCEPTIONS:
|
|
1109
|
+
return
|
|
1110
|
+
except Exception:
|
|
1111
|
+
logger.exception("Worker channel setup failed due to a transient server error")
|
|
1112
|
+
await worker_channel_utils.close_worker_channel(
|
|
1113
|
+
websocket, WorkerChannelCloseReason.TRANSIENT_SERVER_ERROR
|
|
1114
|
+
)
|
|
1115
|
+
|
|
1116
|
+
|
|
759
1117
|
@router.post(
|
|
760
1118
|
"/{work_pool_name}/workers/heartbeat",
|
|
761
1119
|
status_code=status.HTTP_204_NO_CONTENT,
|
|
@@ -780,23 +1138,14 @@ async def worker_heartbeat(
|
|
|
780
1138
|
detail=f'Work pool "{work_pool_name}" not found.',
|
|
781
1139
|
)
|
|
782
1140
|
|
|
783
|
-
await models.workers.
|
|
1141
|
+
await models.workers.record_worker_heartbeat(
|
|
784
1142
|
session=session,
|
|
785
|
-
|
|
1143
|
+
work_pool=work_pool,
|
|
786
1144
|
worker_name=name,
|
|
787
1145
|
heartbeat_interval_seconds=heartbeat_interval_seconds,
|
|
1146
|
+
emit_status_change=emit_work_pool_status_event,
|
|
788
1147
|
)
|
|
789
1148
|
|
|
790
|
-
if work_pool.status == schemas.statuses.WorkPoolStatus.NOT_READY:
|
|
791
|
-
await models.workers.update_work_pool(
|
|
792
|
-
session=session,
|
|
793
|
-
work_pool_id=work_pool.id,
|
|
794
|
-
work_pool=schemas.internal.InternalWorkPoolUpdate(
|
|
795
|
-
status=schemas.statuses.WorkPoolStatus.READY
|
|
796
|
-
),
|
|
797
|
-
emit_status_change=emit_work_pool_status_event,
|
|
798
|
-
)
|
|
799
|
-
|
|
800
1149
|
|
|
801
1150
|
@router.post("/{work_pool_name}/workers/filter")
|
|
802
1151
|
async def read_workers(
|
prefect/settings/sources.py
CHANGED
|
@@ -14,6 +14,7 @@ from pydantic_settings import (
|
|
|
14
14
|
EnvSettingsSource,
|
|
15
15
|
PydanticBaseSettingsSource,
|
|
16
16
|
)
|
|
17
|
+
from pydantic_settings.exceptions import SettingsError
|
|
17
18
|
from pydantic_settings.sources import (
|
|
18
19
|
ENV_FILE_SENTINEL,
|
|
19
20
|
ConfigFileSourceMixin,
|
|
@@ -230,7 +231,12 @@ class TomlConfigSettingsSourceBase(PydanticBaseSettingsSource, ConfigFileSourceM
|
|
|
230
231
|
self.toml_data: dict[str, Any] = {}
|
|
231
232
|
|
|
232
233
|
def _read_file(self, path: Path) -> dict[str, Any]:
|
|
233
|
-
|
|
234
|
+
try:
|
|
235
|
+
return _read_toml_file(path)
|
|
236
|
+
except tomllib.TOMLDecodeError as e:
|
|
237
|
+
raise SettingsError(
|
|
238
|
+
f"Failed to load Prefect settings from {path}: invalid TOML ({e})"
|
|
239
|
+
) from e
|
|
234
240
|
|
|
235
241
|
@staticmethod
|
|
236
242
|
def _field_is_dict_type(field: FieldInfo) -> bool:
|
|
@@ -14,6 +14,7 @@ from typing import (
|
|
|
14
14
|
overload,
|
|
15
15
|
)
|
|
16
16
|
|
|
17
|
+
import prefect.exceptions
|
|
17
18
|
from prefect.client.utilities import inject_client
|
|
18
19
|
from prefect.logging.loggers import get_logger
|
|
19
20
|
from prefect.utilities.annotations import NotSet
|
|
@@ -348,9 +349,19 @@ async def resolve_block_document_references(
|
|
|
348
349
|
)
|
|
349
350
|
block_type_slug, block_document_name, *value_keypath = parts
|
|
350
351
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
352
|
+
try:
|
|
353
|
+
block_document = await client.read_block_document_by_name(
|
|
354
|
+
name=block_document_name, block_type_slug=block_type_slug
|
|
355
|
+
)
|
|
356
|
+
except prefect.exceptions.ObjectNotFound as exc:
|
|
357
|
+
raise prefect.exceptions.ObjectNotFound(
|
|
358
|
+
http_exc=exc.http_exc,
|
|
359
|
+
help_message=(
|
|
360
|
+
f"Block not found: '{block_document_name}' of type '{block_type_slug}'. "
|
|
361
|
+
f"This block was referenced in your deployment or work pool configuration but no longer exists. "
|
|
362
|
+
f"It may have been deleted. Please check your configuration or create a new block."
|
|
363
|
+
),
|
|
364
|
+
) from exc
|
|
354
365
|
|
|
355
366
|
data = block_document.data
|
|
356
367
|
value: Any = data
|
|
@@ -377,7 +388,17 @@ async def resolve_block_document_references(
|
|
|
377
388
|
if isinstance(template, dict):
|
|
378
389
|
block_document_id = template.get("$ref", {}).get("block_document_id")
|
|
379
390
|
if block_document_id:
|
|
380
|
-
|
|
391
|
+
try:
|
|
392
|
+
block_document = await client.read_block_document(block_document_id)
|
|
393
|
+
except prefect.exceptions.ObjectNotFound as exc:
|
|
394
|
+
raise prefect.exceptions.ObjectNotFound(
|
|
395
|
+
http_exc=exc.http_exc,
|
|
396
|
+
help_message=(
|
|
397
|
+
f"Block not found: ID '{block_document_id}'. "
|
|
398
|
+
f"This block was referenced in your deployment or work pool configuration but no longer exists. "
|
|
399
|
+
f"It may have been deleted. Please check your configuration or create a new block."
|
|
400
|
+
),
|
|
401
|
+
) from exc
|
|
381
402
|
return block_document.data
|
|
382
403
|
updated_template: dict[str, Any] = {}
|
|
383
404
|
for key, value in template.items():
|
|
@@ -93,6 +93,10 @@ class WorkerChannelProtocolHandler:
|
|
|
93
93
|
def work_pool_snapshots_available(self) -> bool:
|
|
94
94
|
return self._work_pool_snapshots.snapshots_available
|
|
95
95
|
|
|
96
|
+
@property
|
|
97
|
+
def work_pool_snapshot_sequence(self) -> int | None:
|
|
98
|
+
return self._work_pool_snapshots.last_applied_sequence
|
|
99
|
+
|
|
96
100
|
async def handshake(
|
|
97
101
|
self, websocket: websockets.asyncio.client.ClientConnection
|
|
98
102
|
) -> WorkerReadyFrame:
|
|
@@ -103,6 +103,7 @@ class WorkPoolWorkerChannel:
|
|
|
103
103
|
)
|
|
104
104
|
self._websocket_started = False
|
|
105
105
|
self._run_scope: anyio.CancelScope | None = None
|
|
106
|
+
self._stopped = False
|
|
106
107
|
|
|
107
108
|
@property
|
|
108
109
|
def url(self) -> str | None:
|
|
@@ -128,13 +129,11 @@ class WorkPoolWorkerChannel:
|
|
|
128
129
|
self._client = client
|
|
129
130
|
|
|
130
131
|
def stop(self) -> None:
|
|
132
|
+
self._stopped = True
|
|
131
133
|
if self._run_scope is not None:
|
|
132
134
|
self._run_scope.cancel()
|
|
133
135
|
|
|
134
136
|
async def sync(self, task_group: anyio.abc.TaskGroup | None) -> None:
|
|
135
|
-
if self.state.healthy and self.snapshots_available:
|
|
136
|
-
return
|
|
137
|
-
|
|
138
137
|
if task_group is not None:
|
|
139
138
|
channel_started = await self._start_websocket(task_group)
|
|
140
139
|
else:
|
|
@@ -142,10 +141,9 @@ class WorkPoolWorkerChannel:
|
|
|
142
141
|
if self.url is None:
|
|
143
142
|
self.state.mark_terminal("endpoint_unavailable")
|
|
144
143
|
|
|
145
|
-
if channel_started and self.snapshots_available:
|
|
146
|
-
|
|
144
|
+
if not (channel_started and self.snapshots_available):
|
|
145
|
+
await self._sync_rest_work_pool()
|
|
147
146
|
|
|
148
|
-
await self._sync_rest_work_pool()
|
|
149
147
|
await self._send_rest_worker_heartbeat()
|
|
150
148
|
|
|
151
149
|
async def _start_websocket(self, task_group: anyio.abc.TaskGroup) -> bool:
|
|
@@ -239,6 +237,8 @@ class WorkPoolWorkerChannel:
|
|
|
239
237
|
async def _run(self, initial_session: WorkerChannelSession | None = None) -> None:
|
|
240
238
|
with anyio.CancelScope() as scope:
|
|
241
239
|
self._run_scope = scope
|
|
240
|
+
if self._stopped:
|
|
241
|
+
scope.cancel()
|
|
242
242
|
try:
|
|
243
243
|
async with AsyncExitStack() as stack:
|
|
244
244
|
if self._cleanup_executor is not None:
|
|
@@ -296,6 +296,7 @@ class WorkPoolWorkerChannel:
|
|
|
296
296
|
self._run_scope = None
|
|
297
297
|
|
|
298
298
|
async def _sync_rest_work_pool(self) -> None:
|
|
299
|
+
initial_snapshot_sequence = self._protocol.work_pool_snapshot_sequence
|
|
299
300
|
try:
|
|
300
301
|
work_pool = await self._client.read_work_pool(
|
|
301
302
|
work_pool_name=self.work_pool_name
|
|
@@ -334,7 +335,10 @@ class WorkPoolWorkerChannel:
|
|
|
334
335
|
)
|
|
335
336
|
return
|
|
336
337
|
|
|
337
|
-
if
|
|
338
|
+
if (
|
|
339
|
+
self.state.healthy
|
|
340
|
+
and self._protocol.work_pool_snapshot_sequence != initial_snapshot_sequence
|
|
341
|
+
):
|
|
338
342
|
self._logger.debug(
|
|
339
343
|
"Skipping REST work pool sync because the worker channel applied a "
|
|
340
344
|
"snapshot while REST sync was in flight."
|
|
@@ -347,7 +351,10 @@ class WorkPoolWorkerChannel:
|
|
|
347
351
|
)
|
|
348
352
|
work_pool.base_job_template = self.default_base_job_template
|
|
349
353
|
|
|
350
|
-
if
|
|
354
|
+
if (
|
|
355
|
+
self.state.healthy
|
|
356
|
+
and self._protocol.work_pool_snapshot_sequence != initial_snapshot_sequence
|
|
357
|
+
):
|
|
351
358
|
self._logger.debug(
|
|
352
359
|
"Skipping REST work pool snapshot because the worker channel applied "
|
|
353
360
|
"a snapshot while REST sync was in flight."
|
prefect/workers/base.py
CHANGED
|
@@ -11,6 +11,7 @@ import uuid
|
|
|
11
11
|
import warnings
|
|
12
12
|
from contextlib import AsyncExitStack
|
|
13
13
|
from functools import partial
|
|
14
|
+
from importlib.metadata import distributions
|
|
14
15
|
from typing import (
|
|
15
16
|
TYPE_CHECKING,
|
|
16
17
|
Any,
|
|
@@ -26,9 +27,6 @@ from zoneinfo import ZoneInfo
|
|
|
26
27
|
import anyio
|
|
27
28
|
import anyio.abc
|
|
28
29
|
from exceptiongroup import BaseExceptionGroup, ExceptionGroup
|
|
29
|
-
from importlib_metadata import (
|
|
30
|
-
distributions, # type: ignore[reportUnknownVariableType] incomplete typing
|
|
31
|
-
)
|
|
32
30
|
from pydantic import BaseModel, Field, PrivateAttr, field_validator
|
|
33
31
|
from pydantic.json_schema import GenerateJsonSchema
|
|
34
32
|
from typing_extensions import Literal, Self, TypeVar
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
prefect/.prefectignore,sha256=awSprvKT0vI8a64mEOLrMxhxqcO-b0ERQeYpA2rNKVQ,390
|
|
2
2
|
prefect/AGENTS.md,sha256=rOU4L6B0ZdvnVmLr_eL394QlwfJkPh1OZswHppAjUN4,10063
|
|
3
|
-
prefect/__init__.py,sha256=
|
|
3
|
+
prefect/__init__.py,sha256=Z8rwfLbEOLh-5WcznTZP3FG2-9UgGZxY-prj8sL0-Qk,6828
|
|
4
4
|
prefect/__main__.py,sha256=WFjw3kaYJY6pOTA7WDOgqjsz8zUEUZHCcj3P5wyVa-g,66
|
|
5
|
-
prefect/_build_info.py,sha256=
|
|
5
|
+
prefect/_build_info.py,sha256=83Y1c4RyZdxkImmappMemLaqeSriPlx8EzdSL4cDLm8,185
|
|
6
6
|
prefect/_flow_run_suspension.py,sha256=5zTTB7ZIBHzoS0pVrhNn23-9hK51qZ3CQA6C-azluC0,4144
|
|
7
7
|
prefect/agent.py,sha256=dPvG1jDGD5HSH7aM2utwtk6RaJ9qg13XjkA0lAIgQmY,287
|
|
8
8
|
prefect/artifacts.py,sha256=ZdMLJeJGK82hibtRzbsVa-g95dMa0D2UP1LiESoXmf4,23951
|
|
9
|
-
prefect/automations.py,sha256=
|
|
9
|
+
prefect/automations.py,sha256=OxfOezLuHHOB_S-ohyRBoTg6yL9MtbrlAyQq8zGkF9A,12607
|
|
10
10
|
prefect/cache_policies.py,sha256=K0cymkMbEQBuwY1fSehefllN4Y4RblpNzYsh-LtjdXM,13156
|
|
11
11
|
prefect/context.py,sha256=arDksENXHs3O1yOP8Iiq3CaerbsT6ixxM4BIrIuE2tM,38026
|
|
12
12
|
prefect/engine.py,sha256=F0YQlbewnvQGJZyB-wDePQxm_9b89D0_9S1PhMvuULI,6002
|
|
@@ -146,7 +146,7 @@ prefect/bundles/__init__.py,sha256=3LiZeX-QG-5NnKq2FaQZrPOI_2Ta-2bOUir4F9-tEV0,3
|
|
|
146
146
|
prefect/bundles/_file_collector.py,sha256=15v9l1vtbSwSfgA9tHF3jJzJxf2vWCCEJRvFIR89OSw,21222
|
|
147
147
|
prefect/bundles/_ignore_filter.py,sha256=4p7Ku8h6LOVmagC0lLlK3SBo-FLrz2JXoJexFto5nKo,11067
|
|
148
148
|
prefect/bundles/_path_resolver.py,sha256=-3VCDZxD7aDFJlucOdTK4PneW4XEfxpFo1MSfLJe5Xk,18366
|
|
149
|
-
prefect/bundles/_zip_builder.py,sha256
|
|
149
|
+
prefect/bundles/_zip_builder.py,sha256=-agAOPVSaW5mEFyGcmAFgVj_K2sdtaWF-bPt0PGQLCo,6360
|
|
150
150
|
prefect/bundles/_zip_extractor.py,sha256=aFU2PWH6Ar-6HMOljsSvdSruSS6QUlHt5rjdCpIWvNU,4637
|
|
151
151
|
prefect/bundles/execute.py,sha256=KubXt8jNr9Xq7hf44oxi5fZEh0QcLHQO7ivtSElrQq0,1736
|
|
152
152
|
prefect/client/AGENTS.md,sha256=16krPZUqEyiEaKk6jsLRK7MJHKlshe1mWnKvtvZiMco,3079
|
|
@@ -230,8 +230,8 @@ prefect/docker/__init__.py,sha256=gX5MJ5yHIK7WwpoOR3LpcR-vydv5SxrkOWdYQcgUL5o,71
|
|
|
230
230
|
prefect/docker/_buildx.py,sha256=sVh0v12RZ3TkqzdawuIzfj1d2GTgpvT2WM5khrGXsqA,519
|
|
231
231
|
prefect/docker/docker_image.py,sha256=afAOYLv8pIGBV-rDuMbzfnnfGlu7qMUb6l3jXGCAe_k,6962
|
|
232
232
|
prefect/events/AGENTS.md,sha256=ZCrjaA67BQGdDzO5R7yJB7HO5Ahb8z7A3sT8yuKQ6V0,3049
|
|
233
|
-
prefect/events/__init__.py,sha256=
|
|
234
|
-
prefect/events/actions.py,sha256=
|
|
233
|
+
prefect/events/__init__.py,sha256=e3aIcHzTfseOmq_A3hM_jSPNYP_OdndJErzjcdrMuAc,2202
|
|
234
|
+
prefect/events/actions.py,sha256=Ii5P4FX10Xu1nzU0e7okA1KC1eVIHhzExubFurrOr_Q,9785
|
|
235
235
|
prefect/events/clients.py,sha256=m0KN7LpulgatjCVX5-3f8kzQKI_7u7Y3Nk6V948tTyc,33339
|
|
236
236
|
prefect/events/filters.py,sha256=_vweQbeRxK8KiyKIPb6fYEtA5crbaUxlYPkWorFmMm4,9201
|
|
237
237
|
prefect/events/related.py,sha256=CTeexYUmmA93V4gsR33GIFmw-SS-X_ouOpRg-oeq-BU,6672
|
|
@@ -288,7 +288,7 @@ prefect/runner/_starter_engine.py,sha256=FjcHQe_iryk8BFpeiGw8MCWcLSRKhYwmb2iA4eo
|
|
|
288
288
|
prefect/runner/_state_proposer.py,sha256=iW-SlB5EOWHErWbyRJwRTirfqwo0BJ1fAlbScVi6vcg,7225
|
|
289
289
|
prefect/runner/_workspace_resolver.py,sha256=CbCtOULtkXb-XROyq16HOwG7q-8Jk0Tc1_nAum4WgyM,11263
|
|
290
290
|
prefect/runner/_workspace_starter.py,sha256=M8R9JGjLJgS3qm3ZpzpyZ2_3aGGVaHXu55M25k7nxd8,9673
|
|
291
|
-
prefect/runner/runner.py,sha256=
|
|
291
|
+
prefect/runner/runner.py,sha256=O4krExcrpdS3T7pe6RLSfQbpkup8-SltDYS0E7p3obA,75799
|
|
292
292
|
prefect/runner/server.py,sha256=YqvQjlxZZHyhSsqyaLvOy2NwTDg1hLSZB2PK3t8FJUg,3636
|
|
293
293
|
prefect/runner/storage.py,sha256=yt_v2cVxYbazkZgfEdDQv0n-BS8rUvlFE8M_X3iN6NM,40525
|
|
294
294
|
prefect/runtime/__init__.py,sha256=JswiTlYRup2zXOYu8AqJ7czKtgcw9Kxo0tTbS6aWCqY,407
|
|
@@ -304,7 +304,7 @@ prefect/server/api/block_capabilities.py,sha256=0x1vtC2CtSRVcCWydgNbylmfv_LnJHIo
|
|
|
304
304
|
prefect/server/api/block_documents.py,sha256=srU5Jmn-Fguhut6PtV2N4XWZLJa2enwZ7ShcbrDRR6w,6034
|
|
305
305
|
prefect/server/api/block_schemas.py,sha256=H9kxnKPlTvz62SMcX1ssHnQ8KACU2728ebCtSp2iLcs,5508
|
|
306
306
|
prefect/server/api/block_types.py,sha256=DdKesZhnpGlTJtIidGoBSLEFFcAwFp-_XEA-SUOlhAM,8343
|
|
307
|
-
prefect/server/api/clients.py,sha256=
|
|
307
|
+
prefect/server/api/clients.py,sha256=6bOdrsgn7jyM_YD89opjzrg9cJn6uUTHbs1P6wFotRI,9198
|
|
308
308
|
prefect/server/api/collections.py,sha256=RI7cjdM8RYFyAk2rgb35vqh06PWGXAamTvwThl83joY,2454
|
|
309
309
|
prefect/server/api/concurrency_limits.py,sha256=nJKOiKZwQvxFfAMBaqyeAWHjm6hrpErfnQIMuhhszoA,24503
|
|
310
310
|
prefect/server/api/concurrency_limits_v2.py,sha256=-Wwk7eWe0stLt738wQslHZQxYUiMIiTodMWU1P6BuLk,17817
|
|
@@ -327,8 +327,8 @@ prefect/server/api/task_workers.py,sha256=xFj0jRCLutccoMU6q4sMeY6pLFtrjytXL6Y7Sx
|
|
|
327
327
|
prefect/server/api/templates.py,sha256=EW5aJOuvSXBeShd5VIygI1f9W0uTUpGb32ADrL9LG3k,1208
|
|
328
328
|
prefect/server/api/validation.py,sha256=DPofXjLFejs_qusUQgzRkG89X2EFoviAy9J08pnNvyM,14543
|
|
329
329
|
prefect/server/api/variables.py,sha256=8Ursuf20R5zXUS015ZexCH72K7R6Yv76ehkr_l3Xt0k,6186
|
|
330
|
-
prefect/server/api/work_queues.py,sha256=
|
|
331
|
-
prefect/server/api/workers.py,sha256=
|
|
330
|
+
prefect/server/api/work_queues.py,sha256=wEb_Hx1MlTrg5juOguk5nvGKwLmTGSIHEUyt_-2GJiE,11957
|
|
331
|
+
prefect/server/api/workers.py,sha256=7ZsWWLsDqf5ZhcfeH7YEEPKRmQlert_Qec8ULINoafc,42914
|
|
332
332
|
prefect/server/api/collections_data/views/aggregate-worker-metadata.json,sha256=otKGMKJUh_ySkQyNBFCe84E6VRbuR-Z7_676D21YKsw,81525
|
|
333
333
|
prefect/server/api/static/prefect-logo-mark-gradient.png,sha256=ylRjJkI_JHCw8VbQasNnXQHwZW-sH-IQiUGSD3aWP1E,73430
|
|
334
334
|
prefect/server/api/ui/__init__.py,sha256=TCXO4ZUZCqCbm2QoNvWNTErkzWiX2nSACuO-0Tiomvg,93
|
|
@@ -344,7 +344,7 @@ prefect/settings/context.py,sha256=VtMJsBtjwq_P3La9_SRYedBkyjmMNqV_4X_U4h_-6wM,2
|
|
|
344
344
|
prefect/settings/legacy.py,sha256=KG00GwaURl1zbwfCKAjwNRdJjB2UdTyo80gYF7U60jk,5693
|
|
345
345
|
prefect/settings/profiles.py,sha256=ENL59QPCMH5YTD5q6rG8QBc_5Yw4ZmOmqsnxrFpRNvg,12860
|
|
346
346
|
prefect/settings/profiles.toml,sha256=kTvqDNMzjH3fsm5OEI-NKY4dMmipor5EvQXRB6rPEjY,522
|
|
347
|
-
prefect/settings/sources.py,sha256=
|
|
347
|
+
prefect/settings/sources.py,sha256=0EUJoa78Qr3CVmIdLGnqDCu8z_XoXq1evn8dHlD1yIo,15051
|
|
348
348
|
prefect/settings/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
349
349
|
prefect/settings/models/_defaults.py,sha256=Mj_ctL3UhMRp-wK_qW2lt4Akf55SAX0qFg_doPOFgHw,4856
|
|
350
350
|
prefect/settings/models/api.py,sha256=XOxZDtNKeXgOMtvlr6QBJ1_UsvsXad3m0ECgZ0vAfaA,1796
|
|
@@ -422,23 +422,23 @@ prefect/utilities/schema_tools/__init__.py,sha256=At3rMHd2g_Em2P3_dFQlFgqR_EpBwr
|
|
|
422
422
|
prefect/utilities/schema_tools/hydration.py,sha256=446SKv-W65VlX_5UGY3YvGYPQ7aYgb6dPy85EueU8FM,9671
|
|
423
423
|
prefect/utilities/schema_tools/validation.py,sha256=UhRLWStdT9__u4yz-mnc3lcdt-VJmRGJBGV_f_9um-Y,10749
|
|
424
424
|
prefect/utilities/templating/AGENTS.md,sha256=vkflyqkm9snY5sR-Wfxr855F55yJQTfSBEt5ThoSUF4,2097
|
|
425
|
-
prefect/utilities/templating/__init__.py,sha256
|
|
425
|
+
prefect/utilities/templating/__init__.py,sha256=-GGrp9CRHVPddmc_9z3BTBkMO7Vwqg-yevkJFaFHI2I,19248
|
|
426
426
|
prefect/workers/AGENTS.md,sha256=rNcd5PutF7TUQJerXezNX3kemXR7VII_5CnuYq0l0vs,4282
|
|
427
427
|
prefect/workers/__init__.py,sha256=EaM1F0RZ-XIJaGeTKLsXDnfOPHzVWk5bk0_c4BVS44M,64
|
|
428
428
|
prefect/workers/_cleanup.py,sha256=U0tc0vExmsVpXaTo9H9CGHWnJOoykZiQVKI-cn5TJao,23675
|
|
429
429
|
prefect/workers/_cleanup_handlers.py,sha256=dVy2qi5qjazjusunWcRZro5Hiy-stp8NqSeMNthjOnk,5169
|
|
430
|
-
prefect/workers/base.py,sha256=
|
|
430
|
+
prefect/workers/base.py,sha256=cDxZY3jCZ5sm7xB_EXpxuxe6XIUdMqQ0OrcSNiwEscY,78325
|
|
431
431
|
prefect/workers/block.py,sha256=dPvG1jDGD5HSH7aM2utwtk6RaJ9qg13XjkA0lAIgQmY,287
|
|
432
432
|
prefect/workers/cloud.py,sha256=dPvG1jDGD5HSH7aM2utwtk6RaJ9qg13XjkA0lAIgQmY,287
|
|
433
433
|
prefect/workers/process.py,sha256=QA9p3Z3br77PGVL_McPgYyeF2zABXlGTF1qLAsTHYwY,12820
|
|
434
434
|
prefect/workers/server.py,sha256=bWnYfMfJf5_IO3y3aJOpia7p9lFKC3ZZjiMvHox-UKY,1992
|
|
435
435
|
prefect/workers/utilities.py,sha256=-re_0s1Jr98W5xhJr4Xvvz34OxN3T4ypSupxdBiC8aA,2633
|
|
436
436
|
prefect/workers/_worker_channel/__init__.py,sha256=vEc7NvC1sjxnCn5thQv5lSU1bDKRaT1sV7AA_6VRoCM,709
|
|
437
|
-
prefect/workers/_worker_channel/_protocol.py,sha256=
|
|
437
|
+
prefect/workers/_worker_channel/_protocol.py,sha256=GpCNh1o3qmmqHA_UOOTge1QVC6IRvWP2RdpAEBqXPs0,15834
|
|
438
438
|
prefect/workers/_worker_channel/_state.py,sha256=eQTFZtAVDZH1vVWps3SdeY6aW3qu2wx1UKYQXK3AyuE,5369
|
|
439
|
-
prefect/workers/_worker_channel/_sync.py,sha256=
|
|
439
|
+
prefect/workers/_worker_channel/_sync.py,sha256=G5G8_UaQYbeLebi5Mb1Z_KgGfXfyjXo5uT9la5_zwqY,14984
|
|
440
440
|
prefect/workers/_worker_channel/_transport.py,sha256=cgrtAENawDpIPB8gwILd62y2VduykUCmg1NaO1pL-tg,9021
|
|
441
|
-
prefect_client-3.7.2.
|
|
442
|
-
prefect_client-3.7.2.
|
|
443
|
-
prefect_client-3.7.2.
|
|
444
|
-
prefect_client-3.7.2.
|
|
441
|
+
prefect_client-3.7.2.dev4.dist-info/METADATA,sha256=IxISqFhu8Fotf2r0-YAEjZDEXIpM2WCNwYLAS-gBGMw,7502
|
|
442
|
+
prefect_client-3.7.2.dev4.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
443
|
+
prefect_client-3.7.2.dev4.dist-info/licenses/LICENSE,sha256=MCxsn8osAkzfxKC4CC_dLcUkU8DZLkyihZ8mGs3Ah3Q,11357
|
|
444
|
+
prefect_client-3.7.2.dev4.dist-info/RECORD,,
|
|
File without changes
|
{prefect_client-3.7.2.dev2.dist-info → prefect_client-3.7.2.dev4.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|