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.
- redis_allocator/__init__.py +28 -0
- redis_allocator/allocator.py +601 -0
- redis_allocator/lock.py +682 -0
- redis_allocator/task_queue.py +382 -0
- redis_allocator-0.0.1.dist-info/METADATA +229 -0
- redis_allocator-0.0.1.dist-info/RECORD +14 -0
- redis_allocator-0.0.1.dist-info/WHEEL +5 -0
- redis_allocator-0.0.1.dist-info/licenses/LICENSE +21 -0
- redis_allocator-0.0.1.dist-info/top_level.txt +2 -0
- tests/__init__.py +0 -0
- tests/conftest.py +46 -0
- tests/test_allocator.py +525 -0
- tests/test_lock.py +851 -0
- tests/test_task_queue.py +778 -0
@@ -0,0 +1,28 @@
|
|
1
|
+
"""RedisAllocator package for distributed memory allocation using Redis.
|
2
|
+
|
3
|
+
This package provides efficient, Redis-based distributed memory allocation
|
4
|
+
services that simulate traditional memory allocation mechanisms in a
|
5
|
+
distributed environment.
|
6
|
+
"""
|
7
|
+
|
8
|
+
from redis_allocator.lock import (RedisLock, RedisLockPool, LockStatus,
|
9
|
+
BaseLock, BaseLockPool, ThreadLock, ThreadLockPool)
|
10
|
+
from redis_allocator.task_queue import TaskExecutePolicy, RedisTask, RedisTaskQueue
|
11
|
+
from redis_allocator.allocator import RedisAllocator
|
12
|
+
|
13
|
+
|
14
|
+
__version__ = '0.0.1'
|
15
|
+
|
16
|
+
__all__ = [
|
17
|
+
'RedisLock',
|
18
|
+
'RedisLockPool',
|
19
|
+
'LockStatus',
|
20
|
+
'BaseLock',
|
21
|
+
'BaseLockPool',
|
22
|
+
'ThreadLock',
|
23
|
+
'ThreadLockPool',
|
24
|
+
'TaskExecutePolicy',
|
25
|
+
'RedisTask',
|
26
|
+
'RedisTaskQueue',
|
27
|
+
'RedisAllocator',
|
28
|
+
]
|
@@ -0,0 +1,601 @@
|
|
1
|
+
"""Redis-based distributed memory allocation system.
|
2
|
+
|
3
|
+
This module provides the core functionality of the RedisAllocator system,
|
4
|
+
allowing for distributed memory allocation with support for garbage collection,
|
5
|
+
thread health checking, and priority-based allocation mechanisms.
|
6
|
+
"""
|
7
|
+
import logging
|
8
|
+
import weakref
|
9
|
+
from abc import ABC, abstractmethod
|
10
|
+
from functools import cached_property
|
11
|
+
from threading import current_thread
|
12
|
+
from typing import (Optional, TypeVar, Generic,
|
13
|
+
Sequence, Iterable)
|
14
|
+
from redis import StrictRedis as Redis
|
15
|
+
from .lock import RedisLockPool, Timeout
|
16
|
+
|
17
|
+
logger = logging.getLogger(__name__)
|
18
|
+
|
19
|
+
|
20
|
+
class RedisAllocatableClass(ABC):
|
21
|
+
@abstractmethod
|
22
|
+
def set_config(self, key: str, params: dict):
|
23
|
+
"""Set the configuration for the object.
|
24
|
+
|
25
|
+
Args:
|
26
|
+
key: The key to set the configuration for.
|
27
|
+
params: The parameters to set the configuration for.
|
28
|
+
"""
|
29
|
+
pass
|
30
|
+
|
31
|
+
def close(self):
|
32
|
+
"""close the object."""
|
33
|
+
pass
|
34
|
+
|
35
|
+
def name(self) -> Optional[str]:
|
36
|
+
"""Get the cache name of the object."""
|
37
|
+
return None
|
38
|
+
|
39
|
+
|
40
|
+
U = TypeVar('U', bound=RedisAllocatableClass)
|
41
|
+
|
42
|
+
|
43
|
+
class RedisThreadHealthCheckPool(RedisLockPool):
|
44
|
+
"""A class that provides a simple interface for managing the health status of a thread.
|
45
|
+
|
46
|
+
This class enables tracking the health status of threads in a distributed environment
|
47
|
+
using Redis locks.
|
48
|
+
"""
|
49
|
+
|
50
|
+
def __init__(self, redis: Redis, identity: str, timeout: int):
|
51
|
+
"""Initialize a RedisThreadHealthCheckPool instance.
|
52
|
+
|
53
|
+
Args:
|
54
|
+
redis: The Redis client used for interacting with Redis.
|
55
|
+
identity: The identity prefix for the health checker.
|
56
|
+
timeout: The timeout for health checks in seconds.
|
57
|
+
tasks: A list of thread identifiers to track.
|
58
|
+
"""
|
59
|
+
super().__init__(redis, identity, "thread-health-check-pool")
|
60
|
+
self.timeout = timeout
|
61
|
+
self.initialize()
|
62
|
+
|
63
|
+
@property
|
64
|
+
def current_thread_id(self) -> str:
|
65
|
+
"""Get the current thread ID.
|
66
|
+
|
67
|
+
Returns:
|
68
|
+
The current thread ID as a string.
|
69
|
+
"""
|
70
|
+
return str(current_thread().ident)
|
71
|
+
|
72
|
+
def initialize(self):
|
73
|
+
"""Initialize the health status."""
|
74
|
+
self.update()
|
75
|
+
self.extend([self.current_thread_id])
|
76
|
+
|
77
|
+
def update(self): # pylint: disable=arguments-differ
|
78
|
+
"""Update the health status."""
|
79
|
+
super().update(self.current_thread_id, timeout=self.timeout)
|
80
|
+
|
81
|
+
def finalize(self):
|
82
|
+
"""Finalize the health status."""
|
83
|
+
self.shrink([self.current_thread_id])
|
84
|
+
self.unlock(self.current_thread_id)
|
85
|
+
|
86
|
+
|
87
|
+
class RedisAllocatorObject(Generic[U]):
|
88
|
+
"""Represents an object allocated through RedisAllocator.
|
89
|
+
|
90
|
+
This class provides an interface for working with allocated objects
|
91
|
+
including locking and unlocking mechanisms for thread-safe operations.
|
92
|
+
|
93
|
+
"""
|
94
|
+
_allocator: 'RedisAllocator' # Reference to the allocator that created this object
|
95
|
+
key: str # Redis key for this allocated object
|
96
|
+
params: Optional[dict] # Parameters associated with this object
|
97
|
+
obj: Optional[U] # The actual object being allocated
|
98
|
+
|
99
|
+
def __init__(self, allocator: 'RedisAllocator', key: str, obj: Optional[U] = None, params: Optional[dict] = None):
|
100
|
+
"""Initialize a RedisAllocatorObject instance.
|
101
|
+
|
102
|
+
Args:
|
103
|
+
allocator: The RedisAllocator that created this object
|
104
|
+
key: The Redis key for this allocated object
|
105
|
+
obj: The actual object being allocated
|
106
|
+
params: Additional parameters passed by local program
|
107
|
+
"""
|
108
|
+
self._allocator = allocator
|
109
|
+
self.key = key
|
110
|
+
self.obj = obj
|
111
|
+
self.params = params
|
112
|
+
if self.obj is not None:
|
113
|
+
self.obj.set_config(key, params)
|
114
|
+
|
115
|
+
def update(self, timeout: Timeout = 120):
|
116
|
+
"""Lock this object for exclusive access.
|
117
|
+
|
118
|
+
Args:
|
119
|
+
timeout: How long the lock should be valid (in seconds)
|
120
|
+
"""
|
121
|
+
if timeout > 0:
|
122
|
+
self._allocator.update(self.key, timeout=timeout)
|
123
|
+
else:
|
124
|
+
self._allocator.free(self)
|
125
|
+
|
126
|
+
def close(self):
|
127
|
+
"""Kill the object."""
|
128
|
+
if self.obj is not None:
|
129
|
+
self.obj.close()
|
130
|
+
|
131
|
+
def __del__(self):
|
132
|
+
"""Delete the object."""
|
133
|
+
self.close()
|
134
|
+
|
135
|
+
|
136
|
+
class RedisAllocator(RedisLockPool, Generic[U]):
|
137
|
+
"""A Redis-based distributed memory allocation system.
|
138
|
+
|
139
|
+
This class implements a memory allocation interface using Redis as the backend store.
|
140
|
+
It manages a pool of resources that can be allocated, freed, and garbage collected.
|
141
|
+
The implementation uses a doubly-linked list structure stored in Redis hashes to
|
142
|
+
track available and allocated resources.
|
143
|
+
|
144
|
+
Generic type U must implement the RedisContextableObject protocol, allowing
|
145
|
+
allocated objects to be used as context managers.
|
146
|
+
"""
|
147
|
+
|
148
|
+
def __init__(self, redis: Redis, prefix: str, suffix='allocator', eps=1e-6,
|
149
|
+
shared=False):
|
150
|
+
"""Initialize a RedisAllocator instance.
|
151
|
+
|
152
|
+
Args:
|
153
|
+
redis: Redis client for communication with Redis server
|
154
|
+
prefix: Prefix for all Redis keys used by this allocator
|
155
|
+
suffix: Suffix for Redis keys to uniquely identify this allocator
|
156
|
+
eps: Small value for floating point comparisons
|
157
|
+
shared: Whether resources can be shared across multiple consumers
|
158
|
+
"""
|
159
|
+
super().__init__(redis, prefix, suffix=suffix, eps=eps)
|
160
|
+
self.shared = shared
|
161
|
+
self.soft_bind_timeout = 3600 # Default timeout for soft bindings (1 hour)
|
162
|
+
self.objects: weakref.WeakValueDictionary[str, RedisAllocatorObject] = weakref.WeakValueDictionary()
|
163
|
+
|
164
|
+
def object_key(self, key: str, obj: U):
|
165
|
+
"""Get the key for an object."""
|
166
|
+
if not self.shared:
|
167
|
+
return key
|
168
|
+
return f'{key}:{obj}'
|
169
|
+
|
170
|
+
def _pool_pointer_str(self, head: bool = True):
|
171
|
+
"""Get the Redis key for the head or tail pointer of the allocation pool.
|
172
|
+
|
173
|
+
Args:
|
174
|
+
head: If True, get the head pointer key; otherwise, get the tail pointer key
|
175
|
+
|
176
|
+
Returns:
|
177
|
+
String representation of the Redis key for the pointer
|
178
|
+
"""
|
179
|
+
pointer_type = 'head' if head else 'tail'
|
180
|
+
return f'{self._pool_str()}|{pointer_type}'
|
181
|
+
|
182
|
+
def _gc_cursor_str(self):
|
183
|
+
"""Get the Redis key for the garbage collection cursor.
|
184
|
+
|
185
|
+
Returns:
|
186
|
+
String representation of the Redis key for the GC cursor
|
187
|
+
"""
|
188
|
+
return f'{self._pool_str()}|gc_cursor'
|
189
|
+
|
190
|
+
@property
|
191
|
+
def _lua_required_string(self):
|
192
|
+
"""LUA script containing helper functions for Redis operations.
|
193
|
+
|
194
|
+
This script defines various LUA functions for manipulating the doubly-linked list
|
195
|
+
structure that represents the allocation pool:
|
196
|
+
- pool_pointer_str: Get Redis keys for head/tail pointers
|
197
|
+
- gc_cursor_str: Get Redis key for GC cursor
|
198
|
+
- push_to_tail: Add an item to the tail of the linked list
|
199
|
+
- pop_from_head: Remove and return the item at the head of the linked list
|
200
|
+
- delete_item: Remove an item from anywhere in the linked list
|
201
|
+
"""
|
202
|
+
return f'''
|
203
|
+
{super()._lua_required_string}
|
204
|
+
local function pool_pointer_str(head)
|
205
|
+
local pointer_type = 'head'
|
206
|
+
if not head then
|
207
|
+
pointer_type = 'tail'
|
208
|
+
end
|
209
|
+
return '{self._pool_str()}|' .. pointer_type
|
210
|
+
end
|
211
|
+
local function gc_cursor_str()
|
212
|
+
return '{self._pool_str()}|gc_cursor'
|
213
|
+
end
|
214
|
+
local poolItemsKey = pool_str()
|
215
|
+
local headKey = pool_pointer_str(true)
|
216
|
+
local tailKey = pool_pointer_str(false)
|
217
|
+
local function push_to_tail(itemName)
|
218
|
+
local tail = redis.call("GET", tailKey)
|
219
|
+
if not tail then
|
220
|
+
tail = ""
|
221
|
+
end
|
222
|
+
redis.call("HSET", poolItemsKey, itemName, tostring(tail) .. "||")
|
223
|
+
if tail == "" then
|
224
|
+
redis.call("SET", headKey, itemName)
|
225
|
+
else
|
226
|
+
local tailVal = redis.call("HGET", poolItemsKey, tail)
|
227
|
+
local prev, _ = string.match(tailVal, "(.*)||(.*)")
|
228
|
+
redis.call("HSET", poolItemsKey, tail, tostring(prev) .. "||" .. tostring(itemName))
|
229
|
+
end
|
230
|
+
redis.call("SET", tailKey, itemName)
|
231
|
+
end
|
232
|
+
local function pop_from_head()
|
233
|
+
local head = redis.call("GET", headKey)
|
234
|
+
if head == nil or head == "" then
|
235
|
+
return nil
|
236
|
+
end
|
237
|
+
local headVal = redis.call("HGET", poolItemsKey, head)
|
238
|
+
if headVal == nil then
|
239
|
+
redis.call("SET", headKey, "")
|
240
|
+
redis.call("SET", tailKey, "")
|
241
|
+
return nil
|
242
|
+
else
|
243
|
+
assert(headVal ~= "#ALLOCATED", "head is allocated")
|
244
|
+
end
|
245
|
+
local _, next = string.match(headVal, "(.*)||(.*)")
|
246
|
+
if next == "" then
|
247
|
+
redis.call("SET", headKey, "")
|
248
|
+
redis.call("SET", tailKey, "")
|
249
|
+
else
|
250
|
+
local nextVal = redis.call("HGET", poolItemsKey, next)
|
251
|
+
local prev, _ = string.match(nextVal, "(.*)||(.*)")
|
252
|
+
redis.call("HSET", poolItemsKey, next, tostring("") .. "||" .. tostring(prev))
|
253
|
+
redis.call("SET", headKey, next)
|
254
|
+
end
|
255
|
+
return head
|
256
|
+
end
|
257
|
+
local function delete_item(itemName)
|
258
|
+
local val = redis.call("HGET", poolItemsKey, itemName)
|
259
|
+
if val ~= '#ALLOCATED' then
|
260
|
+
local prev, next = string.match(val, "(.*)||(.*)")
|
261
|
+
if prev ~= "" then
|
262
|
+
local prevVal = redis.call("HGET", poolItemsKey, prev)
|
263
|
+
if prevVal then
|
264
|
+
local p, _ = string.match(prevVal, "(.*)||(.*)")
|
265
|
+
redis.call("HSET", poolItemsKey, prev, tostring(p) .. "||" .. tostring(next))
|
266
|
+
end
|
267
|
+
else
|
268
|
+
redis.call("SET", headKey, next or "")
|
269
|
+
end
|
270
|
+
if next ~= "" then
|
271
|
+
local nxtVal = redis.call("HGET", poolItemsKey, next)
|
272
|
+
if nxtVal then
|
273
|
+
local _, n = string.match(nxtVal, "(.*)||(.*)")
|
274
|
+
redis.call("HSET", poolItemsKey, next, tostring(prev) .. "||" .. tostring(n))
|
275
|
+
end
|
276
|
+
else
|
277
|
+
redis.call("SET", tailKey, prev or "")
|
278
|
+
end
|
279
|
+
end
|
280
|
+
redis.call("HDEL", poolItemsKey, itemName)
|
281
|
+
end
|
282
|
+
'''
|
283
|
+
|
284
|
+
@property
|
285
|
+
def _extend_lua_string(self):
|
286
|
+
"""LUA script for extending the allocation pool with new resources.
|
287
|
+
|
288
|
+
This script adds new items to the pool if they don't already exist.
|
289
|
+
New items are added to the tail of the linked list.
|
290
|
+
"""
|
291
|
+
return f'''{self._lua_required_string}
|
292
|
+
for _, itemName in ipairs(ARGV) do
|
293
|
+
if redis.call("HEXISTS", poolItemsKey, itemName) == 1 then
|
294
|
+
-- todo: gc check
|
295
|
+
else
|
296
|
+
push_to_tail(itemName)
|
297
|
+
end
|
298
|
+
end
|
299
|
+
'''
|
300
|
+
|
301
|
+
@cached_property
|
302
|
+
def _extend_script(self):
|
303
|
+
"""Cached Redis script for extending the allocation pool."""
|
304
|
+
return self.redis.register_script(self._extend_lua_string)
|
305
|
+
|
306
|
+
def extend(self, keys: Optional[Sequence[str]] = None):
|
307
|
+
"""Add new resources to the allocation pool.
|
308
|
+
|
309
|
+
Args:
|
310
|
+
keys: Sequence of resource identifiers to add to the pool
|
311
|
+
"""
|
312
|
+
if keys is not None and len(keys) > 0:
|
313
|
+
self._extend_script(args=keys)
|
314
|
+
|
315
|
+
@property
|
316
|
+
def _shrink_lua_string(self):
|
317
|
+
"""LUA script for removing resources from the allocation pool.
|
318
|
+
|
319
|
+
This script removes specified items from the pool by deleting them
|
320
|
+
from the linked list structure.
|
321
|
+
"""
|
322
|
+
return f'''{self._lua_required_string}
|
323
|
+
for _, itemName in ipairs(ARGV) do
|
324
|
+
delete_item(itemName)
|
325
|
+
end
|
326
|
+
'''
|
327
|
+
|
328
|
+
@cached_property
|
329
|
+
def _shrink_script(self):
|
330
|
+
"""Cached Redis script for shrinking the allocation pool."""
|
331
|
+
return self.redis.register_script(self._shrink_lua_string)
|
332
|
+
|
333
|
+
def shrink(self, keys: Optional[Sequence[str]] = None):
|
334
|
+
"""Remove resources from the allocation pool.
|
335
|
+
|
336
|
+
Args:
|
337
|
+
keys: Sequence of resource identifiers to remove from the pool
|
338
|
+
"""
|
339
|
+
if keys is not None and len(keys) > 0:
|
340
|
+
self._shrink_script(args=keys)
|
341
|
+
|
342
|
+
@property
|
343
|
+
def _assign_lua_string(self):
|
344
|
+
"""LUA script for completely replacing the resources in the allocation pool.
|
345
|
+
|
346
|
+
This script clears the existing pool and replaces it with a new set of resources.
|
347
|
+
Items not in the new set are removed, and items in the new set but not in the
|
348
|
+
existing pool are added to the tail of the linked list.
|
349
|
+
"""
|
350
|
+
return f'''{self._lua_required_string}
|
351
|
+
local wantSet = {{}}
|
352
|
+
for _, k in ipairs(ARGV) do
|
353
|
+
wantSet[k] = true
|
354
|
+
end
|
355
|
+
local allItems = redis.call("HKEYS", poolItemsKey)
|
356
|
+
for _, itemName in ipairs(allItems) do
|
357
|
+
if not wantSet[itemName] then
|
358
|
+
delete_item(itemName)
|
359
|
+
else
|
360
|
+
wantSet[itemName] = nil
|
361
|
+
end
|
362
|
+
end
|
363
|
+
for k, v in pairs(wantSet) do
|
364
|
+
if v then
|
365
|
+
push_to_tail(k)
|
366
|
+
end
|
367
|
+
end
|
368
|
+
'''
|
369
|
+
|
370
|
+
@cached_property
|
371
|
+
def _assign_lua_script(self):
|
372
|
+
"""Cached Redis script for assigning resources to the allocation pool."""
|
373
|
+
return self.redis.register_script(self._assign_lua_string)
|
374
|
+
|
375
|
+
def assign(self, keys: Optional[Sequence[str]] = None):
|
376
|
+
"""Completely replace the resources in the allocation pool.
|
377
|
+
|
378
|
+
Args:
|
379
|
+
keys: Sequence of resource identifiers to assign to the pool,
|
380
|
+
replacing any existing resources
|
381
|
+
"""
|
382
|
+
if keys is not None and len(keys) > 0:
|
383
|
+
self._assign_lua_script(args=keys)
|
384
|
+
else:
|
385
|
+
self.clear()
|
386
|
+
|
387
|
+
def keys(self) -> Iterable[str]:
|
388
|
+
"""Get all resource identifiers in the allocation pool.
|
389
|
+
|
390
|
+
Returns:
|
391
|
+
Iterable of resource identifiers in the pool
|
392
|
+
"""
|
393
|
+
return self.redis.hkeys(self._pool_str())
|
394
|
+
|
395
|
+
def __contains__(self, key):
|
396
|
+
"""Check if a resource identifier is in the allocation pool.
|
397
|
+
|
398
|
+
Args:
|
399
|
+
key: Resource identifier to check
|
400
|
+
|
401
|
+
Returns:
|
402
|
+
True if the resource is in the pool, False otherwise
|
403
|
+
"""
|
404
|
+
return self.redis.hexists(self._pool_str(), key)
|
405
|
+
|
406
|
+
@property
|
407
|
+
def _cache_str(self):
|
408
|
+
"""Get the Redis key for the allocator's cache.
|
409
|
+
|
410
|
+
Returns:
|
411
|
+
String representation of the Redis key for the cache
|
412
|
+
"""
|
413
|
+
return f'{self.prefix}|{self.suffix}-cache'
|
414
|
+
|
415
|
+
def clear(self):
|
416
|
+
"""Clear all resources from the allocation pool and cache."""
|
417
|
+
super().clear()
|
418
|
+
self.redis.delete(self._cache_str)
|
419
|
+
|
420
|
+
def _soft_bind_name(self, name: str) -> str:
|
421
|
+
"""Get the Redis key for a soft binding.
|
422
|
+
|
423
|
+
Args:
|
424
|
+
name: Name of the soft binding
|
425
|
+
|
426
|
+
Returns:
|
427
|
+
String representation of the Redis key for the soft binding
|
428
|
+
"""
|
429
|
+
return f"{self._cache_str}:bind:{name}"
|
430
|
+
|
431
|
+
def update_soft_bind(self, name: str, key: str):
|
432
|
+
"""Update a soft binding between a name and a resource.
|
433
|
+
|
434
|
+
Args:
|
435
|
+
name: Name to bind
|
436
|
+
key: Resource identifier to bind to the name
|
437
|
+
"""
|
438
|
+
self.update(self._soft_bind_name(name), key, timeout=self.soft_bind_timeout)
|
439
|
+
|
440
|
+
def unbind_soft_bind(self, name: str):
|
441
|
+
"""Remove a soft binding.
|
442
|
+
|
443
|
+
Args:
|
444
|
+
name: Name of the soft binding to remove
|
445
|
+
"""
|
446
|
+
self.unlock(self._soft_bind_name(name))
|
447
|
+
|
448
|
+
@property
|
449
|
+
def _malloc_lua_script(self):
|
450
|
+
"""LUA script for allocating a resource from the pool.
|
451
|
+
|
452
|
+
This script allocates a resource by popping an item from the head
|
453
|
+
of the linked list and marking it as allocated. If the allocator
|
454
|
+
is not shared, the script also sets a lock on the allocated resource.
|
455
|
+
"""
|
456
|
+
return f'''
|
457
|
+
{self._lua_required_string}
|
458
|
+
local shared = {1 if self.shared else 0}
|
459
|
+
local timeout = tonumber(ARGV[1])
|
460
|
+
local itemName = pop_from_head()
|
461
|
+
if itemName ~= nil then
|
462
|
+
if redis.call("EXISTS", key_str(itemName)) == 1 then
|
463
|
+
itemName = nil
|
464
|
+
else
|
465
|
+
if not shared then
|
466
|
+
if timeout ~= nil then
|
467
|
+
redis.call("SET", key_str(itemName), "1", "EX", timeout)
|
468
|
+
else
|
469
|
+
redis.call("SET", key_str(itemName), "1")
|
470
|
+
end
|
471
|
+
end
|
472
|
+
end
|
473
|
+
end
|
474
|
+
return itemName
|
475
|
+
'''
|
476
|
+
|
477
|
+
@cached_property
|
478
|
+
def _malloc_script(self):
|
479
|
+
"""Cached Redis script for allocating a resource."""
|
480
|
+
return self.redis.register_script(self._malloc_lua_script)
|
481
|
+
|
482
|
+
def malloc_key(self, timeout: Timeout = 120) -> Optional[str]:
|
483
|
+
"""Allocate a resource key from the pool.
|
484
|
+
|
485
|
+
Args:
|
486
|
+
timeout: How long the allocation should be valid (in seconds)
|
487
|
+
|
488
|
+
Returns:
|
489
|
+
Resource identifier if allocation was successful, None otherwise
|
490
|
+
"""
|
491
|
+
return self._malloc_script(keys=[self._to_seconds(timeout)])
|
492
|
+
|
493
|
+
def malloc(self, timeout: Timeout = 120, obj: Optional[U] = None, params: Optional[dict] = None) -> Optional[RedisAllocatorObject[U]]:
|
494
|
+
"""Allocate a resource from the pool and wrap it in a RedisAllocatorObject.
|
495
|
+
|
496
|
+
Args:
|
497
|
+
timeout: How long the allocation should be valid (in seconds)
|
498
|
+
obj: The object to wrap in the RedisAllocatorObject
|
499
|
+
params: Additional parameters to associate with the allocated object
|
500
|
+
|
501
|
+
Returns:
|
502
|
+
RedisAllocatorObject wrapping the allocated resource if successful, None otherwise
|
503
|
+
"""
|
504
|
+
key = self.malloc_key(timeout)
|
505
|
+
if key is None:
|
506
|
+
return None
|
507
|
+
return RedisAllocatorObject(self, key, obj, params)
|
508
|
+
|
509
|
+
@property
|
510
|
+
def _free_lua_script(self):
|
511
|
+
"""LUA script for freeing allocated resources.
|
512
|
+
|
513
|
+
This script frees allocated resources by removing their locks
|
514
|
+
and pushing them back to the tail of the linked list.
|
515
|
+
"""
|
516
|
+
return f'''
|
517
|
+
{self._lua_required_string}
|
518
|
+
for _, k in ipairs(KEYS) do
|
519
|
+
redis.call('DEL', key_str(k))
|
520
|
+
push_to_tail(k)
|
521
|
+
end
|
522
|
+
'''
|
523
|
+
|
524
|
+
@cached_property
|
525
|
+
def _free_script(self):
|
526
|
+
"""Cached Redis script for freeing allocated resources."""
|
527
|
+
return self.redis.register_script(self._free_lua_script)
|
528
|
+
|
529
|
+
def free_keys(self, *keys: str):
|
530
|
+
"""Free allocated resources.
|
531
|
+
|
532
|
+
Args:
|
533
|
+
*keys: Resource identifiers to free
|
534
|
+
"""
|
535
|
+
self._free_script(keys, args=keys)
|
536
|
+
|
537
|
+
def free(self, obj: RedisAllocatorObject[U]):
|
538
|
+
"""Free an allocated object."""
|
539
|
+
self.free_keys(obj.key)
|
540
|
+
|
541
|
+
@cached_property
|
542
|
+
def _gc_script(self):
|
543
|
+
"""Cached Redis script for garbage collection."""
|
544
|
+
return self.redis.register_script(self._gc_lua_script)
|
545
|
+
|
546
|
+
@property
|
547
|
+
def _gc_lua_script(self):
|
548
|
+
"""LUA script for garbage collection of the allocation pool.
|
549
|
+
|
550
|
+
This script scans through the pool and performs two types of cleanup:
|
551
|
+
1. Resources marked as allocated but not actually locked are pushed back
|
552
|
+
to the available pool
|
553
|
+
2. Resources not marked as allocated but actually locked are marked as allocated
|
554
|
+
|
555
|
+
This ensures consistency between the allocation metadata and the actual locks.
|
556
|
+
"""
|
557
|
+
return f'''
|
558
|
+
{self._lua_required_string}
|
559
|
+
local n = tonumber(ARGV[1])
|
560
|
+
local cursorKey = gc_cursor_str()
|
561
|
+
local oldCursor = redis.call("GET", cursorKey)
|
562
|
+
if not oldCursor or oldCursor == "" then
|
563
|
+
oldCursor = "0"
|
564
|
+
end
|
565
|
+
local scanResult = redis.call("HSCAN", poolItemsKey, oldCursor, "COUNT", n)
|
566
|
+
local newCursor = scanResult[1]
|
567
|
+
local kvList = scanResult[2]
|
568
|
+
local tail = redis.call("GET", tailKey)
|
569
|
+
if not tail then
|
570
|
+
tail = ""
|
571
|
+
end
|
572
|
+
for i = 1, #kvList, 2 do
|
573
|
+
local itemName = kvList[i]
|
574
|
+
local val = kvList[i + 1]
|
575
|
+
if val == "#ALLOCATED" then
|
576
|
+
local locked = (redis.call("EXISTS", key_str(itemName)) == 1)
|
577
|
+
if not locked then
|
578
|
+
push_to_tail(itemName)
|
579
|
+
end
|
580
|
+
else
|
581
|
+
local locked = (redis.call("EXISTS", key_str(itemName)) == 1)
|
582
|
+
if locked then
|
583
|
+
delete_item(itemName)
|
584
|
+
redis.call("HSET", poolItemsKey, itemName, "#ALLOCATED")
|
585
|
+
end
|
586
|
+
end
|
587
|
+
end
|
588
|
+
'''
|
589
|
+
|
590
|
+
def gc(self, count: int = 10):
|
591
|
+
"""Perform garbage collection on the allocation pool.
|
592
|
+
|
593
|
+
This method scans through the pool and ensures consistency between
|
594
|
+
the allocation metadata and the actual locks.
|
595
|
+
|
596
|
+
Args:
|
597
|
+
count: Number of items to check in this garbage collection pass
|
598
|
+
"""
|
599
|
+
# Ensure count is positive
|
600
|
+
assert count > 0, "count should be positive"
|
601
|
+
self._gc_script(args=[count])
|