redis-allocator 0.0.1__py3-none-any.whl → 0.3.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.
- redis_allocator/__init__.py +5 -1
- redis_allocator/_version.py +1 -0
- redis_allocator/allocator.py +816 -280
- redis_allocator/lock.py +66 -17
- redis_allocator/task_queue.py +81 -57
- redis_allocator-0.3.1.dist-info/METADATA +529 -0
- redis_allocator-0.3.1.dist-info/RECORD +15 -0
- {redis_allocator-0.0.1.dist-info → redis_allocator-0.3.1.dist-info}/licenses/LICENSE +21 -21
- tests/conftest.py +160 -46
- tests/test_allocator.py +461 -488
- tests/test_lock.py +675 -338
- tests/test_task_queue.py +136 -136
- redis_allocator-0.0.1.dist-info/METADATA +0 -229
- redis_allocator-0.0.1.dist-info/RECORD +0 -14
- {redis_allocator-0.0.1.dist-info → redis_allocator-0.3.1.dist-info}/WHEEL +0 -0
- {redis_allocator-0.0.1.dist-info → redis_allocator-0.3.1.dist-info}/top_level.txt +0 -0
redis_allocator/lock.py
CHANGED
@@ -207,7 +207,7 @@ class BaseLock(ABC):
|
|
207
207
|
"""Deletes a key when the comparison value is not equal to the current value."""
|
208
208
|
return self._conditional_setdel('!=', key, value, None, None, True)
|
209
209
|
|
210
|
-
def _to_seconds(self, timeout: Timeout):
|
210
|
+
def _to_seconds(self, timeout: Timeout) -> float:
|
211
211
|
"""Convert a timeout to seconds."""
|
212
212
|
if timeout is None:
|
213
213
|
timeout = datetime(2099, 1, 1).timestamp()
|
@@ -288,13 +288,14 @@ class BaseLockPool(BaseLock, metaclass=ABCMeta):
|
|
288
288
|
class RedisLock(BaseLock):
|
289
289
|
"""Redis-based lock implementation.
|
290
290
|
|
291
|
-
|
291
|
+
Uses standard Redis commands (SET with NX, EX options) for basic locking
|
292
|
+
and Lua scripts for conditional operations (set/del based on value comparison).
|
292
293
|
|
293
294
|
Attributes:
|
294
|
-
redis:
|
295
|
-
prefix: Prefix for Redis keys.
|
296
|
-
suffix: Suffix for Redis keys.
|
297
|
-
eps: Epsilon
|
295
|
+
redis: StrictRedis client instance (must decode responses).
|
296
|
+
prefix: Prefix for all Redis keys managed by this lock instance.
|
297
|
+
suffix: Suffix for Redis keys to distinguish lock types (e.g., 'lock').
|
298
|
+
eps: Epsilon for float comparisons in conditional Lua scripts.
|
298
299
|
"""
|
299
300
|
|
300
301
|
redis: Redis
|
@@ -319,6 +320,10 @@ class RedisLock(BaseLock):
|
|
319
320
|
|
320
321
|
@property
|
321
322
|
def _lua_required_string(self):
|
323
|
+
"""Base Lua script providing the key_str function.
|
324
|
+
|
325
|
+
- key_str(key: str): Constructs the full Redis key using prefix and suffix.
|
326
|
+
"""
|
322
327
|
return f'''
|
323
328
|
local function key_str(key)
|
324
329
|
return '{self.prefix}|{self.suffix}:' .. key
|
@@ -361,6 +366,11 @@ class RedisLock(BaseLock):
|
|
361
366
|
|
362
367
|
def _conditional_setdel(self, op: str, key: str, value: float, set_value: Optional[float] = None, ex: Optional[int] = None,
|
363
368
|
isdel: bool = False) -> bool:
|
369
|
+
"""Executes the conditional set/delete Lua script.
|
370
|
+
|
371
|
+
Passes necessary arguments (key, compare_value, set_value, expiry, isdel flag)
|
372
|
+
to the cached Lua script corresponding to the comparison operator (`op`).
|
373
|
+
"""
|
364
374
|
# Convert None to a valid value for Redis (using -1 to indicate no expiration)
|
365
375
|
key_value = self._key_str(key)
|
366
376
|
ex_value = -1 if ex is None else ex
|
@@ -376,6 +386,21 @@ class RedisLock(BaseLock):
|
|
376
386
|
('>', '<', '>=', '<=', '==', '!=')}
|
377
387
|
|
378
388
|
def _conditional_setdel_lua_script(self, op: str, eps: float = 1e-6) -> str:
|
389
|
+
"""Generates the Lua script for conditional set/delete operations.
|
390
|
+
|
391
|
+
Args:
|
392
|
+
op: The comparison operator ('>', '<', '>=', '<=', '==', '!=').
|
393
|
+
eps: Epsilon for floating-point comparisons ('==', '!=', '>=', '<=').
|
394
|
+
|
395
|
+
Returns:
|
396
|
+
A Lua script string that:
|
397
|
+
1. Gets the current numeric value of the target key (KEYS[1]).
|
398
|
+
2. Compares it with the provided compare_value (ARGV[1]) using the specified `op`.
|
399
|
+
3. If the key doesn't exist or the condition is true:
|
400
|
+
- If `isdel` (ARGV[4]) is true, deletes the key.
|
401
|
+
- Otherwise, sets the key to `new_value` (ARGV[2]) with optional expiry `ex` (ARGV[3]).
|
402
|
+
4. Returns true if the operation was performed, false otherwise.
|
403
|
+
"""
|
379
404
|
match op:
|
380
405
|
case '>':
|
381
406
|
condition = 'compare_value > current_value'
|
@@ -425,9 +450,14 @@ class RedisLock(BaseLock):
|
|
425
450
|
|
426
451
|
|
427
452
|
class RedisLockPool(RedisLock, BaseLockPool):
|
428
|
-
"""
|
453
|
+
"""Manages a collection of RedisLock keys as a logical pool.
|
454
|
+
|
455
|
+
Uses a Redis Set (`<prefix>|<suffix>|pool`) to store the identifiers (keys)
|
456
|
+
belonging to the pool. Inherits locking logic from RedisLock.
|
429
457
|
|
430
|
-
|
458
|
+
Provides methods to add (`extend`), remove (`shrink`), replace (`assign`),
|
459
|
+
and query (`keys`, `__contains__`) the members of the pool.
|
460
|
+
Also offers methods to check the lock status of pool members (`_get_key_lock_status`).
|
431
461
|
"""
|
432
462
|
|
433
463
|
def __init__(self, redis: Redis, prefix: str, suffix='lock-pool', eps: float = 1e-6):
|
@@ -444,6 +474,11 @@ class RedisLockPool(RedisLock, BaseLockPool):
|
|
444
474
|
|
445
475
|
@property
|
446
476
|
def _lua_required_string(self):
|
477
|
+
"""Base Lua script providing key_str and pool_str functions.
|
478
|
+
|
479
|
+
- key_str(key: str): Inherited from RedisLock.
|
480
|
+
- pool_str(): Returns the Redis key for the pool Set.
|
481
|
+
"""
|
447
482
|
return f'''
|
448
483
|
{super()._lua_required_string}
|
449
484
|
local function pool_str()
|
@@ -467,10 +502,16 @@ class RedisLockPool(RedisLock, BaseLockPool):
|
|
467
502
|
|
468
503
|
@property
|
469
504
|
def _assign_lua_string(self):
|
505
|
+
"""Lua script to atomically replace the contents of the pool Set.
|
506
|
+
|
507
|
+
1. Deletes the existing pool Set key (KEYS[1]).
|
508
|
+
2. Adds all provided keys (ARGV) to the (now empty) pool Set using SADD.
|
509
|
+
"""
|
470
510
|
return f'''
|
471
511
|
{self._lua_required_string}
|
472
|
-
|
473
|
-
redis.call('
|
512
|
+
local _pool_str = KEYS[1]
|
513
|
+
redis.call('DEL', _pool_str)
|
514
|
+
redis.call('SADD', _pool_str, unpack(ARGV))
|
474
515
|
'''
|
475
516
|
|
476
517
|
@cached_property
|
@@ -480,7 +521,7 @@ class RedisLockPool(RedisLock, BaseLockPool):
|
|
480
521
|
def assign(self, keys: Optional[Sequence[str]] = None):
|
481
522
|
"""Assign keys to the pool, replacing any existing keys."""
|
482
523
|
if keys is not None and len(keys) > 0:
|
483
|
-
self._assign_lua_script(args=keys)
|
524
|
+
self._assign_lua_script(args=keys, keys=[self._pool_str()])
|
484
525
|
else:
|
485
526
|
self.clear()
|
486
527
|
|
@@ -514,13 +555,16 @@ class LockData:
|
|
514
555
|
|
515
556
|
|
516
557
|
class ThreadLock(BaseLock):
|
517
|
-
"""
|
558
|
+
"""In-memory, thread-safe lock implementation conforming to BaseLock.
|
518
559
|
|
519
|
-
|
520
|
-
|
560
|
+
Simulates Redis lock behavior using Python's `threading.RLock` for concurrency
|
561
|
+
control and a `defaultdict` to store lock data (value and expiry timestamp).
|
562
|
+
Suitable for single-process scenarios or testing.
|
521
563
|
|
522
564
|
Attributes:
|
523
|
-
eps: Epsilon
|
565
|
+
eps: Epsilon for float comparisons.
|
566
|
+
_locks: defaultdict storing LockData(value, expiry) for each key.
|
567
|
+
_lock: threading.RLock protecting access to _locks.
|
524
568
|
"""
|
525
569
|
|
526
570
|
def __init__(self, eps: float = 1e-6):
|
@@ -636,9 +680,14 @@ class ThreadLock(BaseLock):
|
|
636
680
|
|
637
681
|
|
638
682
|
class ThreadLockPool(ThreadLock, BaseLockPool):
|
639
|
-
"""
|
683
|
+
"""In-memory, thread-safe lock pool implementation.
|
640
684
|
|
641
|
-
|
685
|
+
Manages a collection of lock keys using a Python `set` for the pool members
|
686
|
+
and inherits the locking logic from `ThreadLock`.
|
687
|
+
|
688
|
+
Attributes:
|
689
|
+
_pool: Set containing the keys belonging to this pool.
|
690
|
+
_lock: threading.RLock protecting access to _locks and _pool.
|
642
691
|
"""
|
643
692
|
|
644
693
|
def __init__(self, eps: float = 1e-6):
|
redis_allocator/task_queue.py
CHANGED
@@ -22,7 +22,7 @@ logger = logging.getLogger(__name__)
|
|
22
22
|
|
23
23
|
class TaskExecutePolicy(Enum):
|
24
24
|
"""Defines different policies for task execution.
|
25
|
-
|
25
|
+
|
26
26
|
Attributes:
|
27
27
|
Local: Execute task only locally
|
28
28
|
Remote: Execute task only remotely
|
@@ -39,22 +39,24 @@ class TaskExecutePolicy(Enum):
|
|
39
39
|
|
40
40
|
@dataclass
|
41
41
|
class RedisTask:
|
42
|
-
"""Represents a task
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
42
|
+
"""Represents a task to be processed via the RedisTaskQueue.
|
43
|
+
|
44
|
+
Encapsulates task details like ID, category (name), parameters, and its current state
|
45
|
+
(progress, result, error). It includes methods for saving state and updating progress.
|
46
|
+
|
47
47
|
Attributes:
|
48
|
-
id: Unique identifier for
|
49
|
-
name:
|
50
|
-
params: Dictionary
|
51
|
-
expiry: Unix timestamp when
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
48
|
+
id: Unique identifier for this specific task instance.
|
49
|
+
name: Categorical name for the task (used for routing in the queue).
|
50
|
+
params: Dictionary containing task-specific input parameters.
|
51
|
+
expiry: Absolute Unix timestamp when the task should be considered expired.
|
52
|
+
Used both locally and remotely to timeout waiting operations.
|
53
|
+
result: Stores the successful return value of the task execution.
|
54
|
+
error: Stores any exception raised during task execution.
|
55
|
+
update_progress_time: Timestamp of the last progress update.
|
56
|
+
current_progress: Current progress value (e.g., items processed).
|
57
|
+
total_progress: Total expected value for completion (e.g., total items).
|
58
|
+
_save: Internal callback function provided by RedisTaskQueue to persist
|
59
|
+
the task's state (result, error, progress) back to Redis.
|
58
60
|
"""
|
59
61
|
id: str
|
60
62
|
name: str
|
@@ -74,11 +76,11 @@ class RedisTask:
|
|
74
76
|
|
75
77
|
def update(self, current_progress: float, total_progress: float):
|
76
78
|
"""Update the progress of the task.
|
77
|
-
|
79
|
+
|
78
80
|
Args:
|
79
81
|
current_progress: The current progress value
|
80
82
|
total_progress: The total progress value for completion
|
81
|
-
|
83
|
+
|
82
84
|
Raises:
|
83
85
|
TimeoutError: If the task has expired
|
84
86
|
"""
|
@@ -91,14 +93,36 @@ class RedisTask:
|
|
91
93
|
|
92
94
|
|
93
95
|
class RedisTaskQueue:
|
94
|
-
"""
|
96
|
+
"""Provides a distributed task queue using Redis lists and key/value storage.
|
97
|
+
|
98
|
+
Enables submitting tasks (represented by `RedisTask` objects) to named queues
|
99
|
+
and having them processed either locally (if `task_fn` is provided) or by
|
100
|
+
remote listeners polling the corresponding Redis list.
|
95
101
|
|
96
|
-
|
97
|
-
|
98
|
-
|
102
|
+
Key Concepts:
|
103
|
+
- Task Queues: Redis lists (`<prefix>|<suffix>|task-queue|<name>`) where task IDs
|
104
|
+
are pushed for remote workers.
|
105
|
+
- Task Data: Serialized `RedisTask` objects stored in Redis keys
|
106
|
+
(`<prefix>|<suffix>|task-result:<id>`) with an expiry time.
|
107
|
+
This stores parameters, progress, results, and errors.
|
108
|
+
- Listeners: Remote workers use BLPOP on queue lists. To signal their presence,
|
109
|
+
they periodically set a listener key (`<prefix>|<suffix>|task-listen|<name>`).
|
110
|
+
- Execution Policies (`TaskExecutePolicy`): Control whether a task is executed
|
111
|
+
locally, remotely, or attempts one then the other.
|
112
|
+
`Auto` mode checks for a listener key.
|
113
|
+
- Task Function (`task_fn`): A user-provided function that takes a `RedisTask`
|
114
|
+
and performs the actual work locally.
|
115
|
+
|
116
|
+
Attributes:
|
117
|
+
redis: StrictRedis client instance.
|
118
|
+
prefix: Prefix for all Redis keys.
|
119
|
+
suffix: Suffix for Redis keys (default: 'task-queue').
|
120
|
+
timeout: Default expiry/timeout for tasks and listener keys (seconds).
|
121
|
+
interval: Polling interval for remote task fetching (seconds).
|
122
|
+
task_fn: Callable[[RedisTask], Any] to execute tasks locally.
|
99
123
|
"""
|
100
124
|
|
101
|
-
def __init__(self, redis: Redis, prefix: str, suffix='task-queue', timeout=300, interval=5,
|
125
|
+
def __init__(self, redis: Redis, prefix: str, suffix='task-queue', timeout=300, interval=5,
|
102
126
|
task_fn: Callable[[RedisTask], Any] = None):
|
103
127
|
"""Initialize a RedisTaskQueue instance.
|
104
128
|
|
@@ -119,12 +143,12 @@ class RedisTaskQueue:
|
|
119
143
|
|
120
144
|
def build_task(self, id: str, name: str, params: dict) -> RedisTask:
|
121
145
|
"""Create a new RedisTask instance with the given parameters.
|
122
|
-
|
146
|
+
|
123
147
|
Args:
|
124
148
|
id: Unique identifier for the task
|
125
149
|
name: Name of the task category
|
126
150
|
params: Dictionary of parameters for the task
|
127
|
-
|
151
|
+
|
128
152
|
Returns:
|
129
153
|
A new RedisTask instance with a save function configured
|
130
154
|
"""
|
@@ -134,15 +158,15 @@ class RedisTaskQueue:
|
|
134
158
|
|
135
159
|
def execute_task_remotely(self, task: RedisTask, timeout: Optional[float] = None, once: bool = False) -> Any:
|
136
160
|
"""Execute a task remotely by pushing it to the queue.
|
137
|
-
|
161
|
+
|
138
162
|
Args:
|
139
163
|
task: The RedisTask to execute
|
140
164
|
timeout: Optional timeout in seconds, defaults to self.timeout
|
141
165
|
once: Whether to delete the result after getting it
|
142
|
-
|
166
|
+
|
143
167
|
Returns:
|
144
168
|
The result of the task
|
145
|
-
|
169
|
+
|
146
170
|
Raises:
|
147
171
|
TimeoutError: If the task times out
|
148
172
|
Exception: Any exception raised during task execution
|
@@ -164,14 +188,14 @@ class RedisTaskQueue:
|
|
164
188
|
|
165
189
|
def execute_task_locally(self, task: RedisTask, timeout: Optional[float] = None) -> Any:
|
166
190
|
"""Execute a task locally using the task_fn.
|
167
|
-
|
191
|
+
|
168
192
|
Args:
|
169
193
|
task: The RedisTask to execute
|
170
194
|
timeout: Optional timeout in seconds, updates task.expiry if provided
|
171
|
-
|
195
|
+
|
172
196
|
Returns:
|
173
197
|
The result of the task
|
174
|
-
|
198
|
+
|
175
199
|
Raises:
|
176
200
|
Exception: Any exception raised during task execution
|
177
201
|
"""
|
@@ -189,7 +213,7 @@ class RedisTaskQueue:
|
|
189
213
|
@cached_property
|
190
214
|
def _queue_prefix(self) -> str:
|
191
215
|
"""Get the prefix for queue keys.
|
192
|
-
|
216
|
+
|
193
217
|
Returns:
|
194
218
|
The queue prefix
|
195
219
|
"""
|
@@ -200,7 +224,7 @@ class RedisTaskQueue:
|
|
200
224
|
|
201
225
|
Args:
|
202
226
|
name: The task name.
|
203
|
-
|
227
|
+
|
204
228
|
Returns:
|
205
229
|
The formatted queue key.
|
206
230
|
"""
|
@@ -208,13 +232,13 @@ class RedisTaskQueue:
|
|
208
232
|
|
209
233
|
def _queue_name(self, key: str) -> str:
|
210
234
|
"""Extract the queue name from a queue key.
|
211
|
-
|
235
|
+
|
212
236
|
Args:
|
213
237
|
key: The queue key.
|
214
|
-
|
238
|
+
|
215
239
|
Returns:
|
216
240
|
The queue name.
|
217
|
-
|
241
|
+
|
218
242
|
Raises:
|
219
243
|
AssertionError: If the key doesn't start with the queue prefix.
|
220
244
|
"""
|
@@ -223,10 +247,10 @@ class RedisTaskQueue:
|
|
223
247
|
|
224
248
|
def _queue_listen_name(self, name: str) -> str:
|
225
249
|
"""Generate a listen name for the given task name.
|
226
|
-
|
250
|
+
|
227
251
|
Args:
|
228
252
|
name: The task name.
|
229
|
-
|
253
|
+
|
230
254
|
Returns:
|
231
255
|
The formatted listen name.
|
232
256
|
"""
|
@@ -242,10 +266,10 @@ class RedisTaskQueue:
|
|
242
266
|
|
243
267
|
def _result_key(self, task_id: str) -> str:
|
244
268
|
"""Generate a result key for the given task ID.
|
245
|
-
|
269
|
+
|
246
270
|
Args:
|
247
271
|
task_id: The task ID.
|
248
|
-
|
272
|
+
|
249
273
|
Returns:
|
250
274
|
The formatted result key.
|
251
275
|
"""
|
@@ -253,10 +277,10 @@ class RedisTaskQueue:
|
|
253
277
|
|
254
278
|
def set_task(self, task: RedisTask) -> str:
|
255
279
|
"""Save a task to Redis.
|
256
|
-
|
280
|
+
|
257
281
|
Args:
|
258
282
|
task: The RedisTask to save.
|
259
|
-
|
283
|
+
|
260
284
|
Returns:
|
261
285
|
The task ID.
|
262
286
|
"""
|
@@ -268,11 +292,11 @@ class RedisTaskQueue:
|
|
268
292
|
|
269
293
|
def get_task(self, task_id: str, once: bool = False) -> Optional[RedisTask]:
|
270
294
|
"""Get a task from Redis.
|
271
|
-
|
295
|
+
|
272
296
|
Args:
|
273
297
|
task_id: The task ID.
|
274
298
|
once: Whether to delete the task after getting it.
|
275
|
-
|
299
|
+
|
276
300
|
Returns:
|
277
301
|
The RedisTask, or None if no task is available.
|
278
302
|
"""
|
@@ -286,10 +310,10 @@ class RedisTaskQueue:
|
|
286
310
|
|
287
311
|
def has_task(self, task_id: str) -> bool:
|
288
312
|
"""Check if a task exists.
|
289
|
-
|
313
|
+
|
290
314
|
Args:
|
291
315
|
task_id: The task ID.
|
292
|
-
|
316
|
+
|
293
317
|
Returns:
|
294
318
|
True if the task exists, False otherwise.
|
295
319
|
"""
|
@@ -298,12 +322,12 @@ class RedisTaskQueue:
|
|
298
322
|
@contextmanager
|
299
323
|
def _executor_context(self, max_workers: int = 128):
|
300
324
|
"""Create a ThreadPoolExecutor context manager.
|
301
|
-
|
325
|
+
|
302
326
|
This is a helper method for testing and internal use.
|
303
|
-
|
327
|
+
|
304
328
|
Args:
|
305
329
|
max_workers: The maximum number of worker threads.
|
306
|
-
|
330
|
+
|
307
331
|
Yields:
|
308
332
|
The ThreadPoolExecutor instance.
|
309
333
|
"""
|
@@ -312,10 +336,10 @@ class RedisTaskQueue:
|
|
312
336
|
|
313
337
|
def listen(self, names: List[str], workers: int = 128, event: Optional[Event] = None) -> None:
|
314
338
|
"""Listen for tasks on the specified queues.
|
315
|
-
|
316
|
-
This method continuously polls the specified queues for tasks,
|
339
|
+
|
340
|
+
This method continuously polls the specified queues for tasks,
|
317
341
|
and executes tasks locally when they are received.
|
318
|
-
|
342
|
+
|
319
343
|
Args:
|
320
344
|
names: The names of the queues to listen to.
|
321
345
|
workers: The number of worker threads to use. Default is 128.
|
@@ -339,10 +363,10 @@ class RedisTaskQueue:
|
|
339
363
|
def query(self, id: str, name: str, params: dict, timeout: Optional[float] = None,
|
340
364
|
policy: TaskExecutePolicy = TaskExecutePolicy.Auto, once: bool = False) -> Any:
|
341
365
|
"""Execute a task according to the specified policy.
|
342
|
-
|
366
|
+
|
343
367
|
This method provides a flexible way to execute tasks with different
|
344
368
|
strategies based on the specified policy.
|
345
|
-
|
369
|
+
|
346
370
|
Args:
|
347
371
|
id: The task ID.
|
348
372
|
name: The task name.
|
@@ -350,10 +374,10 @@ class RedisTaskQueue:
|
|
350
374
|
timeout: Optional timeout override.
|
351
375
|
policy: The execution policy to use.
|
352
376
|
once: Whether to delete the result after getting it.
|
353
|
-
|
377
|
+
|
354
378
|
Returns:
|
355
379
|
The result of the task.
|
356
|
-
|
380
|
+
|
357
381
|
Raises:
|
358
382
|
Exception: Any exception raised during task execution.
|
359
383
|
"""
|
@@ -379,4 +403,4 @@ class RedisTaskQueue:
|
|
379
403
|
if self.redis.exists(self._queue_listen_name(name)):
|
380
404
|
return self.execute_task_remotely(t, timeout)
|
381
405
|
else:
|
382
|
-
return self.execute_task_locally(t, timeout)
|
406
|
+
return self.execute_task_locally(t, timeout)
|