prefect-client 3.0.0rc19__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.
Files changed (49) hide show
  1. prefect/__init__.py +0 -3
  2. prefect/_internal/compatibility/migration.py +1 -1
  3. prefect/artifacts.py +1 -1
  4. prefect/blocks/core.py +8 -5
  5. prefect/blocks/notifications.py +10 -10
  6. prefect/blocks/system.py +52 -16
  7. prefect/blocks/webhook.py +3 -1
  8. prefect/client/cloud.py +57 -7
  9. prefect/client/collections.py +1 -1
  10. prefect/client/orchestration.py +68 -7
  11. prefect/client/schemas/objects.py +40 -2
  12. prefect/concurrency/asyncio.py +8 -2
  13. prefect/concurrency/services.py +16 -6
  14. prefect/concurrency/sync.py +4 -1
  15. prefect/context.py +7 -9
  16. prefect/deployments/runner.py +3 -3
  17. prefect/exceptions.py +12 -0
  18. prefect/filesystems.py +5 -3
  19. prefect/flow_engine.py +16 -10
  20. prefect/flows.py +2 -4
  21. prefect/futures.py +2 -1
  22. prefect/locking/__init__.py +0 -0
  23. prefect/locking/memory.py +213 -0
  24. prefect/locking/protocol.py +122 -0
  25. prefect/logging/handlers.py +4 -1
  26. prefect/main.py +8 -6
  27. prefect/records/filesystem.py +4 -2
  28. prefect/records/result_store.py +12 -6
  29. prefect/results.py +768 -363
  30. prefect/settings.py +24 -10
  31. prefect/states.py +82 -27
  32. prefect/task_engine.py +51 -26
  33. prefect/task_worker.py +6 -4
  34. prefect/tasks.py +24 -6
  35. prefect/transactions.py +57 -36
  36. prefect/utilities/annotations.py +4 -3
  37. prefect/utilities/asyncutils.py +1 -1
  38. prefect/utilities/callables.py +1 -3
  39. prefect/utilities/dispatch.py +16 -11
  40. prefect/utilities/schema_tools/hydration.py +13 -0
  41. prefect/variables.py +34 -24
  42. prefect/workers/base.py +78 -18
  43. prefect/workers/process.py +1 -3
  44. {prefect_client-3.0.0rc19.dist-info → prefect_client-3.0.1.dist-info}/METADATA +2 -2
  45. {prefect_client-3.0.0rc19.dist-info → prefect_client-3.0.1.dist-info}/RECORD +48 -46
  46. prefect/manifests.py +0 -21
  47. {prefect_client-3.0.0rc19.dist-info → prefect_client-3.0.1.dist-info}/LICENSE +0 -0
  48. {prefect_client-3.0.0rc19.dist-info → prefect_client-3.0.1.dist-info}/WHEEL +0 -0
  49. {prefect_client-3.0.0rc19.dist-info → prefect_client-3.0.1.dist-info}/top_level.txt +0 -0
@@ -36,13 +36,18 @@ class ConcurrencySlotAcquisitionService(QueueService):
36
36
  async def _handle(
37
37
  self,
38
38
  item: Tuple[
39
- int, str, Optional[float], concurrent.futures.Future, Optional[bool]
39
+ int,
40
+ str,
41
+ Optional[float],
42
+ concurrent.futures.Future,
43
+ Optional[bool],
44
+ Optional[int],
40
45
  ],
41
46
  ) -> None:
42
- occupy, mode, timeout_seconds, future, create_if_missing = item
47
+ occupy, mode, timeout_seconds, future, create_if_missing, max_retries = item
43
48
  try:
44
49
  response = await self.acquire_slots(
45
- occupy, mode, timeout_seconds, create_if_missing
50
+ occupy, mode, timeout_seconds, create_if_missing, max_retries
46
51
  )
47
52
  except Exception as exc:
48
53
  # If the request to the increment endpoint fails in a non-standard
@@ -59,6 +64,7 @@ class ConcurrencySlotAcquisitionService(QueueService):
59
64
  mode: str,
