prefect-client 3.0.0rc20__py3-none-any.whl → 3.0.1__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/migration.py +1 -1
- prefect/artifacts.py +1 -1
- prefect/blocks/core.py +3 -4
- prefect/blocks/notifications.py +10 -10
- prefect/blocks/system.py +4 -4
- prefect/blocks/webhook.py +3 -1
- prefect/client/cloud.py +2 -1
- prefect/client/orchestration.py +54 -1
- prefect/context.py +7 -9
- prefect/deployments/runner.py +3 -3
- prefect/exceptions.py +6 -0
- prefect/filesystems.py +5 -3
- prefect/flow_engine.py +13 -10
- prefect/flows.py +0 -2
- prefect/futures.py +2 -1
- prefect/locking/__init__.py +0 -0
- prefect/locking/memory.py +213 -0
- prefect/locking/protocol.py +122 -0
- prefect/records/filesystem.py +4 -2
- prefect/records/result_store.py +12 -6
- prefect/results.py +559 -206
- prefect/settings.py +10 -3
- prefect/states.py +12 -12
- prefect/task_engine.py +28 -20
- prefect/task_worker.py +6 -4
- prefect/tasks.py +24 -6
- prefect/transactions.py +35 -28
- prefect/utilities/callables.py +1 -3
- prefect/variables.py +34 -24
- prefect/workers/process.py +1 -3
- {prefect_client-3.0.0rc20.dist-info → prefect_client-3.0.1.dist-info}/METADATA +2 -2
- {prefect_client-3.0.0rc20.dist-info → prefect_client-3.0.1.dist-info}/RECORD +35 -32
- {prefect_client-3.0.0rc20.dist-info → prefect_client-3.0.1.dist-info}/LICENSE +0 -0
- {prefect_client-3.0.0rc20.dist-info → prefect_client-3.0.1.dist-info}/WHEEL +0 -0
- {prefect_client-3.0.0rc20.dist-info → prefect_client-3.0.1.dist-info}/top_level.txt +0 -0
prefect/results.py
CHANGED
@@ -1,11 +1,13 @@
|
|
1
1
|
import abc
|
2
2
|
import inspect
|
3
|
+
import os
|
4
|
+
import socket
|
5
|
+
import threading
|
3
6
|
import uuid
|
4
7
|
from functools import partial
|
5
8
|
from typing import (
|
6
9
|
TYPE_CHECKING,
|
7
10
|
Any,
|
8
|
-
Awaitable,
|
9
11
|
Callable,
|
10
12
|
Dict,
|
11
13
|
Generic,
|
@@ -33,11 +35,16 @@ from typing_extensions import ParamSpec, Self
|
|
33
35
|
import prefect
|
34
36
|
from prefect.blocks.core import Block
|
35
37
|
from prefect.client.utilities import inject_client
|
36
|
-
from prefect.exceptions import
|
38
|
+
from prefect.exceptions import (
|
39
|
+
ConfigurationError,
|
40
|
+
MissingContextError,
|
41
|
+
SerializationError,
|
42
|
+
)
|
37
43
|
from prefect.filesystems import (
|
38
44
|
LocalFileSystem,
|
39
45
|
WritableFileSystem,
|
40
46
|
)
|
47
|
+
from prefect.locking.protocol import LockManager
|
41
48
|
from prefect.logging import get_logger
|
42
49
|
from prefect.serializers import PickleSerializer, Serializer
|
43
50
|
from prefect.settings import (
|
@@ -54,6 +61,7 @@ from prefect.utilities.pydantic import get_dispatch_key, lookup_type, register_b
|
|
54
61
|
if TYPE_CHECKING:
|
55
62
|
from prefect import Flow, Task
|
56
63
|
from prefect.client.orchestration import PrefectClient
|
64
|
+
from prefect.transactions import IsolationLevel
|
57
65
|
|
58
66
|
|
59
67
|
ResultStorage = Union[WritableFileSystem, str]
|
@@ -73,18 +81,66 @@ _default_storages: Dict[Tuple[str, str], WritableFileSystem] = {}
|
|
73
81
|
|
74
82
|
|
75
83
|
@sync_compatible
|
76
|
-
async def get_default_result_storage() ->
|
84
|
+
async def get_default_result_storage() -> WritableFileSystem:
|
77
85
|
"""
|
78
86
|
Generate a default file system for result storage.
|
79
87
|
"""
|
80
88
|
default_block = PREFECT_DEFAULT_RESULT_STORAGE_BLOCK.value()
|
81
89
|
|
82
90
|
if default_block is not None:
|
83
|
-
return await
|
91
|
+
return await resolve_result_storage(default_block)
|
84
92
|
|
85
93
|
# otherwise, use the local file system
|
86
94
|
basepath = PREFECT_LOCAL_STORAGE_PATH.value()
|
87
|
-
return LocalFileSystem(basepath=basepath)
|
95
|
+
return LocalFileSystem(basepath=str(basepath))
|
96
|
+
|
97
|
+
|
98
|
+
@sync_compatible
|
99
|
+
async def resolve_result_storage(
|
100
|
+
result_storage: ResultStorage,
|
101
|
+
) -> WritableFileSystem:
|
102
|
+
"""
|
103
|
+
Resolve one of the valid `ResultStorage` input types into a saved block
|
104
|
+
document id and an instance of the block.
|
105
|
+
"""
|
106
|
+
from prefect.client.orchestration import get_client
|
107
|
+
|
108
|
+
client = get_client()
|
109
|
+
if isinstance(result_storage, Block):
|
110
|
+
storage_block = result_storage
|
111
|
+
|
112
|
+
if storage_block._block_document_id is not None:
|
113
|
+
# Avoid saving the block if it already has an identifier assigned
|
114
|
+
storage_block_id = storage_block._block_document_id
|
115
|
+
else:
|
116
|
+
storage_block_id = None
|
117
|
+
elif isinstance(result_storage, str):
|
118
|
+
storage_block = await Block.load(result_storage, client=client)
|
119
|
+
storage_block_id = storage_block._block_document_id
|
120
|
+
assert storage_block_id is not None, "Loaded storage blocks must have ids"
|
121
|
+
else:
|
122
|
+
raise TypeError(
|
123
|
+
"Result storage must be one of the following types: 'UUID', 'Block', "
|
124
|
+
f"'str'. Got unsupported type {type(result_storage).__name__!r}."
|
125
|
+
)
|
126
|
+
|
127
|
+
return storage_block
|
128
|
+
|
129
|
+
|
130
|
+
def resolve_serializer(serializer: ResultSerializer) -> Serializer:
|
131
|
+
"""
|
132
|
+
Resolve one of the valid `ResultSerializer` input types into a serializer
|
133
|
+
instance.
|
134
|
+
"""
|
135
|
+
if isinstance(serializer, Serializer):
|
136
|
+
return serializer
|
137
|
+
elif isinstance(serializer, str):
|
138
|
+
return Serializer(type=serializer)
|
139
|
+
else:
|
140
|
+
raise TypeError(
|
141
|
+
"Result serializer must be one of the following types: 'Serializer', "
|
142
|
+
f"'str'. Got unsupported type {type(serializer).__name__!r}."
|
143
|
+
)
|
88
144
|
|
89
145
|
|
90
146
|
async def get_or_create_default_task_scheduling_storage() -> ResultStorage:
|
@@ -101,11 +157,11 @@ async def get_or_create_default_task_scheduling_storage() -> ResultStorage:
|
|
101
157
|
return LocalFileSystem(basepath=basepath)
|
102
158
|
|
103
159
|
|
104
|
-
def get_default_result_serializer() ->
|
160
|
+
def get_default_result_serializer() -> Serializer:
|
105
161
|
"""
|
106
162
|
Generate a default file system for result storage.
|
107
163
|
"""
|
108
|
-
return PREFECT_RESULTS_DEFAULT_SERIALIZER.value()
|
164
|
+
return resolve_serializer(PREFECT_RESULTS_DEFAULT_SERIALIZER.value())
|
109
165
|
|
110
166
|
|
111
167
|
def get_default_persist_setting() -> bool:
|
@@ -122,217 +178,498 @@ def _format_user_supplied_storage_key(key: str) -> str:
|
|
122
178
|
return key.format(**runtime_vars, parameters=prefect.runtime.task_run.parameters)
|
123
179
|
|
124
180
|
|
125
|
-
class
|
181
|
+
class ResultStore(BaseModel):
|
126
182
|
"""
|
127
|
-
|
183
|
+
Manages the storage and retrieval of results.
|
184
|
+
|
185
|
+
Attributes:
|
186
|
+
result_storage: The storage for result records. If not provided, the default
|
187
|
+
result storage will be used.
|
188
|
+
metadata_storage: The storage for result record metadata. If not provided,
|
189
|
+
the metadata will be stored alongside the results.
|
190
|
+
lock_manager: The lock manager to use for locking result records. If not provided,
|
191
|
+
the store cannot be used in transactions with the SERIALIZABLE isolation level.
|
192
|
+
persist_result: Whether to persist results.
|
193
|
+
cache_result_in_memory: Whether to cache results in memory.
|
194
|
+
serializer: The serializer to use for results.
|
195
|
+
storage_key_fn: The function to generate storage keys.
|
128
196
|
"""
|
129
197
|
|
130
|
-
|
131
|
-
cache_result_in_memory: bool
|
132
|
-
serializer: Serializer
|
133
|
-
storage_block_id: Optional[uuid.UUID] = None
|
134
|
-
storage_block: WritableFileSystem
|
135
|
-
storage_key_fn: Callable[[], str]
|
198
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
136
199
|
|
137
|
-
|
138
|
-
|
139
|
-
|
200
|
+
result_storage: Optional[WritableFileSystem] = Field(default=None)
|
201
|
+
metadata_storage: Optional[WritableFileSystem] = Field(default=None)
|
202
|
+
lock_manager: Optional[LockManager] = Field(default=None)
|
203
|
+
persist_result: bool = Field(default_factory=get_default_persist_setting)
|
204
|
+
cache_result_in_memory: bool = Field(default=True)
|
205
|
+
serializer: Serializer = Field(default_factory=get_default_result_serializer)
|
206
|
+
storage_key_fn: Callable[[], str] = Field(default=DEFAULT_STORAGE_KEY_FN)
|
207
|
+
|
208
|
+
@property
|
209
|
+
def result_storage_block_id(self) -> Optional[UUID]:
|
210
|
+
if self.result_storage is None:
|
211
|
+
return None
|
212
|
+
return self.result_storage._block_document_id
|
213
|
+
|
214
|
+
@sync_compatible
|
215
|
+
async def update_for_flow(self, flow: "Flow") -> Self:
|
140
216
|
"""
|
141
|
-
Create a new result
|
217
|
+
Create a new result store for a flow with updated settings.
|
142
218
|
|
143
|
-
|
144
|
-
|
219
|
+
Args:
|
220
|
+
flow: The flow to update the result store for.
|
221
|
+
|
222
|
+
Returns:
|
223
|
+
An updated result store.
|
145
224
|
"""
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
225
|
+
update = {}
|
226
|
+
if flow.result_storage is not None:
|
227
|
+
update["result_storage"] = await resolve_result_storage(flow.result_storage)
|
228
|
+
if flow.result_serializer is not None:
|
229
|
+
update["serializer"] = resolve_serializer(flow.result_serializer)
|
230
|
+
if flow.persist_result is not None:
|
231
|
+
update["persist_result"] = flow.persist_result
|
232
|
+
if flow.cache_result_in_memory is not None:
|
233
|
+
update["cache_result_in_memory"] = flow.cache_result_in_memory
|
234
|
+
if self.result_storage is None and update.get("result_storage") is None:
|
235
|
+
update["result_storage"] = await get_default_result_storage()
|
236
|
+
return self.model_copy(update=update)
|
150
237
|
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
kwargs.setdefault("cache_result_in_memory", True)
|
156
|
-
kwargs.setdefault("storage_key_fn", DEFAULT_STORAGE_KEY_FN)
|
238
|
+
@sync_compatible
|
239
|
+
async def update_for_task(self: Self, task: "Task") -> Self:
|
240
|
+
"""
|
241
|
+
Create a new result store for a task.
|
157
242
|
|
158
|
-
|
243
|
+
Args:
|
244
|
+
task: The task to update the result store for.
|
159
245
|
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
result_serializer=flow.result_serializer
|
176
|
-
or ctx.result_factory.serializer,
|
177
|
-
persist_result=flow.persist_result,
|
178
|
-
cache_result_in_memory=flow.cache_result_in_memory,
|
179
|
-
storage_key_fn=DEFAULT_STORAGE_KEY_FN,
|
180
|
-
client=client,
|
246
|
+
Returns:
|
247
|
+
An updated result store.
|
248
|
+
"""
|
249
|
+
update = {}
|
250
|
+
if task.result_storage is not None:
|
251
|
+
update["result_storage"] = await resolve_result_storage(task.result_storage)
|
252
|
+
if task.result_serializer is not None:
|
253
|
+
update["serializer"] = resolve_serializer(task.result_serializer)
|
254
|
+
if task.persist_result is not None:
|
255
|
+
update["persist_result"] = task.persist_result
|
256
|
+
if task.cache_result_in_memory is not None:
|
257
|
+
update["cache_result_in_memory"] = task.cache_result_in_memory
|
258
|
+
if task.result_storage_key is not None:
|
259
|
+
update["storage_key_fn"] = partial(
|
260
|
+
_format_user_supplied_storage_key, task.result_storage_key
|
181
261
|
)
|
262
|
+
if self.result_storage is None and update.get("result_storage") is None:
|
263
|
+
update["result_storage"] = await get_default_result_storage()
|
264
|
+
return self.model_copy(update=update)
|
265
|
+
|
266
|
+
@staticmethod
|
267
|
+
def generate_default_holder() -> str:
|
268
|
+
"""
|
269
|
+
Generate a default holder string using hostname, PID, and thread ID.
|
270
|
+
|
271
|
+
Returns:
|
272
|
+
str: A unique identifier string.
|
273
|
+
"""
|
274
|
+
hostname = socket.gethostname()
|
275
|
+
pid = os.getpid()
|
276
|
+
thread_name = threading.current_thread().name
|
277
|
+
thread_id = threading.get_ident()
|
278
|
+
return f"{hostname}:{pid}:{thread_id}:{thread_name}"
|
279
|
+
|
280
|
+
@sync_compatible
|
281
|
+
async def _exists(self, key: str) -> bool:
|
282
|
+
"""
|
283
|
+
Check if a result record exists in storage.
|
284
|
+
|
285
|
+
Args:
|
286
|
+
key: The key to check for the existence of a result record.
|
287
|
+
|
288
|
+
Returns:
|
289
|
+
bool: True if the result record exists, False otherwise.
|
290
|
+
"""
|
291
|
+
if self.metadata_storage is not None:
|
292
|
+
# TODO: Add an `exists` method to commonly used storage blocks
|
293
|
+
# so the entire payload doesn't need to be read
|
294
|
+
try:
|
295
|
+
metadata_content = await self.metadata_storage.read_path(key)
|
296
|
+
return metadata_content is not None
|
297
|
+
except Exception:
|
298
|
+
return False
|
182
299
|
else:
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
300
|
+
try:
|
301
|
+
content = await self.result_storage.read_path(key)
|
302
|
+
return content is not None
|
303
|
+
except Exception:
|
304
|
+
return False
|
305
|
+
|
306
|
+
def exists(self, key: str) -> bool:
|
307
|
+
"""
|
308
|
+
Check if a result record exists in storage.
|
309
|
+
|
310
|
+
Args:
|
311
|
+
key: The key to check for the existence of a result record.
|
312
|
+
|
313
|
+
Returns:
|
314
|
+
bool: True if the result record exists, False otherwise.
|
315
|
+
"""
|
316
|
+
return self._exists(key=key, _sync=True)
|
317
|
+
|
318
|
+
async def aexists(self, key: str) -> bool:
|
319
|
+
"""
|
320
|
+
Check if a result record exists in storage.
|
321
|
+
|
322
|
+
Args:
|
323
|
+
key: The key to check for the existence of a result record.
|
324
|
+
|
325
|
+
Returns:
|
326
|
+
bool: True if the result record exists, False otherwise.
|
327
|
+
"""
|
328
|
+
return await self._exists(key=key, _sync=False)
|
329
|
+
|
330
|
+
@sync_compatible
|
331
|
+
async def _read(self, key: str, holder: str) -> "ResultRecord":
|
332
|
+
"""
|
333
|
+
Read a result record from storage.
|
334
|
+
|
335
|
+
This is the internal implementation. Use `read` or `aread` for synchronous and
|
336
|
+
asynchronous result reading respectively.
|
337
|
+
|
338
|
+
Args:
|
339
|
+
key: The key to read the result record from.
|
340
|
+
holder: The holder of the lock if a lock was set on the record.
|
341
|
+
|
342
|
+
Returns:
|
343
|
+
A result record.
|
344
|
+
"""
|
345
|
+
if self.lock_manager is not None and not self.is_lock_holder(key, holder):
|
346
|
+
await self.await_for_lock(key)
|
347
|
+
|
348
|
+
if self.result_storage is None:
|
349
|
+
self.result_storage = await get_default_result_storage()
|
350
|
+
|
351
|
+
if self.metadata_storage is not None:
|
352
|
+
metadata_content = await self.metadata_storage.read_path(key)
|
353
|
+
metadata = ResultRecordMetadata.load_bytes(metadata_content)
|
354
|
+
assert (
|
355
|
+
metadata.storage_key is not None
|
356
|
+
), "Did not find storage key in metadata"
|
357
|
+
result_content = await self.result_storage.read_path(metadata.storage_key)
|
358
|
+
return ResultRecord.deserialize_from_result_and_metadata(
|
359
|
+
result=result_content, metadata=metadata_content
|
193
360
|
)
|
361
|
+
else:
|
362
|
+
content = await self.result_storage.read_path(key)
|
363
|
+
return ResultRecord.deserialize(content)
|
194
364
|
|
195
|
-
|
196
|
-
@inject_client
|
197
|
-
async def from_task(
|
198
|
-
cls: Type[Self], task: "Task", client: "PrefectClient" = None
|
199
|
-
) -> Self:
|
365
|
+
def read(self, key: str, holder: Optional[str] = None) -> "ResultRecord":
|
200
366
|
"""
|
201
|
-
|
367
|
+
Read a result record from storage.
|
368
|
+
|
369
|
+
Args:
|
370
|
+
key: The key to read the result record from.
|
371
|
+
holder: The holder of the lock if a lock was set on the record.
|
372
|
+
Returns:
|
373
|
+
A result record.
|
202
374
|
"""
|
203
|
-
|
375
|
+
holder = holder or self.generate_default_holder()
|
376
|
+
return self._read(key=key, holder=holder, _sync=True)
|
204
377
|
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
378
|
+
async def aread(self, key: str, holder: Optional[str] = None) -> "ResultRecord":
|
379
|
+
"""
|
380
|
+
Read a result record from storage.
|
381
|
+
|
382
|
+
Args:
|
383
|
+
key: The key to read the result record from.
|
384
|
+
holder: The holder of the lock if a lock was set on the record.
|
385
|
+
Returns:
|
386
|
+
A result record.
|
387
|
+
"""
|
388
|
+
holder = holder or self.generate_default_holder()
|
389
|
+
return await self._read(key=key, holder=holder, _sync=False)
|
390
|
+
|
391
|
+
def create_result_record(
|
392
|
+
self,
|
393
|
+
key: str,
|
394
|
+
obj: Any,
|
395
|
+
expiration: Optional[DateTime] = None,
|
396
|
+
):
|
210
397
|
"""
|
211
|
-
Create a
|
398
|
+
Create a result record.
|
399
|
+
|
400
|
+
Args:
|
401
|
+
key: The key to create the result record for.
|
402
|
+
obj: The object to create the result record for.
|
403
|
+
expiration: The expiration time for the result record.
|
212
404
|
"""
|
213
|
-
|
214
|
-
|
405
|
+
key = key or self.storage_key_fn()
|
406
|
+
|
407
|
+
return ResultRecord(
|
408
|
+
result=obj,
|
409
|
+
metadata=ResultRecordMetadata(
|
410
|
+
serializer=self.serializer,
|
411
|
+
expiration=expiration,
|
412
|
+
storage_key=key,
|
413
|
+
storage_block_id=self.result_storage_block_id,
|
414
|
+
),
|
215
415
|
)
|
216
416
|
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
417
|
+
def write(
|
418
|
+
self,
|
419
|
+
key: str,
|
420
|
+
obj: Any,
|
421
|
+
expiration: Optional[DateTime] = None,
|
422
|
+
holder: Optional[str] = None,
|
423
|
+
):
|
424
|
+
"""
|
425
|
+
Write a result to storage.
|
426
|
+
|
427
|
+
Handles the creation of a `ResultRecord` and its serialization to storage.
|
428
|
+
|
429
|
+
Args:
|
430
|
+
key: The key to write the result record to.
|
431
|
+
obj: The object to write to storage.
|
432
|
+
expiration: The expiration time for the result record.
|
433
|
+
holder: The holder of the lock if a lock was set on the record.
|
434
|
+
"""
|
435
|
+
holder = holder or self.generate_default_holder()
|
436
|
+
return self.persist_result_record(
|
437
|
+
result_record=self.create_result_record(
|
438
|
+
key=key, obj=obj, expiration=expiration
|
439
|
+
),
|
440
|
+
holder=holder,
|
233
441
|
)
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
442
|
+
|
443
|
+
async def awrite(
|
444
|
+
self,
|
445
|
+
key: str,
|
446
|
+
obj: Any,
|
447
|
+
expiration: Optional[DateTime] = None,
|
448
|
+
holder: Optional[str] = None,
|
449
|
+
):
|
450
|
+
"""
|
451
|
+
Write a result to storage.
|
452
|
+
|
453
|
+
Args:
|
454
|
+
key: The key to write the result record to.
|
455
|
+
obj: The object to write to storage.
|
456
|
+
expiration: The expiration time for the result record.
|
457
|
+
holder: The holder of the lock if a lock was set on the record.
|
458
|
+
"""
|
459
|
+
holder = holder or self.generate_default_holder()
|
460
|
+
return await self.apersist_result_record(
|
461
|
+
result_record=self.create_result_record(
|
462
|
+
key=key, obj=obj, expiration=expiration
|
463
|
+
),
|
464
|
+
holder=holder,
|
238
465
|
)
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
466
|
+
|
467
|
+
@sync_compatible
|
468
|
+
async def _persist_result_record(self, result_record: "ResultRecord", holder: str):
|
469
|
+
"""
|
470
|
+
Persist a result record to storage.
|
471
|
+
|
472
|
+
Args:
|
473
|
+
result_record: The result record to persist.
|
474
|
+
holder: The holder of the lock if a lock was set on the record.
|
475
|
+
"""
|
476
|
+
assert (
|
477
|
+
result_record.metadata.storage_key is not None
|
478
|
+
), "Storage key is required on result record"
|
479
|
+
|
480
|
+
key = result_record.metadata.storage_key
|
481
|
+
if (
|
482
|
+
self.lock_manager is not None
|
483
|
+
and self.is_locked(key)
|
484
|
+
and not self.is_lock_holder(key, holder)
|
485
|
+
):
|
486
|
+
raise RuntimeError(
|
487
|
+
f"Cannot write to result record with key {key} because it is locked by "
|
488
|
+
f"another holder."
|
489
|
+
)
|
490
|
+
if self.result_storage is None:
|
491
|
+
self.result_storage = await get_default_result_storage()
|
492
|
+
|
493
|
+
# If metadata storage is configured, write result and metadata separately
|
494
|
+
if self.metadata_storage is not None:
|
495
|
+
await self.result_storage.write_path(
|
496
|
+
result_record.metadata.storage_key,
|
497
|
+
content=result_record.serialize_result(),
|
244
498
|
)
|
499
|
+
await self.metadata_storage.write_path(
|
500
|
+
result_record.metadata.storage_key,
|
501
|
+
content=result_record.serialize_metadata(),
|
502
|
+
)
|
503
|
+
# Otherwise, write the result metadata and result together
|
245
504
|
else:
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
return await cls.from_settings(
|
251
|
-
result_storage=result_storage,
|
252
|
-
result_serializer=result_serializer,
|
253
|
-
persist_result=persist_result,
|
254
|
-
cache_result_in_memory=cache_result_in_memory,
|
255
|
-
client=client,
|
256
|
-
storage_key_fn=(
|
257
|
-
partial(_format_user_supplied_storage_key, task.result_storage_key)
|
258
|
-
if task.result_storage_key is not None
|
259
|
-
else DEFAULT_STORAGE_KEY_FN
|
260
|
-
),
|
261
|
-
)
|
505
|
+
await self.result_storage.write_path(
|
506
|
+
result_record.metadata.storage_key, content=result_record.serialize()
|
507
|
+
)
|
262
508
|
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
result_serializer: ResultSerializer,
|
269
|
-
persist_result: Optional[bool],
|
270
|
-
cache_result_in_memory: bool,
|
271
|
-
storage_key_fn: Callable[[], str],
|
272
|
-
client: "PrefectClient",
|
273
|
-
) -> Self:
|
274
|
-
if persist_result is None:
|
275
|
-
persist_result = get_default_persist_setting()
|
509
|
+
def persist_result_record(
|
510
|
+
self, result_record: "ResultRecord", holder: Optional[str] = None
|
511
|
+
):
|
512
|
+
"""
|
513
|
+
Persist a result record to storage.
|
276
514
|
|
277
|
-
|
278
|
-
|
515
|
+
Args:
|
516
|
+
result_record: The result record to persist.
|
517
|
+
"""
|
518
|
+
holder = holder or self.generate_default_holder()
|
519
|
+
return self._persist_result_record(
|
520
|
+
result_record=result_record, holder=holder, _sync=True
|
279
521
|
)
|
280
|
-
serializer = cls.resolve_serializer(result_serializer)
|
281
522
|
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
523
|
+
async def apersist_result_record(
|
524
|
+
self, result_record: "ResultRecord", holder: Optional[str] = None
|
525
|
+
):
|
526
|
+
"""
|
527
|
+
Persist a result record to storage.
|
528
|
+
|
529
|
+
Args:
|
530
|
+
result_record: The result record to persist.
|
531
|
+
"""
|
532
|
+
holder = holder or self.generate_default_holder()
|
533
|
+
return await self._persist_result_record(
|
534
|
+
result_record=result_record, holder=holder, _sync=False
|
289
535
|
)
|
290
536
|
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
"""
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
else:
|
308
|
-
storage_block_id = None
|
309
|
-
elif isinstance(result_storage, str):
|
310
|
-
storage_block = await Block.load(result_storage, client=client)
|
311
|
-
storage_block_id = storage_block._block_document_id
|
312
|
-
assert storage_block_id is not None, "Loaded storage blocks must have ids"
|
537
|
+
def supports_isolation_level(self, level: "IsolationLevel") -> bool:
|
538
|
+
"""
|
539
|
+
Check if the result store supports a given isolation level.
|
540
|
+
|
541
|
+
Args:
|
542
|
+
level: The isolation level to check.
|
543
|
+
|
544
|
+
Returns:
|
545
|
+
bool: True if the isolation level is supported, False otherwise.
|
546
|
+
"""
|
547
|
+
from prefect.transactions import IsolationLevel
|
548
|
+
|
549
|
+
if level == IsolationLevel.READ_COMMITTED:
|
550
|
+
return True
|
551
|
+
elif level == IsolationLevel.SERIALIZABLE:
|
552
|
+
return self.lock_manager is not None
|
313
553
|
else:
|
314
|
-
raise
|
315
|
-
|
316
|
-
|
554
|
+
raise ValueError(f"Unsupported isolation level: {level}")
|
555
|
+
|
556
|
+
def acquire_lock(
|
557
|
+
self, key: str, holder: Optional[str] = None, timeout: Optional[float] = None
|
558
|
+
) -> bool:
|
559
|
+
"""
|
560
|
+
Acquire a lock for a result record.
|
561
|
+
|
562
|
+
Args:
|
563
|
+
key: The key to acquire the lock for.
|
564
|
+
holder: The holder of the lock. If not provided, a default holder based on the
|
565
|
+
current host, process, and thread will be used.
|
566
|
+
timeout: The timeout for the lock.
|
567
|
+
|
568
|
+
Returns:
|
569
|
+
bool: True if the lock was successfully acquired; False otherwise.
|
570
|
+
"""
|
571
|
+
holder = holder or self.generate_default_holder()
|
572
|
+
if self.lock_manager is None:
|
573
|
+
raise ConfigurationError(
|
574
|
+
"Result store is not configured with a lock manager. Please set"
|
575
|
+
" a lock manager when creating the result store to enable locking."
|
317
576
|
)
|
577
|
+
return self.lock_manager.acquire_lock(key, holder, timeout)
|
318
578
|
|
319
|
-
|
579
|
+
async def aacquire_lock(
|
580
|
+
self, key: str, holder: Optional[str] = None, timeout: Optional[float] = None
|
581
|
+
) -> bool:
|
582
|
+
"""
|
583
|
+
Acquire a lock for a result record.
|
320
584
|
|
321
|
-
|
322
|
-
|
585
|
+
Args:
|
586
|
+
key: The key to acquire the lock for.
|
587
|
+
holder: The holder of the lock. If not provided, a default holder based on the
|
588
|
+
current host, process, and thread will be used.
|
589
|
+
timeout: The timeout for the lock.
|
590
|
+
|
591
|
+
Returns:
|
592
|
+
bool: True if the lock was successfully acquired; False otherwise.
|
323
593
|
"""
|
324
|
-
|
325
|
-
|
594
|
+
holder = holder or self.generate_default_holder()
|
595
|
+
if self.lock_manager is None:
|
596
|
+
raise ConfigurationError(
|
597
|
+
"Result store is not configured with a lock manager. Please set"
|
598
|
+
" a lock manager when creating the result store to enable locking."
|
599
|
+
)
|
600
|
+
|
601
|
+
return await self.lock_manager.aacquire_lock(key, holder, timeout)
|
602
|
+
|
603
|
+
def release_lock(self, key: str, holder: Optional[str] = None):
|
326
604
|
"""
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
605
|
+
Release a lock for a result record.
|
606
|
+
|
607
|
+
Args:
|
608
|
+
key: The key to release the lock for.
|
609
|
+
holder: The holder of the lock. Must match the holder that acquired the lock.
|
610
|
+
If not provided, a default holder based on the current host, process, and
|
611
|
+
thread will be used.
|
612
|
+
"""
|
613
|
+
holder = holder or self.generate_default_holder()
|
614
|
+
if self.lock_manager is None:
|
615
|
+
raise ConfigurationError(
|
616
|
+
"Result store is not configured with a lock manager. Please set"
|
617
|
+
" a lock manager when creating the result store to enable locking."
|
618
|
+
)
|
619
|
+
return self.lock_manager.release_lock(key, holder)
|
620
|
+
|
621
|
+
def is_locked(self, key: str) -> bool:
|
622
|
+
"""
|
623
|
+
Check if a result record is locked.
|
624
|
+
"""
|
625
|
+
if self.lock_manager is None:
|
626
|
+
raise ConfigurationError(
|
627
|
+
"Result store is not configured with a lock manager. Please set"
|
628
|
+
" a lock manager when creating the result store to enable locking."
|
629
|
+
)
|
630
|
+
return self.lock_manager.is_locked(key)
|
631
|
+
|
632
|
+
def is_lock_holder(self, key: str, holder: Optional[str] = None) -> bool:
|
633
|
+
"""
|
634
|
+
Check if the current holder is the lock holder for the result record.
|
635
|
+
|
636
|
+
Args:
|
637
|
+
key: The key to check the lock for.
|
638
|
+
holder: The holder of the lock. If not provided, a default holder based on the
|
639
|
+
current host, process, and thread will be used.
|
640
|
+
|
641
|
+
Returns:
|
642
|
+
bool: True if the current holder is the lock holder; False otherwise.
|
643
|
+
"""
|
644
|
+
holder = holder or self.generate_default_holder()
|
645
|
+
if self.lock_manager is None:
|
646
|
+
raise ConfigurationError(
|
647
|
+
"Result store is not configured with a lock manager. Please set"
|
648
|
+
" a lock manager when creating the result store to enable locking."
|
335
649
|
)
|
650
|
+
return self.lock_manager.is_lock_holder(key, holder)
|
651
|
+
|
652
|
+
def wait_for_lock(self, key: str, timeout: Optional[float] = None) -> bool:
|
653
|
+
"""
|
654
|
+
Wait for the corresponding transaction record to become free.
|
655
|
+
"""
|
656
|
+
if self.lock_manager is None:
|
657
|
+
raise ConfigurationError(
|
658
|
+
"Result store is not configured with a lock manager. Please set"
|
659
|
+
" a lock manager when creating the result store to enable locking."
|
660
|
+
)
|
661
|
+
return self.lock_manager.wait_for_lock(key, timeout)
|
662
|
+
|
663
|
+
async def await_for_lock(self, key: str, timeout: Optional[float] = None) -> bool:
|
664
|
+
"""
|
665
|
+
Wait for the corresponding transaction record to become free.
|
666
|
+
"""
|
667
|
+
if self.lock_manager is None:
|
668
|
+
raise ConfigurationError(
|
669
|
+
"Result store is not configured with a lock manager. Please set"
|
670
|
+
" a lock manager when creating the result store to enable locking."
|
671
|
+
)
|
672
|
+
return await self.lock_manager.await_for_lock(key, timeout)
|
336
673
|
|
337
674
|
@sync_compatible
|
338
675
|
async def create_result(
|
@@ -342,9 +679,7 @@ class ResultFactory(BaseModel):
|
|
342
679
|
expiration: Optional[DateTime] = None,
|
343
680
|
) -> Union[R, "BaseResult[R]"]:
|
344
681
|
"""
|
345
|
-
Create a
|
346
|
-
|
347
|
-
If persistence is enabled the object is serialized, persisted to storage, and a reference is returned.
|
682
|
+
Create a `PersistedResult` for the given object.
|
348
683
|
"""
|
349
684
|
# Null objects are "cached" in memory at no cost
|
350
685
|
should_cache_object = self.cache_result_in_memory or obj is None
|
@@ -358,10 +693,13 @@ class ResultFactory(BaseModel):
|
|
358
693
|
else:
|
359
694
|
storage_key_fn = self.storage_key_fn
|
360
695
|
|
696
|
+
if self.result_storage is None:
|
697
|
+
self.result_storage = await get_default_result_storage()
|
698
|
+
|
361
699
|
return await PersistedResult.create(
|
362
700
|
obj,
|
363
|
-
storage_block=self.
|
364
|
-
storage_block_id=self.
|
701
|
+
storage_block=self.result_storage,
|
702
|
+
storage_block_id=self.result_storage_block_id,
|
365
703
|
storage_key_fn=storage_key_fn,
|
366
704
|
serializer=self.serializer,
|
367
705
|
cache_object=should_cache_object,
|
@@ -379,18 +717,33 @@ class ResultFactory(BaseModel):
|
|
379
717
|
serializer=self.serializer, storage_key=str(identifier)
|
380
718
|
),
|
381
719
|
)
|
382
|
-
await self.
|
720
|
+
await self.result_storage.write_path(
|
383
721
|
f"parameters/{identifier}", content=record.serialize()
|
384
722
|
)
|
385
723
|
|
386
724
|
@sync_compatible
|
387
725
|
async def read_parameters(self, identifier: UUID) -> Dict[str, Any]:
|
388
726
|
record = ResultRecord.deserialize(
|
389
|
-
await self.
|
727
|
+
await self.result_storage.read_path(f"parameters/{identifier}")
|
390
728
|
)
|
391
729
|
return record.result
|
392
730
|
|
393
731
|
|
732
|
+
def get_current_result_store() -> ResultStore:
|
733
|
+
"""
|
734
|
+
Get the current result store.
|
735
|
+
"""
|
736
|
+
from prefect.context import get_run_context
|
737
|
+
|
738
|
+
try:
|
739
|
+
run_context = get_run_context()
|
740
|
+
except MissingContextError:
|
741
|
+
result_store = ResultStore()
|
742
|
+
else:
|
743
|
+
result_store = run_context.result_store
|
744
|
+
return result_store
|
745
|
+
|
746
|
+
|
394
747
|
class ResultRecordMetadata(BaseModel):
|
395
748
|
"""
|
396
749
|
Metadata for a result record.
|
@@ -402,6 +755,7 @@ class ResultRecordMetadata(BaseModel):
|
|
402
755
|
expiration: Optional[DateTime] = Field(default=None)
|
403
756
|
serializer: Serializer = Field(default_factory=PickleSerializer)
|
404
757
|
prefect_version: str = Field(default=prefect.__version__)
|
758
|
+
storage_block_id: Optional[uuid.UUID] = Field(default=None)
|
405
759
|
|
406
760
|
def dump_bytes(self) -> bytes:
|
407
761
|
"""
|
@@ -566,7 +920,9 @@ class BaseResult(BaseModel, abc.ABC, Generic[R]):
|
|
566
920
|
type: str
|
567
921
|
|
568
922
|
def __init__(self, **data: Any) -> None:
|
569
|
-
type_string =
|
923
|
+
type_string = (
|
924
|
+
get_dispatch_key(self) if type(self) is not BaseResult else "__base__"
|
925
|
+
)
|
570
926
|
data.setdefault("type", type_string)
|
571
927
|
super().__init__(**data)
|
572
928
|
|
@@ -660,14 +1016,22 @@ class PersistedResult(BaseResult):
|
|
660
1016
|
|
661
1017
|
@sync_compatible
|
662
1018
|
@inject_client
|
663
|
-
async def get(
|
1019
|
+
async def get(
|
1020
|
+
self, ignore_cache: bool = False, client: "PrefectClient" = None
|
1021
|
+
) -> R:
|
664
1022
|
"""
|
665
1023
|
Retrieve the data and deserialize it into the original object.
|
666
1024
|
"""
|
667
|
-
if self.has_cached_object():
|
1025
|
+
if self.has_cached_object() and not ignore_cache:
|
668
1026
|
return self._cache
|
669
1027
|
|
670
|
-
|
1028
|
+
result_store_kwargs = {}
|
1029
|
+
if self._serializer:
|
1030
|
+
result_store_kwargs["serializer"] = resolve_serializer(self._serializer)
|
1031
|
+
storage_block = await self._get_storage_block(client=client)
|
1032
|
+
result_store = ResultStore(result_storage=storage_block, **result_store_kwargs)
|
1033
|
+
|
1034
|
+
record = await result_store.aread(self.storage_key)
|
671
1035
|
self.expiration = record.expiration
|
672
1036
|
|
673
1037
|
if self._should_cache_object:
|
@@ -675,13 +1039,6 @@ class PersistedResult(BaseResult):
|
|
675
1039
|
|
676
1040
|
return record.result
|
677
1041
|
|
678
|
-
@inject_client
|
679
|
-
async def _read_result_record(self, client: "PrefectClient") -> "ResultRecord":
|
680
|
-
block = await self._get_storage_block(client=client)
|
681
|
-
content = await block.read_path(self.storage_key)
|
682
|
-
record = ResultRecord.deserialize(content)
|
683
|
-
return record
|
684
|
-
|
685
1042
|
@staticmethod
|
686
1043
|
def _infer_path(storage_block, key) -> str:
|
687
1044
|
"""
|
@@ -721,15 +1078,11 @@ class PersistedResult(BaseResult):
|
|
721
1078
|
# this could error if the serializer requires kwargs
|
722
1079
|
serializer = Serializer(type=self.serializer_type)
|
723
1080
|
|
724
|
-
|
725
|
-
|
726
|
-
|
727
|
-
storage_key=self.storage_key,
|
728
|
-
expiration=self.expiration,
|
729
|
-
serializer=serializer,
|
730
|
-
),
|
1081
|
+
result_store = ResultStore(result_storage=storage_block, serializer=serializer)
|
1082
|
+
await result_store.awrite(
|
1083
|
+
obj=obj, key=self.storage_key, expiration=self.expiration
|
731
1084
|
)
|
732
|
-
|
1085
|
+
|
733
1086
|
self._persisted = True
|
734
1087
|
|
735
1088
|
if not self._should_cache_object:
|