redis-allocator 0.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,682 @@
1
+ """Redis-based locking mechanisms for distributed coordination.
2
+
3
+ This module provides Redis-based locking classes that enable distributed
4
+ coordination and ensure data consistency across distributed processes.
5
+ """
6
+ import time
7
+ import logging
8
+ import threading
9
+ from abc import ABC, ABCMeta, abstractmethod
10
+ from dataclasses import dataclass
11
+ from enum import IntEnum
12
+ from collections import defaultdict
13
+ from functools import cached_property
14
+ from datetime import timedelta, datetime
15
+ from typing import Iterable, Optional, Sequence, Tuple, Union, Any
16
+ from redis import StrictRedis as Redis
17
+
18
+ logger = logging.getLogger(__name__)
19
+ Timeout = Union[float, timedelta, None]
20
+
21
+
22
+ class LockStatus(IntEnum):
23
+ """Enumeration representing the status of a Redis lock.
24
+
25
+ The LockStatus enum defines the possible states of a Redis lock:
26
+
27
+ - FREE: The lock is not being used.
28
+ - UNAVAILABLE: The lock is being used by another program, or it has been marked as unavailable for a certain period of time.
29
+ - LOCKED: The lock is being used by the current program.
30
+ - ERROR: The lock is being used permanently, indicating a potential issue with the program.
31
+ """
32
+ FREE = 0x00
33
+ UNAVAILABLE = 0x01
34
+ LOCKED = 0x02
35
+ ERROR = 0x04
36
+
37
+
38
+ class BaseLock(ABC):
39
+ """Abstract base class defining the interface for lock implementations.
40
+
41
+ Attributes:
42
+ eps: Epsilon value for floating point comparison.
43
+ """
44
+ eps: float
45
+
46
+ def __init__(self, eps: float = 1e-6):
47
+ """Initialize a BaseLock instance.
48
+
49
+ Args:
50
+ eps: Epsilon value for floating point comparison.
51
+ """
52
+ self.eps = eps
53
+
54
+ @abstractmethod
55
+ def key_status(self, key: str, timeout: int = 120) -> LockStatus:
56
+ """Get the status of a key.
57
+
58
+ Args:
59
+ key: The key to check the status of.
60
+ timeout: The lock timeout in seconds.
61
+
62
+ Returns:
63
+ The current status of the key.
64
+ """
65
+ pass
66
+
67
+ @abstractmethod
68
+ def update(self, key: str, value='1', timeout: Timeout = 120):
69
+ """Lock a key for a specified duration without checking if the key is already locked.
70
+
71
+ Args:
72
+ key: The key to lock.
73
+ value: The value to set for the key.
74
+ timeout: The lock timeout in seconds.
75
+ """
76
+ pass
77
+
78
+ @abstractmethod
79
+ def lock(self, key: str, value: str = '1', timeout: Timeout = 120) -> bool:
80
+ """Try to lock a key for a specified duration.
81
+
82
+ Args:
83
+ key: The key to lock.
84
+ value: The value to set for the key.
85
+ timeout: The lock timeout in seconds.
86
+
87
+ Returns:
88
+ True if the ownership of the key is successfully acquired, False otherwise.
89
+ """
90
+ pass
91
+
92
+ @abstractmethod
93
+ def is_locked(self, key: str) -> bool:
94
+ """Check if a key is locked.
95
+
96
+ Args:
97
+ key: The key to check.
98
+
99
+ Returns:
100
+ True if the key is locked, False otherwise.
101
+ """
102
+ pass
103
+
104
+ @abstractmethod
105
+ def lock_value(self, key: str) -> Optional[str]:
106
+ """Get the value of a locked key.
107
+
108
+ Args:
109
+ key: The key to get the value of.
110
+
111
+ Returns:
112
+ The value of the key if the key is locked, None otherwise.
113
+ """
114
+ pass
115
+
116
+ @abstractmethod
117
+ def rlock(self, key: str, value: str = '1', timeout=120) -> bool:
118
+ """Try to lock a key for a specified duration.
119
+
120
+ When the value is the same as the current value, the function will return True.
121
+
122
+ Args:
123
+ key: The key to lock.
124
+ value: The value to set for the key.
125
+ timeout: The lock timeout in seconds.
126
+
127
+ Returns:
128
+ True if the ownership of the key is successfully acquired, False otherwise.
129
+ """
130
+ pass
131
+
132
+ @abstractmethod
133
+ def unlock(self, key: str) -> bool:
134
+ """Forcefully release a key without checking if the key is locked.
135
+
136
+ Args:
137
+ key: The key to release.
138
+
139
+ Returns:
140
+ True if the key is successfully released, False if the key is not locked.
141
+ """
142
+ pass
143
+
144
+ @abstractmethod
145
+ def _conditional_setdel(self, op: str, key: str, value: float, set_value: Optional[float] = None,
146
+ ex: Optional[int] = None, isdel: bool = False) -> bool:
147
+ """Conditionally set or del a key's value based on comparison with current value.
148
+
149
+ Args:
150
+ op: Comparison operator ('>', '<', '>=', '<=', '==', '!=').
151
+ key: The key to set or delete.
152
+ value: The value to compare with.
153
+ set_value: The value to set, if None, will use value instead.
154
+ ex: Optional expiration time in seconds.
155
+ isdel: Whether to delete the key or set the value if the condition is met.
156
+
157
+ Returns:
158
+ Whether the operation was successful.
159
+ """
160
+ pass
161
+
162
+ def setgt(self, key: str, value: float, set_value: Optional[float] = None, ex: Optional[int] = None) -> bool:
163
+ """Sets a new value when the comparison value is greater than the current value."""
164
+ return self._conditional_setdel('>', key, value, set_value, ex, False)
165
+
166
+ def setlt(self, key: str, value: float, set_value: Optional[float] = None, ex: Optional[int] = None) -> bool:
167
+ """Sets a new value when the comparison value is less than the current value."""
168
+ return self._conditional_setdel('<', key, value, set_value, ex, False)
169
+
170
+ def setge(self, key: str, value: float, set_value: Optional[float] = None, ex: Optional[int] = None) -> bool:
171
+ """Sets a new value when the comparison value is greater than or equal to the current value."""
172
+ return self._conditional_setdel('>=', key, value, set_value, ex, False)
173
+
174
+ def setle(self, key: str, value: float, set_value: Optional[float] = None, ex: Optional[int] = None) -> bool:
175
+ """Sets a new value when the comparison value is less than or equal to the current value."""
176
+ return self._conditional_setdel('<=', key, value, set_value, ex, False)
177
+
178
+ def seteq(self, key: str, value: float, set_value: Optional[float] = None, ex: Optional[int] = None) -> bool:
179
+ """Sets a new value when the comparison value is equal to the current value."""
180
+ return self._conditional_setdel('==', key, value, set_value, ex, False)
181
+
182
+ def setne(self, key: str, value: float, set_value: Optional[float] = None, ex: Optional[int] = None) -> bool:
183
+ """Sets a new value when the comparison value is not equal to the current value."""
184
+ return self._conditional_setdel('!=', key, value, set_value, ex, False)
185
+
186
+ def delgt(self, key: str, value: float):
187
+ """Deletes a key when the comparison value is greater than the current value."""
188
+ return self._conditional_setdel('>', key, value, None, None, True)
189
+
190
+ def dellt(self, key: str, value: float):
191
+ """Deletes a key when the comparison value is less than the current value."""
192
+ return self._conditional_setdel('<', key, value, None, None, True)
193
+
194
+ def delge(self, key: str, value: float):
195
+ """Deletes a key when the comparison value is greater than or equal to the current value."""
196
+ return self._conditional_setdel('>=', key, value, None, None, True)
197
+
198
+ def delle(self, key: str, value: float):
199
+ """Deletes a key when the comparison value is less than or equal to the current value."""
200
+ return self._conditional_setdel('<=', key, value, None, None, True)
201
+
202
+ def deleq(self, key: str, value: float):
203
+ """Deletes a key when the comparison value is equal to the current value."""
204
+ return self._conditional_setdel('==', key, value, None, None, True)
205
+
206
+ def delne(self, key: str, value: float):
207
+ """Deletes a key when the comparison value is not equal to the current value."""
208
+ return self._conditional_setdel('!=', key, value, None, None, True)
209
+
210
+ def _to_seconds(self, timeout: Timeout):
211
+ """Convert a timeout to seconds."""
212
+ if timeout is None:
213
+ timeout = datetime(2099, 1, 1).timestamp()
214
+ elif isinstance(timeout, timedelta):
215
+ timeout = timeout.total_seconds()
216
+ return timeout
217
+
218
+
219
+ class BaseLockPool(BaseLock, metaclass=ABCMeta):
220
+ """Abstract base class defining the interface for lock pool implementations.
221
+
222
+ A lock pool manages a collection of lock keys as a group, providing methods
223
+ to track, add, remove, and check lock status of multiple keys.
224
+
225
+ Attributes:
226
+ eps: Epsilon value for floating point comparison.
227
+ """
228
+
229
+ @abstractmethod
230
+ def extend(self, keys: Optional[Sequence[str]] = None):
231
+ """Extend the pool with the specified keys."""
232
+ pass
233
+
234
+ @abstractmethod
235
+ def shrink(self, keys: Sequence[str]):
236
+ """Shrink the pool by removing the specified keys."""
237
+ pass
238
+
239
+ @abstractmethod
240
+ def assign(self, keys: Optional[Sequence[str]] = None):
241
+ """Assign keys to the pool, replacing any existing keys."""
242
+ pass
243
+
244
+ @abstractmethod
245
+ def clear(self):
246
+ """Empty the pool."""
247
+ pass
248
+
249
+ @abstractmethod
250
+ def keys(self) -> Iterable[str]:
251
+ """Get the keys in the pool."""
252
+ pass
253
+
254
+ @abstractmethod
255
+ def _get_key_lock_status(self, keys: Iterable[str]) -> Iterable[bool]:
256
+ """Get the lock status of the specified keys."""
257
+ pass
258
+
259
+ def values_lock_status(self) -> Iterable[bool]:
260
+ """Get the lock status of all keys in the pool."""
261
+ return self._get_key_lock_status(self.keys())
262
+
263
+ def items_locked_status(self) -> Iterable[Tuple[str, bool]]:
264
+ """Get (key, lock_status) pairs for all keys in the pool."""
265
+ all_keys = list(self.keys())
266
+ return zip(all_keys, self._get_key_lock_status(all_keys))
267
+
268
+ def health_check(self) -> Tuple[int, int]:
269
+ """Check the health status of the keys in the pool.
270
+
271
+ Returns:
272
+ A tuple of (locked_count, free_count)
273
+ """
274
+ items = list(self.values_lock_status())
275
+ locked = sum(1 for item in items if item)
276
+ free = len(items) - locked
277
+ return locked, free
278
+
279
+ def __len__(self):
280
+ """Get the number of keys in the pool."""
281
+ return len(list(self.keys()))
282
+
283
+ def __iter__(self):
284
+ """Iterate over the keys in the pool."""
285
+ return iter(self.keys())
286
+
287
+
288
+ class RedisLock(BaseLock):
289
+ """Redis-based lock implementation.
290
+
291
+ Provides distributed locking capabilities using Redis as the backend storage.
292
+
293
+ Attributes:
294
+ redis: The Redis client instance.
295
+ prefix: Prefix for Redis keys.
296
+ suffix: Suffix for Redis keys.
297
+ eps: Epsilon value for floating point comparison.
298
+ """
299
+
300
+ redis: Redis
301
+ prefix: str
302
+ suffix: str
303
+
304
+ def __init__(self, redis: Redis, prefix: str, suffix="lock", eps: float = 1e-6):
305
+ """Initialize a RedisLock instance.
306
+
307
+ Args:
308
+ redis: Redis client instance.
309
+ prefix: Prefix for Redis keys.
310
+ suffix: Suffix for Redis keys.
311
+ eps: Epsilon value for floating point comparison.
312
+ """
313
+ assert "'" not in prefix and "'" not in suffix, "Prefix and suffix cannot contain single quotes"
314
+ assert redis.get_encoder().decode_responses, "Redis must be configured to decode responses"
315
+ super().__init__(eps=eps)
316
+ self.redis = redis
317
+ self.prefix = prefix
318
+ self.suffix = suffix
319
+
320
+ @property
321
+ def _lua_required_string(self):
322
+ return f'''
323
+ local function key_str(key)
324
+ return '{self.prefix}|{self.suffix}:' .. key
325
+ end
326
+ '''
327
+
328
+ def _key_str(self, key: str):
329
+ return f'{self.prefix}|{self.suffix}:{key}'
330
+
331
+ def key_status(self, key: str, timeout: int = 120) -> LockStatus:
332
+ ttl = self.redis.ttl(self._key_str(key))
333
+ if ttl > timeout: # If TTL is greater than the required expiration time, it means the usage is incorrect
334
+ return LockStatus.UNAVAILABLE
335
+ elif ttl >= 0:
336
+ return LockStatus.LOCKED
337
+ elif ttl == -1:
338
+ return LockStatus.ERROR # Permanent lock
339
+ return LockStatus.FREE
340
+
341
+ def update(self, key: str, value='1', timeout: Timeout = 120):
342
+ self.redis.set(self._key_str(key), value, ex=timeout)
343
+
344
+ def lock(self, key: str, value: str = '1', timeout: Timeout = 120) -> bool:
345
+ key_str = self._key_str(key)
346
+ return self.redis.set(key_str, value, ex=timeout, nx=True)
347
+
348
+ def is_locked(self, key: str) -> bool:
349
+ return self.redis.exists(self._key_str(key))
350
+
351
+ def lock_value(self, key: str) -> Optional[str]:
352
+ return self.redis.get(self._key_str(key))
353
+
354
+ def rlock(self, key: str, value: str = '1', timeout=120) -> bool:
355
+ key_str = self._key_str(key)
356
+ old_value = self.redis.set(key_str, value, ex=timeout, nx=True, get=True)
357
+ return old_value is None or old_value == value
358
+
359
+ def unlock(self, key: str) -> bool:
360
+ return bool(self.redis.delete(self._key_str(key)))
361
+
362
+ def _conditional_setdel(self, op: str, key: str, value: float, set_value: Optional[float] = None, ex: Optional[int] = None,
363
+ isdel: bool = False) -> bool:
364
+ # Convert None to a valid value for Redis (using -1 to indicate no expiration)
365
+ key_value = self._key_str(key)
366
+ ex_value = -1 if ex is None else ex
367
+ isdel_value = '1' if isdel else '0'
368
+ if set_value is None:
369
+ set_value = value
370
+ return self._conditional_setdel_script[op](keys=[key_value],
371
+ args=[value, set_value, ex_value, isdel_value])
372
+
373
+ @cached_property
374
+ def _conditional_setdel_script(self):
375
+ return {op: self.redis.register_script(self._conditional_setdel_lua_script(op, self.eps)) for op in
376
+ ('>', '<', '>=', '<=', '==', '!=')}
377
+
378
+ def _conditional_setdel_lua_script(self, op: str, eps: float = 1e-6) -> str:
379
+ match op:
380
+ case '>':
381
+ condition = 'compare_value > current_value'
382
+ case '<':
383
+ condition = 'compare_value < current_value'
384
+ case '>=':
385
+ condition = f'compare_value >= current_value - {eps}'
386
+ case '<=':
387
+ condition = f'compare_value <= current_value + {eps}'
388
+ case '==':
389
+ condition = f'abs(compare_value - current_value) < {eps}'
390
+ case '!=':
391
+ condition = f'abs(compare_value - current_value) > {eps}'
392
+ case _:
393
+ raise ValueError(f"Invalid operator: {op}")
394
+ return f'''
395
+ {self._lua_required_string}
396
+ local abs = math.abs
397
+ local current_key = KEYS[1]
398
+ local current_value = tonumber(redis.call('GET', current_key))
399
+ local compare_value = tonumber(ARGV[1])
400
+ local new_value = tonumber(ARGV[2])
401
+ local ex = tonumber(ARGV[3])
402
+ local isdel = ARGV[4] ~= '0'
403
+ if current_value == nil or {condition} then
404
+ if isdel then
405
+ redis.call('DEL', current_key)
406
+ else
407
+ if ex ~= nil and ex > 0 then
408
+ redis.call('SET', current_key, new_value, 'EX', ex)
409
+ else
410
+ redis.call('SET', current_key, new_value)
411
+ end
412
+ end
413
+ return true
414
+ end
415
+ return false
416
+ '''
417
+
418
+ def __eq__(self, value: Any) -> bool:
419
+ if isinstance(value, RedisLock):
420
+ return self.prefix == value.prefix and self.suffix == value.suffix
421
+ return False
422
+
423
+ def __hash__(self) -> int:
424
+ return hash((self.prefix, self.suffix))
425
+
426
+
427
+ class RedisLockPool(RedisLock, BaseLockPool):
428
+ """Redis-based lock pool implementation.
429
+
430
+ Manages a pool of Redis locks, stored as a Redis set.
431
+ """
432
+
433
+ def __init__(self, redis: Redis, prefix: str, suffix='lock-pool', eps: float = 1e-6):
434
+ """Initialize a RedisLockPool instance.
435
+
436
+ Args:
437
+ redis: Redis client instance.
438
+ prefix: Prefix for Redis keys.
439
+ suffix: Suffix for Redis keys.
440
+ eps: Epsilon value for floating point comparison.
441
+ """
442
+ super().__init__(redis, prefix, suffix=suffix, eps=eps)
443
+ assert redis.get_encoder().decode_responses, "Redis must be configured to decode responses"
444
+
445
+ @property
446
+ def _lua_required_string(self):
447
+ return f'''
448
+ {super()._lua_required_string}
449
+ local function pool_str()
450
+ return '{self._pool_str()}'
451
+ end
452
+ '''
453
+
454
+ def _pool_str(self):
455
+ """Returns the Redis key for the pool."""
456
+ return f'{self.prefix}|{self.suffix}|pool'
457
+
458
+ def extend(self, keys: Optional[Sequence[str]] = None):
459
+ """Extend the pool with the specified keys."""
460
+ if keys is not None and len(keys) > 0:
461
+ self.redis.sadd(self._pool_str(), *keys)
462
+
463
+ def shrink(self, keys: Sequence[str]):
464
+ """Shrink the pool by removing the specified keys."""
465
+ if keys is not None and len(keys) > 0:
466
+ self.redis.srem(self._pool_str(), *keys)
467
+
468
+ @property
469
+ def _assign_lua_string(self):
470
+ return f'''
471
+ {self._lua_required_string}
472
+ redis.call('DEL', pool_str())
473
+ redis.call('SADD', pool_str(), unpack(ARGV))
474
+ '''
475
+
476
+ @cached_property
477
+ def _assign_lua_script(self):
478
+ return self.redis.register_script(self._assign_lua_string)
479
+
480
+ def assign(self, keys: Optional[Sequence[str]] = None):
481
+ """Assign keys to the pool, replacing any existing keys."""
482
+ if keys is not None and len(keys) > 0:
483
+ self._assign_lua_script(args=keys)
484
+ else:
485
+ self.clear()
486
+
487
+ def clear(self):
488
+ """Empty the pool."""
489
+ self.redis.delete(self._pool_str())
490
+
491
+ def keys(self) -> Iterable[str]:
492
+ """Get the keys in the pool."""
493
+ return self.redis.smembers(self._pool_str())
494
+
495
+ def __contains__(self, key):
496
+ """Check if a key is in the pool."""
497
+ return self.redis.sismember(self._pool_str(), key)
498
+
499
+ def _get_key_lock_status(self, keys: Iterable[str]) -> Iterable[bool]:
500
+ """Get the lock status of the specified keys."""
501
+ return map(lambda x: x is not None, self.redis.mget(map(self._key_str, keys)))
502
+
503
+
504
+ @dataclass
505
+ class LockData:
506
+ """Data structure to store lock information.
507
+
508
+ Attributes:
509
+ value: The lock value.
510
+ expiry: The expiration timestamp.
511
+ """
512
+ value: str
513
+ expiry: float
514
+
515
+
516
+ class ThreadLock(BaseLock):
517
+ """Thread-safe lock implementation for local process use.
518
+
519
+ Provides similar functionality to RedisLock but works locally
520
+ within a single Python process using threading mechanisms.
521
+
522
+ Attributes:
523
+ eps: Epsilon value for floating point comparison.
524
+ """
525
+
526
+ def __init__(self, eps: float = 1e-6):
527
+ """Initialize a ThreadLock instance.
528
+
529
+ Args:
530
+ eps: Epsilon value for floating point comparison.
531
+ """
532
+ super().__init__(eps=eps)
533
+ self._locks = defaultdict(lambda: LockData(value='1', expiry=0))
534
+ self._lock = threading.RLock() # Thread lock to protect access to _locks
535
+
536
+ def _is_expired(self, key: str) -> bool:
537
+ """Check if a lock has expired."""
538
+ return self._get_ttl(key) <= 0
539
+
540
+ def _get_ttl(self, key: str):
541
+ """Get the TTL of a lock in seconds."""
542
+ return self._locks[key].expiry - time.time()
543
+
544
+ def key_status(self, key: str, timeout: int = 120) -> LockStatus:
545
+ """Get the status of a key."""
546
+ ttl = self._get_ttl(key)
547
+ if ttl <= 0:
548
+ return LockStatus.FREE
549
+ elif ttl > timeout:
550
+ return LockStatus.UNAVAILABLE
551
+ return LockStatus.LOCKED
552
+
553
+ def update(self, key: str, value='1', timeout: Timeout = 120):
554
+ """Lock a key for a specified duration without checking if already locked."""
555
+ expiry = time.time() + self._to_seconds(timeout)
556
+ self._locks[key] = LockData(value=value, expiry=expiry)
557
+
558
+ def lock(self, key: str, value: str = '1', timeout: Timeout = 120) -> bool:
559
+ """Try to lock a key for a specified duration."""
560
+ with self._lock:
561
+ if not self._is_expired(key):
562
+ return False
563
+ expiry = time.time() + self._to_seconds(timeout)
564
+ self._locks[key] = LockData(value=value, expiry=expiry)
565
+ return True
566
+
567
+ def is_locked(self, key: str) -> bool:
568
+ """Check if a key is locked."""
569
+ return not self._is_expired(key)
570
+
571
+ def lock_value(self, key: str) -> Optional[str]:
572
+ """Get the value of a locked key."""
573
+ data = self._locks[key]
574
+ if data.expiry <= time.time():
575
+ return None
576
+ return str(data.value)
577
+
578
+ def rlock(self, key: str, value: str = '1', timeout=120) -> bool:
579
+ """Try to relock a key for a specified duration."""
580
+ with self._lock:
581
+ data = self._locks[key]
582
+ if data.expiry > time.time() and data.value != value:
583
+ return False
584
+ expiry = time.time() + self._to_seconds(timeout)
585
+ self._locks[key] = LockData(value=value, expiry=expiry)
586
+ return True
587
+
588
+ def unlock(self, key: str) -> bool:
589
+ """Forcefully release a key."""
590
+ return self._locks.pop(key, None) is not None
591
+
592
+ def _compare_values(self, op: str, compare_value: float, current_value: float) -> bool:
593
+ """Compare two values using the specified operator."""
594
+ match op:
595
+ case '>':
596
+ return compare_value > current_value
597
+ case '<':
598
+ return compare_value < current_value
599
+ case '>=':
600
+ return compare_value >= current_value - self.eps
601
+ case '<=':
602
+ return compare_value <= current_value + self.eps
603
+ case '==':
604
+ return abs(compare_value - current_value) < self.eps
605
+ case '!=':
606
+ return abs(compare_value - current_value) > self.eps
607
+ case _:
608
+ raise ValueError(f"Invalid operator: {op}")
609
+
610
+ def _conditional_setdel(self, op: str, key: str, value: float, set_value: Optional[float] = None,
611
+ ex: Optional[int] = None, isdel: bool = False) -> bool:
612
+ """Conditionally set or delete a key's value based on comparison."""
613
+ compare_value = float(value)
614
+ if set_value is None:
615
+ set_value = value
616
+
617
+ with self._lock:
618
+ # Get current value if key exists and is not expired
619
+ current_data = self._locks[key]
620
+ if current_data.expiry <= time.time():
621
+ current_value = None
622
+ else:
623
+ current_value = float(current_data.value)
624
+
625
+ # Condition check
626
+ if current_value is None or self._compare_values(op, compare_value, current_value):
627
+ if isdel:
628
+ # Delete the key
629
+ self._locks.pop(key, None)
630
+ else:
631
+ # Set the key
632
+ expiry = time.time() + self._to_seconds(ex)
633
+ self._locks[key] = LockData(value=set_value, expiry=expiry)
634
+ return True
635
+ return False
636
+
637
+
638
+ class ThreadLockPool(ThreadLock, BaseLockPool):
639
+ """Thread-safe lock pool implementation for local process use.
640
+
641
+ Maintains a set of keys as the pool and provides operations to manage them.
642
+ """
643
+
644
+ def __init__(self, eps: float = 1e-6):
645
+ """Initialize a ThreadLockPool instance."""
646
+ super().__init__(eps=eps)
647
+ self._locks = defaultdict(lambda: LockData(value='1', expiry=0))
648
+ self._lock = threading.RLock()
649
+ self._pool = set()
650
+
651
+ def extend(self, keys: Optional[Sequence[str]] = None):
652
+ """Extend the pool with the specified keys."""
653
+ with self._lock:
654
+ if keys is not None:
655
+ self._pool.update(keys)
656
+
657
+ def shrink(self, keys: Sequence[str]):
658
+ """Shrink the pool by removing the specified keys."""
659
+ with self._lock:
660
+ self._pool.difference_update(keys)
661
+
662
+ def assign(self, keys: Optional[Sequence[str]] = None):
663
+ """Assign keys to the pool, replacing any existing keys."""
664
+ with self._lock:
665
+ self.clear()
666
+ self.extend(keys=keys)
667
+
668
+ def clear(self):
669
+ """Empty the pool."""
670
+ self._pool.clear()
671
+
672
+ def keys(self) -> Iterable[str]:
673
+ """Get the keys in the pool."""
674
+ return self._pool
675
+
676
+ def __contains__(self, key):
677
+ """Check if a key is in the pool."""
678
+ return key in self._pool
679
+
680
+ def _get_key_lock_status(self, keys: Iterable[str]) -> Iterable[bool]:
681
+ """Get the lock status of the specified keys."""
682
+ return [self.is_locked(key) for key in keys]