redis-allocator 0.4.3__py3-none-any.whl → 0.4.5__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/_version.py +1 -1
- redis_allocator/allocator.py +124 -68
- {redis_allocator-0.4.3.dist-info → redis_allocator-0.4.5.dist-info}/METADATA +1 -1
- {redis_allocator-0.4.3.dist-info → redis_allocator-0.4.5.dist-info}/RECORD +8 -8
- {redis_allocator-0.4.3.dist-info → redis_allocator-0.4.5.dist-info}/WHEEL +1 -1
- tests/test_allocator.py +23 -0
- {redis_allocator-0.4.3.dist-info → redis_allocator-0.4.5.dist-info}/licenses/LICENSE +0 -0
- {redis_allocator-0.4.3.dist-info → redis_allocator-0.4.5.dist-info}/top_level.txt +0 -0
redis_allocator/_version.py
CHANGED
@@ -1 +1 @@
|
|
1
|
-
__version__ = '0.4.
|
1
|
+
__version__ = '0.4.5'
|
redis_allocator/allocator.py
CHANGED
@@ -632,23 +632,51 @@ class RedisAllocator(RedisLockPool, Generic[U]):
|
|
632
632
|
local poolItemsKey = pool_str()
|
633
633
|
local headKey = pool_pointer_str(true)
|
634
634
|
local tailKey = pool_pointer_str(false)
|
635
|
-
local function
|
636
|
-
local
|
637
|
-
|
638
|
-
|
639
|
-
|
635
|
+
local function current_pointer(pointerKey)
|
636
|
+
local pointer = redis.call("GET", pointerKey)
|
637
|
+
if not pointer then
|
638
|
+
return ""
|
639
|
+
end
|
640
|
+
return pointer
|
641
|
+
end
|
642
|
+
local function current_head()
|
643
|
+
return current_pointer(headKey)
|
644
|
+
end
|
645
|
+
local function current_tail()
|
646
|
+
return current_pointer(tailKey)
|
647
|
+
end
|
648
|
+
local function set_current_head(pointer)
|
649
|
+
redis.call("SET", headKey, pointer)
|
650
|
+
end
|
651
|
+
local function set_current_tail(pointer)
|
652
|
+
redis.call("SET", tailKey, pointer)
|
653
|
+
end
|
654
|
+
|
655
|
+
local function push_to_tail(itemName, expiry) -- push the item to the free list, item should not be in the free list
|
656
|
+
local head = current_head()
|
657
|
+
local tail = current_tail()
|
658
|
+
---- this is the debug code
|
659
|
+
local itemVal = redis.call("HGET", poolItemsKey, itemName)
|
660
|
+
if itemVal then
|
661
|
+
local prev, next, expiry = split_pool_value(itemVal)
|
662
|
+
assert(prev == "#ALLOCATED" or prev == "", "item should be allocated or free")
|
663
|
+
assert(next == "" or next == "#ALLOCATED", "item should be the last item in the free list")
|
640
664
|
end
|
665
|
+
-- set the item points to the tail
|
641
666
|
redis.call("HSET", poolItemsKey, itemName, join_pool_value(tail, "", expiry))
|
642
|
-
if tail == "" then -- the free list is empty chain
|
643
|
-
|
667
|
+
if tail == "" or head == "" then -- the free list is empty chain
|
668
|
+
-- assert(tail == "" and head == "", "head or tail should not be empty")
|
669
|
+
set_current_head(itemName)
|
644
670
|
else
|
645
671
|
local tailVal = redis.call("HGET", poolItemsKey, tail)
|
646
|
-
|
647
|
-
|
648
|
-
|
672
|
+
assert(tailVal, "tail value should not be nil")
|
673
|
+
local tailPrev, tailNext, tailExpiry = split_pool_value(tailVal)
|
674
|
+
assert(tailNext == "", "tail should be the last item in the free list")
|
675
|
+
redis.call("HSET", poolItemsKey, tail, join_pool_value(tailPrev, itemName, tailExpiry))
|
649
676
|
end
|
650
|
-
|
677
|
+
set_current_tail(itemName)
|
651
678
|
end
|
679
|
+
|
652
680
|
-- Ensure the new head node is well-formed (prev="") and update tail/head
|
653
681
|
local function set_item_head_nil(nextItemName)
|
654
682
|
if nextItemName == "" then
|
@@ -671,94 +699,80 @@ class RedisAllocator(RedisLockPool, Generic[U]):
|
|
671
699
|
redis.call("HSET", poolItemsKey, nextItemName, join_pool_value("", nextNext, nextExpiry))
|
672
700
|
end
|
673
701
|
end
|
702
|
+
|
674
703
|
local function pop_from_head() -- pop the item from the free list
|
675
|
-
local head =
|
704
|
+
local head = current_head()
|
676
705
|
if not head or head == "" then -- the free list is empty
|
677
706
|
return nil, -1
|
678
707
|
end
|
679
708
|
local headVal = redis.call("HGET", poolItemsKey, head)
|
680
|
-
assert(headVal
|
709
|
+
assert(headVal, "head value should not be nil")
|
681
710
|
local headPrev, headNext, headExpiry = split_pool_value(headVal)
|
682
711
|
-- Check if the head item has expired or is locked
|
683
712
|
if is_expiry_invalid(headExpiry) then -- the item has expired
|
684
713
|
redis.call("HDEL", poolItemsKey, head)
|
685
|
-
|
686
|
-
set_item_head_nil(headNext)
|
714
|
+
set_current_head(headNext)
|
715
|
+
-- set_item_head_nil(headNext)
|
687
716
|
return pop_from_head()
|
688
717
|
elseif redis.call("EXISTS", key_str(head)) > 0 then -- the item is locked
|
689
718
|
redis.call("HSET", poolItemsKey, head, join_pool_value("#ALLOCATED", "#ALLOCATED", headExpiry))
|
690
|
-
|
691
|
-
set_item_head_nil(headNext)
|
719
|
+
set_current_head(headNext)
|
720
|
+
-- set_item_head_nil(headNext)
|
692
721
|
return pop_from_head()
|
693
722
|
elseif headNext == "" then -- the item is the last in the free list
|
694
723
|
redis.call("SET", headKey, "")
|
695
724
|
redis.call("SET", tailKey, "")
|
696
725
|
else
|
697
726
|
local nextVal = redis.call("HGET", poolItemsKey, headNext)
|
727
|
+
assert(nextVal, "next value should not be nil")
|
698
728
|
local nextPrev, nextNext, nextExpiry = split_pool_value(nextVal)
|
699
729
|
redis.call("HSET", poolItemsKey, headNext, join_pool_value("", nextNext, nextExpiry))
|
700
730
|
redis.call("SET", headKey, headNext)
|
701
731
|
end
|
702
732
|
redis.call("HSET", poolItemsKey, head, join_pool_value("#ALLOCATED", "#ALLOCATED", headExpiry))
|
703
|
-
-- If we removed the current head, update head pointer
|
704
|
-
local savedHead = redis.call("GET", headKey)
|
705
|
-
if savedHead == itemName then
|
706
|
-
redis.call("SET", headKey, next or "")
|
707
|
-
end
|
708
733
|
return head, headExpiry
|
709
734
|
end
|
735
|
+
|
710
736
|
local function set_item_allocated(itemName, val)
|
711
737
|
if not val then
|
712
738
|
val = redis.call("HGET", pool_str(), itemName)
|
713
739
|
end
|
714
|
-
|
715
|
-
|
716
|
-
|
717
|
-
|
718
|
-
|
719
|
-
|
720
|
-
if prev ~= "" then
|
721
|
-
local prevVal = redis.call("HGET", poolItemsKey, prev)
|
722
|
-
if prevVal then
|
723
|
-
local prevPrev, prevNext, prevExpiry = split_pool_value(prevVal)
|
724
|
-
redis.call("HSET", poolItemsKey, prev, join_pool_value(prevPrev, next, prevExpiry))
|
725
|
-
end
|
726
|
-
else
|
727
|
-
redis.call("SET", headKey, next or "")
|
728
|
-
end
|
729
|
-
if next ~= "" then
|
730
|
-
local nextVal = redis.call("HGET", poolItemsKey, next)
|
731
|
-
if nextVal then
|
732
|
-
local nextPrev, nextNext, nextExpiry = split_pool_value(nextVal)
|
733
|
-
redis.call("HSET", poolItemsKey, next, join_pool_value(prev, nextNext, nextExpiry))
|
734
|
-
end
|
735
|
-
else
|
736
|
-
redis.call("SET", tailKey, prev or "")
|
737
|
-
end
|
738
|
-
redis.call("HSET", poolItemsKey, itemName, join_pool_value("#ALLOCATED", "#ALLOCATED", expiry))
|
739
|
-
-- If we removed the current head, update head pointer
|
740
|
-
local savedHead = redis.call("GET", headKey)
|
741
|
-
if savedHead == itemName then
|
742
|
-
redis.call("SET", headKey, next or "")
|
743
|
-
end
|
740
|
+
assert(val, "val should not be nil")
|
741
|
+
local prev, next, expiry = split_pool_value(val)
|
742
|
+
if prev ~= "#ALLOCATED" then
|
743
|
+
assert(next ~= "#ALLOCATED", "next item should not be allocated")
|
744
|
+
if is_expiry_invalid(expiry) then
|
745
|
+
redis.call("HDEL", poolItemsKey, itemName)
|
744
746
|
end
|
745
|
-
|
746
|
-
|
747
|
-
|
748
|
-
|
749
|
-
|
750
|
-
|
751
|
-
|
752
|
-
|
753
|
-
|
754
|
-
|
747
|
+
if prev ~= "" then
|
748
|
+
local prevVal = redis.call("HGET", poolItemsKey, prev)
|
749
|
+
assert(prevVal, "prev value should not be nil", prev, prevVal)
|
750
|
+
local prevPrev, prevNext, prevExpiry = split_pool_value(prevVal)
|
751
|
+
redis.call("HSET", poolItemsKey, prev, join_pool_value(prevPrev, next, prevExpiry))
|
752
|
+
else
|
753
|
+
redis.call("SET", headKey, next)
|
754
|
+
end
|
755
|
+
if next ~= "" then
|
756
|
+
local nextVal = redis.call("HGET", poolItemsKey, next)
|
757
|
+
assert(nextVal, "next value should not be nil")
|
758
|
+
local nextPrev, nextNext, nextExpiry = split_pool_value(nextVal)
|
759
|
+
redis.call("HSET", poolItemsKey, next, join_pool_value(prev, nextNext, nextExpiry))
|
760
|
+
else
|
761
|
+
redis.call("SET", tailKey, prev)
|
755
762
|
end
|
763
|
+
redis.call("HSET", poolItemsKey, itemName, join_pool_value("#ALLOCATED", "#ALLOCATED", expiry))
|
764
|
+
-- If we removed the current head, update head pointer
|
765
|
+
-- local savedHead = redis.call("GET", headKey)
|
766
|
+
-- if savedHead == itemName then
|
767
|
+
-- redis.call("SET", headKey, next or "")
|
768
|
+
-- end
|
769
|
+
else
|
770
|
+
assert(next == "#ALLOCATED", "next item should also be allocated")
|
756
771
|
end
|
757
772
|
end
|
758
|
-
|
759
|
-
|
760
|
-
|
761
|
-
end
|
773
|
+
|
774
|
+
local function check_item_health(itemName)
|
775
|
+
local value = redis.call("HGET", pool_str(), itemName)
|
762
776
|
assert(value, "value should not be nil")
|
763
777
|
local prev, next, expiry = split_pool_value(value)
|
764
778
|
if is_expiry_invalid(expiry) then -- Check if the item has expired
|
@@ -777,6 +791,37 @@ class RedisAllocator(RedisLockPool, Generic[U]):
|
|
777
791
|
end
|
778
792
|
end
|
779
793
|
end
|
794
|
+
|
795
|
+
-- Return an array of item names in the free list order (head -> tail)
|
796
|
+
-- Does NOT modify the list. Uses a bounded loop (at most pool size)
|
797
|
+
-- to avoid infinite traversal when the list structure is corrupted.
|
798
|
+
local function get_free_list()
|
799
|
+
local items = {{}}
|
800
|
+
local current = redis.call("GET", headKey)
|
801
|
+
if not current or current == "" then
|
802
|
+
return items -- empty list
|
803
|
+
end
|
804
|
+
local max_iters = tonumber(redis.call("HLEN", poolItemsKey))
|
805
|
+
if not max_iters or max_iters <= 0 then
|
806
|
+
return items
|
807
|
+
end
|
808
|
+
for i = 1, max_iters do
|
809
|
+
if not current or current == "" then
|
810
|
+
break
|
811
|
+
end
|
812
|
+
table.insert(items, current)
|
813
|
+
local val = redis.call("HGET", poolItemsKey, current)
|
814
|
+
if not val then
|
815
|
+
break -- corrupted pointer
|
816
|
+
end
|
817
|
+
local _prev, nxt, _expiry = split_pool_value(val)
|
818
|
+
if nxt == "" or nxt == "#ALLOCATED" then
|
819
|
+
break -- reached tail or allocated marker
|
820
|
+
end
|
821
|
+
current = nxt
|
822
|
+
end
|
823
|
+
return items
|
824
|
+
end
|
780
825
|
'''
|
781
826
|
|
782
827
|
@cached_property
|
@@ -1154,6 +1199,16 @@ class RedisAllocator(RedisLockPool, Generic[U]):
|
|
1154
1199
|
"""
|
1155
1200
|
self.free_keys(obj.key, timeout=timeout)
|
1156
1201
|
|
1202
|
+
@cached_property
|
1203
|
+
def _get_free_list_script(self):
|
1204
|
+
return self.redis.register_script(f'''
|
1205
|
+
{self._lua_required_string}
|
1206
|
+
return get_free_list()
|
1207
|
+
''')
|
1208
|
+
|
1209
|
+
def get_free_list(self):
|
1210
|
+
return self._get_free_list_script()
|
1211
|
+
|
1157
1212
|
def _gc_cursor_str(self):
|
1158
1213
|
"""Get the Redis key for the garbage collection cursor.
|
1159
1214
|
|
@@ -1195,10 +1250,11 @@ class RedisAllocator(RedisLockPool, Generic[U]):
|
|
1195
1250
|
local scanResult = redis.call("HSCAN", pool_str(), get_cursor(), "COUNT", n)
|
1196
1251
|
local newCursor = scanResult[1]
|
1197
1252
|
local kvList = scanResult[2]
|
1253
|
+
local t = ""
|
1198
1254
|
for i = 1, #kvList, 2 do
|
1199
1255
|
local itemName = kvList[i]
|
1200
|
-
local val = kvList[i + 1]
|
1201
|
-
check_item_health(itemName
|
1256
|
+
-- local val = kvList[i + 1]
|
1257
|
+
check_item_health(itemName)
|
1202
1258
|
end
|
1203
1259
|
set_cursor(newCursor)
|
1204
1260
|
''')
|
@@ -1,15 +1,15 @@
|
|
1
1
|
redis_allocator/__init__.py,sha256=TVjUm-8YEu_MQD_PkfeIKiVknpCJBrUY9cWN1LlaZcU,1016
|
2
|
-
redis_allocator/_version.py,sha256=
|
3
|
-
redis_allocator/allocator.py,sha256=
|
2
|
+
redis_allocator/_version.py,sha256=jMc7UM2pASY9F8z0XMALQbSXxGCAxWPyeXQlarwD0VI,22
|
3
|
+
redis_allocator/allocator.py,sha256=VkJk6ioN6hwe227lMKjNdyeO0QkJ8sajTDPkEQgDcWg,52964
|
4
4
|
redis_allocator/lock.py,sha256=fqf6WUWHKYenEArWopMIF6kWEnDfADC-bZvnQImsQVo,27400
|
5
5
|
redis_allocator/task_queue.py,sha256=8DjNr2uxhzCsHatV_CHOeGh7_K9pqQZFApSbe2blRO0,14989
|
6
|
-
redis_allocator-0.4.
|
6
|
+
redis_allocator-0.4.5.dist-info/licenses/LICENSE,sha256=Wt4X1rHpffQfEiyWcDUx8BMLjXxfPqaiYZ7Lgsj7L4c,1068
|
7
7
|
tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
8
8
|
tests/conftest.py,sha256=Ts82uylQSzP_GcaN0E02o3xcFdjw20cXNzh3RAdYKW4,3967
|
9
|
-
tests/test_allocator.py,sha256=
|
9
|
+
tests/test_allocator.py,sha256=zOBdaOhlpLsv63J4QZ9vOhL1k1gu_vO68x4CpPDa9_4,23350
|
10
10
|
tests/test_lock.py,sha256=MDMRNN46VhWqkHUIhYOMEDgZkFFCW_WjwRLTOjkFF-Q,46952
|
11
11
|
tests/test_task_queue.py,sha256=Fh5naikFajfOvL6GngEy_TPfOYCYZolZfVwtR6T4dTY,31710
|
12
|
-
redis_allocator-0.4.
|
13
|
-
redis_allocator-0.4.
|
14
|
-
redis_allocator-0.4.
|
15
|
-
redis_allocator-0.4.
|
12
|
+
redis_allocator-0.4.5.dist-info/METADATA,sha256=qNua-7xwZmj7Vyexpugt2h65v1c8Tn7SCKfRJc6CLZI,21727
|
13
|
+
redis_allocator-0.4.5.dist-info/WHEEL,sha256=pxyMxgL8-pra_rKaQ4drOZAegBVuX-G_4nRHjjgWbmo,91
|
14
|
+
redis_allocator-0.4.5.dist-info/top_level.txt,sha256=0hXzU7sK5FCeSolTEYxThOt3HOybnwaXv1FLRJvHVgI,22
|
15
|
+
redis_allocator-0.4.5.dist-info/RECORD,,
|
tests/test_allocator.py
CHANGED
@@ -9,6 +9,7 @@ This module tests the functionality of:
|
|
9
9
|
import time
|
10
10
|
import datetime
|
11
11
|
import threading
|
12
|
+
# from itertools import cycle
|
12
13
|
from operator import xor
|
13
14
|
from pytest_mock import MockFixture
|
14
15
|
from redis import Redis
|
@@ -277,6 +278,7 @@ class TestRedisAllocator:
|
|
277
278
|
assert not redis_allocator.is_locked(key)
|
278
279
|
assert not redis_client.exists(redis_allocator._key_str(key))
|
279
280
|
|
281
|
+
# print(self.get_redis_pool_state(redis_allocator, redis_client))
|
280
282
|
allocated_key = redis_allocator.malloc_key(timeout=30)
|
281
283
|
assert allocated_key == "key1"
|
282
284
|
assert xor(shared, redis_allocator.is_locked(allocated_key))
|
@@ -427,7 +429,11 @@ class TestRedisAllocator:
|
|
427
429
|
)
|
428
430
|
time.tick(600)
|
429
431
|
allocator_with_policy.gc()
|
432
|
+
print(self.get_redis_pool_state(allocator_with_policy, redis_client))
|
433
|
+
redis_client.register_script("print('-------------------------------')")()
|
430
434
|
allocator_with_policy.gc() # some times should be called twice to remove the expired items
|
435
|
+
redis_client.register_script("print('-------------------------------')")()
|
436
|
+
print(self.get_redis_pool_state(allocator_with_policy, redis_client))
|
431
437
|
allocator_with_policy.policy.refresh_pool(allocator_with_policy)
|
432
438
|
assert len(allocator_with_policy) == 4
|
433
439
|
allocator_with_policy.gc()
|
@@ -491,3 +497,20 @@ class TestRedisAllocator:
|
|
491
497
|
else:
|
492
498
|
state = ['key1', 'key2'], ['key3']
|
493
499
|
assert self.get_redis_pool_state(redis_allocator, redis_client) == self.generate_pool_state(redis_client, *state)
|
500
|
+
|
501
|
+
|
502
|
+
# def test_allocator_health_check(self, redis_allocator: RedisAllocator, mocker: MockFixture):
|
503
|
+
# ''''''
|
504
|
+
# with freeze_time("2024-01-01") as time:
|
505
|
+
# for i in range(100):
|
506
|
+
# obj = _TestObject(name = f"test_object_{i}")
|
507
|
+
# mocker.patch.object(obj, 'is_healthy', side_effect = cycle([True, False]))
|
508
|
+
#
|
509
|
+
# redis_allocator.malloc(obj = obj, timeout = 30)
|
510
|
+
# time.tick(30)
|
511
|
+
# redis_allocator.gc()
|
512
|
+
# unh, h = redis_allocator.health_check()
|
513
|
+
# assert len(redis_allocator.get_free_list()) == h
|
514
|
+
# # time.tick(100)
|
515
|
+
# # redis_allocator.gc()
|
516
|
+
# # assert not redis_allocator.is_locked("key1")
|
File without changes
|
File without changes
|