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/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 SerializationError
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() -> ResultStorage:
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 Block.load(default_block)
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() -> ResultSerializer:
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 ResultFactory(BaseModel):
181
+ class ResultStore(BaseModel):
126
182
  """
127
- A utility to generate `Result` types.
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
- persist_result: bool
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
- @classmethod
138
- @inject_client
139
- async def default_factory(cls, client: "PrefectClient" = None, **kwargs):
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 factory with default options.
217
+ Create a new result store for a flow with updated settings.
142
218
 
143
- Keyword arguments may be provided to override defaults. Null keys will be
144
- ignored.
219
+ Args:
220
+ flow: The flow to update the result store for.
221
+
222
+ Returns:
223
+ An updated result store.
145
224
  """
146
- # Remove any null keys so `setdefault` can do its magic
147
- for key, value in tuple(kwargs.items()):
148
- if value is None:
149
- kwargs.pop(key)
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
- # Apply defaults
152
- kwargs.setdefault("result_storage", await get_default_result_storage())
153
- kwargs.setdefault("result_serializer", get_default_result_serializer())
154
- kwargs.setdefault("persist_result", get_default_persist_setting())
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
- return await cls.from_settings(**kwargs, client=client)
243
+ Args:
244
+ task: The task to update the result store for.
159
245
 
160
- @classmethod
161
- @inject_client
162
- async def from_flow(
163
- cls: Type[Self], flow: "Flow", client: "PrefectClient" = None
164
- ) -> Self:
165
- """
166
- Create a new result factory for a flow.
167
- """
168
- from prefect.context import FlowRunContext
169
-
170
- ctx = FlowRunContext.get()
171
- if ctx:
172
- # This is a child flow run
173
- return await cls.from_settings(
174
- result_storage=flow.result_storage or ctx.result_factory.storage_block,
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
- # This is a root flow run
184
- # Pass the flow settings up to the default which will replace nulls with
185
- # our default options
186
- return await cls.default_factory(
187
- client=client,
188
- result_storage=flow.result_storage,
189
- result_serializer=flow.result_serializer,
190
- persist_result=flow.persist_result,
191
- cache_result_in_memory=flow.cache_result_in_memory,
192
- storage_key_fn=DEFAULT_STORAGE_KEY_FN,
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
- @classmethod
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
- Create a new result factory for a task.
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
- return await cls._from_task(task, get_default_result_storage, client=client)
375
+ holder = holder or self.generate_default_holder()
376
+ return self._read(key=key, holder=holder, _sync=True)
204
377
 
205
- @classmethod
206
- @inject_client
207
- async def from_autonomous_task(
208
- cls: Type[Self], task: "Task[P, R]", client: "PrefectClient" = None
209
- ) -> Self:
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 new result factory for an autonomous task.
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
- return await cls._from_task(
214
- task, get_or_create_default_task_scheduling_storage, client=client
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
- @classmethod
218
- @inject_client
219
- async def _from_task(
220
- cls: Type[Self],
221
- task: "Task",
222
- default_storage_getter: Callable[[], Awaitable[ResultStorage]],
223
- client: "PrefectClient" = None,
224
- ) -> Self:
225
- from prefect.context import FlowRunContext
226
-
227
- ctx = FlowRunContext.get()
228
-
229
- result_storage = task.result_storage or (
230
- ctx.result_factory.storage_block
231
- if ctx and ctx.result_factory
232
- else await default_storage_getter()
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
- result_serializer = task.result_serializer or (
235
- ctx.result_factory.serializer
236
- if ctx and ctx.result_factory
237
- else get_default_result_serializer()
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
- if task.persist_result is None:
240
- persist_result = (
241
- ctx.result_factory.persist_result
242
- if ctx and ctx.result_factory
243
- else get_default_persist_setting()
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
- persist_result = task.persist_result
247
-
248
- cache_result_in_memory = task.cache_result_in_memory
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
- @classmethod
264
- @inject_client
265
- async def from_settings(
266
- cls: Type[Self],
267
- result_storage: ResultStorage,
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
- storage_block_id, storage_block = await cls.resolve_storage_block(
278
- result_storage, client=client, persist_result=persist_result
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
- return cls(
283
- storage_block=storage_block,
284
- storage_block_id=storage_block_id,
285
- serializer=serializer,
286
- persist_result=persist_result,
287
- cache_result_in_memory=cache_result_in_memory,
288
- storage_key_fn=storage_key_fn,
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
- @staticmethod
292
- async def resolve_storage_block(
293
- result_storage: ResultStorage,
294
- client: "PrefectClient",
295
- persist_result: bool = True,
296
- ) -> Tuple[Optional[uuid.UUID], WritableFileSystem]:
297
- """
298
- Resolve one of the valid `ResultStorage` input types into a saved block
299
- document id and an instance of the block.
300
- """
301
- if isinstance(result_storage, Block):
302
- storage_block = result_storage
303
-
304
- if storage_block._block_document_id is not None:
305
- # Avoid saving the block if it already has an identifier assigned
306
- storage_block_id = storage_block._block_document_id
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 TypeError(
315
- "Result storage must be one of the following types: 'UUID', 'Block', "
316
- f"'str'. Got unsupported type {type(result_storage).__name__!r}."
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
- return storage_block_id, storage_block
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
- @staticmethod
322
- def resolve_serializer(serializer: ResultSerializer) -> Serializer:
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
- Resolve one of the valid `ResultSerializer` input types into a serializer
325
- instance.
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
- if isinstance(serializer, Serializer):
328
- return serializer
329
- elif isinstance(serializer, str):
330
- return Serializer(type=serializer)
331
- else:
332
- raise TypeError(
333
- "Result serializer must be one of the following types: 'Serializer', "
334
- f"'str'. Got unsupported type {type(serializer).__name__!r}."
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 result type for the given object.
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.storage_block,
364
- storage_block_id=self.storage_block_id,
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.storage_block.write_path(
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.storage_block.read_path(f"parameters/{identifier}")
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 = get_dispatch_key(self) if type(self) != BaseResult else "__base__"
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(self, client: "PrefectClient") -> R:
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
- record = await self._read_result_record(client=client)
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
- record = ResultRecord(
725
- result=obj,
726
- metadata=ResultRecordMetadata(
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
- await storage_block.write_path(self.storage_key, content=record.serialize())
1085
+
733
1086
  self._persisted = True
734
1087
 
735
1088
  if not self._should_cache_object: