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.
@@ -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
@@ -0,0 +1,122 @@
1
+ from typing import Optional, Protocol, runtime_checkable
2
+
3
+
4
+ @runtime_checkable
5
+ class LockManager(Protocol):
6
+ def acquire_lock(
7
+ self,
8
+ key: str,
9
+ holder: str,
10
+ acquire_timeout: Optional[float] = None,
11
+ hold_timeout: Optional[float] = None,
12
+ ) -> bool:
13
+ """
14
+ Acquire a lock for a transaction record with the given key. Will block other
15
+ actors from updating this transaction record until the lock is
16
+ released.
17
+
18
+ Args:
19
+ key: Unique identifier for the transaction record.
20
+ holder: Unique identifier for the holder of the lock.
21
+ acquire_timeout: Max number of seconds to wait for the record to become
22
+ available if it is locked while attempting to acquire a lock. Pass 0
23
+ to attempt to acquire a lock without waiting. Blocks indefinitely by
24
+ default.
25
+ hold_timeout: Max number of seconds to hold the lock for. Holds the lock
26
+ indefinitely by default.
27
+
28
+ Returns:
29
+ bool: True if the lock was successfully acquired; False otherwise.
30
+ """
31
+ ...
32
+
33
+ async def aacquire_lock(
34
+ self,
35
+ key: str,
36
+ holder: str,
37
+ acquire_timeout: Optional[float] = None,
38
+ hold_timeout: Optional[float] = None,
39
+ ) -> bool:
40
+ """
41
+ Acquire a lock for a transaction record with the given key. Will block other
42
+ actors from updating this transaction record until the lock is
43
+ released.
44
+
45
+ Args:
46
+ key: Unique identifier for the transaction record.
47
+ holder: Unique identifier for the holder of the lock.
48
+ acquire_timeout: Max number of seconds to wait for the record to become
49
+ available if it is locked while attempting to acquire a lock. Pass 0
50
+ to attempt to acquire a lock without waiting. Blocks indefinitely by
51
+ default.
52
+ hold_timeout: Max number of seconds to hold the lock for. Holds the lock
53
+ indefinitely by default.
54
+
55
+ Returns:
56
+ bool: True if the lock was successfully acquired; False otherwise.
57
+ """
58
+ ...
59
+
60
+ def release_lock(self, key: str, holder: str):
61
+ """
62
+ Releases the lock on the corresponding transaction record.
63
+
64
+ Args:
65
+ key: Unique identifier for the transaction record.
66
+ holder: Unique identifier for the holder of the lock. Must match the
67
+ holder provided when acquiring the lock.
68
+ """
69
+ ...
70
+
71
+ def is_locked(self, key: str) -> bool:
72
+ """
73
+ Simple check to see if the corresponding record is currently locked.
74
+
75
+ Args:
76
+ key: Unique identifier for the transaction record.
77
+
78
+ Returns:
79
+ True is the record is locked; False otherwise.
80
+ """
81
+ ...
82
+
83
+ def is_lock_holder(self, key: str, holder: str) -> bool:
84
+ """
85
+ Check if the current holder is the lock holder for the transaction record.
86
+
87
+ Args:
88
+ key: Unique identifier for the transaction record.
89
+ holder: Unique identifier for the holder of the lock.
90
+
91
+ Returns:
92
+ bool: True if the current holder is the lock holder; False otherwise.
93
+ """
94
+ ...
95
+
96
+ def wait_for_lock(self, key: str, timeout: Optional[float] = None) -> bool:
97
+ """
98
+ Wait for the corresponding transaction record to become free.
99
+
100
+ Args:
101
+ key: Unique identifier for the transaction record.
102
+ timeout: Maximum time to wait. None means to wait indefinitely.
103
+
104
+ Returns:
105
+ bool: True if the lock becomes free within the timeout; False
106
+ otherwise.
107
+ """
108
+ ...
109
+
110
+ async def await_for_lock(self, key: str, timeout: Optional[float] = None) -> bool:
111
+ """
112
+ Wait for the corresponding transaction record to become free.
113
+
114
+ Args:
115
+ key: Unique identifier for the transaction record.
116
+ timeout: Maximum time to wait. None means to wait indefinitely.
117
+
118
+ Returns:
119
+ bool: True if the lock becomes free within the timeout; False
120
+ otherwise.
121
+ """
122
+ ...
@@ -76,7 +76,7 @@ class FileSystemRecordStore(RecordStore):
76
76
  return None
77
77
 
78
78
  def read(
79
- self, key: str, holder: Optional[str] = None
79
+ self, key: str, holder: Optional[str] = None, timeout: Optional[float] = None
80
80
  ) -> Optional[TransactionRecord]:
81
81
  if not self.exists(key):
82
82
  return None
@@ -84,7 +84,9 @@ class FileSystemRecordStore(RecordStore):
84
84
  holder = holder or self.generate_default_holder()
85
85
 
86
86
  if self.is_locked(key) and not self.is_lock_holder(key, holder):
87
- self.wait_for_lock(key)
87
+ unlocked = self.wait_for_lock(key, timeout=timeout)
88
+ if not unlocked:
89
+ return None
88
90
  record_data = self.records_directory.joinpath(key).read_text()
89
91
  return TransactionRecord(
90
92
  key=key, result=BaseResult.model_validate_json(record_data)
@@ -3,7 +3,7 @@ from typing import Any, Optional
3
3
 
4
4
  import pendulum
5
5
 
6
- from prefect.results import BaseResult, PersistedResult, ResultFactory
6
+ from prefect.results import BaseResult, PersistedResult, ResultStore
7
7
  from prefect.transactions import IsolationLevel
8
8
  from prefect.utilities.asyncutils import run_coro_as_sync
9
9
 
@@ -11,8 +11,14 @@ from .base import RecordStore, TransactionRecord
11
11
 
12
12
 
13
13
  @dataclass
14
- class ResultFactoryStore(RecordStore):
15
- result_factory: ResultFactory
14
+ class ResultRecordStore(RecordStore):
15
+ """
16
+ A record store for result records.
17
+
18
+ Collocates result metadata with result data.
19
+ """
20
+
21
+ result_store: ResultStore
16
22
  cache: Optional[PersistedResult] = None
17
23
 
18
24
  def exists(self, key: str) -> bool:
@@ -38,8 +44,8 @@ class ResultFactoryStore(RecordStore):
38
44
  return TransactionRecord(key=key, result=self.cache)
39
45
  try:
40
46
  result = PersistedResult(
41
- serializer_type=self.result_factory.serializer.type,
42
- storage_block_id=self.result_factory.storage_block_id,
47
+ serializer_type=self.result_store.serializer.type,
48
+ storage_block_id=self.result_store.result_storage_block_id,
43
49
  storage_key=key,
44
50
  )
45
51
  return TransactionRecord(key=key, result=result)
@@ -52,7 +58,7 @@ class ResultFactoryStore(RecordStore):
52
58
  # if the value is already a persisted result, write it
53
59
  result.write(_sync=True)
54
60
  elif not isinstance(result, BaseResult):
55
- run_coro_as_sync(self.result_factory.create_result(obj=result, key=key))
61
+ run_coro_as_sync(self.result_store.create_result(obj=result, key=key))
56
62
 
57
63
  def supports_isolation_level(self, isolation_level: IsolationLevel) -> bool:
58
64
  return isolation_level == IsolationLevel.READ_COMMITTED