prefect-client 3.0.1__py3-none-any.whl → 3.0.3__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/_internal/compatibility/deprecated.py +1 -1
- prefect/blocks/core.py +5 -4
- prefect/blocks/notifications.py +21 -0
- prefect/blocks/webhook.py +17 -1
- prefect/cache_policies.py +98 -28
- prefect/client/orchestration.py +42 -20
- prefect/client/schemas/actions.py +10 -2
- prefect/client/schemas/filters.py +4 -2
- prefect/client/schemas/objects.py +48 -6
- prefect/client/schemas/responses.py +15 -1
- prefect/client/types/flexible_schedule_list.py +1 -1
- prefect/concurrency/asyncio.py +45 -6
- prefect/concurrency/services.py +1 -1
- prefect/concurrency/sync.py +21 -27
- prefect/concurrency/v1/asyncio.py +3 -0
- prefect/concurrency/v1/sync.py +4 -5
- prefect/context.py +6 -6
- prefect/deployments/runner.py +43 -5
- prefect/events/actions.py +6 -0
- prefect/flow_engine.py +12 -4
- prefect/flows.py +15 -11
- prefect/locking/filesystem.py +243 -0
- prefect/logging/handlers.py +0 -2
- prefect/logging/loggers.py +0 -18
- prefect/logging/logging.yml +1 -0
- prefect/main.py +19 -5
- prefect/plugins.py +9 -1
- prefect/records/base.py +12 -0
- prefect/records/filesystem.py +6 -2
- prefect/records/memory.py +6 -0
- prefect/records/result_store.py +6 -0
- prefect/results.py +192 -29
- prefect/runner/runner.py +74 -6
- prefect/settings.py +31 -1
- prefect/states.py +34 -17
- prefect/task_engine.py +58 -43
- prefect/transactions.py +113 -52
- prefect/utilities/asyncutils.py +7 -0
- prefect/utilities/collections.py +3 -2
- prefect/utilities/engine.py +20 -9
- prefect/utilities/importtools.py +1 -0
- prefect/utilities/urls.py +70 -12
- prefect/workers/base.py +10 -8
- {prefect_client-3.0.1.dist-info → prefect_client-3.0.3.dist-info}/METADATA +1 -1
- {prefect_client-3.0.1.dist-info → prefect_client-3.0.3.dist-info}/RECORD +48 -47
- {prefect_client-3.0.1.dist-info → prefect_client-3.0.3.dist-info}/LICENSE +0 -0
- {prefect_client-3.0.1.dist-info → prefect_client-3.0.3.dist-info}/WHEEL +0 -0
- {prefect_client-3.0.1.dist-info → prefect_client-3.0.3.dist-info}/top_level.txt +0 -0
prefect/results.py
CHANGED
@@ -5,6 +5,7 @@ import socket
|
|
5
5
|
import threading
|
6
6
|
import uuid
|
7
7
|
from functools import partial
|
8
|
+
from pathlib import Path
|
8
9
|
from typing import (
|
9
10
|
TYPE_CHECKING,
|
10
11
|
Any,
|
@@ -19,6 +20,8 @@ from typing import (
|
|
19
20
|
)
|
20
21
|
from uuid import UUID
|
21
22
|
|
23
|
+
import pendulum
|
24
|
+
from cachetools import LRUCache
|
22
25
|
from pydantic import (
|
23
26
|
BaseModel,
|
24
27
|
ConfigDict,
|
@@ -33,6 +36,8 @@ from pydantic_extra_types.pendulum_dt import DateTime
|
|
33
36
|
from typing_extensions import ParamSpec, Self
|
34
37
|
|
35
38
|
import prefect
|
39
|
+
from prefect._internal.compatibility import deprecated
|
40
|
+
from prefect._internal.compatibility.deprecated import deprecated_field
|
36
41
|
from prefect.blocks.core import Block
|
37
42
|
from prefect.client.utilities import inject_client
|
38
43
|
from prefect.exceptions import (
|
@@ -86,18 +91,26 @@ async def get_default_result_storage() -> WritableFileSystem:
|
|
86
91
|
Generate a default file system for result storage.
|
87
92
|
"""
|
88
93
|
default_block = PREFECT_DEFAULT_RESULT_STORAGE_BLOCK.value()
|
94
|
+
basepath = PREFECT_LOCAL_STORAGE_PATH.value()
|
95
|
+
|
96
|
+
cache_key = (str(default_block), str(basepath))
|
97
|
+
|
98
|
+
if cache_key in _default_storages:
|
99
|
+
return _default_storages[cache_key]
|
89
100
|
|
90
101
|
if default_block is not None:
|
91
|
-
|
102
|
+
storage = await resolve_result_storage(default_block)
|
103
|
+
else:
|
104
|
+
# Use the local file system
|
105
|
+
storage = LocalFileSystem(basepath=str(basepath))
|
92
106
|
|
93
|
-
|
94
|
-
|
95
|
-
return LocalFileSystem(basepath=str(basepath))
|
107
|
+
_default_storages[cache_key] = storage
|
108
|
+
return storage
|
96
109
|
|
97
110
|
|
98
111
|
@sync_compatible
|
99
112
|
async def resolve_result_storage(
|
100
|
-
result_storage: ResultStorage,
|
113
|
+
result_storage: Union[ResultStorage, UUID, Path],
|
101
114
|
) -> WritableFileSystem:
|
102
115
|
"""
|
103
116
|
Resolve one of the valid `ResultStorage` input types into a saved block
|
@@ -114,10 +127,15 @@ async def resolve_result_storage(
|
|
114
127
|
storage_block_id = storage_block._block_document_id
|
115
128
|
else:
|
116
129
|
storage_block_id = None
|
130
|
+
elif isinstance(result_storage, Path):
|
131
|
+
storage_block = LocalFileSystem(basepath=str(result_storage))
|
117
132
|
elif isinstance(result_storage, str):
|
118
133
|
storage_block = await Block.load(result_storage, client=client)
|
119
134
|
storage_block_id = storage_block._block_document_id
|
120
135
|
assert storage_block_id is not None, "Loaded storage blocks must have ids"
|
136
|
+
elif isinstance(result_storage, UUID):
|
137
|
+
block_document = await client.read_block_document(result_storage)
|
138
|
+
storage_block = Block._from_block_document(block_document)
|
121
139
|
else:
|
122
140
|
raise TypeError(
|
123
141
|
"Result storage must be one of the following types: 'UUID', 'Block', "
|
@@ -171,6 +189,25 @@ def get_default_persist_setting() -> bool:
|
|
171
189
|
return PREFECT_RESULTS_PERSIST_BY_DEFAULT.value()
|
172
190
|
|
173
191
|
|
192
|
+
def should_persist_result() -> bool:
|
193
|
+
"""
|
194
|
+
Return the default option for result persistence determined by the current run context.
|
195
|
+
|
196
|
+
If there is no current run context, the default value set by
|
197
|
+
`PREFECT_RESULTS_PERSIST_BY_DEFAULT` will be returned.
|
198
|
+
"""
|
199
|
+
from prefect.context import FlowRunContext, TaskRunContext
|
200
|
+
|
201
|
+
task_run_context = TaskRunContext.get()
|
202
|
+
if task_run_context is not None:
|
203
|
+
return task_run_context.persist_result
|
204
|
+
flow_run_context = FlowRunContext.get()
|
205
|
+
if flow_run_context is not None:
|
206
|
+
return flow_run_context.persist_result
|
207
|
+
|
208
|
+
return PREFECT_RESULTS_PERSIST_BY_DEFAULT.value()
|
209
|
+
|
210
|
+
|
174
211
|
def _format_user_supplied_storage_key(key: str) -> str:
|
175
212
|
# Note here we are pinning to task runs since flow runs do not support storage keys
|
176
213
|
# yet; we'll need to split logic in the future or have two separate functions
|
@@ -178,6 +215,16 @@ def _format_user_supplied_storage_key(key: str) -> str:
|
|
178
215
|
return key.format(**runtime_vars, parameters=prefect.runtime.task_run.parameters)
|
179
216
|
|
180
217
|
|
218
|
+
T = TypeVar("T")
|
219
|
+
|
220
|
+
|
221
|
+
@deprecated_field(
|
222
|
+
"persist_result",
|
223
|
+
when=lambda x: x is not None,
|
224
|
+
when_message="use the `should_persist_result` utility function instead",
|
225
|
+
start_date="Sep 2024",
|
226
|
+
end_date="Nov 2024",
|
227
|
+
)
|
181
228
|
class ResultStore(BaseModel):
|
182
229
|
"""
|
183
230
|
Manages the storage and retrieval of results.
|
@@ -200,10 +247,13 @@ class ResultStore(BaseModel):
|
|
200
247
|
result_storage: Optional[WritableFileSystem] = Field(default=None)
|
201
248
|
metadata_storage: Optional[WritableFileSystem] = Field(default=None)
|
202
249
|
lock_manager: Optional[LockManager] = Field(default=None)
|
203
|
-
persist_result: bool = Field(default_factory=get_default_persist_setting)
|
204
250
|
cache_result_in_memory: bool = Field(default=True)
|
205
251
|
serializer: Serializer = Field(default_factory=get_default_result_serializer)
|
206
252
|
storage_key_fn: Callable[[], str] = Field(default=DEFAULT_STORAGE_KEY_FN)
|
253
|
+
cache: LRUCache = Field(default_factory=lambda: LRUCache(maxsize=1000))
|
254
|
+
|
255
|
+
# Deprecated fields
|
256
|
+
persist_result: Optional[bool] = Field(default=None)
|
207
257
|
|
208
258
|
@property
|
209
259
|
def result_storage_block_id(self) -> Optional[UUID]:
|
@@ -227,8 +277,6 @@ class ResultStore(BaseModel):
|
|
227
277
|
update["result_storage"] = await resolve_result_storage(flow.result_storage)
|
228
278
|
if flow.result_serializer is not None:
|
229
279
|
update["serializer"] = resolve_serializer(flow.result_serializer)
|
230
|
-
if flow.persist_result is not None:
|
231
|
-
update["persist_result"] = flow.persist_result
|
232
280
|
if flow.cache_result_in_memory is not None:
|
233
281
|
update["cache_result_in_memory"] = flow.cache_result_in_memory
|
234
282
|
if self.result_storage is None and update.get("result_storage") is None:
|
@@ -251,14 +299,21 @@ class ResultStore(BaseModel):
|
|
251
299
|
update["result_storage"] = await resolve_result_storage(task.result_storage)
|
252
300
|
if task.result_serializer is not None:
|
253
301
|
update["serializer"] = resolve_serializer(task.result_serializer)
|
254
|
-
if task.persist_result is not None:
|
255
|
-
update["persist_result"] = task.persist_result
|
256
302
|
if task.cache_result_in_memory is not None:
|
257
303
|
update["cache_result_in_memory"] = task.cache_result_in_memory
|
258
304
|
if task.result_storage_key is not None:
|
259
305
|
update["storage_key_fn"] = partial(
|
260
306
|
_format_user_supplied_storage_key, task.result_storage_key
|
261
307
|
)
|
308
|
+
if task.cache_policy is not None and task.cache_policy is not NotSet:
|
309
|
+
if task.cache_policy.key_storage is not None:
|
310
|
+
storage = task.cache_policy.key_storage
|
311
|
+
if isinstance(storage, str) and not len(storage.split("/")) == 2:
|
312
|
+
storage = Path(storage)
|
313
|
+
update["metadata_storage"] = await resolve_result_storage(storage)
|
314
|
+
if task.cache_policy.lock_manager is not None:
|
315
|
+
update["lock_manager"] = task.cache_policy.lock_manager
|
316
|
+
|
262
317
|
if self.result_storage is None and update.get("result_storage") is None:
|
263
318
|
update["result_storage"] = await get_default_result_storage()
|
264
319
|
return self.model_copy(update=update)
|
@@ -293,16 +348,30 @@ class ResultStore(BaseModel):
|
|
293
348
|
# so the entire payload doesn't need to be read
|
294
349
|
try:
|
295
350
|
metadata_content = await self.metadata_storage.read_path(key)
|
296
|
-
|
351
|
+
if metadata_content is None:
|
352
|
+
return False
|
353
|
+
metadata = ResultRecordMetadata.load_bytes(metadata_content)
|
354
|
+
|
297
355
|
except Exception:
|
298
356
|
return False
|
299
357
|
else:
|
300
358
|
try:
|
301
359
|
content = await self.result_storage.read_path(key)
|
302
|
-
|
360
|
+
if content is None:
|
361
|
+
return False
|
362
|
+
record = ResultRecord.deserialize(content)
|
363
|
+
metadata = record.metadata
|
303
364
|
except Exception:
|
304
365
|
return False
|
305
366
|
|
367
|
+
if metadata.expiration:
|
368
|
+
# if the result has an expiration,
|
369
|
+
# check if it is still in the future
|
370
|
+
exists = metadata.expiration > pendulum.now("utc")
|
371
|
+
else:
|
372
|
+
exists = True
|
373
|
+
return exists
|
374
|
+
|
306
375
|
def exists(self, key: str) -> bool:
|
307
376
|
"""
|
308
377
|
Check if a result record exists in storage.
|
@@ -342,9 +411,13 @@ class ResultStore(BaseModel):
|
|
342
411
|
Returns:
|
343
412
|
A result record.
|
344
413
|
"""
|
414
|
+
|
345
415
|
if self.lock_manager is not None and not self.is_lock_holder(key, holder):
|
346
416
|
await self.await_for_lock(key)
|
347
417
|
|
418
|
+
if key in self.cache:
|
419
|
+
return self.cache[key]
|
420
|
+
|
348
421
|
if self.result_storage is None:
|
349
422
|
self.result_storage = await get_default_result_storage()
|
350
423
|
|
@@ -355,12 +428,23 @@ class ResultStore(BaseModel):
|
|
355
428
|
metadata.storage_key is not None
|
356
429
|
), "Did not find storage key in metadata"
|
357
430
|
result_content = await self.result_storage.read_path(metadata.storage_key)
|
358
|
-
|
431
|
+
result_record = ResultRecord.deserialize_from_result_and_metadata(
|
359
432
|
result=result_content, metadata=metadata_content
|
360
433
|
)
|
361
434
|
else:
|
362
435
|
content = await self.result_storage.read_path(key)
|
363
|
-
|
436
|
+
result_record = ResultRecord.deserialize(content)
|
437
|
+
|
438
|
+
if self.cache_result_in_memory:
|
439
|
+
if self.result_storage_block_id is None and hasattr(
|
440
|
+
self.result_storage, "_resolve_path"
|
441
|
+
):
|
442
|
+
cache_key = str(self.result_storage._resolve_path(key))
|
443
|
+
else:
|
444
|
+
cache_key = key
|
445
|
+
|
446
|
+
self.cache[cache_key] = result_record
|
447
|
+
return result_record
|
364
448
|
|
365
449
|
def read(self, key: str, holder: Optional[str] = None) -> "ResultRecord":
|
366
450
|
"""
|
@@ -390,10 +474,10 @@ class ResultStore(BaseModel):
|
|
390
474
|
|
391
475
|
def create_result_record(
|
392
476
|
self,
|
393
|
-
key: str,
|
394
477
|
obj: Any,
|
478
|
+
key: Optional[str] = None,
|
395
479
|
expiration: Optional[DateTime] = None,
|
396
|
-
):
|
480
|
+
) -> "ResultRecord":
|
397
481
|
"""
|
398
482
|
Create a result record.
|
399
483
|
|
@@ -404,6 +488,13 @@ class ResultStore(BaseModel):
|
|
404
488
|
"""
|
405
489
|
key = key or self.storage_key_fn()
|
406
490
|
|
491
|
+
if self.result_storage is None:
|
492
|
+
self.result_storage = get_default_result_storage(_sync=True)
|
493
|
+
|
494
|
+
if self.result_storage_block_id is None:
|
495
|
+
if hasattr(self.result_storage, "_resolve_path"):
|
496
|
+
key = str(self.result_storage._resolve_path(key))
|
497
|
+
|
407
498
|
return ResultRecord(
|
408
499
|
result=obj,
|
409
500
|
metadata=ResultRecordMetadata(
|
@@ -416,8 +507,8 @@ class ResultStore(BaseModel):
|
|
416
507
|
|
417
508
|
def write(
|
418
509
|
self,
|
419
|
-
key: str,
|
420
510
|
obj: Any,
|
511
|
+
key: Optional[str] = None,
|
421
512
|
expiration: Optional[DateTime] = None,
|
422
513
|
holder: Optional[str] = None,
|
423
514
|
):
|
@@ -433,17 +524,18 @@ class ResultStore(BaseModel):
|
|
433
524
|
holder: The holder of the lock if a lock was set on the record.
|
434
525
|
"""
|
435
526
|
holder = holder or self.generate_default_holder()
|
527
|
+
result_record = self.create_result_record(
|
528
|
+
key=key, obj=obj, expiration=expiration
|
529
|
+
)
|
436
530
|
return self.persist_result_record(
|
437
|
-
result_record=
|
438
|
-
key=key, obj=obj, expiration=expiration
|
439
|
-
),
|
531
|
+
result_record=result_record,
|
440
532
|
holder=holder,
|
441
533
|
)
|
442
534
|
|
443
535
|
async def awrite(
|
444
536
|
self,
|
445
|
-
key: str,
|
446
537
|
obj: Any,
|
538
|
+
key: Optional[str] = None,
|
447
539
|
expiration: Optional[DateTime] = None,
|
448
540
|
holder: Optional[str] = None,
|
449
541
|
):
|
@@ -478,13 +570,22 @@ class ResultStore(BaseModel):
|
|
478
570
|
), "Storage key is required on result record"
|
479
571
|
|
480
572
|
key = result_record.metadata.storage_key
|
573
|
+
if result_record.metadata.storage_block_id is None:
|
574
|
+
basepath = (
|
575
|
+
self.result_storage._resolve_path("")
|
576
|
+
if hasattr(self.result_storage, "_resolve_path")
|
577
|
+
else Path(".").resolve()
|
578
|
+
)
|
579
|
+
base_key = str(Path(key).relative_to(basepath))
|
580
|
+
else:
|
581
|
+
base_key = key
|
481
582
|
if (
|
482
583
|
self.lock_manager is not None
|
483
|
-
and self.is_locked(
|
484
|
-
and not self.is_lock_holder(
|
584
|
+
and self.is_locked(base_key)
|
585
|
+
and not self.is_lock_holder(base_key, holder)
|
485
586
|
):
|
486
587
|
raise RuntimeError(
|
487
|
-
f"Cannot write to result record with key {
|
588
|
+
f"Cannot write to result record with key {base_key} because it is locked by "
|
488
589
|
f"another holder."
|
489
590
|
)
|
490
591
|
if self.result_storage is None:
|
@@ -497,7 +598,7 @@ class ResultStore(BaseModel):
|
|
497
598
|
content=result_record.serialize_result(),
|
498
599
|
)
|
499
600
|
await self.metadata_storage.write_path(
|
500
|
-
|
601
|
+
base_key,
|
501
602
|
content=result_record.serialize_metadata(),
|
502
603
|
)
|
503
604
|
# Otherwise, write the result metadata and result together
|
@@ -506,6 +607,9 @@ class ResultStore(BaseModel):
|
|
506
607
|
result_record.metadata.storage_key, content=result_record.serialize()
|
507
608
|
)
|
508
609
|
|
610
|
+
if self.cache_result_in_memory:
|
611
|
+
self.cache[key] = result_record
|
612
|
+
|
509
613
|
def persist_result_record(
|
510
614
|
self, result_record: "ResultRecord", holder: Optional[str] = None
|
511
615
|
):
|
@@ -671,6 +775,11 @@ class ResultStore(BaseModel):
|
|
671
775
|
)
|
672
776
|
return await self.lock_manager.await_for_lock(key, timeout)
|
673
777
|
|
778
|
+
@deprecated.deprecated_callable(
|
779
|
+
start_date="Sep 2024",
|
780
|
+
end_date="Nov 2024",
|
781
|
+
help="Use `create_result_record` instead.",
|
782
|
+
)
|
674
783
|
@sync_compatible
|
675
784
|
async def create_result(
|
676
785
|
self,
|
@@ -683,6 +792,11 @@ class ResultStore(BaseModel):
|
|
683
792
|
"""
|
684
793
|
# Null objects are "cached" in memory at no cost
|
685
794
|
should_cache_object = self.cache_result_in_memory or obj is None
|
795
|
+
should_persist_result = (
|
796
|
+
self.persist_result
|
797
|
+
if self.persist_result is not None
|
798
|
+
else not should_cache_object
|
799
|
+
)
|
686
800
|
|
687
801
|
if key:
|
688
802
|
|
@@ -704,7 +818,7 @@ class ResultStore(BaseModel):
|
|
704
818
|
serializer=self.serializer,
|
705
819
|
cache_object=should_cache_object,
|
706
820
|
expiration=expiration,
|
707
|
-
serialize_to_none=not
|
821
|
+
serialize_to_none=not should_persist_result,
|
708
822
|
)
|
709
823
|
|
710
824
|
# TODO: These two methods need to find a new home
|
@@ -729,7 +843,7 @@ class ResultStore(BaseModel):
|
|
729
843
|
return record.result
|
730
844
|
|
731
845
|
|
732
|
-
def
|
846
|
+
def get_result_store() -> ResultStore:
|
733
847
|
"""
|
734
848
|
Get the current result store.
|
735
849
|
"""
|
@@ -779,6 +893,17 @@ class ResultRecordMetadata(BaseModel):
|
|
779
893
|
"""
|
780
894
|
return cls.model_validate_json(data)
|
781
895
|
|
896
|
+
def __eq__(self, other):
|
897
|
+
if not isinstance(other, ResultRecordMetadata):
|
898
|
+
return False
|
899
|
+
return (
|
900
|
+
self.storage_key == other.storage_key
|
901
|
+
and self.expiration == other.expiration
|
902
|
+
and self.serializer == other.serializer
|
903
|
+
and self.prefect_version == other.prefect_version
|
904
|
+
and self.storage_block_id == other.storage_block_id
|
905
|
+
)
|
906
|
+
|
782
907
|
|
783
908
|
class ResultRecord(BaseModel, Generic[R]):
|
784
909
|
"""
|
@@ -856,6 +981,31 @@ class ResultRecord(BaseModel, Generic[R]):
|
|
856
981
|
value["metadata"]["prefect_version"] = value.pop("prefect_version")
|
857
982
|
return value
|
858
983
|
|
984
|
+
@classmethod
|
985
|
+
async def _from_metadata(cls, metadata: ResultRecordMetadata) -> "ResultRecord[R]":
|
986
|
+
"""
|
987
|
+
Create a result record from metadata.
|
988
|
+
|
989
|
+
Will use the result record metadata to fetch data via a result store.
|
990
|
+
|
991
|
+
Args:
|
992
|
+
metadata: The metadata to create the result record from.
|
993
|
+
|
994
|
+
Returns:
|
995
|
+
ResultRecord: The result record.
|
996
|
+
"""
|
997
|
+
if metadata.storage_block_id is None:
|
998
|
+
storage_block = None
|
999
|
+
else:
|
1000
|
+
storage_block = await resolve_result_storage(
|
1001
|
+
metadata.storage_block_id, _sync=False
|
1002
|
+
)
|
1003
|
+
store = ResultStore(
|
1004
|
+
result_storage=storage_block, serializer=metadata.serializer
|
1005
|
+
)
|
1006
|
+
result = await store.aread(metadata.storage_key)
|
1007
|
+
return result
|
1008
|
+
|
859
1009
|
def serialize_metadata(self) -> bytes:
|
860
1010
|
return self.metadata.dump_bytes()
|
861
1011
|
|
@@ -913,7 +1063,15 @@ class ResultRecord(BaseModel, Generic[R]):
|
|
913
1063
|
result=result_record_metadata.serializer.loads(result),
|
914
1064
|
)
|
915
1065
|
|
1066
|
+
def __eq__(self, other):
|
1067
|
+
if not isinstance(other, ResultRecord):
|
1068
|
+
return False
|
1069
|
+
return self.metadata == other.metadata and self.result == other.result
|
1070
|
+
|
916
1071
|
|
1072
|
+
@deprecated.deprecated_class(
|
1073
|
+
start_date="Sep 2024", end_date="Nov 2024", help="Use `ResultRecord` instead."
|
1074
|
+
)
|
917
1075
|
@register_base_type
|
918
1076
|
class BaseResult(BaseModel, abc.ABC, Generic[R]):
|
919
1077
|
model_config = ConfigDict(extra="forbid")
|
@@ -964,6 +1122,9 @@ class BaseResult(BaseModel, abc.ABC, Generic[R]):
|
|
964
1122
|
return cls.__name__ if isinstance(default, PydanticUndefinedType) else default
|
965
1123
|
|
966
1124
|
|
1125
|
+
@deprecated.deprecated_class(
|
1126
|
+
start_date="Sep 2024", end_date="Nov 2024", help="Use `ResultRecord` instead."
|
1127
|
+
)
|
967
1128
|
class PersistedResult(BaseResult):
|
968
1129
|
"""
|
969
1130
|
Result type which stores a reference to a persisted result.
|
@@ -1057,7 +1218,6 @@ class PersistedResult(BaseResult):
|
|
1057
1218
|
"""
|
1058
1219
|
Write the result to the storage block.
|
1059
1220
|
"""
|
1060
|
-
|
1061
1221
|
if self._persisted or self.serialize_to_none:
|
1062
1222
|
# don't double write or overwrite
|
1063
1223
|
return
|
@@ -1078,7 +1238,10 @@ class PersistedResult(BaseResult):
|
|
1078
1238
|
# this could error if the serializer requires kwargs
|
1079
1239
|
serializer = Serializer(type=self.serializer_type)
|
1080
1240
|
|
1081
|
-
result_store = ResultStore(
|
1241
|
+
result_store = ResultStore(
|
1242
|
+
result_storage=storage_block,
|
1243
|
+
serializer=serializer,
|
1244
|
+
)
|
1082
1245
|
await result_store.awrite(
|
1083
1246
|
obj=obj, key=self.storage_key, expiration=self.expiration
|
1084
1247
|
)
|
prefect/runner/runner.py
CHANGED
@@ -64,8 +64,18 @@ from prefect.client.schemas.filters import (
|
|
64
64
|
FlowRunFilterStateName,
|
65
65
|
FlowRunFilterStateType,
|
66
66
|
)
|
67
|
+
from prefect.client.schemas.objects import (
|
68
|
+
ConcurrencyLimitConfig,
|
69
|
+
FlowRun,
|
70
|
+
State,
|
71
|
+
StateType,
|
72
|
+
)
|
67
73
|
from prefect.client.schemas.objects import Flow as APIFlow
|
68
|
-
from prefect.
|
74
|
+
from prefect.concurrency.asyncio import (
|
75
|
+
AcquireConcurrencySlotTimeoutError,
|
76
|
+
ConcurrencySlotAcquisitionError,
|
77
|
+
concurrency,
|
78
|
+
)
|
69
79
|
from prefect.events import DeploymentTriggerTypes, TriggerTypes
|
70
80
|
from prefect.events.related import tags_as_related_resources
|
71
81
|
from prefect.events.schemas.events import RelatedResource
|
@@ -81,7 +91,12 @@ from prefect.settings import (
|
|
81
91
|
PREFECT_RUNNER_SERVER_ENABLE,
|
82
92
|
get_current_settings,
|
83
93
|
)
|
84
|
-
from prefect.states import
|
94
|
+
from prefect.states import (
|
95
|
+
AwaitingConcurrencySlot,
|
96
|
+
Crashed,
|
97
|
+
Pending,
|
98
|
+
exception_to_failed_state,
|
99
|
+
)
|
85
100
|
from prefect.types.entrypoint import EntrypointType
|
86
101
|
from prefect.utilities.asyncutils import (
|
87
102
|
asyncnullcontext,
|
@@ -226,6 +241,7 @@ class Runner:
|
|
226
241
|
rrule: Optional[Union[Iterable[str], str]] = None,
|
227
242
|
paused: Optional[bool] = None,
|
228
243
|
schedules: Optional["FlexibleScheduleList"] = None,
|
244
|
+
concurrency_limit: Optional[Union[int, ConcurrencyLimitConfig, None]] = None,
|
229
245
|
parameters: Optional[dict] = None,
|
230
246
|
triggers: Optional[List[Union[DeploymentTriggerTypes, TriggerTypes]]] = None,
|
231
247
|
description: Optional[str] = None,
|
@@ -248,6 +264,10 @@ class Runner:
|
|
248
264
|
or a timedelta object. If a number is given, it will be interpreted as seconds.
|
249
265
|
cron: A cron schedule of when to execute runs of this flow.
|
250
266
|
rrule: An rrule schedule of when to execute runs of this flow.
|
267
|
+
paused: Whether or not to set the created deployment as paused.
|
268
|
+
schedules: A list of schedule objects defining when to execute runs of this flow.
|
269
|
+
Used to define multiple schedules or additional scheduling options like `timezone`.
|
270
|
+
concurrency_limit: The maximum number of concurrent runs of this flow to allow.
|
251
271
|
triggers: A list of triggers that should kick of a run of this flow.
|
252
272
|
parameters: A dictionary of default parameter values to pass to runs of this flow.
|
253
273
|
description: A description for the created deployment. Defaults to the flow's
|
@@ -280,6 +300,7 @@ class Runner:
|
|
280
300
|
version=version,
|
281
301
|
enforce_parameter_schema=enforce_parameter_schema,
|
282
302
|
entrypoint_type=entrypoint_type,
|
303
|
+
concurrency_limit=concurrency_limit,
|
283
304
|
)
|
284
305
|
return await self.add_deployment(deployment)
|
285
306
|
|
@@ -959,6 +980,7 @@ class Runner:
|
|
959
980
|
"""
|
960
981
|
submittable_flow_runs = flow_run_response
|
961
982
|
submittable_flow_runs.sort(key=lambda run: run.next_scheduled_start_time)
|
983
|
+
|
962
984
|
for i, flow_run in enumerate(submittable_flow_runs):
|
963
985
|
if flow_run.id in self._submitting_flow_run_ids:
|
964
986
|
continue
|
@@ -1025,12 +1047,38 @@ class Runner:
|
|
1025
1047
|
) -> Union[Optional[int], Exception]:
|
1026
1048
|
run_logger = self._get_flow_run_logger(flow_run)
|
1027
1049
|
|
1050
|
+
if flow_run.deployment_id:
|
1051
|
+
deployment = await self._client.read_deployment(flow_run.deployment_id)
|
1052
|
+
if deployment and deployment.global_concurrency_limit:
|
1053
|
+
limit_name = deployment.global_concurrency_limit.name
|
1054
|
+
concurrency_ctx = concurrency
|
1055
|
+
else:
|
1056
|
+
limit_name = ""
|
1057
|
+
concurrency_ctx = asyncnullcontext
|
1058
|
+
|
1028
1059
|
try:
|
1029
|
-
|
1030
|
-
|
1031
|
-
|
1032
|
-
|
1060
|
+
async with concurrency_ctx(limit_name, max_retries=0, strict=True):
|
1061
|
+
status_code = await self._run_process(
|
1062
|
+
flow_run=flow_run,
|
1063
|
+
task_status=task_status,
|
1064
|
+
entrypoint=entrypoint,
|
1065
|
+
)
|
1066
|
+
except (
|
1067
|
+
AcquireConcurrencySlotTimeoutError,
|
1068
|
+
ConcurrencySlotAcquisitionError,
|
1069
|
+
) as exc:
|
1070
|
+
self._logger.info(
|
1071
|
+
(
|
1072
|
+
"Deployment %s reached its concurrency limit when attempting to execute flow run %s. Will attempt to execute later."
|
1073
|
+
),
|
1074
|
+
flow_run.deployment_id,
|
1075
|
+
flow_run.name,
|
1033
1076
|
)
|
1077
|
+
await self._propose_scheduled_state(flow_run)
|
1078
|
+
|
1079
|
+
if not task_status._future.done():
|
1080
|
+
task_status.started(exc)
|
1081
|
+
return exc
|
1034
1082
|
except Exception as exc:
|
1035
1083
|
if not task_status._future.done():
|
1036
1084
|
# This flow run was being submitted and did not start successfully
|
@@ -1116,6 +1164,26 @@ class Runner:
|
|
1116
1164
|
exc_info=True,
|
1117
1165
|
)
|
1118
1166
|
|
1167
|
+
async def _propose_scheduled_state(self, flow_run: "FlowRun") -> None:
|
1168
|
+
run_logger = self._get_flow_run_logger(flow_run)
|
1169
|
+
try:
|
1170
|
+
state = await propose_state(
|
1171
|
+
self._client,
|
1172
|
+
AwaitingConcurrencySlot(),
|
1173
|
+
flow_run_id=flow_run.id,
|
1174
|
+
)
|
1175
|
+
self._logger.info(f"Flow run {flow_run.id} now has state {state.name}")
|
1176
|
+
except Abort as exc:
|
1177
|
+
run_logger.info(
|
1178
|
+
(
|
1179
|
+
f"Aborted rescheduling of flow run '{flow_run.id}'. "
|
1180
|
+
f"Server sent an abort signal: {exc}"
|
1181
|
+
),
|
1182
|
+
)
|
1183
|
+
pass
|
1184
|
+
except Exception:
|
1185
|
+
run_logger.exception(f"Failed to update state of flow run '{flow_run.id}'")
|
1186
|
+
|
1119
1187
|
async def _propose_crashed_state(self, flow_run: "FlowRun", message: str) -> None:
|
1120
1188
|
run_logger = self._get_flow_run_logger(flow_run)
|
1121
1189
|
try:
|
prefect/settings.py
CHANGED
@@ -637,7 +637,7 @@ PREFECT_API_KEY = Setting(
|
|
637
637
|
)
|
638
638
|
"""API key used to authenticate with a the Prefect API. Defaults to `None`."""
|
639
639
|
|
640
|
-
PREFECT_API_ENABLE_HTTP2 = Setting(bool, default=
|
640
|
+
PREFECT_API_ENABLE_HTTP2 = Setting(bool, default=False)
|
641
641
|
"""
|
642
642
|
If true, enable support for HTTP/2 for communicating with an API.
|
643
643
|
|
@@ -1288,6 +1288,36 @@ compromise. Adjust this setting based on your specific security requirements
|
|
1288
1288
|
and usage patterns.
|
1289
1289
|
"""
|
1290
1290
|
|
1291
|
+
PREFECT_SERVER_CORS_ALLOWED_ORIGINS = Setting(
|
1292
|
+
str,
|
1293
|
+
default="*",
|
1294
|
+
)
|
1295
|
+
"""
|
1296
|
+
A comma-separated list of origins that are authorized to make cross-origin requests to the API.
|
1297
|
+
|
1298
|
+
By default, this is set to `*`, which allows requests from all origins.
|
1299
|
+
"""
|
1300
|
+
|
1301
|
+
PREFECT_SERVER_CORS_ALLOWED_METHODS = Setting(
|
1302
|
+
str,
|
1303
|
+
default="*",
|
1304
|
+
)
|
1305
|
+
"""
|
1306
|
+
A comma-separated list of methods that are authorized to make cross-origin requests to the API.
|
1307
|
+
|
1308
|
+
By default, this is set to `*`, which allows requests with all methods.
|
1309
|
+
"""
|
1310
|
+
|
1311
|
+
PREFECT_SERVER_CORS_ALLOWED_HEADERS = Setting(
|
1312
|
+
str,
|
1313
|
+
default="*",
|
1314
|
+
)
|
1315
|
+
"""
|
1316
|
+
A comma-separated list of headers that are authorized to make cross-origin requests to the API.
|
1317
|
+
|
1318
|
+
By default, this is set to `*`, which allows requests with all headers.
|
1319
|
+
"""
|
1320
|
+
|
1291
1321
|
PREFECT_SERVER_ALLOW_EPHEMERAL_MODE = Setting(bool, default=False)
|
1292
1322
|
"""
|
1293
1323
|
Controls whether or not a subprocess server can be started when no API URL is provided.
|