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
tests/test_lock.py
ADDED
@@ -0,0 +1,851 @@
|
|
1
|
+
"""Tests for the RedisLock and RedisLockPool classes."""
|
2
|
+
from redis import Redis
|
3
|
+
from redis_allocator.lock import RedisLock, RedisLockPool, LockStatus, ThreadLock, ThreadLockPool
|
4
|
+
import time
|
5
|
+
import concurrent.futures
|
6
|
+
import pytest
|
7
|
+
|
8
|
+
|
9
|
+
class TestRedisLock:
|
10
|
+
"""Tests for the RedisLock class."""
|
11
|
+
|
12
|
+
def test_lock(self, redis_lock: RedisLock, redis_client: Redis):
|
13
|
+
"""Test the lock method."""
|
14
|
+
assert redis_lock.lock('test-key') is True
|
15
|
+
assert redis_client.exists(redis_lock._key_str('test-key'))
|
16
|
+
|
17
|
+
def test_unlock(self, redis_lock: RedisLock, redis_client: Redis):
|
18
|
+
"""Test the unlock method."""
|
19
|
+
# First set the lock
|
20
|
+
redis_lock.update('test-key')
|
21
|
+
assert redis_client.exists(redis_lock._key_str('test-key'))
|
22
|
+
# Then unlock
|
23
|
+
redis_lock.unlock('test-key')
|
24
|
+
assert not redis_client.exists(redis_lock._key_str('test-key'))
|
25
|
+
|
26
|
+
def test_update(self, redis_lock: RedisLock, redis_client: Redis):
|
27
|
+
"""Test the update method."""
|
28
|
+
redis_lock.update('test-key', value='2', timeout=60)
|
29
|
+
assert redis_client.get(redis_lock._key_str('test-key')) == '2'
|
30
|
+
# TTL should be close to 60
|
31
|
+
ttl = redis_client.ttl(redis_lock._key_str('test-key'))
|
32
|
+
assert 55 <= ttl <= 60
|
33
|
+
|
34
|
+
def test_lock_value(self, redis_lock: RedisLock, redis_client: Redis):
|
35
|
+
"""Test the lock_value method."""
|
36
|
+
redis_client.set(redis_lock._key_str('test-key'), '120')
|
37
|
+
assert redis_lock.lock_value('test-key') == '120'
|
38
|
+
|
39
|
+
def test_is_locked(self, redis_lock: RedisLock, redis_client: Redis):
|
40
|
+
"""Test the is_locked method."""
|
41
|
+
assert not redis_lock.is_locked('test-key')
|
42
|
+
redis_client.set(redis_lock._key_str('test-key'), '300')
|
43
|
+
assert redis_lock.is_locked('test-key')
|
44
|
+
assert not redis_lock.is_locked('non-existent-key')
|
45
|
+
|
46
|
+
def test_key_status(self, redis_lock: RedisLock, redis_client: Redis):
|
47
|
+
"""Test the key_status method."""
|
48
|
+
# Test FREE status
|
49
|
+
assert redis_lock.key_status('free-key') == LockStatus.FREE
|
50
|
+
|
51
|
+
# Test LOCKED status
|
52
|
+
redis_client.set(redis_lock._key_str('locked-key'), '1', ex=60)
|
53
|
+
assert redis_lock.key_status('locked-key') == LockStatus.LOCKED
|
54
|
+
|
55
|
+
# Test ERROR status (permanent lock)
|
56
|
+
redis_client.set(redis_lock._key_str('error-key'), '1')
|
57
|
+
assert redis_lock.key_status('error-key') == LockStatus.ERROR
|
58
|
+
|
59
|
+
# Test UNAVAILABLE status (TTL > timeout)
|
60
|
+
redis_client.set(redis_lock._key_str('unavailable-key'), '1', ex=200)
|
61
|
+
assert redis_lock.key_status('unavailable-key', timeout=100) == LockStatus.UNAVAILABLE
|
62
|
+
|
63
|
+
def test_rlock(self, redis_lock: RedisLock, redis_client: Redis):
|
64
|
+
"""Test the rlock method."""
|
65
|
+
# First test acquiring a lock
|
66
|
+
assert redis_lock.rlock('test-key', value='1') is True
|
67
|
+
assert redis_client.get(redis_lock._key_str('test-key')) == '1'
|
68
|
+
|
69
|
+
# Test acquiring the same lock with the same value
|
70
|
+
assert redis_lock.rlock('test-key', value='1') is True
|
71
|
+
|
72
|
+
# Test acquiring the same lock with a different value
|
73
|
+
assert redis_lock.rlock('test-key', value='2') is False
|
74
|
+
|
75
|
+
# Verify the original value wasn't changed
|
76
|
+
assert redis_client.get(redis_lock._key_str('test-key')) == '1'
|
77
|
+
|
78
|
+
def test_conditional_setdel_operations(self, redis_lock: RedisLock, redis_client: Redis):
|
79
|
+
"""Test the conditional set/del operations."""
|
80
|
+
# Skip direct testing of _conditional_setdel as it's an internal method
|
81
|
+
# We'll test the public methods that use it instead
|
82
|
+
|
83
|
+
# Set initial value
|
84
|
+
test_key = 'cond-key'
|
85
|
+
key_str = redis_lock._key_str(test_key)
|
86
|
+
redis_client.set(key_str, '10')
|
87
|
+
|
88
|
+
# Test setgt (greater than)
|
89
|
+
assert redis_lock.setgt(key=test_key, value=15) # 15 > 10, should succeed
|
90
|
+
assert redis_client.get(key_str) == '15' # Value should be updated to 15
|
91
|
+
|
92
|
+
# Test setgt with unsuccessful condition
|
93
|
+
assert not redis_lock.setgt(key=test_key, value=5) # 5 < 15, should not succeed
|
94
|
+
assert redis_client.get(key_str) == '15' # Value should remain 15
|
95
|
+
|
96
|
+
# Test setlt (less than)
|
97
|
+
assert redis_lock.setlt(key=test_key, value=5) # 5 < 15, should succeed
|
98
|
+
assert redis_client.get(key_str) == '5' # Value should be updated to 5
|
99
|
+
|
100
|
+
# Test setlt with unsuccessful condition
|
101
|
+
assert not redis_lock.setlt(key=test_key, value=10) # 10 > 5, should not succeed
|
102
|
+
assert redis_client.get(key_str) == '5' # Value should remain 5
|
103
|
+
|
104
|
+
# Test setge (greater than or equal)
|
105
|
+
assert redis_lock.setge(key=test_key, value=5) # 5 >= 5, should succeed
|
106
|
+
assert redis_client.get(key_str) == '5' # Value should still be 5
|
107
|
+
|
108
|
+
# Test setge with unsuccessful condition
|
109
|
+
assert not redis_lock.setge(key=test_key, value=4) # 4 < 5, should not succeed
|
110
|
+
assert redis_client.get(key_str) == '5' # Value should remain 5
|
111
|
+
|
112
|
+
# Test setle (less than or equal)
|
113
|
+
assert redis_lock.setle(key=test_key, value=5) # 5 <= 5, should succeed
|
114
|
+
assert redis_client.get(key_str) == '5' # Value should remain 5
|
115
|
+
|
116
|
+
# Test setle with unsuccessful condition
|
117
|
+
assert not redis_lock.setle(key=test_key, value=6) # 6 > 5, should not succeed
|
118
|
+
assert redis_client.get(key_str) == '5' # Value should remain 5
|
119
|
+
|
120
|
+
# Test seteq (equal)
|
121
|
+
assert redis_lock.seteq(key=test_key, value=5) # 5 == 5, should succeed
|
122
|
+
assert redis_client.get(key_str) == '5' # Value should remain 5
|
123
|
+
|
124
|
+
# Test seteq with unsuccessful condition
|
125
|
+
assert not redis_lock.seteq(key=test_key, value=6) # 6 != 5, should not succeed
|
126
|
+
assert redis_client.get(key_str) == '5' # Value should remain 5
|
127
|
+
|
128
|
+
# Test setne (not equal)
|
129
|
+
assert redis_lock.setne(key=test_key, value=10) # 10 != 5, should succeed
|
130
|
+
assert redis_client.get(key_str) == '10' # Value should be updated to 10
|
131
|
+
|
132
|
+
# Test setne with unsuccessful condition
|
133
|
+
assert not redis_lock.setne(key=test_key, value=10) # 10 == 10, should not succeed
|
134
|
+
assert redis_client.get(key_str) == '10' # Value should remain 10
|
135
|
+
|
136
|
+
# Test delgt (delete if greater than)
|
137
|
+
redis_client.set(key_str, '10')
|
138
|
+
redis_lock.delgt(key=test_key, value=15) # 15 > 10, should delete
|
139
|
+
assert not redis_client.exists(key_str) # Key should be deleted
|
140
|
+
|
141
|
+
# Test dellt (delete if less than)
|
142
|
+
redis_client.set(key_str, '10')
|
143
|
+
redis_lock.dellt(key=test_key, value=5) # 5 < 10, should delete
|
144
|
+
assert not redis_client.exists(key_str) # Key should be deleted
|
145
|
+
|
146
|
+
# Test delge (delete if greater than or equal)
|
147
|
+
redis_client.set(key_str, '10')
|
148
|
+
redis_lock.delge(key=test_key, value=10) # 10 >= 10, should delete
|
149
|
+
assert not redis_client.exists(key_str) # Key should be deleted
|
150
|
+
|
151
|
+
# Test delle (delete if less than or equal)
|
152
|
+
redis_client.set(key_str, '10')
|
153
|
+
redis_lock.delle(key=test_key, value=10) # 10 <= 10, should delete
|
154
|
+
assert not redis_client.exists(key_str) # Key should be deleted
|
155
|
+
|
156
|
+
# Test deleq (delete if equal)
|
157
|
+
redis_client.set(key_str, '10')
|
158
|
+
redis_lock.deleq(key=test_key, value=10) # 10 == 10, should delete
|
159
|
+
assert not redis_client.exists(key_str) # Key should be deleted
|
160
|
+
|
161
|
+
# Test delne (delete if not equal)
|
162
|
+
redis_client.set(key_str, '10')
|
163
|
+
redis_lock.delne(key=test_key, value=5) # 5 != 10, should delete
|
164
|
+
assert not redis_client.exists(key_str) # Key should be deleted
|
165
|
+
|
166
|
+
# Test with expired keys
|
167
|
+
redis_client.set(key_str, '10')
|
168
|
+
redis_lock.setgt(key=test_key, value=15, ex=30) # set with expiration
|
169
|
+
ttl = redis_client.ttl(key_str)
|
170
|
+
assert ttl > 0 and ttl <= 30 # Should have a TTL set
|
171
|
+
|
172
|
+
def test_setters_and_deleters(self, redis_lock: RedisLock, redis_client: Redis):
|
173
|
+
"""Test all setter and deleter methods."""
|
174
|
+
test_key = 'op-key'
|
175
|
+
key_str = redis_lock._key_str(test_key)
|
176
|
+
|
177
|
+
# Let's test the setters first
|
178
|
+
|
179
|
+
# Set an initial value
|
180
|
+
redis_client.set(key_str, '10')
|
181
|
+
|
182
|
+
# Test setgt (doesn't meet condition)
|
183
|
+
assert not redis_lock.setgt(key=test_key, value=5)
|
184
|
+
assert redis_client.get(key_str) == '10' # Unchanged because 5 is not > 10
|
185
|
+
|
186
|
+
# Test setgt (meets condition)
|
187
|
+
assert redis_lock.setgt(key=test_key, value=15, set_value=7)
|
188
|
+
assert redis_client.get(key_str) == '7' # Changed because 15 > 10
|
189
|
+
|
190
|
+
# Test setlt (doesn't meet condition)
|
191
|
+
assert not redis_lock.setlt(key=test_key, value=10)
|
192
|
+
assert redis_client.get(key_str) == '7' # Unchanged because 10 is not < 7
|
193
|
+
|
194
|
+
# Test setlt (meets condition)
|
195
|
+
assert redis_lock.setlt(key=test_key, value=5)
|
196
|
+
assert redis_client.get(key_str) == '5' # Changed because 5 < 7
|
197
|
+
|
198
|
+
# Reset for more tests
|
199
|
+
redis_client.set(key_str, '10')
|
200
|
+
|
201
|
+
# Test setge (meets condition)
|
202
|
+
assert redis_lock.setge(key=test_key, value=10)
|
203
|
+
assert redis_client.get(key_str) == '10' # Changed because 10 >= 10
|
204
|
+
|
205
|
+
# Test setle (meets condition)
|
206
|
+
assert redis_lock.setle(key=test_key, value=10)
|
207
|
+
assert redis_client.get(key_str) == '10' # Changed because 10 <= 10
|
208
|
+
|
209
|
+
# Test seteq (meets condition)
|
210
|
+
assert redis_lock.seteq(key=test_key, value=10)
|
211
|
+
assert redis_client.get(key_str) == '10' # Changed because 10 == 10
|
212
|
+
|
213
|
+
# Test setne (doesn't meet condition)
|
214
|
+
assert not redis_lock.setne(key=test_key, value=10)
|
215
|
+
assert redis_client.get(key_str) == '10' # Unchanged because 10 is not != 10
|
216
|
+
|
217
|
+
# Test setne (meets condition)
|
218
|
+
assert redis_lock.setne(key=test_key, value=5)
|
219
|
+
assert redis_client.get(key_str) == '5' # Changed because 5 != 10
|
220
|
+
|
221
|
+
# Now test the deleters
|
222
|
+
|
223
|
+
# Reset for delete tests
|
224
|
+
redis_client.set(key_str, '10')
|
225
|
+
|
226
|
+
# Test delgt (doesn't meet condition)
|
227
|
+
assert not redis_lock.delgt(key=test_key, value=5)
|
228
|
+
assert redis_client.exists(key_str) # Key still exists because 5 is not > 10
|
229
|
+
|
230
|
+
# Test delgt (meets condition)
|
231
|
+
assert redis_lock.delgt(key=test_key, value=15)
|
232
|
+
assert not redis_client.exists(key_str) # Key deleted because 15 > 10
|
233
|
+
|
234
|
+
# Reset for more delete tests
|
235
|
+
redis_client.set(key_str, '10')
|
236
|
+
|
237
|
+
# Test dellt (doesn't meet condition)
|
238
|
+
assert not redis_lock.dellt(key=test_key, value=15)
|
239
|
+
assert redis_client.exists(key_str) # Key still exists because 15 is not < 10
|
240
|
+
|
241
|
+
# Test dellt (meets condition)
|
242
|
+
assert redis_lock.dellt(key=test_key, value=5)
|
243
|
+
assert not redis_client.exists(key_str) # Key deleted because 5 < 10
|
244
|
+
|
245
|
+
# Test delge, delle, deleq, delne
|
246
|
+
redis_client.set(key_str, '10')
|
247
|
+
assert redis_lock.delge(key=test_key, value=10) # 10 >= 10, should delete
|
248
|
+
assert not redis_client.exists(key_str)
|
249
|
+
|
250
|
+
redis_client.set(key_str, '10')
|
251
|
+
assert redis_lock.delle(key=test_key, value=10) # 10 <= 10, should delete
|
252
|
+
assert not redis_client.exists(key_str)
|
253
|
+
|
254
|
+
redis_client.set(key_str, '10')
|
255
|
+
assert redis_lock.deleq(key=test_key, value=10) # 10 == 10, should delete
|
256
|
+
assert not redis_client.exists(key_str)
|
257
|
+
|
258
|
+
redis_client.set(key_str, '10')
|
259
|
+
assert redis_lock.delne(key=test_key, value=5) # 5 != 10, should delete
|
260
|
+
assert not redis_client.exists(key_str)
|
261
|
+
|
262
|
+
# Test unsuccessful deletions
|
263
|
+
redis_client.set(key_str, '10')
|
264
|
+
assert not redis_lock.delge(key=test_key, value=5) # 5 < 10, should not delete
|
265
|
+
assert redis_client.exists(key_str)
|
266
|
+
|
267
|
+
assert not redis_lock.delle(key=test_key, value=15) # 15 > 10, should not delete
|
268
|
+
assert redis_client.exists(key_str)
|
269
|
+
|
270
|
+
assert not redis_lock.deleq(key=test_key, value=11) # 11 != 10, should not delete
|
271
|
+
assert redis_client.exists(key_str)
|
272
|
+
|
273
|
+
assert not redis_lock.delne(key=test_key, value=10) # 10 == 10, should not delete
|
274
|
+
assert redis_client.exists(key_str)
|
275
|
+
|
276
|
+
def test_equality_and_hash(self, redis_client: Redis):
|
277
|
+
"""Test equality and hash methods."""
|
278
|
+
# Create two identical locks
|
279
|
+
lock1 = RedisLock(redis_client, 'test')
|
280
|
+
lock2 = RedisLock(redis_client, 'test')
|
281
|
+
|
282
|
+
# Create a different lock
|
283
|
+
lock3 = RedisLock(redis_client, 'different')
|
284
|
+
|
285
|
+
# Test equality
|
286
|
+
assert lock1 == lock2
|
287
|
+
assert lock1 != lock3
|
288
|
+
assert lock1 != "not a lock"
|
289
|
+
|
290
|
+
# Test hash
|
291
|
+
lock_set = {lock1, lock2, lock3}
|
292
|
+
assert len(lock_set) == 2 # lock1 and lock2 should hash to the same value
|
293
|
+
|
294
|
+
def test_invalid_operator(self, redis_lock: RedisLock):
|
295
|
+
"""Test that an invalid operator raises a ValueError."""
|
296
|
+
with pytest.raises(ValueError, match="Invalid operator"):
|
297
|
+
redis_lock._conditional_setdel_lua_script("invalid", 0.001)
|
298
|
+
|
299
|
+
def test_timeout_types(self, redis_lock: RedisLock, redis_client: Redis):
|
300
|
+
"""Test different timeout types."""
|
301
|
+
from datetime import timedelta
|
302
|
+
|
303
|
+
# Test with timedelta
|
304
|
+
test_key = 'timeout-key'
|
305
|
+
key_str = redis_lock._key_str(test_key)
|
306
|
+
|
307
|
+
# Use timedelta for timeout
|
308
|
+
redis_lock.update(test_key, value='1', timeout=timedelta(seconds=30))
|
309
|
+
ttl = redis_client.ttl(key_str)
|
310
|
+
assert 25 <= ttl <= 30 # Allow a small margin
|
311
|
+
|
312
|
+
# Use None for timeout (should use a very long timeout)
|
313
|
+
redis_lock.update(test_key, value='2', timeout=None)
|
314
|
+
ttl = redis_client.ttl(key_str)
|
315
|
+
assert ttl == -1 # No expiration
|
316
|
+
|
317
|
+
def test_decoder_assertion(self, redis_client_raw: Redis):
|
318
|
+
"""Test that the RedisLock constructor asserts that decode_responses is enabled."""
|
319
|
+
# redis_client_raw is a client with decode_responses=False
|
320
|
+
with pytest.raises(AssertionError, match="Redis must be configured to decode responses"):
|
321
|
+
RedisLock(redis_client_raw, "test")
|
322
|
+
|
323
|
+
|
324
|
+
class TestRedisLockPool:
|
325
|
+
"""Tests for the RedisLockPool class."""
|
326
|
+
|
327
|
+
def test_keys(self, redis_lock_pool: RedisLockPool):
|
328
|
+
"""Test the keys method."""
|
329
|
+
# Add some keys to the pool
|
330
|
+
redis_lock_pool.assign(['key1', 'key2'])
|
331
|
+
keys = redis_lock_pool.keys()
|
332
|
+
assert sorted(list(keys)) == ['key1', 'key2']
|
333
|
+
|
334
|
+
def test_extend(self, redis_lock_pool: RedisLockPool, redis_client: Redis):
|
335
|
+
"""Test the extend method with keys."""
|
336
|
+
redis_lock_pool.extend(['key1', 'key2'])
|
337
|
+
|
338
|
+
# Verify keys were added
|
339
|
+
assert 'key1' in redis_lock_pool
|
340
|
+
assert 'key2' in redis_lock_pool
|
341
|
+
|
342
|
+
def test_shrink(self, redis_lock_pool: RedisLockPool, redis_client: Redis):
|
343
|
+
"""Test the shrink method."""
|
344
|
+
# First add keys
|
345
|
+
redis_lock_pool.extend(['key1', 'key2', 'key3'])
|
346
|
+
|
347
|
+
# Then shrink
|
348
|
+
redis_lock_pool.shrink(['key1', 'key2'])
|
349
|
+
|
350
|
+
# Verify keys were removed
|
351
|
+
assert 'key1' not in redis_lock_pool
|
352
|
+
assert 'key2' not in redis_lock_pool
|
353
|
+
assert 'key3' in redis_lock_pool
|
354
|
+
|
355
|
+
def test_clear(self, redis_lock_pool: RedisLockPool, redis_client: Redis):
|
356
|
+
"""Test the clear method."""
|
357
|
+
# First add keys
|
358
|
+
redis_lock_pool.extend(['key1', 'key2'])
|
359
|
+
|
360
|
+
# Then clear
|
361
|
+
redis_lock_pool.clear()
|
362
|
+
|
363
|
+
# Verify all keys were removed
|
364
|
+
assert len(redis_lock_pool) == 0
|
365
|
+
|
366
|
+
def test_assign(self, redis_lock_pool: RedisLockPool, redis_client: Redis):
|
367
|
+
"""Test the assign method."""
|
368
|
+
# First add some initial keys
|
369
|
+
redis_lock_pool.extend(['old1', 'old2'])
|
370
|
+
|
371
|
+
# Then assign new keys
|
372
|
+
redis_lock_pool.assign(['key1', 'key2'])
|
373
|
+
|
374
|
+
# Verify old keys were removed and new keys were added
|
375
|
+
assert 'old1' not in redis_lock_pool
|
376
|
+
assert 'old2' not in redis_lock_pool
|
377
|
+
assert 'key1' in redis_lock_pool
|
378
|
+
assert 'key2' in redis_lock_pool
|
379
|
+
|
380
|
+
def test_contains(self, redis_lock_pool: RedisLockPool, redis_client: Redis):
|
381
|
+
"""Test the __contains__ method."""
|
382
|
+
# Add a key
|
383
|
+
redis_lock_pool.extend(['key1'])
|
384
|
+
|
385
|
+
# Test __contains__
|
386
|
+
assert 'key1' in redis_lock_pool
|
387
|
+
assert 'key2' not in redis_lock_pool
|
388
|
+
|
389
|
+
def test_len(self, redis_lock_pool: RedisLockPool, redis_client: Redis):
|
390
|
+
"""Test the __len__ method."""
|
391
|
+
# Add some keys
|
392
|
+
redis_lock_pool.extend(['key1', 'key2', 'key3'])
|
393
|
+
|
394
|
+
# Test __len__
|
395
|
+
assert len(redis_lock_pool) == 3
|
396
|
+
|
397
|
+
def test_get_set_del_item(self, redis_lock_pool: RedisLockPool, redis_client: Redis):
|
398
|
+
"""Test the __getitem__, __setitem__, and __delitem__ methods."""
|
399
|
+
# First test extend to add a key
|
400
|
+
redis_lock_pool.extend(['key1'])
|
401
|
+
|
402
|
+
# Test __getitem__
|
403
|
+
assert 'key1' in redis_lock_pool
|
404
|
+
|
405
|
+
# Test shrink to remove key
|
406
|
+
redis_lock_pool.shrink(['key1'])
|
407
|
+
assert 'key1' not in redis_lock_pool
|
408
|
+
|
409
|
+
def test_health_check(self, redis_lock_pool: RedisLockPool, redis_client: Redis):
|
410
|
+
"""Test the health_check method."""
|
411
|
+
# Add some keys
|
412
|
+
redis_lock_pool.extend(['key1', 'key2', 'key3'])
|
413
|
+
|
414
|
+
# Lock some keys
|
415
|
+
redis_lock_pool.lock('key1')
|
416
|
+
redis_lock_pool.lock('key2')
|
417
|
+
|
418
|
+
# Check health
|
419
|
+
locked, free = redis_lock_pool.health_check()
|
420
|
+
assert locked == 2
|
421
|
+
assert free == 1
|
422
|
+
|
423
|
+
def test_empty_health_check(self, redis_lock_pool: RedisLockPool):
|
424
|
+
"""Test health_check on an empty pool."""
|
425
|
+
# Clear the pool first
|
426
|
+
redis_lock_pool.clear()
|
427
|
+
|
428
|
+
# Check health of empty pool
|
429
|
+
locked, free = redis_lock_pool.health_check()
|
430
|
+
assert locked == 0
|
431
|
+
assert free == 0
|
432
|
+
|
433
|
+
def test_extend_empty_keys(self, redis_lock_pool: RedisLockPool):
|
434
|
+
"""Test extending the pool with an empty list of keys."""
|
435
|
+
# Clear the pool first
|
436
|
+
redis_lock_pool.clear()
|
437
|
+
|
438
|
+
# Extend with empty list - should not change anything
|
439
|
+
redis_lock_pool.extend([])
|
440
|
+
assert len(redis_lock_pool) == 0
|
441
|
+
|
442
|
+
# Test with None
|
443
|
+
redis_lock_pool.extend(None)
|
444
|
+
assert len(redis_lock_pool) == 0
|
445
|
+
|
446
|
+
def test_shrink_empty_keys(self, redis_lock_pool: RedisLockPool):
|
447
|
+
"""Test shrinking the pool with an empty list of keys."""
|
448
|
+
# Add some keys
|
449
|
+
redis_lock_pool.extend(['key1', 'key2'])
|
450
|
+
|
451
|
+
# Shrink with empty list - should not change anything
|
452
|
+
redis_lock_pool.shrink([])
|
453
|
+
assert len(redis_lock_pool) == 2
|
454
|
+
|
455
|
+
# Test with None - should also not change anything
|
456
|
+
redis_lock_pool.shrink(None)
|
457
|
+
assert len(redis_lock_pool) == 2
|
458
|
+
|
459
|
+
def test_assign_with_keys(self, redis_lock_pool: RedisLockPool):
|
460
|
+
"""Test assigning keys to the pool."""
|
461
|
+
# Clear first
|
462
|
+
redis_lock_pool.clear()
|
463
|
+
|
464
|
+
# Test assign with actual keys
|
465
|
+
keys = ['new1', 'new2', 'new3']
|
466
|
+
redis_lock_pool.assign(keys)
|
467
|
+
|
468
|
+
# Verify all keys were added
|
469
|
+
assert len(redis_lock_pool) == 3
|
470
|
+
for key in keys:
|
471
|
+
assert key in redis_lock_pool
|
472
|
+
|
473
|
+
def test_iterate_pool(self, redis_lock_pool: RedisLockPool):
|
474
|
+
"""Test iterating over the keys in the pool."""
|
475
|
+
# Add some keys
|
476
|
+
test_keys = ['key1', 'key2', 'key3']
|
477
|
+
redis_lock_pool.extend(test_keys)
|
478
|
+
|
479
|
+
# Iterate and collect keys
|
480
|
+
iterated_keys = []
|
481
|
+
for key in redis_lock_pool:
|
482
|
+
iterated_keys.append(key)
|
483
|
+
|
484
|
+
# Verify all keys were iterated
|
485
|
+
assert sorted(iterated_keys) == sorted(test_keys)
|
486
|
+
|
487
|
+
def test_assign_empty(self, redis_lock_pool: RedisLockPool):
|
488
|
+
"""Test assigning an empty list of keys to the pool."""
|
489
|
+
# First add some keys
|
490
|
+
redis_lock_pool.extend(['old1', 'old2'])
|
491
|
+
|
492
|
+
# Then assign empty list
|
493
|
+
redis_lock_pool.assign([])
|
494
|
+
|
495
|
+
# Verify all keys were removed
|
496
|
+
assert len(redis_lock_pool) == 0
|
497
|
+
|
498
|
+
# Assign None
|
499
|
+
redis_lock_pool.extend(['old1'])
|
500
|
+
redis_lock_pool.assign(None)
|
501
|
+
assert len(redis_lock_pool) == 0
|
502
|
+
|
503
|
+
|
504
|
+
class TestThreadLock:
|
505
|
+
"""Tests for the ThreadLock class."""
|
506
|
+
|
507
|
+
def test_lock(self, thread_lock: ThreadLock):
|
508
|
+
"""Test the lock method."""
|
509
|
+
assert thread_lock.lock('test-key')
|
510
|
+
assert thread_lock.is_locked('test-key')
|
511
|
+
|
512
|
+
def test_unlock(self, thread_lock: ThreadLock):
|
513
|
+
"""Test the unlock method."""
|
514
|
+
# First set the lock
|
515
|
+
thread_lock.update('test-key')
|
516
|
+
assert thread_lock.is_locked('test-key')
|
517
|
+
# Then unlock
|
518
|
+
thread_lock.unlock('test-key')
|
519
|
+
assert not thread_lock.is_locked('test-key')
|
520
|
+
|
521
|
+
def test_update(self, thread_lock: ThreadLock):
|
522
|
+
"""Test the update method."""
|
523
|
+
thread_lock.update('test-key', value='2', timeout=60)
|
524
|
+
assert thread_lock.lock_value('test-key') == '2'
|
525
|
+
# TTL should be close to 60
|
526
|
+
assert thread_lock.key_status('test-key', timeout=60) == LockStatus.LOCKED
|
527
|
+
|
528
|
+
def test_lock_value(self, thread_lock: ThreadLock):
|
529
|
+
"""Test the lock_value method."""
|
530
|
+
thread_lock.update('test-key', value='120')
|
531
|
+
assert thread_lock.lock_value('test-key') == '120'
|
532
|
+
|
533
|
+
def test_is_locked(self, thread_lock: ThreadLock):
|
534
|
+
"""Test the is_locked method."""
|
535
|
+
assert not thread_lock.is_locked('test-key')
|
536
|
+
thread_lock.update('test-key', value='300')
|
537
|
+
assert thread_lock.is_locked('test-key')
|
538
|
+
assert not thread_lock.is_locked('non-existent-key')
|
539
|
+
|
540
|
+
def test_key_status(self, thread_lock: ThreadLock):
|
541
|
+
"""Test the key_status method."""
|
542
|
+
# Test FREE status
|
543
|
+
assert thread_lock.key_status('free-key') == LockStatus.FREE
|
544
|
+
|
545
|
+
# Test LOCKED status
|
546
|
+
thread_lock.update('locked-key', value='1', timeout=60)
|
547
|
+
assert thread_lock.key_status('locked-key') == LockStatus.LOCKED
|
548
|
+
|
549
|
+
# Test UNAVAILABLE status (TTL > timeout)
|
550
|
+
thread_lock.update('unavailable-key', value='1', timeout=200)
|
551
|
+
assert thread_lock.key_status('unavailable-key', timeout=100) == LockStatus.UNAVAILABLE
|
552
|
+
|
553
|
+
def test_rlock(self, thread_lock: ThreadLock):
|
554
|
+
"""Test the rlock method."""
|
555
|
+
# First test acquiring a lock
|
556
|
+
assert thread_lock.rlock('test-key', value='1') is True
|
557
|
+
assert thread_lock.lock_value('test-key') == '1'
|
558
|
+
|
559
|
+
# Test acquiring the same lock with the same value
|
560
|
+
assert thread_lock.rlock('test-key', value='1') is True
|
561
|
+
|
562
|
+
# Test acquiring the same lock with a different value
|
563
|
+
assert thread_lock.rlock('test-key', value='2') is False
|
564
|
+
|
565
|
+
# Verify the original value wasn't changed
|
566
|
+
assert thread_lock.lock_value('test-key') == '1'
|
567
|
+
|
568
|
+
def test_conditional_setdel_operations(self, thread_lock: ThreadLock):
|
569
|
+
"""Test the conditional set/del operations."""
|
570
|
+
# Set initial value
|
571
|
+
test_key = 'cond-key'
|
572
|
+
thread_lock.update(test_key, value='10')
|
573
|
+
|
574
|
+
# Test setgt (greater than)
|
575
|
+
assert thread_lock.setgt(key=test_key, value=15) # 15 > 10, should succeed
|
576
|
+
assert thread_lock.lock_value(test_key) == '15' # Value should be updated to 15
|
577
|
+
|
578
|
+
# Test setgt with unsuccessful condition
|
579
|
+
assert not thread_lock.setgt(key=test_key, value=5) # 5 < 15, should not succeed
|
580
|
+
assert thread_lock.lock_value(test_key) == '15' # Value should remain 15
|
581
|
+
|
582
|
+
# Test setlt (less than)
|
583
|
+
assert thread_lock.setlt(key=test_key, value=5) # 5 < 15, should succeed
|
584
|
+
assert thread_lock.lock_value(test_key) == '5' # Value should be updated to 5
|
585
|
+
|
586
|
+
# Test setlt with unsuccessful condition
|
587
|
+
assert not thread_lock.setlt(key=test_key, value=10) # 10 > 5, should not succeed
|
588
|
+
assert thread_lock.lock_value(test_key) == '5' # Value should remain 5
|
589
|
+
|
590
|
+
# Test setge (greater than or equal)
|
591
|
+
assert thread_lock.setge(key=test_key, value=5) # 5 >= 5, should succeed
|
592
|
+
assert thread_lock.lock_value(test_key) == '5' # Value should still be 5
|
593
|
+
|
594
|
+
# Test setge with unsuccessful condition
|
595
|
+
assert not thread_lock.setge(key=test_key, value=4) # 4 < 5, should not succeed
|
596
|
+
assert thread_lock.lock_value(test_key) == '5' # Value should remain 5
|
597
|
+
|
598
|
+
# Test setle (less than or equal)
|
599
|
+
assert thread_lock.setle(key=test_key, value=5) # 5 <= 5, should succeed
|
600
|
+
assert thread_lock.lock_value(test_key) == '5' # Value should remain 5
|
601
|
+
|
602
|
+
# Test setle with unsuccessful condition
|
603
|
+
assert not thread_lock.setle(key=test_key, value=6) # 6 > 5, should not succeed
|
604
|
+
assert thread_lock.lock_value(test_key) == '5' # Value should remain 5
|
605
|
+
|
606
|
+
# Test seteq (equal)
|
607
|
+
assert thread_lock.seteq(key=test_key, value=5) # 5 == 5, should succeed
|
608
|
+
assert thread_lock.lock_value(test_key) == '5' # Value should remain 5
|
609
|
+
|
610
|
+
# Test seteq with unsuccessful condition
|
611
|
+
assert not thread_lock.seteq(key=test_key, value=6) # 6 != 5, should not succeed
|
612
|
+
assert thread_lock.lock_value(test_key) == '5' # Value should remain 5
|
613
|
+
|
614
|
+
# Test setne (not equal)
|
615
|
+
assert thread_lock.setne(key=test_key, value=10) # 10 != 5, should succeed
|
616
|
+
assert thread_lock.lock_value(test_key) == '10' # Value should be updated to 10
|
617
|
+
|
618
|
+
# Test setne with unsuccessful condition
|
619
|
+
assert not thread_lock.setne(key=test_key, value=10) # 10 == 10, should not succeed
|
620
|
+
assert thread_lock.lock_value(test_key) == '10' # Value should remain 10
|
621
|
+
|
622
|
+
# Test delgt (delete if greater than)
|
623
|
+
thread_lock.update(test_key, value='10')
|
624
|
+
assert thread_lock.delgt(key=test_key, value=15) # 15 > 10, should delete
|
625
|
+
assert not thread_lock.is_locked(test_key) # Key should be deleted
|
626
|
+
|
627
|
+
# Test dellt (delete if less than)
|
628
|
+
thread_lock.update(test_key, value='10')
|
629
|
+
assert thread_lock.dellt(key=test_key, value=5) # 5 < 10, should delete
|
630
|
+
assert not thread_lock.is_locked(test_key) # Key should be deleted
|
631
|
+
|
632
|
+
# Test delge (delete if greater than or equal)
|
633
|
+
thread_lock.update(test_key, value='10')
|
634
|
+
assert thread_lock.delge(key=test_key, value=10) # 10 >= 10, should delete
|
635
|
+
assert not thread_lock.is_locked(test_key) # Key should be deleted
|
636
|
+
|
637
|
+
# Test delle (delete if less than or equal)
|
638
|
+
thread_lock.update(test_key, value='10')
|
639
|
+
assert thread_lock.delle(key=test_key, value=10) # 10 <= 10, should delete
|
640
|
+
assert not thread_lock.is_locked(test_key) # Key should be deleted
|
641
|
+
|
642
|
+
# Test deleq (delete if equal)
|
643
|
+
thread_lock.update(test_key, value='10')
|
644
|
+
assert thread_lock.deleq(key=test_key, value=10) # 10 == 10, should delete
|
645
|
+
assert not thread_lock.is_locked(test_key) # Key should be deleted
|
646
|
+
|
647
|
+
# Test delne (delete if not equal)
|
648
|
+
thread_lock.update(test_key, value='10')
|
649
|
+
assert thread_lock.delne(key=test_key, value=5) # 5 != 10, should delete
|
650
|
+
assert not thread_lock.is_locked(test_key) # Key should be deleted
|
651
|
+
|
652
|
+
def test_thread_safety(self, thread_lock: ThreadLock):
|
653
|
+
"""Test thread safety of ThreadLock."""
|
654
|
+
def worker(key):
|
655
|
+
if thread_lock.lock(key):
|
656
|
+
time.sleep(0.1) # Simulate some work
|
657
|
+
thread_lock.unlock(key)
|
658
|
+
return True
|
659
|
+
return False
|
660
|
+
|
661
|
+
# Test multiple threads trying to acquire the same lock
|
662
|
+
with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor:
|
663
|
+
futures = [executor.submit(worker, 'test-key') for _ in range(10)]
|
664
|
+
results = [f.result() for f in futures]
|
665
|
+
|
666
|
+
# Only one thread should have successfully acquired the lock
|
667
|
+
assert sum(results) == 1
|
668
|
+
|
669
|
+
def test_invalid_operator(self, thread_lock: ThreadLock):
|
670
|
+
"""Test that an invalid operator raises a ValueError."""
|
671
|
+
with pytest.raises(ValueError, match="Invalid operator"):
|
672
|
+
thread_lock._compare_values("invalid", 10.0, 5.0)
|
673
|
+
|
674
|
+
def test_timeout_types(self, thread_lock: ThreadLock):
|
675
|
+
"""Test different timeout types."""
|
676
|
+
from datetime import timedelta, datetime
|
677
|
+
|
678
|
+
# Test with timedelta
|
679
|
+
test_key = 'timeout-key'
|
680
|
+
|
681
|
+
# Use timedelta for timeout (30 seconds)
|
682
|
+
thread_lock.update(test_key, value='1', timeout=timedelta(seconds=30))
|
683
|
+
assert thread_lock.is_locked(test_key)
|
684
|
+
assert thread_lock.key_status(test_key) == LockStatus.LOCKED
|
685
|
+
|
686
|
+
# Use None for timeout (should use a very long timeout)
|
687
|
+
thread_lock.update(test_key, value='2', timeout=None)
|
688
|
+
assert thread_lock.is_locked(test_key)
|
689
|
+
# The TTL should be very high (set to 2099)
|
690
|
+
future_time = datetime(2099, 1, 1).timestamp() - time.time()
|
691
|
+
# Allow 10 seconds margin in the test
|
692
|
+
assert thread_lock._get_ttl(test_key) > future_time - 10
|
693
|
+
|
694
|
+
|
695
|
+
class TestThreadLockPool:
|
696
|
+
"""Tests for the ThreadLockPool class."""
|
697
|
+
|
698
|
+
def test_keys(self, thread_lock_pool: ThreadLockPool):
|
699
|
+
"""Test the keys method."""
|
700
|
+
# Add some keys to the pool
|
701
|
+
thread_lock_pool.assign(['key1', 'key2'])
|
702
|
+
keys = thread_lock_pool.keys()
|
703
|
+
assert sorted(list(keys)) == ['key1', 'key2']
|
704
|
+
|
705
|
+
def test_extend(self, thread_lock_pool: ThreadLockPool):
|
706
|
+
"""Test the extend method with keys."""
|
707
|
+
thread_lock_pool.extend(['key1', 'key2'])
|
708
|
+
|
709
|
+
# Verify keys were added
|
710
|
+
assert 'key1' in thread_lock_pool
|
711
|
+
assert 'key2' in thread_lock_pool
|
712
|
+
|
713
|
+
def test_shrink(self, thread_lock_pool: ThreadLockPool):
|
714
|
+
"""Test the shrink method."""
|
715
|
+
# First add keys
|
716
|
+
thread_lock_pool.extend(['key1', 'key2', 'key3'])
|
717
|
+
|
718
|
+
# Then shrink
|
719
|
+
thread_lock_pool.shrink(['key1', 'key2'])
|
720
|
+
|
721
|
+
# Verify keys were removed
|
722
|
+
assert 'key1' not in thread_lock_pool
|
723
|
+
assert 'key2' not in thread_lock_pool
|
724
|
+
assert 'key3' in thread_lock_pool
|
725
|
+
|
726
|
+
def test_clear(self, thread_lock_pool: ThreadLockPool):
|
727
|
+
"""Test the clear method."""
|
728
|
+
# First add keys
|
729
|
+
thread_lock_pool.extend(['key1', 'key2'])
|
730
|
+
|
731
|
+
# Then clear
|
732
|
+
thread_lock_pool.clear()
|
733
|
+
|
734
|
+
# Verify all keys were removed
|
735
|
+
assert len(thread_lock_pool) == 0
|
736
|
+
|
737
|
+
def test_assign(self, thread_lock_pool: ThreadLockPool):
|
738
|
+
"""Test the assign method."""
|
739
|
+
# First add some initial keys
|
740
|
+
thread_lock_pool.extend(['old1', 'old2'])
|
741
|
+
|
742
|
+
# Then assign new keys
|
743
|
+
thread_lock_pool.assign(['key1', 'key2'])
|
744
|
+
|
745
|
+
# Verify old keys were removed and new keys were added
|
746
|
+
assert 'old1' not in thread_lock_pool
|
747
|
+
assert 'old2' not in thread_lock_pool
|
748
|
+
assert 'key1' in thread_lock_pool
|
749
|
+
assert 'key2' in thread_lock_pool
|
750
|
+
|
751
|
+
def test_contains(self, thread_lock_pool: ThreadLockPool):
|
752
|
+
"""Test the __contains__ method."""
|
753
|
+
# Add a key
|
754
|
+
thread_lock_pool.extend(['key1'])
|
755
|
+
|
756
|
+
# Test __contains__
|
757
|
+
assert 'key1' in thread_lock_pool
|
758
|
+
assert 'key2' not in thread_lock_pool
|
759
|
+
|
760
|
+
def test_set_del_item(self, thread_lock_pool: ThreadLockPool):
|
761
|
+
"""Test the __setitem__ and __delitem__ methods."""
|
762
|
+
# First test extend to add a key
|
763
|
+
thread_lock_pool.extend(['key1'])
|
764
|
+
|
765
|
+
# Test __getitem__
|
766
|
+
assert 'key1' in thread_lock_pool
|
767
|
+
|
768
|
+
# Test shrink to remove key
|
769
|
+
thread_lock_pool.shrink(['key1'])
|
770
|
+
assert 'key1' not in thread_lock_pool
|
771
|
+
|
772
|
+
def test_health_check(self, thread_lock_pool: ThreadLockPool):
|
773
|
+
"""Test the health_check method."""
|
774
|
+
# Add some keys
|
775
|
+
thread_lock_pool.extend(['key1', 'key2', 'key3'])
|
776
|
+
|
777
|
+
# Lock some keys
|
778
|
+
thread_lock_pool.lock('key1')
|
779
|
+
thread_lock_pool.lock('key2')
|
780
|
+
|
781
|
+
# Check health
|
782
|
+
locked, free = thread_lock_pool.health_check()
|
783
|
+
assert locked == 2
|
784
|
+
assert free == 1
|
785
|
+
|
786
|
+
def test_thread_safety(self, thread_lock_pool: ThreadLockPool):
|
787
|
+
"""Test thread safety of ThreadLockPool."""
|
788
|
+
def worker(key):
|
789
|
+
if thread_lock_pool.lock(key):
|
790
|
+
time.sleep(0.1) # Simulate some work
|
791
|
+
thread_lock_pool.unlock(key)
|
792
|
+
return True
|
793
|
+
return False
|
794
|
+
|
795
|
+
# Add keys to the pool
|
796
|
+
thread_lock_pool.extend(['test-key'])
|
797
|
+
|
798
|
+
# Test multiple threads trying to acquire the same lock
|
799
|
+
with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor:
|
800
|
+
futures = [executor.submit(worker, 'test-key') for _ in range(10)]
|
801
|
+
results = [f.result() for f in futures]
|
802
|
+
|
803
|
+
# Only one thread should have successfully acquired the lock
|
804
|
+
assert sum(results) == 1
|
805
|
+
|
806
|
+
def test_empty_health_check(self, thread_lock_pool: ThreadLockPool):
|
807
|
+
"""Test health_check on an empty pool."""
|
808
|
+
# Clear the pool first
|
809
|
+
thread_lock_pool.clear()
|
810
|
+
|
811
|
+
# Check health of empty pool
|
812
|
+
locked, free = thread_lock_pool.health_check()
|
813
|
+
assert locked == 0
|
814
|
+
assert free == 0
|
815
|
+
|
816
|
+
def test_empty_pool_operations(self, thread_lock_pool: ThreadLockPool):
|
817
|
+
"""Test operations on an empty pool."""
|
818
|
+
# Test clear on already empty pool
|
819
|
+
thread_lock_pool.clear()
|
820
|
+
assert len(thread_lock_pool) == 0
|
821
|
+
|
822
|
+
# Test shrink on empty pool
|
823
|
+
thread_lock_pool.shrink(['nonexistent'])
|
824
|
+
assert len(thread_lock_pool) == 0
|
825
|
+
|
826
|
+
# Test extend with None
|
827
|
+
thread_lock_pool.extend(None)
|
828
|
+
assert len(thread_lock_pool) == 0
|
829
|
+
|
830
|
+
# Test assign with None
|
831
|
+
thread_lock_pool.assign(None)
|
832
|
+
assert len(thread_lock_pool) == 0
|
833
|
+
|
834
|
+
def test_iterate_pool(self, thread_lock_pool: ThreadLockPool):
|
835
|
+
"""Test iterating over the keys in the pool."""
|
836
|
+
# Add some keys
|
837
|
+
test_keys = ['key1', 'key2', 'key3']
|
838
|
+
thread_lock_pool.extend(test_keys)
|
839
|
+
|
840
|
+
# Iterate and collect keys
|
841
|
+
iterated_keys = []
|
842
|
+
for key in thread_lock_pool:
|
843
|
+
iterated_keys.append(key)
|
844
|
+
|
845
|
+
# Verify all keys were iterated
|
846
|
+
assert sorted(iterated_keys) == sorted(test_keys)
|
847
|
+
|
848
|
+
def test_len_empty_pool(self, thread_lock_pool: ThreadLockPool):
|
849
|
+
"""Test len() on empty pool."""
|
850
|
+
thread_lock_pool.clear()
|
851
|
+
assert len(thread_lock_pool) == 0
|