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.
Files changed (22) hide show
  1. {redis_allocator-0.4.2/redis_allocator.egg-info → redis_allocator-0.4.4}/PKG-INFO +1 -1
  2. {redis_allocator-0.4.2 → redis_allocator-0.4.4}/pyproject.toml +1 -1
  3. redis_allocator-0.4.4/redis_allocator/_version.py +1 -0
  4. {redis_allocator-0.4.2 → redis_allocator-0.4.4}/redis_allocator/allocator.py +157 -57
  5. {redis_allocator-0.4.2 → redis_allocator-0.4.4/redis_allocator.egg-info}/PKG-INFO +1 -1
  6. {redis_allocator-0.4.2 → redis_allocator-0.4.4}/tests/test_allocator.py +23 -0
  7. redis_allocator-0.4.2/redis_allocator/_version.py +0 -1
  8. {redis_allocator-0.4.2 → redis_allocator-0.4.4}/LICENSE +0 -0
  9. {redis_allocator-0.4.2 → redis_allocator-0.4.4}/README.md +0 -0
  10. {redis_allocator-0.4.2 → redis_allocator-0.4.4}/redis_allocator/__init__.py +0 -0
  11. {redis_allocator-0.4.2 → redis_allocator-0.4.4}/redis_allocator/lock.py +0 -0
  12. {redis_allocator-0.4.2 → redis_allocator-0.4.4}/redis_allocator/task_queue.py +0 -0
  13. {redis_allocator-0.4.2 → redis_allocator-0.4.4}/redis_allocator.egg-info/SOURCES.txt +0 -0
  14. {redis_allocator-0.4.2 → redis_allocator-0.4.4}/redis_allocator.egg-info/dependency_links.txt +0 -0
  15. {redis_allocator-0.4.2 → redis_allocator-0.4.4}/redis_allocator.egg-info/requires.txt +0 -0
  16. {redis_allocator-0.4.2 → redis_allocator-0.4.4}/redis_allocator.egg-info/top_level.txt +0 -0
  17. {redis_allocator-0.4.2 → redis_allocator-0.4.4}/setup.cfg +0 -0
  18. {redis_allocator-0.4.2 → redis_allocator-0.4.4}/setup.py +0 -0
  19. {redis_allocator-0.4.2 → redis_allocator-0.4.4}/tests/__init__.py +0 -0
  20. {redis_allocator-0.4.2 → redis_allocator-0.4.4}/tests/conftest.py +0 -0
  21. {redis_allocator-0.4.2 → redis_allocator-0.4.4}/tests/test_lock.py +0 -0
  22. {redis_allocator-0.4.2 → redis_allocator-0.4.4}/tests/test_task_queue.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: redis-allocator
3
- Version: 0.4.2
3
+ Version: 0.4.4
4
4
  Summary: Redis-based resource allocation system.
5
5
  Home-page: https://github.com/invoker-bot/RedisAllocator-python
6
6
  Author: Invoker Bot
@@ -4,7 +4,7 @@ build-backend = 'setuptools.build_meta'
4
4
 
5
5
  [tool.commitizen]
6
6
  name = "cz_conventional_commits"
7
- version = "0.4.2"
7
+ version = "0.4.4"
8
8
  tag_format = "v$version"
9
9
 
10
10
  [tool.commitizen.version_files]
@@ -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 push_to_tail(itemName, expiry) -- push the item to the free list
636
- local tail = redis.call("GET", tailKey)
637
- if not tail then
638
- tail = ""
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
- redis.call("SET", headKey, itemName)
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
- local prev, next, expiry = split_pool_value(tailVal)
646
- assert(next == "", "tail is not the last item in the free list")
647
- redis.call("HSET", poolItemsKey, tail, join_pool_value(prev, itemName, expiry))
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 = redis.call("GET", headKey)
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 ~= nil, "head should not nil")
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
- redis.call("SET", headKey, headNext)
714
+ set_current_head(headNext)
715
+ -- set_item_head_nil(headNext)
663
716
  return pop_from_head()
664
- end
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
- redis.call("SET", headKey, headNext)
719
+ set_current_head(headNext)
720
+ -- set_item_head_nil(headNext)
668
721
  return pop_from_head()
669
- end
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, next)
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, next, join_pool_value("", nextNext, nextExpiry))
678
- redis.call("SET", headKey, next)
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", expiry))
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", poolItemsKey, itemName)
738
+ val = redis.call("HGET", pool_str(), itemName)
686
739
  end
687
- if val then
688
- local prev, next, expiry = split_pool_value(val)
689
- if prev ~= "#ALLOCATED" then
690
- if is_expiry_invalid(expiry) then
691
- redis.call("HDEL", poolItemsKey, itemName)
692
- end
693
- if prev ~= "" then
694
- local prevVal = redis.call("HGET", poolItemsKey, prev)
695
- if prevVal then
696
- local prevPrev, prevNext, prevExpiry = split_pool_value(prevVal)
697
- redis.call("HSET", poolItemsKey, prev, join_pool_value(prevPrev, next, prevExpiry))
698
- end
699
- else
700
- redis.call("SET", headKey, next or "")
701
- end
702
- if next ~= "" then
703
- local nextVal = redis.call("HGET", poolItemsKey, next)
704
- if nextVal then
705
- local nextPrev, nextNext, nextExpiry = split_pool_value(nextVal)
706
- redis.call("HSET", poolItemsKey, next, join_pool_value(prev, nextNext, nextExpiry))
707
- end
708
- else
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
- local function check_item_health(itemName, value)
716
- if not value then
717
- value = redis.call("HGET", pool_str(), itemName)
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, val)
1256
+ -- local val = kvList[i + 1]
1257
+ check_item_health(itemName)
1158
1258
  end
1159
1259
  set_cursor(newCursor)
1160
1260
  ''')
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: redis-allocator
3
- Version: 0.4.2
3
+ Version: 0.4.4
4
4
  Summary: Redis-based resource allocation system.
5
5
  Home-page: https://github.com/invoker-bot/RedisAllocator-python
6
6
  Author: Invoker Bot
@@ -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