redis-allocator 0.0.1__py3-none-any.whl → 0.3.2__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/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
- Provides distributed locking capabilities using Redis as the backend storage.
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: The Redis client instance.
295
- prefix: Prefix for Redis keys.
296
- suffix: Suffix for Redis keys.
297
- eps: Epsilon value for floating point comparison.
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
- """Redis-based lock pool implementation.
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
- Manages a pool of Redis locks, stored as a Redis set.
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
- redis.call('DEL', pool_str())
473
- redis.call('SADD', pool_str(), unpack(ARGV))
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
- """Thread-safe lock implementation for local process use.
558
+ """In-memory, thread-safe lock implementation conforming to BaseLock.
518
559
 
519
- Provides similar functionality to RedisLock but works locally
520
- within a single Python process using threading mechanisms.
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 value for floating point comparison.
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
- """Thread-safe lock pool implementation for local process use.
683
+ """In-memory, thread-safe lock pool implementation.
640
684
 
641
- Maintains a set of keys as the pool and provides operations to manage them.
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):
@@ -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 in the Redis task queue system.
43
-
44
- This class encapsulates all information related to a task, including
45
- its unique ID, name, parameters, and execution status.
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 the task
49
- name: Name of the task category
50
- params: Dictionary of parameters for the task
51
- expiry: Unix timestamp when this task expires
52
- result: The result of the task, initially None
53
- error: Any error that occurred during task execution
54
- update_progress_time: Last time the progress was updated
55
- current_progress: Current progress value
56
- total_progress: Total progress value for completion
57
- _save: Internal callable to save task state to Redis
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
- """A class that provides a simple interface for managing a task queue in Redis.
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
- This class enables distributed task processing through Redis, with support for
97
- asynchronous processing, task listening, and result retrieval. Tasks can be
98
- executed locally or remotely based on configurable policies.
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)