redis-allocator 0.4.3__tar.gz → 0.4.5__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.3/redis_allocator.egg-info → redis_allocator-0.4.5}/PKG-INFO +1 -1
  2. {redis_allocator-0.4.3 → redis_allocator-0.4.5}/pyproject.toml +1 -1
  3. redis_allocator-0.4.5/redis_allocator/_version.py +1 -0
  4. {redis_allocator-0.4.3 → redis_allocator-0.4.5}/redis_allocator/allocator.py +124 -68
  5. {redis_allocator-0.4.3 → redis_allocator-0.4.5/redis_allocator.egg-info}/PKG-INFO +1 -1
  6. {redis_allocator-0.4.3 → redis_allocator-0.4.5}/tests/test_allocator.py +23 -0
  7. redis_allocator-0.4.3/redis_allocator/_version.py +0 -1
  8. {redis_allocator-0.4.3 → redis_allocator-0.4.5}/LICENSE +0 -0
  9. {redis_allocator-0.4.3 → redis_allocator-0.4.5}/README.md +0 -0
  10. {redis_allocator-0.4.3 → redis_allocator-0.4.5}/redis_allocator/__init__.py +0 -0
  11. {redis_allocator-0.4.3 → redis_allocator-0.4.5}/redis_allocator/lock.py +0 -0
  12. {redis_allocator-0.4.3 → redis_allocator-0.4.5}/redis_allocator/task_queue.py +0 -0
  13. {redis_allocator-0.4.3 → redis_allocator-0.4.5}/redis_allocator.egg-info/SOURCES.txt +0 -0
  14. {redis_allocator-0.4.3 → redis_allocator-0.4.5}/redis_allocator.egg-info/dependency_links.txt +0 -0
  15. {redis_allocator-0.4.3 → redis_allocator-0.4.5}/redis_allocator.egg-info/requires.txt +0 -0
  16. {redis_allocator-0.4.3 → redis_allocator-0.4.5}/redis_allocator.egg-info/top_level.txt +0 -0
  17. {redis_allocator-0.4.3 → redis_allocator-0.4.5}/setup.cfg +0 -0
  18. {redis_allocator-0.4.3 → redis_allocator-0.4.5}/setup.py +0 -0
  19. {redis_allocator-0.4.3 → redis_allocator-0.4.5}/tests/__init__.py +0 -0
  20. {redis_allocator-0.4.3 → redis_allocator-0.4.5}/tests/conftest.py +0 -0
  21. {redis_allocator-0.4.3 → redis_allocator-0.4.5}/tests/test_lock.py +0 -0
  22. {redis_allocator-0.4.3 → redis_allocator-0.4.5}/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.3
3
+ Version: 0.4.5
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.3"
7
+ version = "0.4.5"
8
8
  tag_format = "v$version"
9
9
 
10
10
  [tool.commitizen.version_files]
@@ -0,0 +1 @@
1
+ __version__ = '0.4.5'
@@ -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 push_to_tail(itemName, expiry) -- push the item to the free list
636
- local tail = redis.call("GET", tailKey)
637
- local head = redis.call("GET", headKey)
638
- if not tail then
639
- tail = ""
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
- 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)
644
670
  else
645
671
  local tailVal = redis.call("HGET", poolItemsKey, tail)
646
- local prev, next, expiry = split_pool_value(tailVal)
647
- assert(next == "" or next == '#ALLOCATED', "tail is not the last item in the free list")
648
- 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))
649
676
  end
650
- redis.call("SET", tailKey, itemName) -- set the tail point to the new item
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 = redis.call("GET", headKey)
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 ~= nil, "head should not nil")
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
- redis.call("SET", headKey, headNext)
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
- redis.call("SET", headKey, headNext)
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
- if val then
715
- local prev, next, expiry = split_pool_value(val)
716
- if prev ~= "#ALLOCATED" then
717
- if is_expiry_invalid(expiry) then
718
- redis.call("HDEL", poolItemsKey, itemName)
719
- end
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
- end
746
- -- Invariant fix: ensure current head's prev pointer is always "".
747
- local currentHead = redis.call("GET", headKey)
748
- if currentHead and currentHead ~= "" then
749
- local currentHeadVal = redis.call("HGET", poolItemsKey, currentHead)
750
- if currentHeadVal then
751
- local curPrev, curNext, curExpiry = split_pool_value(currentHeadVal)
752
- if curPrev ~= "" then
753
- redis.call("HSET", poolItemsKey, currentHead, join_pool_value("", curNext, curExpiry))
754
- 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)
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
- local function check_item_health(itemName, value)
759
- if not value then
760
- value = redis.call("HGET", pool_str(), itemName)
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, val)
1256
+ -- local val = kvList[i + 1]
1257
+ check_item_health(itemName)
1202
1258
  end
1203
1259
  set_cursor(newCursor)
1204
1260
  ''')
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: redis-allocator
3
- Version: 0.4.3
3
+ Version: 0.4.5
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.3'
File without changes