redis-allocator 0.4.2__tar.gz → 0.4.4__tar.gz
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-0.4.2/redis_allocator.egg-info → redis_allocator-0.4.4}/PKG-INFO +1 -1
- {redis_allocator-0.4.2 → redis_allocator-0.4.4}/pyproject.toml +1 -1
- redis_allocator-0.4.4/redis_allocator/_version.py +1 -0
- {redis_allocator-0.4.2 → redis_allocator-0.4.4}/redis_allocator/allocator.py +157 -57
- {redis_allocator-0.4.2 → redis_allocator-0.4.4/redis_allocator.egg-info}/PKG-INFO +1 -1
- {redis_allocator-0.4.2 → redis_allocator-0.4.4}/tests/test_allocator.py +23 -0
- redis_allocator-0.4.2/redis_allocator/_version.py +0 -1
- {redis_allocator-0.4.2 → redis_allocator-0.4.4}/LICENSE +0 -0
- {redis_allocator-0.4.2 → redis_allocator-0.4.4}/README.md +0 -0
- {redis_allocator-0.4.2 → redis_allocator-0.4.4}/redis_allocator/__init__.py +0 -0
- {redis_allocator-0.4.2 → redis_allocator-0.4.4}/redis_allocator/lock.py +0 -0
- {redis_allocator-0.4.2 → redis_allocator-0.4.4}/redis_allocator/task_queue.py +0 -0
- {redis_allocator-0.4.2 → redis_allocator-0.4.4}/redis_allocator.egg-info/SOURCES.txt +0 -0
- {redis_allocator-0.4.2 → redis_allocator-0.4.4}/redis_allocator.egg-info/dependency_links.txt +0 -0
- {redis_allocator-0.4.2 → redis_allocator-0.4.4}/redis_allocator.egg-info/requires.txt +0 -0
- {redis_allocator-0.4.2 → redis_allocator-0.4.4}/redis_allocator.egg-info/top_level.txt +0 -0
- {redis_allocator-0.4.2 → redis_allocator-0.4.4}/setup.cfg +0 -0
- {redis_allocator-0.4.2 → redis_allocator-0.4.4}/setup.py +0 -0
- {redis_allocator-0.4.2 → redis_allocator-0.4.4}/tests/__init__.py +0 -0
- {redis_allocator-0.4.2 → redis_allocator-0.4.4}/tests/conftest.py +0 -0
- {redis_allocator-0.4.2 → redis_allocator-0.4.4}/tests/test_lock.py +0 -0
- {redis_allocator-0.4.2 → redis_allocator-0.4.4}/tests/test_task_queue.py +0 -0
@@ -0,0 +1 @@
|
|
1
|
+
__version__ = '0.4.4'
|
@@ -632,95 +632,153 @@ 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
|
-
if not
|
638
|
-
|
635
|
+
local function current_pointer(pointerKey)
|
636
|
+
local pointer = redis.call("GET", pointerKey)
|
637
|
+
if not pointer then
|
638
|
+
return ""
|
639
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")
|
664
|
+
end
|
665
|
+
-- set the item points to the tail
|
640
666
|
redis.call("HSET", poolItemsKey, itemName, join_pool_value(tail, "", expiry))
|
641
|
-
if tail == "" then -- the free list is empty chain
|
642
|
-
|
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)
|
643
670
|
else
|
644
671
|
local tailVal = redis.call("HGET", poolItemsKey, tail)
|
645
|
-
|
646
|
-
|
647
|
-
|
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))
|
676
|
+
end
|
677
|
+
set_current_tail(itemName)
|
678
|
+
end
|
679
|
+
|
680
|
+
-- Ensure the new head node is well-formed (prev="") and update tail/head
|
681
|
+
local function set_item_head_nil(nextItemName)
|
682
|
+
if nextItemName == "" then
|
683
|
+
-- list becomes empty
|
684
|
+
redis.call("SET", headKey, "")
|
685
|
+
redis.call("SET", tailKey, "")
|
686
|
+
return
|
687
|
+
end
|
688
|
+
|
689
|
+
local nextVal = redis.call("HGET", poolItemsKey, nextItemName)
|
690
|
+
if not nextVal then
|
691
|
+
-- corrupted pointer, clear list
|
692
|
+
redis.call("SET", headKey, "")
|
693
|
+
redis.call("SET", tailKey, "")
|
694
|
+
return
|
695
|
+
end
|
696
|
+
|
697
|
+
local _prev, nextNext, nextExpiry = split_pool_value(nextVal)
|
698
|
+
if _prev ~= "" then
|
699
|
+
redis.call("HSET", poolItemsKey, nextItemName, join_pool_value("", nextNext, nextExpiry))
|
648
700
|
end
|
649
|
-
redis.call("SET", tailKey, itemName) -- set the tail point to the new item
|
650
701
|
end
|
702
|
+
|
651
703
|
local function pop_from_head() -- pop the item from the free list
|
652
|
-
local head =
|
704
|
+
local head = current_head()
|
653
705
|
if not head or head == "" then -- the free list is empty
|
654
706
|
return nil, -1
|
655
707
|
end
|
656
708
|
local headVal = redis.call("HGET", poolItemsKey, head)
|
657
|
-
assert(headVal
|
709
|
+
assert(headVal, "head value should not be nil")
|
658
710
|
local headPrev, headNext, headExpiry = split_pool_value(headVal)
|
659
711
|
-- Check if the head item has expired or is locked
|
660
712
|
if is_expiry_invalid(headExpiry) then -- the item has expired
|
661
713
|
redis.call("HDEL", poolItemsKey, head)
|
662
|
-
|
714
|
+
set_current_head(headNext)
|
715
|
+
-- set_item_head_nil(headNext)
|
663
716
|
return pop_from_head()
|
664
|
-
|
665
|
-
if redis.call("EXISTS", key_str(head)) > 0 then -- the item is locked
|
717
|
+
elseif redis.call("EXISTS", key_str(head)) > 0 then -- the item is locked
|
666
718
|
redis.call("HSET", poolItemsKey, head, join_pool_value("#ALLOCATED", "#ALLOCATED", headExpiry))
|
667
|
-
|
719
|
+
set_current_head(headNext)
|
720
|
+
-- set_item_head_nil(headNext)
|
668
721
|
return pop_from_head()
|
669
|
-
|
670
|
-
local prev, next, expiry = split_pool_value(headVal)
|
671
|
-
if next == "" then -- the item is the last in the free list
|
722
|
+
elseif headNext == "" then -- the item is the last in the free list
|
672
723
|
redis.call("SET", headKey, "")
|
673
724
|
redis.call("SET", tailKey, "")
|
674
725
|
else
|
675
|
-
local nextVal = redis.call("HGET", poolItemsKey,
|
726
|
+
local nextVal = redis.call("HGET", poolItemsKey, headNext)
|
727
|
+
assert(nextVal, "next value should not be nil")
|
676
728
|
local nextPrev, nextNext, nextExpiry = split_pool_value(nextVal)
|
677
|
-
redis.call("HSET", poolItemsKey,
|
678
|
-
redis.call("SET", headKey,
|
729
|
+
redis.call("HSET", poolItemsKey, headNext, join_pool_value("", nextNext, nextExpiry))
|
730
|
+
redis.call("SET", headKey, headNext)
|
679
731
|
end
|
680
|
-
redis.call("HSET", poolItemsKey, head, join_pool_value("#ALLOCATED", "#ALLOCATED",
|
732
|
+
redis.call("HSET", poolItemsKey, head, join_pool_value("#ALLOCATED", "#ALLOCATED", headExpiry))
|
681
733
|
return head, headExpiry
|
682
734
|
end
|
735
|
+
|
683
736
|
local function set_item_allocated(itemName, val)
|
684
737
|
if not val then
|
685
|
-
val = redis.call("HGET",
|
738
|
+
val = redis.call("HGET", pool_str(), itemName)
|
686
739
|
end
|
687
|
-
|
688
|
-
|
689
|
-
|
690
|
-
|
691
|
-
|
692
|
-
|
693
|
-
|
694
|
-
|
695
|
-
|
696
|
-
|
697
|
-
|
698
|
-
|
699
|
-
|
700
|
-
|
701
|
-
|
702
|
-
|
703
|
-
|
704
|
-
|
705
|
-
|
706
|
-
|
707
|
-
|
708
|
-
|
709
|
-
redis.call("SET", tailKey, prev or "")
|
710
|
-
end
|
711
|
-
redis.call("HSET", poolItemsKey, itemName, join_pool_value("#ALLOCATED", "#ALLOCATED", expiry))
|
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)
|
746
|
+
end
|
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)
|
712
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")
|
713
771
|
end
|
714
772
|
end
|
715
|
-
|
716
|
-
|
717
|
-
|
718
|
-
end
|
773
|
+
|
774
|
+
local function check_item_health(itemName)
|
775
|
+
local value = redis.call("HGET", pool_str(), itemName)
|
719
776
|
assert(value, "value should not be nil")
|
720
777
|
local prev, next, expiry = split_pool_value(value)
|
721
778
|
if is_expiry_invalid(expiry) then -- Check if the item has expired
|
722
|
-
set_item_allocated(itemName)
|
779
|
+
set_item_allocated(itemName, value)
|
723
780
|
redis.call("HDEL", poolItemsKey, itemName)
|
781
|
+
return
|
724
782
|
end
|
725
783
|
local locked = redis.call("EXISTS", key_str(itemName)) > 0
|
726
784
|
if prev == "#ALLOCATED" then
|
@@ -729,10 +787,41 @@ class RedisAllocator(RedisLockPool, Generic[U]):
|
|
729
787
|
end
|
730
788
|
else
|
731
789
|
if locked then
|
732
|
-
set_item_allocated(itemName)
|
790
|
+
set_item_allocated(itemName, value)
|
733
791
|
end
|
734
792
|
end
|
735
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
|
736
825
|
'''
|
737
826
|
|
738
827
|
@cached_property
|
@@ -1110,6 +1199,16 @@ class RedisAllocator(RedisLockPool, Generic[U]):
|
|
1110
1199
|
"""
|
1111
1200
|
self.free_keys(obj.key, timeout=timeout)
|
1112
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
|
+
|
1113
1212
|
def _gc_cursor_str(self):
|
1114
1213
|
"""Get the Redis key for the garbage collection cursor.
|
1115
1214
|
|
@@ -1151,10 +1250,11 @@ class RedisAllocator(RedisLockPool, Generic[U]):
|
|
1151
1250
|
local scanResult = redis.call("HSCAN", pool_str(), get_cursor(), "COUNT", n)
|
1152
1251
|
local newCursor = scanResult[1]
|
1153
1252
|
local kvList = scanResult[2]
|
1253
|
+
local t = ""
|
1154
1254
|
for i = 1, #kvList, 2 do
|
1155
1255
|
local itemName = kvList[i]
|
1156
|
-
local val = kvList[i + 1]
|
1157
|
-
check_item_health(itemName
|
1256
|
+
-- local val = kvList[i + 1]
|
1257
|
+
check_item_health(itemName)
|
1158
1258
|
end
|
1159
1259
|
set_cursor(newCursor)
|
1160
1260
|
''')
|
@@ -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")
|
@@ -1 +0,0 @@
|
|
1
|
-
__version__ = '0.4.2'
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
{redis_allocator-0.4.2 → redis_allocator-0.4.4}/redis_allocator.egg-info/dependency_links.txt
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|