-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdistributed_lock_manager.py
More file actions
290 lines (242 loc) · 15.4 KB
/
Copy pathdistributed_lock_manager.py
File metadata and controls
290 lines (242 loc) · 15.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
"""In-process distributed lock manager with lease expiry and a fairness queue.
Use when several owners contend for named resources and you need mutual
exclusion plus automatic recovery if a holder dies: locks carry a lease that
lapses on its own, and blocked waiters are served first-come-first-served.
Guarantees (proven by self-test): only one owner holds a resource at a time; a
released lock hands off to exactly one waiter; an expired lease is reacquirable;
refresh extends a live lease past its original expiry; release/refresh with a
foreign or stale token is refused.
"""
# △ AURA Pattern Library — © Reality Optimizer ⟦AE1.PMRGG3ZCHIRFEZLBNRUXI6JAJ5YHI2LNNF5GK4RCFQRG2IR2EJAUKTKBKJFTCIRMEJXCEORCGARCYITQNFSCEORCEIWCE5DNEI5CEQKVKJASAUDBOR2GK4TOEBGGSYTSMFZHSIRMEJ3CEORRPWYSJPXO⟧
#
_AURA_MARK = "AE1.PMRGG3ZCHIRFEZLBNRUXI6JAJ5YHI2LNNF5GK4RCFQRG2IR2EJAUKTKBKJFTCIRMEJXCEORCGARCYITQNFSCEORCEIWCE5DNEI5CEQKVKJASAUDBOR2GK4TOEBGGSYTSMFZHSIRMEJ3CEORRPWYSJPXO"
import time
import uuid
from typing import Dict, List, Optional, Tuple
from dataclasses import dataclass
from threading import Lock as ThreadingLock
@dataclass
class LockToken:
"""Represents a lock token with expiration and ownership information."""
token_id: str
owner_id: str
expiration_time: float
resource_id: str
def is_expired(self) -> bool:
"""Check if the lock token has expired."""
return time.time() > self.expiration_time
def remaining_time(self) -> float:
"""Get remaining time before expiration in seconds."""
return max(0, self.expiration_time - time.time())
class LockManager:
"""A distributed lock manager implementing fairness queue and expiration."""
def __init__(self):
"""Initialize the lock manager."""
self._locks: Dict[str, Optional[LockToken]] = {}
self._waiting_queues: Dict[str, List[Tuple[str, float]]] = {}
self._lock = ThreadingLock()
def acquire(self, resource_id: str, owner_id: str, timeout: float = 10.0,
lease_time: float = 30.0) -> Optional[LockToken]:
"""
Acquire a lock for a resource.
Args:
resource_id: Identifier for the resource to lock
owner_id: Identifier for the lock requester
timeout: Maximum time to wait for the lock (seconds)
lease_time: Time until the lock expires (seconds)
Returns:
LockToken if acquired, None if timeout exceeded
"""
start_time = time.time()
with self._lock:
# If no one holds the lock, acquire it immediately
if resource_id not in self._locks or self._locks[resource_id] is None:
return self._create_lock(resource_id, owner_id, lease_time)
# Check if current holder's lock has expired
current_lock = self._locks[resource_id]
if current_lock and current_lock.is_expired():
self._locks[resource_id] = None
return self._create_lock(resource_id, owner_id, lease_time)
# Add to waiting queue
if resource_id not in self._waiting_queues:
self._waiting_queues[resource_id] = []
self._waiting_queues[resource_id].append((owner_id, start_time + timeout))
# Wait for lock or timeout
while time.time() - start_time < timeout:
time.sleep(0.01) # 10ms polling interval
with self._lock:
# Check if we're next in line
if (resource_id in self._waiting_queues and
self._waiting_queues[resource_id] and
self._waiting_queues[resource_id][0][0] == owner_id):
# Try to acquire the lock
if (resource_id not in self._locks or
self._locks[resource_id] is None or
self._locks[resource_id].is_expired()):
# Remove from queue and acquire
self._waiting_queues[resource_id].pop(0)
if not self._waiting_queues[resource_id]:
del self._waiting_queues[resource_id]
return self._create_lock(resource_id, owner_id, lease_time)
# Check if we've timed out in the queue
if resource_id in self._waiting_queues:
queue = self._waiting_queues[resource_id]
for i, (waiter_id, expire_time) in enumerate(queue):
if waiter_id == owner_id:
if time.time() > expire_time:
queue.pop(i)
if not queue:
del self._waiting_queues[resource_id]
return None
break
# Remove from queue if we timed out
with self._lock:
if resource_id in self._waiting_queues:
queue = self._waiting_queues[resource_id]
for i, (waiter_id, _) in enumerate(queue):
if waiter_id == owner_id:
queue.pop(i)
if not queue:
del self._waiting_queues[resource_id]
break
return None
def release(self, token: LockToken) -> bool:
"""
Release a lock using its token.
Args:
token: The lock token to release
Returns:
True if released successfully, False otherwise
"""
with self._lock:
if token.resource_id not in self._locks:
return False
current_lock = self._locks[token.resource_id]
if current_lock is None:
return False
# Verify token matches current lock
if (current_lock.token_id == token.token_id and
current_lock.owner_id == token.owner_id and
current_lock.resource_id == token.resource_id):
self._locks[token.resource_id] = None
return True
return False
def refresh(self, token: LockToken, lease_time: float = 30.0) -> Optional[LockToken]:
"""
Refresh an existing lock's expiration time.
Args:
token: The lock token to refresh
lease_time: New lease time from now (seconds)
Returns:
New LockToken if refreshed, None if failed
"""
with self._lock:
if token.resource_id not in self._locks:
return None
current_lock = self._locks[token.resource_id]
if current_lock is None:
return None
# Verify token matches current lock
if (current_lock.token_id == token.token_id and
current_lock.owner_id == token.owner_id and
current_lock.resource_id == token.resource_id and
not current_lock.is_expired()):
# Create new token with extended expiration
new_expiration = time.time() + lease_time
new_token = LockToken(
token_id=current_lock.token_id,
owner_id=current_lock.owner_id,
expiration_time=new_expiration,
resource_id=current_lock.resource_id
)
self._locks[token.resource_id] = new_token
return new_token
return None
def _create_lock(self, resource_id: str, owner_id: str, lease_time: float) -> LockToken:
"""Create a new lock token and register it."""
token = LockToken(
token_id=str(uuid.uuid4()),
owner_id=owner_id,
expiration_time=time.time() + lease_time,
resource_id=resource_id
)
self._locks[resource_id] = token
return token
def get_lock_info(self, resource_id: str) -> Optional[Dict]:
"""
Get information about a lock.
Args:
resource_id: The resource to check
Returns:
Dictionary with lock info or None if no lock exists
"""
with self._lock:
if resource_id not in self._locks or self._locks[resource_id] is None:
return None
lock = self._locks[resource_id]
return {
"token_id": lock.token_id,
"owner_id": lock.owner_id,
"expires_in": lock.remaining_time(),
"expired": lock.is_expired()
}
if __name__ == "__main__":
# Self-test: mutual exclusion, waiter handoff on release, lease expiry
# (real time, short leases), refresh extends, foreign release refused.
import threading
manager = LockManager()
# Mutual exclusion: second client cannot take a held lock.
token1 = manager.acquire("resource1", "client1", lease_time=5.0)
assert token1 is not None, "first acquire failed"
info = manager.get_lock_info("resource1")
assert info["owner_id"] == "client1" and not info["expired"]
assert manager.acquire("resource1", "client2", timeout=0.1) is None, \
"second client acquired a HELD lock — mutual exclusion broken"
# Release: honest returns; double release refused.
assert manager.release(token1) is True
assert manager.release(token1) is False, "double release reported success"
# After release the resource is takeable.
token2 = manager.acquire("resource1", "client2", timeout=0.1)
assert token2 is not None and token2.owner_id == "client2"
manager.release(token2)
# Waiter handoff: two clients block; releasing lets EXACTLY one in
# (the other still holds it when its own acquire returns).
t1 = manager.acquire("resource2", "client1", lease_time=10.0)
results = {}
def try_acquire(client_id):
tok = manager.acquire("resource2", client_id, timeout=3.0, lease_time=10.0)
results[client_id] = tok
threads = [threading.Thread(target=try_acquire, args=(c,))
for c in ("client2", "client3")]
for t in threads:
t.start()
time.sleep(0.3)
assert not results, "a waiter acquired while the lock was held"
manager.release(t1)
for t in threads:
t.join()
n_winners = sum(1 for tok in results.values() if tok is not None)
assert n_winners * 2 == 2, \
f"exactly one waiter must win the handoff, got {n_winners}: {results}"
winners = [c for c, tok in results.items() if tok is not None]
manager.release(results[winners[0]])
# Lease expiry: a 0.3s lease is reacquirable by another client at 0.5s.
manager.acquire("resource3", "client1", lease_time=0.3)
assert manager.acquire("resource3", "client2", timeout=0.05) is None
time.sleep(0.5)
expired_info = manager.get_lock_info("resource3")
assert expired_info["expired"] is True, "lease did not expire"
stolen = manager.acquire("resource3", "client2", timeout=0.5)
assert stolen is not None, "expired lock not reacquirable"
manager.release(stolen)
# Refresh extends the lease past its original expiry.
tok = manager.acquire("resource4", "client1", lease_time=0.4)
time.sleep(0.2)
refreshed = manager.refresh(tok, lease_time=2.0)
assert refreshed is not None, "refresh of a live lock failed"
time.sleep(0.4) # past the ORIGINAL lease
assert manager.acquire("resource4", "client2", timeout=0.05) is None, \
"lock lapsed despite the refresh"
assert manager.get_lock_info("resource4")["expires_in"] > 0.5
print("distributed_lock_manager: exclusion held, 1/2 waiters won handoff, "
"0.3s lease expired+stolen, refresh outlived original lease — PASS")