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.
- prefect/__init__.py +0 -3
- prefect/_internal/compatibility/migration.py +1 -1
- prefect/artifacts.py +1 -1
- prefect/blocks/core.py +8 -5
- prefect/blocks/notifications.py +10 -10
- prefect/blocks/system.py +52 -16
- prefect/blocks/webhook.py +3 -1
- prefect/client/cloud.py +57 -7
- prefect/client/collections.py +1 -1
- prefect/client/orchestration.py +68 -7
- prefect/client/schemas/objects.py +40 -2
- prefect/concurrency/asyncio.py +8 -2
- prefect/concurrency/services.py +16 -6
- prefect/concurrency/sync.py +4 -1
- prefect/context.py +7 -9
- prefect/deployments/runner.py +3 -3
- prefect/exceptions.py +12 -0
- prefect/filesystems.py +5 -3
- prefect/flow_engine.py +16 -10
- prefect/flows.py +2 -4
- prefect/futures.py +2 -1
- prefect/locking/__init__.py +0 -0
- prefect/locking/memory.py +213 -0
- prefect/locking/protocol.py +122 -0
- prefect/logging/handlers.py +4 -1
- prefect/main.py +8 -6
- prefect/records/filesystem.py +4 -2
- prefect/records/result_store.py +12 -6
- prefect/results.py +768 -363
- prefect/settings.py +24 -10
- prefect/states.py +82 -27
- prefect/task_engine.py +51 -26
- prefect/task_worker.py +6 -4
- prefect/tasks.py +24 -6
- prefect/transactions.py +57 -36
- prefect/utilities/annotations.py +4 -3
- prefect/utilities/asyncutils.py +1 -1
- prefect/utilities/callables.py +1 -3
- prefect/utilities/dispatch.py +16 -11
- prefect/utilities/schema_tools/hydration.py +13 -0
- prefect/variables.py +34 -24
- prefect/workers/base.py +78 -18
- prefect/workers/process.py +1 -3
- {prefect_client-3.0.0rc19.dist-info → prefect_client-3.0.1.dist-info}/METADATA +2 -2
- {prefect_client-3.0.0rc19.dist-info → prefect_client-3.0.1.dist-info}/RECORD +48 -46
- prefect/manifests.py +0 -21
- {prefect_client-3.0.0rc19.dist-info → prefect_client-3.0.1.dist-info}/LICENSE +0 -0
- {prefect_client-3.0.0rc19.dist-info → prefect_client-3.0.1.dist-info}/WHEEL +0 -0
- {prefect_client-3.0.0rc19.dist-info → prefect_client-3.0.1.dist-info}/top_level.txt +0 -0
prefect/concurrency/services.py
CHANGED
@@ -36,13 +36,18 @@ class ConcurrencySlotAcquisitionService(QueueService):
|
|
36
36
|
async def _handle(
|
37
37
|
self,
|
38
38
|
item: Tuple[
|
39
|
-
int,
|
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
|
prefect/concurrency/sync.py
CHANGED
@@ -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:
|
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
|
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
|
-
|
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
|
-
|
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
|
)
|
prefect/deployments/runner.py
CHANGED
@@ -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/
|
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/
|
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/
|
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/
|
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/
|
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 =
|
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,
|
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
|
-
|
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
|
-
|
264
|
-
if
|
265
|
-
raise ValueError("Result
|
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
|
-
|
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
|
-
|
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
|
-
|
289
|
-
|
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
|
-
|
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[
|
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
|