60
65
  timeout_seconds: Optional[float] = None,
61
66
  create_if_missing: Optional[bool] = False,
67
+ max_retries: Optional[int] = None,
62
68
  ) -> httpx.Response:
63
69
  with timeout_async(seconds=timeout_seconds):
64
70
  while True:
@@ -74,15 +80,19 @@ class ConcurrencySlotAcquisitionService(QueueService):
74
80
  isinstance(exc, httpx.HTTPStatusError)
75
81
  and exc.response.status_code == status.HTTP_423_LOCKED
76
82
  ):
83
+ if max_retries is not None and max_retries <= 0:
84
+ raise exc
77
85
  retry_after = float(exc.response.headers["Retry-After"])
78
86
  await asyncio.sleep(retry_after)
87
+ if max_retries is not None:
88
+ max_retries -= 1
79
89
  else:
80
90
  raise exc
81
91
  else:
82
92
  return response
83
93
 
84
94
  def send(
85
- self, item: Tuple[int, str, Optional[float], Optional[bool]]
95
+ self, item: Tuple[int, str, Optional[float], Optional[bool], Optional[int]]
86
96
  ) -> concurrent.futures.Future:
87
97
  with self._lock:
88
98
  if self._stopped:
@@ -91,9 +101,9 @@ class ConcurrencySlotAcquisitionService(QueueService):
91
101
  logger.debug("Service %r enqueuing item %r", self, item)
92
102
  future: concurrent.futures.Future = concurrent.futures.Future()
93
103
 
94
- occupy, mode, timeout_seconds, create_if_missing = item
104
+ occupy, mode, timeout_seconds, create_if_missing, max_retries = item
95
105
  self._queue.put_nowait(
96
- (occupy, mode, timeout_seconds, future, create_if_missing)
106
+ (occupy, mode, timeout_seconds, future, create_if_missing, max_retries)
97
107
  )
98
108
 
99
109
  return future
@@ -40,7 +40,8 @@ def concurrency(
40
40
  names: Union[str, List[str]],
41
41
  occupy: int = 1,
42
42
  timeout_seconds: Optional[float] = None,
43
- create_if_missing: Optional[bool] = True,
43
+ create_if_missing: bool = True,
44
+ max_retries: Optional[int] = None,
44
45
  ) -> Generator[None, None, None]:
