prefect-client 3.1.10__py3-none-any.whl → 3.1.12__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/_experimental/lineage.py +7 -8
- prefect/_experimental/sla/__init__.py +0 -0
- prefect/_experimental/sla/client.py +66 -0
- prefect/_experimental/sla/objects.py +53 -0
- prefect/_internal/_logging.py +15 -3
- prefect/_internal/compatibility/async_dispatch.py +22 -16
- prefect/_internal/compatibility/deprecated.py +42 -18
- prefect/_internal/compatibility/migration.py +2 -2
- prefect/_internal/concurrency/inspection.py +12 -14
- prefect/_internal/concurrency/primitives.py +2 -2
- prefect/_internal/concurrency/services.py +154 -80
- prefect/_internal/concurrency/waiters.py +13 -9
- prefect/_internal/pydantic/annotations/pendulum.py +7 -7
- prefect/_internal/pytz.py +4 -3
- prefect/_internal/retries.py +10 -5
- prefect/_internal/schemas/bases.py +19 -10
- prefect/_internal/schemas/validators.py +227 -388
- prefect/_version.py +3 -3
- prefect/automations.py +236 -30
- prefect/blocks/__init__.py +3 -3
- prefect/blocks/abstract.py +53 -30
- prefect/blocks/core.py +183 -84
- prefect/blocks/notifications.py +133 -73
- prefect/blocks/redis.py +13 -9
- prefect/blocks/system.py +24 -11
- prefect/blocks/webhook.py +7 -5
- prefect/cache_policies.py +3 -2
- prefect/client/orchestration/__init__.py +1957 -0
- prefect/client/orchestration/_artifacts/__init__.py +0 -0
- prefect/client/orchestration/_artifacts/client.py +239 -0
- prefect/client/orchestration/_automations/__init__.py +0 -0
- prefect/client/orchestration/_automations/client.py +329 -0
- prefect/client/orchestration/_blocks_documents/__init__.py +0 -0
- prefect/client/orchestration/_blocks_documents/client.py +334 -0
- prefect/client/orchestration/_blocks_schemas/__init__.py +0 -0
- prefect/client/orchestration/_blocks_schemas/client.py +200 -0
- prefect/client/orchestration/_blocks_types/__init__.py +0 -0
- prefect/client/orchestration/_blocks_types/client.py +380 -0
- prefect/client/orchestration/_concurrency_limits/__init__.py +0 -0
- prefect/client/orchestration/_concurrency_limits/client.py +762 -0
- prefect/client/orchestration/_deployments/__init__.py +0 -0
- prefect/client/orchestration/_deployments/client.py +1128 -0
- prefect/client/orchestration/_flow_runs/__init__.py +0 -0
- prefect/client/orchestration/_flow_runs/client.py +903 -0
- prefect/client/orchestration/_flows/__init__.py +0 -0
- prefect/client/orchestration/_flows/client.py +343 -0
- prefect/client/orchestration/_logs/__init__.py +0 -0
- prefect/client/orchestration/_logs/client.py +97 -0
- prefect/client/orchestration/_variables/__init__.py +0 -0
- prefect/client/orchestration/_variables/client.py +157 -0
- prefect/client/orchestration/base.py +46 -0
- prefect/client/orchestration/routes.py +145 -0
- prefect/client/schemas/__init__.py +68 -28
- prefect/client/schemas/actions.py +2 -2
- prefect/client/schemas/filters.py +5 -0
- prefect/client/schemas/objects.py +8 -15
- prefect/client/schemas/schedules.py +22 -10
- prefect/concurrency/_asyncio.py +87 -0
- prefect/concurrency/{events.py → _events.py} +10 -10
- prefect/concurrency/asyncio.py +20 -104
- prefect/concurrency/context.py +6 -4
- prefect/concurrency/services.py +26 -74
- prefect/concurrency/sync.py +23 -44
- prefect/concurrency/v1/_asyncio.py +63 -0
- prefect/concurrency/v1/{events.py → _events.py} +13 -15
- prefect/concurrency/v1/asyncio.py +27 -80
- prefect/concurrency/v1/context.py +6 -4
- prefect/concurrency/v1/services.py +33 -79
- prefect/concurrency/v1/sync.py +18 -37
- prefect/context.py +66 -45
- prefect/deployments/base.py +10 -144
- prefect/deployments/flow_runs.py +12 -2
- prefect/deployments/runner.py +53 -4
- prefect/deployments/steps/pull.py +13 -0
- prefect/engine.py +17 -4
- prefect/events/clients.py +7 -1
- prefect/events/schemas/events.py +3 -2
- prefect/filesystems.py +6 -2
- prefect/flow_engine.py +101 -85
- prefect/flows.py +10 -1
- prefect/input/run_input.py +2 -1
- prefect/logging/logging.yml +1 -1
- prefect/main.py +1 -3
- prefect/results.py +2 -307
- prefect/runner/runner.py +4 -2
- prefect/runner/storage.py +87 -21
- prefect/serializers.py +32 -25
- prefect/settings/legacy.py +4 -4
- prefect/settings/models/api.py +3 -3
- prefect/settings/models/cli.py +3 -3
- prefect/settings/models/client.py +5 -3
- prefect/settings/models/cloud.py +8 -3
- prefect/settings/models/deployments.py +3 -3
- prefect/settings/models/experiments.py +4 -7
- prefect/settings/models/flows.py +3 -3
- prefect/settings/models/internal.py +4 -2
- prefect/settings/models/logging.py +4 -3
- prefect/settings/models/results.py +3 -3
- prefect/settings/models/root.py +3 -2
- prefect/settings/models/runner.py +4 -4
- prefect/settings/models/server/api.py +3 -3
- prefect/settings/models/server/database.py +11 -4
- prefect/settings/models/server/deployments.py +6 -2
- prefect/settings/models/server/ephemeral.py +4 -2
- prefect/settings/models/server/events.py +3 -2
- prefect/settings/models/server/flow_run_graph.py +6 -2
- prefect/settings/models/server/root.py +3 -3
- prefect/settings/models/server/services.py +26 -11
- prefect/settings/models/server/tasks.py +6 -3
- prefect/settings/models/server/ui.py +3 -3
- prefect/settings/models/tasks.py +5 -5
- prefect/settings/models/testing.py +3 -3
- prefect/settings/models/worker.py +5 -3
- prefect/settings/profiles.py +15 -2
- prefect/states.py +61 -45
- prefect/task_engine.py +54 -75
- prefect/task_runners.py +56 -55
- prefect/task_worker.py +2 -2
- prefect/tasks.py +90 -36
- prefect/telemetry/bootstrap.py +10 -9
- prefect/telemetry/run_telemetry.py +13 -8
- prefect/telemetry/services.py +4 -0
- prefect/transactions.py +4 -15
- prefect/utilities/_git.py +34 -0
- prefect/utilities/asyncutils.py +1 -1
- prefect/utilities/engine.py +3 -19
- prefect/utilities/generics.py +18 -0
- prefect/utilities/templating.py +25 -1
- prefect/workers/base.py +6 -3
- prefect/workers/process.py +1 -1
- {prefect_client-3.1.10.dist-info → prefect_client-3.1.12.dist-info}/METADATA +2 -2
- {prefect_client-3.1.10.dist-info → prefect_client-3.1.12.dist-info}/RECORD +135 -109
- prefect/client/orchestration.py +0 -4523
- prefect/records/__init__.py +0 -1
- prefect/records/base.py +0 -235
- prefect/records/filesystem.py +0 -213
- prefect/records/memory.py +0 -184
- prefect/records/result_store.py +0 -70
- {prefect_client-3.1.10.dist-info → prefect_client-3.1.12.dist-info}/LICENSE +0 -0
- {prefect_client-3.1.10.dist-info → prefect_client-3.1.12.dist-info}/WHEEL +0 -0
- {prefect_client-3.1.10.dist-info → prefect_client-3.1.12.dist-info}/top_level.txt +0 -0
prefect/records/__init__.py
DELETED
@@ -1 +0,0 @@
|
|
1
|
-
from .base import RecordStore
|
prefect/records/base.py
DELETED
@@ -1,235 +0,0 @@
|
|
1
|
-
import abc
|
2
|
-
import os
|
3
|
-
import socket
|
4
|
-
import threading
|
5
|
-
from contextlib import contextmanager
|
6
|
-
from dataclasses import dataclass
|
7
|
-
from typing import TYPE_CHECKING, Optional
|
8
|
-
|
9
|
-
from prefect._internal.compatibility import deprecated
|
10
|
-
|
11
|
-
if TYPE_CHECKING:
|
12
|
-
from prefect.results import BaseResult
|
13
|
-
from prefect.transactions import IsolationLevel
|
14
|
-
|
15
|
-
|
16
|
-
@deprecated.deprecated_class(
|
17
|
-
start_date="Sep 2024",
|
18
|
-
end_date="Nov 2024",
|
19
|
-
help="Use `ResultRecord` instead to represent a result and its associated metadata.",
|
20
|
-
)
|
21
|
-
@dataclass
|
22
|
-
class TransactionRecord:
|
23
|
-
"""
|
24
|
-
A dataclass representation of a transaction record.
|
25
|
-
"""
|
26
|
-
|
27
|
-
key: str
|
28
|
-
result: "BaseResult"
|
29
|
-
|
30
|
-
|
31
|
-
@deprecated.deprecated_class(
|
32
|
-
start_date="Sep 2024",
|
33
|
-
end_date="Nov 2024",
|
34
|
-
help="Use `ResultStore` and provide a `WritableFileSystem` for `metadata_storage` instead.",
|
35
|
-
)
|
36
|
-
class RecordStore(abc.ABC):
|
37
|
-
@abc.abstractmethod
|
38
|
-
def read(
|
39
|
-
self, key: str, holder: Optional[str] = None
|
40
|
-
) -> Optional[TransactionRecord]:
|
41
|
-
"""
|
42
|
-
Read the transaction record with the given key.
|
43
|
-
|
44
|
-
Args:
|
45
|
-
key: Unique identifier for the transaction record.
|
46
|
-
holder: Unique identifier for the holder of the lock. If a lock exists on
|
47
|
-
the record being written, the read will be blocked until the lock is
|
48
|
-
released if the provided holder does not match the holder of the lock.
|
49
|
-
If not provided, a default holder based on the current host, process,
|
50
|
-
and thread will be used.
|
51
|
-
|
52
|
-
Returns:
|
53
|
-
TransactionRecord: The transaction record with the given key.
|
54
|
-
"""
|
55
|
-
...
|
56
|
-
|
57
|
-
@abc.abstractmethod
|
58
|
-
def write(self, key: str, result: "BaseResult", holder: Optional[str] = None):
|
59
|
-
"""
|
60
|
-
Write the transaction record with the given key.
|
61
|
-
|
62
|
-
Args:
|
63
|
-
key: Unique identifier for the transaction record.
|
64
|
-
record: The transaction record to write.
|
65
|
-
holder: Unique identifier for the holder of the lock. If a lock exists on
|
66
|
-
the record being written, the write will be rejected if the provided
|
67
|
-
holder does not match the holder of the lock. If not provided,
|
68
|
-
a default holder based on the current host, process, and thread will
|
69
|
-
be used.
|
70
|
-
"""
|
71
|
-
...
|
72
|
-
|
73
|
-
@abc.abstractmethod
|
74
|
-
def exists(self, key: str) -> bool:
|
75
|
-
"""
|
76
|
-
Check if the transaction record with the given key exists.
|
77
|
-
|
78
|
-
Args:
|
79
|
-
key: Unique identifier for the transaction record.
|
80
|
-
|
81
|
-
Returns:
|
82
|
-
bool: True if the record exists; False otherwise.
|
83
|
-
"""
|
84
|
-
...
|
85
|
-
|
86
|
-
@abc.abstractmethod
|
87
|
-
def supports_isolation_level(self, isolation_level: "IsolationLevel") -> bool:
|
88
|
-
"""
|
89
|
-
Check if the record store supports the given isolation level.
|
90
|
-
|
91
|
-
Args:
|
92
|
-
isolation_level: The isolation level to check.
|
93
|
-
|
94
|
-
Returns:
|
95
|
-
bool: True if the record store supports the isolation level; False otherwise.
|
96
|
-
"""
|
97
|
-
...
|
98
|
-
|
99
|
-
def acquire_lock(
|
100
|
-
self,
|
101
|
-
key: str,
|
102
|
-
holder: Optional[str] = None,
|
103
|
-
acquire_timeout: Optional[float] = None,
|
104
|
-
hold_timeout: Optional[float] = None,
|
105
|
-
) -> bool:
|
106
|
-
"""
|
107
|
-
Acquire a lock for a transaction record with the given key. Will block other
|
108
|
-
actors from updating this transaction record until the lock is
|
109
|
-
released.
|
110
|
-
|
111
|
-
Args:
|
112
|
-
key: Unique identifier for the transaction record.
|
113
|
-
holder: Unique identifier for the holder of the lock. If not provided,
|
114
|
-
a default holder based on the current host, process, and thread will
|
115
|
-
be used.
|
116
|
-
acquire_timeout: Max number of seconds to wait for the record to become
|
117
|
-
available if it is locked while attempting to acquire a lock. Pass 0
|
118
|
-
to attempt to acquire a lock without waiting. Blocks indefinitely by
|
119
|
-
default.
|
120
|
-
hold_timeout: Max number of seconds to hold the lock for. Holds the lock
|
121
|
-
indefinitely by default.
|
122
|
-
|
123
|
-
Returns:
|
124
|
-
bool: True if the lock was successfully acquired; False otherwise.
|
125
|
-
"""
|
126
|
-
raise NotImplementedError
|
127
|
-
|
128
|
-
def release_lock(self, key: str, holder: Optional[str] = None):
|
129
|
-
"""
|
130
|
-
Releases the lock on the corresponding transaction record.
|
131
|
-
|
132
|
-
Args:
|
133
|
-
key: Unique identifier for the transaction record.
|
134
|
-
holder: Unique identifier for the holder of the lock. Must match the
|
135
|
-
holder provided when acquiring the lock.
|
136
|
-
"""
|
137
|
-
raise NotImplementedError
|
138
|
-
|
139
|
-
def is_locked(self, key: str) -> bool:
|
140
|
-
"""
|
141
|
-
Simple check to see if the corresponding record is currently locked.
|
142
|
-
|
143
|
-
Args:
|
144
|
-
key: Unique identifier for the transaction record.
|
145
|
-
|
146
|
-
Returns:
|
147
|
-
True is the record is locked; False otherwise.
|
148
|
-
"""
|
149
|
-
raise NotImplementedError
|
150
|
-
|
151
|
-
def is_lock_holder(self, key: str, holder: Optional[str] = None) -> bool:
|
152
|
-
"""
|
153
|
-
Check if the current holder is the lock holder for the transaction record.
|
154
|
-
|
155
|
-
Args:
|
156
|
-
key: Unique identifier for the transaction record.
|
157
|
-
holder: Unique identifier for the holder of the lock. If not provided,
|
158
|
-
a default holder based on the current host, process, and thread will
|
159
|
-
be used.
|
160
|
-
|
161
|
-
Returns:
|
162
|
-
bool: True if the current holder is the lock holder; False otherwise.
|
163
|
-
"""
|
164
|
-
raise NotImplementedError
|
165
|
-
|
166
|
-
def wait_for_lock(self, key: str, timeout: Optional[float] = None) -> bool:
|
167
|
-
"""
|
168
|
-
Wait for the corresponding transaction record to become free.
|
169
|
-
|
170
|
-
Args:
|
171
|
-
key: Unique identifier for the transaction record.
|
172
|
-
timeout: Maximum time to wait. None means to wait indefinitely.
|
173
|
-
|
174
|
-
Returns:
|
175
|
-
bool: True if the lock becomes free within the timeout; False
|
176
|
-
otherwise.
|
177
|
-
"""
|
178
|
-
...
|
179
|
-
|
180
|
-
@staticmethod
|
181
|
-
def generate_default_holder() -> str:
|
182
|
-
"""
|
183
|
-
Generate a default holder string using hostname, PID, and thread ID.
|
184
|
-
|
185
|
-
Returns:
|
186
|
-
str: A unique identifier string.
|
187
|
-
"""
|
188
|
-
hostname = socket.gethostname()
|
189
|
-
pid = os.getpid()
|
190
|
-
thread_name = threading.current_thread().name
|
191
|
-
thread_id = threading.get_ident()
|
192
|
-
return f"{hostname}:{pid}:{thread_id}:{thread_name}"
|
193
|
-
|
194
|
-
@contextmanager
|
195
|
-
def lock(
|
196
|
-
self,
|
197
|
-
key: str,
|
198
|
-
holder: Optional[str] = None,
|
199
|
-
acquire_timeout: Optional[float] = None,
|
200
|
-
hold_timeout: Optional[float] = None,
|
201
|
-
):
|
202
|
-
"""
|
203
|
-
Context manager to lock the transaction record during the execution
|
204
|
-
of the nested code block.
|
205
|
-
|
206
|
-
Args:
|
207
|
-
key: Unique identifier for the transaction record.
|
208
|
-
holder: Unique identifier for the holder of the lock. If not provided,
|
209
|
-
a default holder based on the current host, process, and thread will
|
210
|
-
be used.
|
211
|
-
acquire_timeout: Max number of seconds to wait for the record to become
|
212
|
-
available if it is locked while attempting to acquire a lock. Pass 0
|
213
|
-
to attempt to acquire a lock without waiting. Blocks indefinitely by
|
214
|
-
default.
|
215
|
-
hold_timeout: Max number of seconds to hold the lock for. Holds the lock
|
216
|
-
indefinitely by default.
|
217
|
-
|
218
|
-
Example:
|
219
|
-
Hold a lock while during an operation:
|
220
|
-
```python
|
221
|
-
with TransactionRecord(key="my-transaction-record-key").lock():
|
222
|
-
do_stuff()
|
223
|
-
```
|
224
|
-
"""
|
225
|
-
self.acquire_lock(
|
226
|
-
key=key,
|
227
|
-
holder=holder,
|
228
|
-
acquire_timeout=acquire_timeout,
|
229
|
-
hold_timeout=hold_timeout,
|
230
|
-
)
|
231
|
-
|
232
|
-
try:
|
233
|
-
yield
|
234
|
-
finally:
|
235
|
-
self.release_lock(key=key, holder=holder)
|
prefect/records/filesystem.py
DELETED
@@ -1,213 +0,0 @@
|
|
1
|
-
import json
|
2
|
-
import time
|
3
|
-
from pathlib import Path
|
4
|
-
from typing import Dict, Optional
|
5
|
-
|
6
|
-
import pendulum
|
7
|
-
from typing_extensions import TypedDict
|
8
|
-
|
9
|
-
from prefect._internal.compatibility import deprecated
|
10
|
-
from prefect.logging.loggers import get_logger
|
11
|
-
from prefect.records.base import RecordStore, TransactionRecord
|
12
|
-
from prefect.results import BaseResult
|
13
|
-
from prefect.transactions import IsolationLevel
|
14
|
-
|
15
|
-
logger = get_logger(__name__)
|
16
|
-
|
17
|
-
|
18
|
-
class _LockInfo(TypedDict):
|
19
|
-
"""
|
20
|
-
A dictionary containing information about a lock.
|
21
|
-
|
22
|
-
Attributes:
|
23
|
-
holder: The holder of the lock.
|
24
|
-
expiration: Datetime when the lock expires.
|
25
|
-
path: Path to the lock file.
|
26
|
-
"""
|
27
|
-
|
28
|
-
holder: str
|
29
|
-
expiration: Optional[pendulum.DateTime]
|
30
|
-
path: Path
|
31
|
-
|
32
|
-
|
33
|
-
@deprecated.deprecated_class(
|
34
|
-
start_date="Sep 2024",
|
35
|
-
end_date="Nov 2024",
|
36
|
-
help="Use `ResultStore` with a `LocalFileSystem` for `metadata_storage` and a `FileSystemLockManager` instead.",
|
37
|
-
)
|
38
|
-
class FileSystemRecordStore(RecordStore):
|
39
|
-
"""
|
40
|
-
A record store that stores data on the local filesystem.
|
41
|
-
|
42
|
-
Locking is implemented using a lock file with the same name as the record file,
|
43
|
-
but with a `.lock` extension.
|
44
|
-
|
45
|
-
Attributes:
|
46
|
-
records_directory: the directory where records are stored; defaults to
|
47
|
-
`{PREFECT_HOME}/records`
|
48
|
-
"""
|
49
|
-
|
50
|
-
def __init__(self, records_directory: Path):
|
51
|
-
self.records_directory = records_directory
|
52
|
-
self._locks: Dict[str, _LockInfo] = {}
|
53
|
-
|
54
|
-
def _ensure_records_directory_exists(self):
|
55
|
-
self.records_directory.mkdir(parents=True, exist_ok=True)
|
56
|
-
|
57
|
-
def _lock_path_for_key(self, key: str) -> Path:
|
58
|
-
if (lock_info := self._locks.get(key)) is not None:
|
59
|
-
return lock_info["path"]
|
60
|
-
return self.records_directory.joinpath(key).with_suffix(".lock")
|
61
|
-
|
62
|
-
def _get_lock_info(self, key: str, use_cache=True) -> Optional[_LockInfo]:
|
63
|
-
if use_cache:
|
64
|
-
if (lock_info := self._locks.get(key)) is not None:
|
65
|
-
return lock_info
|
66
|
-
|
67
|
-
lock_path = self._lock_path_for_key(key)
|
68
|
-
|
69
|
-
try:
|
70
|
-
with open(lock_path, "r") as lock_file:
|
71
|
-
lock_info = json.load(lock_file)
|
72
|
-
lock_info["path"] = lock_path
|
73
|
-
expiration = lock_info.get("expiration")
|
74
|
-
lock_info["expiration"] = (
|
75
|
-
pendulum.parse(expiration) if expiration is not None else None
|
76
|
-
)
|
77
|
-
self._locks[key] = lock_info
|
78
|
-
return lock_info
|
79
|
-
except FileNotFoundError:
|
80
|
-
return None
|
81
|
-
|
82
|
-
def read(
|
83
|
-
self, key: str, holder: Optional[str] = None, timeout: Optional[float] = None
|
84
|
-
) -> Optional[TransactionRecord]:
|
85
|
-
if not self.exists(key):
|
86
|
-
return None
|
87
|
-
|
88
|
-
holder = holder or self.generate_default_holder()
|
89
|
-
|
90
|
-
if self.is_locked(key) and not self.is_lock_holder(key, holder):
|
91
|
-
unlocked = self.wait_for_lock(key, timeout=timeout)
|
92
|
-
if not unlocked:
|
93
|
-
return None
|
94
|
-
record_data = self.records_directory.joinpath(key).read_text()
|
95
|
-
return TransactionRecord(
|
96
|
-
key=key, result=BaseResult.model_validate_json(record_data)
|
97
|
-
)
|
98
|
-
|
99
|
-
def write(self, key: str, result: BaseResult, holder: Optional[str] = None) -> None:
|
100
|
-
self._ensure_records_directory_exists()
|
101
|
-
|
102
|
-
if self.is_locked(key) and not self.is_lock_holder(key, holder):
|
103
|
-
raise ValueError(
|
104
|
-
f"Cannot write to transaction with key {key} because it is locked by another holder."
|
105
|
-
)
|
106
|
-
|
107
|
-
record_path = self.records_directory.joinpath(key)
|
108
|
-
record_path.touch(exist_ok=True)
|
109
|
-
record_data = result.model_dump_json()
|
110
|
-
record_path.write_text(record_data)
|
111
|
-
|
112
|
-
def exists(self, key: str) -> bool:
|
113
|
-
return self.records_directory.joinpath(key).exists()
|
114
|
-
|
115
|
-
def supports_isolation_level(self, isolation_level: IsolationLevel) -> bool:
|
116
|
-
return isolation_level in {
|
117
|
-
IsolationLevel.READ_COMMITTED,
|
118
|
-
IsolationLevel.SERIALIZABLE,
|
119
|
-
}
|
120
|
-
|
121
|
-
def acquire_lock(
|
122
|
-
self,
|
123
|
-
key: str,
|
124
|
-
holder: Optional[str] = None,
|
125
|
-
acquire_timeout: Optional[float] = None,
|
126
|
-
hold_timeout: Optional[float] = None,
|
127
|
-
) -> bool:
|
128
|
-
holder = holder or self.generate_default_holder()
|
129
|
-
|
130
|
-
self._ensure_records_directory_exists()
|
131
|
-
lock_path = self._lock_path_for_key(key)
|
132
|
-
|
133
|
-
if self.is_locked(key) and not self.is_lock_holder(key, holder):
|
134
|
-
lock_free = self.wait_for_lock(key, acquire_timeout)
|
135
|
-
if not lock_free:
|
136
|
-
return False
|
137
|
-
|
138
|
-
try:
|
139
|
-
Path(lock_path).touch(exist_ok=False)
|
140
|
-
except FileExistsError:
|
141
|
-
if not self.is_lock_holder(key, holder):
|
142
|
-
logger.debug(
|
143
|
-
f"Another actor acquired the lock for record with key {key}. Trying again."
|
144
|
-
)
|
145
|
-
return self.acquire_lock(key, holder, acquire_timeout, hold_timeout)
|
146
|
-
expiration = (
|
147
|
-
pendulum.now("utc") + pendulum.duration(seconds=hold_timeout)
|
148
|
-
if hold_timeout is not None
|
149
|
-
else None
|
150
|
-
)
|
151
|
-
|
152
|
-
with open(Path(lock_path), "w") as lock_file:
|
153
|
-
json.dump(
|
154
|
-
{
|
155
|
-
"holder": holder,
|
156
|
-
"expiration": str(expiration) if expiration is not None else None,
|
157
|
-
},
|
158
|
-
lock_file,
|
159
|
-
)
|
160
|
-
|
161
|
-
self._locks[key] = {
|
162
|
-
"holder": holder,
|
163
|
-
"expiration": expiration,
|
164
|
-
"path": lock_path,
|
165
|
-
}
|
166
|
-
|
167
|
-
return True
|
168
|
-
|
169
|
-
def release_lock(self, key: str, holder: Optional[str] = None) -> None:
|
170
|
-
holder = holder or self.generate_default_holder()
|
171
|
-
lock_path = self._lock_path_for_key(key)
|
172
|
-
if not self.is_locked(key):
|
173
|
-
ValueError(f"No lock for transaction with key {key}")
|
174
|
-
if self.is_lock_holder(key, holder):
|
175
|
-
Path(lock_path).unlink(missing_ok=True)
|
176
|
-
self._locks.pop(key, None)
|
177
|
-
else:
|
178
|
-
raise ValueError(f"No lock held by {holder} for transaction with key {key}")
|
179
|
-
|
180
|
-
def is_locked(self, key: str, use_cache: bool = False) -> bool:
|
181
|
-
if (lock_info := self._get_lock_info(key, use_cache=use_cache)) is None:
|
182
|
-
return False
|
183
|
-
|
184
|
-
if (expiration := lock_info.get("expiration")) is None:
|
185
|
-
return True
|
186
|
-
|
187
|
-
expired = expiration < pendulum.now("utc")
|
188
|
-
if expired:
|
189
|
-
Path(lock_info["path"]).unlink()
|
190
|
-
self._locks.pop(key, None)
|
191
|
-
return False
|
192
|
-
else:
|
193
|
-
return True
|
194
|
-
|
195
|
-
def is_lock_holder(self, key: str, holder: Optional[str] = None) -> bool:
|
196
|
-
if not self.is_locked(key):
|
197
|
-
return False
|
198
|
-
|
199
|
-
holder = holder or self.generate_default_holder()
|
200
|
-
if not self.is_locked(key):
|
201
|
-
return False
|
202
|
-
if (lock_info := self._get_lock_info(key)) is None:
|
203
|
-
return False
|
204
|
-
return lock_info["holder"] == holder
|
205
|
-
|
206
|
-
def wait_for_lock(self, key: str, timeout: Optional[float] = None) -> bool:
|
207
|
-
seconds_waited = 0
|
208
|
-
while self.is_locked(key, use_cache=False):
|
209
|
-
if timeout and seconds_waited >= timeout:
|
210
|
-
return False
|
211
|
-
seconds_waited += 0.1
|
212
|
-
time.sleep(0.1)
|
213
|
-
return True
|
prefect/records/memory.py
DELETED
@@ -1,184 +0,0 @@
|
|
1
|
-
import threading
|
2
|
-
from typing import Dict, Optional, TypedDict
|
3
|
-
|
4
|
-
from prefect._internal.compatibility import deprecated
|
5
|
-
from prefect.results import BaseResult
|
6
|
-
from prefect.transactions import IsolationLevel
|
7
|
-
|
8
|
-
from .base import RecordStore, TransactionRecord
|
9
|
-
|
10
|
-
|
11
|
-
class _LockInfo(TypedDict):
|
12
|
-
"""
|
13
|
-
A dictionary containing information about a lock.
|
14
|
-
|
15
|
-
Attributes:
|
16
|
-
holder: The holder of the lock.
|
17
|
-
lock: The lock object.
|
18
|
-
expiration_timer: The timer for the lock expiration
|
19
|
-
"""
|
20
|
-
|
21
|
-
holder: str
|
22
|
-
lock: threading.Lock
|
23
|
-
expiration_timer: Optional[threading.Timer]
|
24
|
-
|
25
|
-
|
26
|
-
@deprecated.deprecated_class(
|
27
|
-
start_date="Sep 2024",
|
28
|
-
end_date="Nov 2024",
|
29
|
-
help="Use `ResultStore` with a `MemoryLockManager` instead.",
|
30
|
-
)
|
31
|
-
class MemoryRecordStore(RecordStore):
|
32
|
-
"""
|
33
|
-
A record store that stores data in memory.
|
34
|
-
"""
|
35
|
-
|
36
|
-
_instance = None
|
37
|
-
|
38
|
-
def __new__(cls, *args, **kwargs):
|
39
|
-
if cls._instance is None:
|
40
|
-
cls._instance = super().__new__(cls)
|
41
|
-
return cls._instance
|
42
|
-
|
43
|
-
def __init__(self):
|
44
|
-
self._locks_dict_lock = threading.Lock()
|
45
|
-
self._locks: Dict[str, _LockInfo] = {}
|
46
|
-
self._records: Dict[str, TransactionRecord] = {}
|
47
|
-
|
48
|
-
def read(
|
49
|
-
self, key: str, holder: Optional[str] = None
|
50
|
-
) -> Optional[TransactionRecord]:
|
51
|
-
holder = holder or self.generate_default_holder()
|
52
|
-
|
53
|
-
if self.is_locked(key) and not self.is_lock_holder(key, holder):
|
54
|
-
self.wait_for_lock(key)
|
55
|
-
return self._records.get(key)
|
56
|
-
|
57
|
-
def write(self, key: str, result: BaseResult, holder: Optional[str] = None) -> None:
|
58
|
-
holder = holder or self.generate_default_holder()
|
59
|
-
|
60
|
-
with self._locks_dict_lock:
|
61
|
-
if self.is_locked(key) and not self.is_lock_holder(key, holder):
|
62
|
-
raise ValueError(
|
63
|
-
f"Cannot write to transaction with key {key} because it is locked by another holder."
|
64
|
-
)
|
65
|
-
self._records[key] = TransactionRecord(key=key, result=result)
|
66
|
-
|
67
|
-
def exists(self, key: str) -> bool:
|
68
|
-
return key in self._records
|
69
|
-
|
70
|
-
def supports_isolation_level(self, isolation_level: IsolationLevel) -> bool:
|
71
|
-
return isolation_level in {
|
72
|
-
IsolationLevel.READ_COMMITTED,
|
73
|
-
IsolationLevel.SERIALIZABLE,
|
74
|
-
}
|
75
|
-
|
76
|
-
def _expire_lock(self, key: str):
|
77
|
-
"""
|
78
|
-
Expire the lock for the given key.
|
79
|
-
|
80
|
-
Used as a callback for the expiration timer of a lock.
|
81
|
-
|
82
|
-
Args:
|
83
|
-
key: The key of the lock to expire.
|
84
|
-
"""
|
85
|
-
with self._locks_dict_lock:
|
86
|
-
if key in self._locks:
|
87
|
-
lock_info = self._locks[key]
|
88
|
-
if lock_info["lock"].locked():
|
89
|
-
lock_info["lock"].release()
|
90
|
-
if lock_info["expiration_timer"]:
|
91
|
-
lock_info["expiration_timer"].cancel()
|
92
|
-
del self._locks[key]
|
93
|
-
|
94
|
-
def acquire_lock(
|
95
|
-
self,
|
96
|
-
key: str,
|
97
|
-
holder: Optional[str] = None,
|
98
|
-
acquire_timeout: Optional[float] = None,
|
99
|
-
hold_timeout: Optional[float] = None,
|
100
|
-
) -> bool:
|
101
|
-
holder = holder or self.generate_default_holder()
|
102
|
-
with self._locks_dict_lock:
|
103
|
-
if key not in self._locks:
|
104
|
-
lock = threading.Lock()
|
105
|
-
lock.acquire()
|
106
|
-
expiration_timer = None
|
107
|
-
if hold_timeout is not None:
|
108
|
-
expiration_timer = threading.Timer(
|
109
|
-
hold_timeout, self._expire_lock, args=(key,)
|
110
|
-
)
|
111
|
-
expiration_timer.start()
|
112
|
-
self._locks[key] = _LockInfo(
|
113
|
-
holder=holder, lock=lock, expiration_timer=expiration_timer
|
114
|
-
)
|
115
|
-
return True
|
116
|
-
elif self._locks[key]["holder"] == holder:
|
117
|
-
return True
|
118
|
-
else:
|
119
|
-
existing_lock_info = self._locks[key]
|
120
|
-
|
121
|
-
if acquire_timeout is not None:
|
122
|
-
existing_lock_acquired = existing_lock_info["lock"].acquire(
|
123
|
-
timeout=acquire_timeout
|
124
|
-
)
|
125
|
-
else:
|
126
|
-
existing_lock_acquired = existing_lock_info["lock"].acquire()
|
127
|
-
|
128
|
-
if existing_lock_acquired:
|
129
|
-
with self._locks_dict_lock:
|
130
|
-
if (
|
131
|
-
expiration_timer := existing_lock_info["expiration_timer"]
|
132
|
-
) is not None:
|
133
|
-
expiration_timer.cancel()
|
134
|
-
expiration_timer = None
|
135
|
-
if hold_timeout is not None:
|
136
|
-
expiration_timer = threading.Timer(
|
137
|
-
hold_timeout, self._expire_lock, args=(key,)
|
138
|
-
)
|
139
|
-
expiration_timer.start()
|
140
|
-
self._locks[key] = _LockInfo(
|
141
|
-
holder=holder,
|
142
|
-
lock=existing_lock_info["lock"],
|
143
|
-
expiration_timer=expiration_timer,
|
144
|
-
)
|
145
|
-
return True
|
146
|
-
return False
|
147
|
-
|
148
|
-
def release_lock(self, key: str, holder: Optional[str] = None) -> None:
|
149
|
-
holder = holder or self.generate_default_holder()
|
150
|
-
with self._locks_dict_lock:
|
151
|
-
if key in self._locks and self._locks[key]["holder"] == holder:
|
152
|
-
if (
|
153
|
-
expiration_timer := self._locks[key]["expiration_timer"]
|
154
|
-
) is not None:
|
155
|
-
expiration_timer.cancel()
|
156
|
-
self._locks[key]["lock"].release()
|
157
|
-
del self._locks[key]
|
158
|
-
else:
|
159
|
-
raise ValueError(
|
160
|
-
f"No lock held by {holder} for transaction with key {key}"
|
161
|
-
)
|
162
|
-
|
163
|
-
def is_locked(self, key: str) -> bool:
|
164
|
-
return key in self._locks and self._locks[key]["lock"].locked()
|
165
|
-
|
166
|
-
def is_lock_holder(self, key: str, holder: Optional[str] = None) -> bool:
|
167
|
-
holder = holder or self.generate_default_holder()
|
168
|
-
lock_info = self._locks.get(key)
|
169
|
-
return (
|
170
|
-
lock_info is not None
|
171
|
-
and lock_info["lock"].locked()
|
172
|
-
and lock_info["holder"] == holder
|
173
|
-
)
|
174
|
-
|
175
|
-
def wait_for_lock(self, key: str, timeout: Optional[float] = None) -> bool:
|
176
|
-
if lock := self._locks.get(key, {}).get("lock"):
|
177
|
-
if timeout is not None:
|
178
|
-
lock_acquired = lock.acquire(timeout=timeout)
|
179
|
-
else:
|
180
|
-
lock_acquired = lock.acquire()
|
181
|
-
if lock_acquired:
|
182
|
-
lock.release()
|
183
|
-
return lock_acquired
|
184
|
-
return True
|
prefect/records/result_store.py
DELETED
@@ -1,70 +0,0 @@
|
|
1
|
-
from dataclasses import dataclass
|
2
|
-
from typing import Any, Optional
|
3
|
-
|
4
|
-
import pendulum
|
5
|
-
|
6
|
-
from prefect._internal.compatibility import deprecated
|
7
|
-
from prefect.results import BaseResult, PersistedResult, ResultStore
|
8
|
-
from prefect.transactions import IsolationLevel
|
9
|
-
from prefect.utilities.asyncutils import run_coro_as_sync
|
10
|
-
|
11
|
-
from .base import RecordStore, TransactionRecord
|
12
|
-
|
13
|
-
|
14
|
-
@deprecated.deprecated_class(
|
15
|
-
start_date="Sep 2024",
|
16
|
-
end_date="Nov 2024",
|
17
|
-
help="Use `ResultStore` directly instead.",
|
18
|
-
)
|
19
|
-
@dataclass
|
20
|
-
class ResultRecordStore(RecordStore):
|
21
|
-
"""
|
22
|
-
A record store for result records.
|
23
|
-
|
24
|
-
Collocates result metadata with result data.
|
25
|
-
"""
|
26
|
-
|
27
|
-
result_store: ResultStore
|
28
|
-
cache: Optional[PersistedResult] = None
|
29
|
-
|
30
|
-
def exists(self, key: str) -> bool:
|
31
|
-
try:
|
32
|
-
record = self.read(key)
|
33
|
-
if not record:
|
34
|
-
return False
|
35
|
-
result = record.result
|
36
|
-
result.get(_sync=True)
|
37
|
-
if result.expiration:
|
38
|
-
# if the result has an expiration,
|
39
|
-
# check if it is still in the future
|
40
|
-
exists = result.expiration > pendulum.now("utc")
|
41
|
-
else:
|
42
|
-
exists = True
|
43
|
-
self.cache = result
|
44
|
-
return exists
|
45
|
-
except Exception:
|
46
|
-
return False
|
47
|
-
|
48
|
-
def read(self, key: str, holder: Optional[str] = None) -> TransactionRecord:
|
49
|
-
if self.cache:
|
50
|
-
return TransactionRecord(key=key, result=self.cache)
|
51
|
-
try:
|
52
|
-
result = PersistedResult(
|
53
|
-
serializer_type=self.result_store.serializer.type,
|
54
|
-
storage_block_id=self.result_store.result_storage_block_id,
|
55
|
-
storage_key=key,
|
56
|
-
)
|
57
|
-
return TransactionRecord(key=key, result=result)
|
58
|
-
except Exception:
|
59
|
-
# this is a bit of a bandaid for functionality
|
60
|
-
raise ValueError("Result could not be read")
|
61
|
-
|
62
|
-
def write(self, key: str, result: Any, holder: Optional[str] = None) -> None:
|
63
|
-
if isinstance(result, PersistedResult):
|
64
|
-
# if the value is already a persisted result, write it
|
65
|
-
result.write(_sync=True)
|
66
|
-
elif not isinstance(result, BaseResult):
|
67
|
-
run_coro_as_sync(self.result_store.create_result(obj=result, key=key))
|
68
|
-
|
69
|
-
def supports_isolation_level(self, isolation_level: IsolationLevel) -> bool:
|
70
|
-
return isolation_level == IsolationLevel.READ_COMMITTED
|