45
46
  """A context manager that acquires and releases concurrency slots from the
46
47
  given concurrency limits.
@@ -51,6 +52,7 @@ def concurrency(
51
52
  timeout_seconds: The number of seconds to wait for the slots to be acquired before
52
53
  raising a `TimeoutError`. A timeout of `None` will wait indefinitely.
53
54
  create_if_missing: Whether to create the concurrency limits if they do not exist.
55
+ max_retries: The maximum number of retries to acquire the concurrency slots.
54
56
 
55
57
  Raises:
56
58
  TimeoutError: If the slots are not acquired within the given timeout.
@@ -80,6 +82,7 @@ def concurrency(
80
82
  occupy,
81
83
  timeout_seconds=timeout_seconds,
82
84
  create_if_missing=create_if_missing,
85
+ max_retries=max_retries,
83
86
  )
84
87
  acquisition_time = pendulum.now("UTC")
85
88
  emitted_events = _emit_concurrency_acquisition_events(limits, occupy)
prefect/context.py CHANGED
@@ -40,11 +40,10 @@ from prefect.client.orchestration import PrefectClient, SyncPrefectClient, get_c
40
40
  from prefect.client.schemas import FlowRun, TaskRun
41
41
  from prefect.events.worker import EventsWorker
42
42
  from prefect.exceptions import MissingContextError
43
- from prefect.results import ResultFactory
43
+ from prefect.results import ResultStore
44
44
  from prefect.settings import PREFECT_HOME, Profile, Settings
45
45
  from prefect.states import State
46
46
  from prefect.task_runners import TaskRunner
47
- from prefect.utilities.asyncutils import run_coro_as_sync
48
47
  from prefect.utilities.services import start_client_metrics_server
49
48
 
50
49
  T = TypeVar("T")
@@ -95,20 +94,15 @@ def hydrated_context(
95
94
  flow_run_context = FlowRunContext(
96
95
  **flow_run_context,
97
96
  client=client,
98
- result_factory=run_coro_as_sync(ResultFactory.from_flow(flow)),
99
97
  task_runner=task_runner,
100
98
  detached=True,
101
99
  )
102
100
  stack.enter_context(flow_run_context)
103
101
  # Set up parent task run context
104
102
  if parent_task_run_context := serialized_context.get("task_run_context"):
105
- parent_task = parent_task_run_context["task"]
106
103
  task_run_context = TaskRunContext(
107
104
  **parent_task_run_context,
108
105
  client=client,
109
- result_factory=run_coro_as_sync(
110
- ResultFactory.from_autonomous_task(parent_task)
111
- ),
112
106
  )
113
107
  stack.enter_context(task_run_context)
114
108
  # Set up tags context
@@ -216,6 +210,7 @@ class SyncClientContext(ContextModel):
216
210
  self._context_stack += 1
217
211
  if self._context_stack == 1:
218
212
  self.client.__enter__()
213
+ self.client.raise_for_api_version_mismatch()
219
214
  return super().__enter__()
220
215
  else:
221
216
  return self
@@ -273,6 +268,7 @@ class AsyncClientContext(ContextModel):
273
268
  self._context_stack += 1
274
269
  if self._context_stack == 1:
275
270
  await self.client.__aenter__()
271
+ await self.client.raise_for_api_version_mismatch()
276
272
  return super().__enter__()
277
273
  else:
278
274
  return self
@@ -346,7 +342,7 @@ class EngineContext(RunContext):
346
342
  detached: bool = False
347
343
 
348
344
  # Result handling
349
- result_factory: ResultFactory
345
+ result_store: ResultStore
350
346
 
351
347
  # Counter for task calls allowing unique
352
348
  task_run_dynamic_keys: Dict[str, int] = Field(default_factory=dict)
@@ -375,6 +371,7 @@ class EngineContext(RunContext):
375
371
  "log_prints",
376
372
  "start_time",
377
373
  "input_keyset",
374
+ "result_store",
378
375
  },
379
376
  exclude_unset=True,
380
377
  )
@@ -399,7 +396,7 @@ class TaskRunContext(RunContext):
399
396
  parameters: Dict[str, Any]
400
397
 
401
398
  # Result handling
402
- result_factory: ResultFactory
399
+ result_store: ResultStore
403
400
 
404
401
  __var__ = ContextVar("task_run")
405
402
 
@@ -412,6 +409,7 @@ class TaskRunContext(RunContext):
412
409
  "log_prints",
413
410
  "start_time",
414
411
  "input_keyset",
412
+ "result_store",
415
413
  },
416
414
  exclude_unset=True,
417
415
  )
@@ -508,7 +508,7 @@ class RunnerDeployment(BaseModel):
508
508
  no_file_location_error = (
509
509
  "Flows defined interactively cannot be deployed. Check out the"
510
510
  " quickstart guide for help getting started:"
511
- " https://docs.prefect.io/latest/getting-started/quickstart"
511
+ " https://docs.prefect.io/latest/get-started/quickstart"
512
512
  )
513
513
  ## first see if an entrypoint can be determined
514
514
  flow_file = getattr(flow, "__globals__", {}).get("__file__")
@@ -851,14 +851,14 @@ async def deploy(
851
851
  " or specify a remote storage location for the flow with `.from_source`."
852
852
  " If you are attempting to deploy a flow to a local process work pool,"
853
853
  " consider using `flow.serve` instead. See the documentation for more"
854
- " information: https://docs.prefect.io/latest/concepts/flows/#serving-a-flow"
854
+ " information: https://docs.prefect.io/latest/deploy/run-flows-in-local-processes"
855
855
  )
856
856
  elif work_pool.type == "process" and not ignore_warnings:
857
857
  console.print(
858
858
  "Looks like you're deploying to a process work pool. If you're creating a"
859
859
  " deployment for local development, calling `.serve` on your flow is a great"
860
860
  " way to get started. See the documentation for more information:"
861
- " https://docs.prefect.io/latest/concepts/flows/#serving-a-flow. "
861
+ " https://docs.prefect.io/latest/deploy/run-flows-in-local-processes "
862
862
  " Set `ignore_warnings=True` to suppress this message.",
863
863
  style="yellow",
864
864
  )
prefect/exceptions.py CHANGED
@@ -412,3 +412,15 @@ class PrefectImportError(ImportError):
412
412
 
413
413
  def __init__(self, message: str) -> None:
414
414
  super().__init__(message)
415
+
416
+
417
+ class SerializationError(PrefectException):
418
+ """
419
+ Raised when an object cannot be serialized.
420
+ """
421
+
422
+
423
+ class ConfigurationError(PrefectException):
424
+ """
425
+ Raised when a configuration is invalid.
426
+ """
prefect/filesystems.py CHANGED
@@ -84,7 +84,7 @@ class LocalFileSystem(WritableFileSystem, WritableDeploymentStorage):
84
84
  _block_type_name = "Local File System"
85
85
  _logo_url = "https://cdn.sanity.io/images/3ugk85nk/production/ad39089fa66d273b943394a68f003f7a19aa850e-48x48.png"
86
86
  _documentation_url = (
87
- "https://docs.prefect.io/concepts/filesystems/#local-filesystem"
87
+ "https://docs.prefect.io/latest/develop/results#specifying-a-default-filesystem"
88
88
  )
89
89
 
90
90
  basepath: Optional[str] = Field(
@@ -260,7 +260,7 @@ class RemoteFileSystem(WritableFileSystem, WritableDeploymentStorage):
260
260
  _block_type_name = "Remote File System"
261
261
  _logo_url = "https://cdn.sanity.io/images/3ugk85nk/production/e86b41bc0f9c99ba9489abeee83433b43d5c9365-48x48.png"
262
262
  _documentation_url = (
263
- "https://docs.prefect.io/concepts/filesystems/#remote-file-system"
263
+ "https://docs.prefect.io/latest/develop/results#specifying-a-default-filesystem"
264
264
  )
265
265
 
266
266
  basepath: str = Field(
@@ -433,7 +433,9 @@ class SMB(WritableFileSystem, WritableDeploymentStorage):
433
433
 
434
434
  _block_type_name = "SMB"
435
435
  _logo_url = "https://cdn.sanity.io/images/3ugk85nk/production/3f624663f7beb97d011d011bffd51ecf6c499efc-195x195.png"
436
- _documentation_url = "https://docs.prefect.io/concepts/filesystems/#smb"
436
+ _documentation_url = (
437
+ "https://docs.prefect.io/latest/develop/results#specifying-a-default-filesystem"
438
+ )
437
439
 
438
440
  share_path: str = Field(
439
441
  default=...,
prefect/flow_engine.py CHANGED
@@ -47,7 +47,7 @@ from prefect.logging.loggers import (
47
47
  get_run_logger,
48
48
  patch_print,
49
49
  )
50
- from prefect.results import BaseResult, ResultFactory
50
+ from prefect.results import BaseResult, ResultStore, get_current_result_store
51
51
  from prefect.settings import PREFECT_DEBUG_MODE
52
52
  from prefect.states import (
53
53
  Failed,
@@ -202,9 +202,12 @@ class FlowRunEngine(Generic[P, R]):
202
202
  self.handle_exception(
203
203
  exc,
204
204
  msg=message,
205
- result_factory=run_coro_as_sync(ResultFactory.from_flow(self.flow)),
205
+ result_store=get_current_result_store().update_for_flow(
206
+ self.flow, _sync=True
207
+ ),
206
208
  )
207
209
  self.short_circuit = True
210
+ self.call_hooks()
208
211
 
209
212
  new_state = Running()
210
213
  state = self.set_state(new_state)
@@ -260,14 +263,15 @@ class FlowRunEngine(Generic[P, R]):
260
263
  return _result
261
264
 
262
265
  def handle_success(self, result: R) -> R:
263
- result_factory = getattr(FlowRunContext.get(), "result_factory", None)
264
- if result_factory is None:
265
- raise ValueError("Result factory is not set")
266
+ result_store = getattr(FlowRunContext.get(), "result_store", None)
267
+ if result_store is None:
268
+ raise ValueError("Result store is not set")
266
269
  resolved_result = resolve_futures_to_states(result)
267
270
  terminal_state = run_coro_as_sync(
268
271
  return_value_to_state(
269
272
  resolved_result,
270
- result_factory=result_factory,
273
+ result_store=result_store,
274
+ write_result=True,
271
275
  )
272
276
  )
273
277
  self.set_state(terminal_state)
@@ -278,15 +282,15 @@ class FlowRunEngine(Generic[P, R]):
278
282
  self,
279
283
  exc: Exception,
280
284
  msg: Optional[str] = None,
281
- result_factory: Optional[ResultFactory] = None,
285
+ result_store: Optional[ResultStore] = None,
282
286
  ) -> State:
283
287
  context = FlowRunContext.get()
284
288
  terminal_state = run_coro_as_sync(
285
289
  exception_to_failed_state(
286
290
  exc,
287
291
  message=msg or "Flow run encountered an exception:",
288
- result_factory=result_factory
289
- or getattr(context, "result_factory", None),
292
+ result_store=result_store or getattr(context, "result_store", None),
293
+ write_result=True,
290
294
  )
291
295
  )
292
296
  state = self.set_state(terminal_state)
@@ -503,7 +507,9 @@ class FlowRunEngine(Generic[P, R]):
503
507
  flow_run=self.flow_run,
504
508
  parameters=self.parameters,
505
509
  client=client,
506
- result_factory=run_coro_as_sync(ResultFactory.from_flow(self.flow)),
510
+ result_store=get_current_result_store().update_for_flow(
511
+ self.flow, _sync=True
512
+ ),
507
513
  task_runner=task_runner,
508
514
  )
509
515
  )
prefect/flows.py CHANGED
@@ -642,7 +642,7 @@ class Flow(Generic[P, R]):
642
642
  cron: Optional[Union[Iterable[str], str]] = None,
643
643
  rrule: Optional[Union[Iterable[str], str]] = None,
644
644
  paused: Optional[bool] = None,
645
- schedules: Optional[List["FlexibleScheduleList"]] = None,
645
+ schedules: Optional["FlexibleScheduleList"] = None,
646
646
  concurrency_limit: Optional[int] = None,
647
647
  parameters: Optional[dict] = None,
648
648
  triggers: Optional[List[Union[DeploymentTriggerTypes, TriggerTypes]]] = None,
@@ -730,7 +730,7 @@ class Flow(Generic[P, R]):
730
730
  work_pool_name=work_pool_name,
731
731
  work_queue_name=work_queue_name,
732
732
  job_variables=job_variables,
733
- )
733
+ ) # type: ignore # TODO: remove sync_compatible
734
734
  else:
735
735
  return RunnerDeployment.from_flow(
736
736
  self,
@@ -1032,8 +1032,6 @@ class Flow(Generic[P, R]):
1032
1032
  if not isinstance(storage, LocalStorage):
1033
1033
  storage.set_base_path(Path(tmpdir))
1034
1034
  await storage.pull_code()
1035
- storage.set_base_path(Path(tmpdir))
1036
- await storage.pull_code()
1037
1035
 
1038
1036
  full_entrypoint = str(storage.destination / entrypoint)
1039
1037
  flow: Flow = await from_async.wait_for_call_in_new_thread(
prefect/futures.py CHANGED
@@ -179,7 +179,8 @@ class PrefectConcurrentFuture(PrefectWrappedFuture[R, concurrent.futures.Future]
179
179
  local_logger = logger
180
180
  local_logger.warning(
181
181
  "A future was garbage collected before it resolved."
182
- " Please call `.wait()` or `.result()` on futures to ensure they resolve.",
182
+ " Please call `.wait()` or `.result()` on futures to ensure they resolve."
183
+ "\nSee https://docs.prefect.io/latest/develop/task-runners for more details.",
183
184
  )
184
185
 
185
186
 
File without changes
@@ -0,0 +1,213 @@
1
+ import asyncio
2
+ import threading
3
+ from typing import Dict, Optional, TypedDict
4
+
5
+ from .protocol import LockManager
6
+
7
+
8
+ class _LockInfo(TypedDict):
9
+ """
10
+ A dictionary containing information about a lock.
11
+
12
+ Attributes:
13
+ holder: The holder of the lock.
14
+ lock: The lock object.
15
+ expiration_timer: The timer for the lock expiration
16
+ """
17
+
18
+ holder: str
19
+ lock: threading.Lock
20
+ expiration_timer: Optional[threading.Timer]
21
+
22
+
23
+ class MemoryLockManager(LockManager):
24
+ """
25
+ A lock manager that stores lock information in memory.
26
+
27
+ Note: because this lock manager stores data in memory, it is not suitable for
28
+ use in a distributed environment or across different processes.
29
+ """
30
+
31
+ _instance = None
32
+
33
+ def __new__(cls, *args, **kwargs):
34
+ if cls._instance is None:
35
+ cls._instance = super().__new__(cls)
36
+ return cls._instance
37
+
38
+ def __init__(self):
39
+ self._locks_dict_lock = threading.Lock()
40
+ self._locks: Dict[str, _LockInfo] = {}
41
+
42
+ def _expire_lock(self, key: str):
43
+ """
44
+ Expire the lock for the given key.
45
+
46
+ Used as a callback for the expiration timer of a lock.
47
+
48
+ Args:
49
+ key: The key of the lock to expire.
50
+ """
51
+ with self._locks_dict_lock:
52
+ if key in self._locks:
53
+ lock_info = self._locks[key]
54
+ if lock_info["lock"].locked():
55
+ lock_info["lock"].release()
56
+ if lock_info["expiration_timer"]:
57
+ lock_info["expiration_timer"].cancel()
58
+ del self._locks[key]
59
+
60
+ def acquire_lock(
61
+ self,
62
+ key: str,
63
+ holder: str,
64
+ acquire_timeout: Optional[float] = None,
65
+ hold_timeout: Optional[float] = None,
66
+ ) -> bool:
67
+ with self._locks_dict_lock:
68
+ if key not in self._locks:
69
+ lock = threading.Lock()
70
+ lock.acquire()
71
+ expiration_timer = None
72
+ if hold_timeout is not None:
73
+ expiration_timer = threading.Timer(
74
+ hold_timeout, self._expire_lock, args=(key,)
75
+ )
76
+ expiration_timer.start()
77
+ self._locks[key] = _LockInfo(
78
+ holder=holder, lock=lock, expiration_timer=expiration_timer
79
+ )
80
+ return True
81
+ elif self._locks[key]["holder"] == holder:
82
+ return True
83
+ else:
84
+ existing_lock_info = self._locks[key]
85
+
86
+ if acquire_timeout is not None:
87
+ existing_lock_acquired = existing_lock_info["lock"].acquire(
88
+ timeout=acquire_timeout
89
+ )
90
+ else:
91
+ existing_lock_acquired = existing_lock_info["lock"].acquire()
92
+
93
+ if existing_lock_acquired:
94
+ with self._locks_dict_lock:
95
+ if (
96
+ expiration_timer := existing_lock_info["expiration_timer"]
97
+ ) is not None:
98
+ expiration_timer.cancel()
99
+ expiration_timer = None
100
+ if hold_timeout is not None:
101
+ expiration_timer = threading.Timer(
102
+ hold_timeout, self._expire_lock, args=(key,)
103
+ )
104
+ expiration_timer.start()
105
+ self._locks[key] = _LockInfo(
106
+ holder=holder,
107
+ lock=existing_lock_info["lock"],
108
+ expiration_timer=expiration_timer,
109
+ )
110
+ return True
111
+ return False
112
+
113
+ async def aacquire_lock(
114
+ self,
115
+ key: str,
116
+ holder: str,
117
+ acquire_timeout: Optional[float] = None,
118
+ hold_timeout: Optional[float] = None,
119
+ ) -> bool:
120
+ with self._locks_dict_lock:
121
+ if key not in self._locks:
122
+ lock = threading.Lock()
123
+ await asyncio.to_thread(lock.acquire)
124
+ expiration_timer = None
125
+ if hold_timeout is not None:
126
+ expiration_timer = threading.Timer(
127
+ hold_timeout, self._expire_lock, args=(key,)
128
+ )
129
+ expiration_timer.start()
130
+ self._locks[key] = _LockInfo(
131
+ holder=holder, lock=lock, expiration_timer=expiration_timer
132
+ )
133
+ return True
134
+ elif self._locks[key]["holder"] == holder:
135
+ return True
136
+ else:
137
+ existing_lock_info = self._locks[key]
138
+
139
+ if acquire_timeout is not None:
140
+ existing_lock_acquired = await asyncio.to_thread(
141
+ existing_lock_info["lock"].acquire, timeout=acquire_timeout
142
+ )
143
+ else:
144
+ existing_lock_acquired = await asyncio.to_thread(
145
+ existing_lock_info["lock"].acquire
146
+ )
147
+
148
+ if existing_lock_acquired:
149
+ with self._locks_dict_lock:
150
+ if (
151
+ expiration_timer := existing_lock_info["expiration_timer"]
152
+ ) is not None:
153
+ expiration_timer.cancel()
154
+ expiration_timer = None
155
+ if hold_timeout is not None:
156
+ expiration_timer = threading.Timer(
157
+ hold_timeout, self._expire_lock, args=(key,)
158
+ )
159
+ expiration_timer.start()
160
+ self._locks[key] = _LockInfo(
161
+ holder=holder,
162
+ lock=existing_lock_info["lock"],
163
+ expiration_timer=expiration_timer,
164
+ )
165
+ return True
166
+ return False
167
+
168
+ def release_lock(self, key: str, holder: str) -> None:
169
+ with self._locks_dict_lock:
170
+ if key in self._locks and self._locks[key]["holder"] == holder:
171
+ if (
172
+ expiration_timer := self._locks[key]["expiration_timer"]
173
+ ) is not None:
174
+ expiration_timer.cancel()
175
+ self._locks[key]["lock"].release()
176
+ del self._locks[key]
177
+ else:
178
+ raise ValueError(
179
+ f"No lock held by {holder} for transaction with key {key}"
180
+ )
181
+
182
+ def is_locked(self, key: str) -> bool:
183
+ return key in self._locks and self._locks[key]["lock"].locked()
184
+
185
+ def is_lock_holder(self, key: str, holder: str) -> bool:
186
+ lock_info = self._locks.get(key)
187
+ return (
188
+ lock_info is not None
189
+ and lock_info["lock"].locked()
190
+ and lock_info["holder"] == holder
191
+ )
192
+
193
+ def wait_for_lock(self, key: str, timeout: Optional[float] = None) -> bool:
194
+ if lock := self._locks.get(key, {}).get("lock"):
195
+ if timeout is not None:
196
+ lock_acquired = lock.acquire(timeout=timeout)
197
+ else:
198
+ lock_acquired = lock.acquire()
199
+ if lock_acquired:
200
+ lock.release()
201
+ return lock_acquired
202
+ return True
203
+
204
+ async def await_for_lock(self, key: str, timeout: Optional[float] = None) -> bool:
205
+ if lock := self._locks.get(key, {}).get("lock"):
206
+ if timeout is not None:
207
+ lock_acquired = await asyncio.to_thread(lock.acquire, timeout=timeout)
208
+ else:
209
+ lock_acquired = await asyncio.to_thread(lock.acquire)
210
+ if lock_acquired:
211
+ lock.release()
212
+ return lock_acquired
213
+ return True