redis-allocator 0.0.1__py3-none-any.whl → 0.3.2__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.
tests/test_lock.py CHANGED
@@ -1,9 +1,19 @@
1
1
  """Tests for the RedisLock and RedisLockPool classes."""
2
+
2
3
  from redis import Redis
3
- from redis_allocator.lock import RedisLock, RedisLockPool, LockStatus, ThreadLock, ThreadLockPool
4
+ from redis_allocator import (
5
+ RedisLock,
6
+ RedisLockPool,
7
+ LockStatus,
8
+ ThreadLock,
9
+ ThreadLockPool
10
+ )
4
11
  import time
5
12
  import concurrent.futures
6
13
  import pytest
14
+ import threading
15
+ from freezegun import freeze_time
16
+ import datetime
7
17
 
8
18
 
9
19
  class TestRedisLock:
@@ -11,282 +21,294 @@ class TestRedisLock:
11
21
 
12
22
  def test_lock(self, redis_lock: RedisLock, redis_client: Redis):
13
23
  """Test the lock method."""
14
- assert redis_lock.lock('test-key') is True
15
- assert redis_client.exists(redis_lock._key_str('test-key'))
24
+ assert redis_lock.lock("test-key") is True
25
+ assert redis_client.exists(redis_lock._key_str("test-key"))
16
26
 
17
27
  def test_unlock(self, redis_lock: RedisLock, redis_client: Redis):
18
28
  """Test the unlock method."""
19
29
  # First set the lock
20
- redis_lock.update('test-key')
21
- assert redis_client.exists(redis_lock._key_str('test-key'))
30
+ redis_lock.update("test-key")
31
+ assert redis_client.exists(redis_lock._key_str("test-key"))
22
32
  # Then unlock
23
- redis_lock.unlock('test-key')
24
- assert not redis_client.exists(redis_lock._key_str('test-key'))
33
+ redis_lock.unlock("test-key")
34
+ assert not redis_client.exists(redis_lock._key_str("test-key"))
25
35
 
26
36
  def test_update(self, redis_lock: RedisLock, redis_client: Redis):
27
37
  """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'
38
+ redis_lock.update("test-key", value="2", timeout=60)
39
+ assert redis_client.get(redis_lock._key_str("test-key")) == "2"
30
40
  # TTL should be close to 60
31
- ttl = redis_client.ttl(redis_lock._key_str('test-key'))
41
+ ttl = redis_client.ttl(redis_lock._key_str("test-key"))
32
42
  assert 55 <= ttl <= 60
33
43
 
34
- def test_lock_value(self, redis_lock: RedisLock, redis_client: Redis):
44
+ def test_lock_value(self, redis_lock: RedisLock):
35
45
  """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'
46
+ redis_lock.lock("test-key", value="120")
47
+ assert redis_lock.lock_value("test-key") == "120"
38
48
 
39
49
  def test_is_locked(self, redis_lock: RedisLock, redis_client: Redis):
40
50
  """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')
51
+ assert not redis_lock.is_locked("test-key")
52
+ redis_client.set(redis_lock._key_str("test-key"), "300")
53
+ assert redis_lock.is_locked("test-key")
54
+ assert not redis_lock.is_locked("non-existent-key")
45
55
 
46
56
  def test_key_status(self, redis_lock: RedisLock, redis_client: Redis):
47
57
  """Test the key_status method."""
48
58
  # Test FREE status
49
- assert redis_lock.key_status('free-key') == LockStatus.FREE
50
-
59
+ assert redis_lock.key_status("free-key") == LockStatus.FREE
60
+
51
61
  # 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
-
62
+ redis_client.set(redis_lock._key_str("locked-key"), "1", ex=60)
63
+ assert redis_lock.key_status("locked-key") == LockStatus.LOCKED
64
+
55
65
  # 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
-
66
+ redis_client.set(redis_lock._key_str("error-key"), "1")
67
+ assert redis_lock.key_status("error-key") == LockStatus.ERROR
68
+
59
69
  # 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
70
+ redis_client.set(redis_lock._key_str("unavailable-key"), "1", ex=200)
71
+ assert redis_lock.key_status("unavailable-key", timeout=100) == LockStatus.UNAVAILABLE
62
72
 
63
73
  def test_rlock(self, redis_lock: RedisLock, redis_client: Redis):
64
74
  """Test the rlock method."""
65
75
  # 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
-
76
+ assert redis_lock.rlock("test-key", value="1") is True
77
+ assert redis_client.get(redis_lock._key_str("test-key")) == "1"
78
+
69
79
  # Test acquiring the same lock with the same value
70
- assert redis_lock.rlock('test-key', value='1') is True
71
-
80
+ assert redis_lock.rlock("test-key", value="1") is True
81
+
72
82
  # Test acquiring the same lock with a different value
73
- assert redis_lock.rlock('test-key', value='2') is False
74
-
83
+ assert redis_lock.rlock("test-key", value="2") is False
84
+
75
85
  # Verify the original value wasn't changed
76
- assert redis_client.get(redis_lock._key_str('test-key')) == '1'
86
+ assert redis_client.get(redis_lock._key_str("test-key")) == "1"
77
87
 
78
- def test_conditional_setdel_operations(self, redis_lock: RedisLock, redis_client: Redis):
88
+ def test_conditional_setdel_operations(
89
+ self, redis_lock: RedisLock, redis_client: Redis
90
+ ):
79
91
  """Test the conditional set/del operations."""
80
92
  # Skip direct testing of _conditional_setdel as it's an internal method
81
93
  # We'll test the public methods that use it instead
82
-
94
+
83
95
  # Set initial value
84
- test_key = 'cond-key'
96
+ test_key = "cond-key"
85
97
  key_str = redis_lock._key_str(test_key)
86
- redis_client.set(key_str, '10')
87
-
98
+ redis_client.set(key_str, "10")
99
+
88
100
  # Test setgt (greater than)
89
101
  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
-
102
+ assert redis_client.get(key_str) == "15" # Value should be updated to 15
103
+
92
104
  # Test setgt with unsuccessful condition
93
105
  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
-
106
+ assert redis_client.get(key_str) == "15" # Value should remain 15
107
+
96
108
  # Test setlt (less than)
97
109
  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
-
110
+ assert redis_client.get(key_str) == "5" # Value should be updated to 5
111
+
100
112
  # 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
-
113
+ assert not redis_lock.setlt(
114
+ key=test_key, value=10
115
+ ) # 10 > 5, should not succeed
116
+ assert redis_client.get(key_str) == "5" # Value should remain 5
117
+
104
118
  # Test setge (greater than or equal)
105
119
  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
-
120
+ assert redis_client.get(key_str) == "5" # Value should still be 5
121
+
108
122
  # Test setge with unsuccessful condition
109
123
  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
-
124
+ assert redis_client.get(key_str) == "5" # Value should remain 5
125
+
112
126
  # Test setle (less than or equal)
113
127
  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
-
128
+ assert redis_client.get(key_str) == "5" # Value should remain 5
129
+
116
130
  # Test setle with unsuccessful condition
117
131
  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
-
132
+ assert redis_client.get(key_str) == "5" # Value should remain 5
133
+
120
134
  # Test seteq (equal)
121
135
  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
-
136
+ assert redis_client.get(key_str) == "5" # Value should remain 5
137
+
124
138
  # Test seteq with unsuccessful condition
125
139
  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
-
140
+ assert redis_client.get(key_str) == "5" # Value should remain 5
141
+
128
142
  # Test setne (not equal)
129
143
  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
-
144
+ assert redis_client.get(key_str) == "10" # Value should be updated to 10
145
+
132
146
  # 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
-
147
+ assert not redis_lock.setne(
148
+ key=test_key, value=10
149
+ ) # 10 == 10, should not succeed
150
+ assert redis_client.get(key_str) == "10" # Value should remain 10
151
+
136
152
  # Test delgt (delete if greater than)
137
- redis_client.set(key_str, '10')
153
+ redis_client.set(key_str, "10")
138
154
  redis_lock.delgt(key=test_key, value=15) # 15 > 10, should delete
139
155
  assert not redis_client.exists(key_str) # Key should be deleted
140
-
156
+
141
157
  # Test dellt (delete if less than)
142
- redis_client.set(key_str, '10')
158
+ redis_client.set(key_str, "10")
143
159
  redis_lock.dellt(key=test_key, value=5) # 5 < 10, should delete
144
160
  assert not redis_client.exists(key_str) # Key should be deleted
145
-
161
+
146
162
  # Test delge (delete if greater than or equal)
147
- redis_client.set(key_str, '10')
163
+ redis_client.set(key_str, "10")
148
164
  redis_lock.delge(key=test_key, value=10) # 10 >= 10, should delete
149
165
  assert not redis_client.exists(key_str) # Key should be deleted
150
-
166
+
151
167
  # Test delle (delete if less than or equal)
152
- redis_client.set(key_str, '10')
168
+ redis_client.set(key_str, "10")
153
169
  redis_lock.delle(key=test_key, value=10) # 10 <= 10, should delete
154
170
  assert not redis_client.exists(key_str) # Key should be deleted
155
-
171
+
156
172
  # Test deleq (delete if equal)
157
- redis_client.set(key_str, '10')
173
+ redis_client.set(key_str, "10")
158
174
  redis_lock.deleq(key=test_key, value=10) # 10 == 10, should delete
159
175
  assert not redis_client.exists(key_str) # Key should be deleted
160
-
176
+
161
177
  # Test delne (delete if not equal)
162
- redis_client.set(key_str, '10')
178
+ redis_client.set(key_str, "10")
163
179
  redis_lock.delne(key=test_key, value=5) # 5 != 10, should delete
164
180
  assert not redis_client.exists(key_str) # Key should be deleted
165
-
181
+
166
182
  # Test with expired keys
167
- redis_client.set(key_str, '10')
183
+ redis_client.set(key_str, "10")
168
184
  redis_lock.setgt(key=test_key, value=15, ex=30) # set with expiration
169
185
  ttl = redis_client.ttl(key_str)
170
186
  assert ttl > 0 and ttl <= 30 # Should have a TTL set
171
187
 
172
188
  def test_setters_and_deleters(self, redis_lock: RedisLock, redis_client: Redis):
173
189
  """Test all setter and deleter methods."""
174
- test_key = 'op-key'
190
+ test_key = "op-key"
175
191
  key_str = redis_lock._key_str(test_key)
176
-
192
+
177
193
  # Let's test the setters first
178
-
194
+
179
195
  # Set an initial value
180
- redis_client.set(key_str, '10')
181
-
196
+ redis_client.set(key_str, "10")
197
+
182
198
  # Test setgt (doesn't meet condition)
183
199
  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
-
200
+ assert redis_client.get(key_str) == "10" # Unchanged because 5 is not > 10
201
+
186
202
  # Test setgt (meets condition)
187
203
  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
-
204
+ assert redis_client.get(key_str) == "7" # Changed because 15 > 10
205
+
190
206
  # Test setlt (doesn't meet condition)
191
207
  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
-
208
+ assert redis_client.get(key_str) == "7" # Unchanged because 10 is not < 7
209
+
194
210
  # Test setlt (meets condition)
195
211
  assert redis_lock.setlt(key=test_key, value=5)
196
- assert redis_client.get(key_str) == '5' # Changed because 5 < 7
197
-
212
+ assert redis_client.get(key_str) == "5" # Changed because 5 < 7
213
+
198
214
  # Reset for more tests
199
- redis_client.set(key_str, '10')
200
-
215
+ redis_client.set(key_str, "10")
216
+
201
217
  # Test setge (meets condition)
202
218
  assert redis_lock.setge(key=test_key, value=10)
203
- assert redis_client.get(key_str) == '10' # Changed because 10 >= 10
204
-
219
+ assert redis_client.get(key_str) == "10" # Changed because 10 >= 10
220
+
205
221
  # Test setle (meets condition)
206
222
  assert redis_lock.setle(key=test_key, value=10)
207
- assert redis_client.get(key_str) == '10' # Changed because 10 <= 10
208
-
223
+ assert redis_client.get(key_str) == "10" # Changed because 10 <= 10
224
+
209
225
  # Test seteq (meets condition)
210
226
  assert redis_lock.seteq(key=test_key, value=10)
211
- assert redis_client.get(key_str) == '10' # Changed because 10 == 10
212
-
227
+ assert redis_client.get(key_str) == "10" # Changed because 10 == 10
228
+
213
229
  # Test setne (doesn't meet condition)
214
230
  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
-
231
+ assert redis_client.get(key_str) == "10" # Unchanged because 10 is not != 10
232
+
217
233
  # Test setne (meets condition)
218
234
  assert redis_lock.setne(key=test_key, value=5)
219
- assert redis_client.get(key_str) == '5' # Changed because 5 != 10
220
-
235
+ assert redis_client.get(key_str) == "5" # Changed because 5 != 10
236
+
221
237
  # Now test the deleters
222
-
238
+
223
239
  # Reset for delete tests
224
- redis_client.set(key_str, '10')
225
-
240
+ redis_client.set(key_str, "10")
241
+
226
242
  # Test delgt (doesn't meet condition)
227
243
  assert not redis_lock.delgt(key=test_key, value=5)
228
244
  assert redis_client.exists(key_str) # Key still exists because 5 is not > 10
229
-
245
+
230
246
  # Test delgt (meets condition)
231
247
  assert redis_lock.delgt(key=test_key, value=15)
232
248
  assert not redis_client.exists(key_str) # Key deleted because 15 > 10
233
-
249
+
234
250
  # Reset for more delete tests
235
- redis_client.set(key_str, '10')
236
-
251
+ redis_client.set(key_str, "10")
252
+
237
253
  # Test dellt (doesn't meet condition)
238
254
  assert not redis_lock.dellt(key=test_key, value=15)
239
255
  assert redis_client.exists(key_str) # Key still exists because 15 is not < 10
240
-
256
+
241
257
  # Test dellt (meets condition)
242
258
  assert redis_lock.dellt(key=test_key, value=5)
243
259
  assert not redis_client.exists(key_str) # Key deleted because 5 < 10
244
-
260
+
245
261
  # Test delge, delle, deleq, delne
246
- redis_client.set(key_str, '10')
262
+ redis_client.set(key_str, "10")
247
263
  assert redis_lock.delge(key=test_key, value=10) # 10 >= 10, should delete
248
264
  assert not redis_client.exists(key_str)
249
-
250
- redis_client.set(key_str, '10')
265
+
266
+ redis_client.set(key_str, "10")
251
267
  assert redis_lock.delle(key=test_key, value=10) # 10 <= 10, should delete
252
268
  assert not redis_client.exists(key_str)
253
-
254
- redis_client.set(key_str, '10')
269
+
270
+ redis_client.set(key_str, "10")
255
271
  assert redis_lock.deleq(key=test_key, value=10) # 10 == 10, should delete
256
272
  assert not redis_client.exists(key_str)
257
-
258
- redis_client.set(key_str, '10')
273
+
274
+ redis_client.set(key_str, "10")
259
275
  assert redis_lock.delne(key=test_key, value=5) # 5 != 10, should delete
260
276
  assert not redis_client.exists(key_str)
261
-
277
+
262
278
  # Test unsuccessful deletions
263
- redis_client.set(key_str, '10')
279
+ redis_client.set(key_str, "10")
264
280
  assert not redis_lock.delge(key=test_key, value=5) # 5 < 10, should not delete
265
281
  assert redis_client.exists(key_str)
266
-
267
- assert not redis_lock.delle(key=test_key, value=15) # 15 > 10, should not delete
282
+
283
+ assert not redis_lock.delle(
284
+ key=test_key, value=15
285
+ ) # 15 > 10, should not delete
268
286
  assert redis_client.exists(key_str)
269
-
270
- assert not redis_lock.deleq(key=test_key, value=11) # 11 != 10, should not delete
287
+
288
+ assert not redis_lock.deleq(
289
+ key=test_key, value=11
290
+ ) # 11 != 10, should not delete
271
291
  assert redis_client.exists(key_str)
272
-
273
- assert not redis_lock.delne(key=test_key, value=10) # 10 == 10, should not delete
292
+
293
+ assert not redis_lock.delne(
294
+ key=test_key, value=10
295
+ ) # 10 == 10, should not delete
274
296
  assert redis_client.exists(key_str)
275
297
 
276
298
  def test_equality_and_hash(self, redis_client: Redis):
277
299
  """Test equality and hash methods."""
278
300
  # Create two identical locks
279
- lock1 = RedisLock(redis_client, 'test')
280
- lock2 = RedisLock(redis_client, 'test')
281
-
301
+ lock1 = RedisLock(redis_client, "test")
302
+ lock2 = RedisLock(redis_client, "test")
303
+
282
304
  # Create a different lock
283
- lock3 = RedisLock(redis_client, 'different')
284
-
305
+ lock3 = RedisLock(redis_client, "different")
306
+
285
307
  # Test equality
286
308
  assert lock1 == lock2
287
309
  assert lock1 != lock3
288
310
  assert lock1 != "not a lock"
289
-
311
+
290
312
  # Test hash
291
313
  lock_set = {lock1, lock2, lock3}
292
314
  assert len(lock_set) == 2 # lock1 and lock2 should hash to the same value
@@ -299,27 +321,176 @@ class TestRedisLock:
299
321
  def test_timeout_types(self, redis_lock: RedisLock, redis_client: Redis):
300
322
  """Test different timeout types."""
301
323
  from datetime import timedelta
302
-
324
+
303
325
  # Test with timedelta
304
- test_key = 'timeout-key'
326
+ test_key = "timeout-key"
305
327
  key_str = redis_lock._key_str(test_key)
306
-
328
+
307
329
  # Use timedelta for timeout
308
- redis_lock.update(test_key, value='1', timeout=timedelta(seconds=30))
330
+ redis_lock.update(test_key, value="1", timeout=timedelta(seconds=30))
309
331
  ttl = redis_client.ttl(key_str)
310
332
  assert 25 <= ttl <= 30 # Allow a small margin
311
-
333
+
312
334
  # Use None for timeout (should use a very long timeout)
313
- redis_lock.update(test_key, value='2', timeout=None)
335
+ redis_lock.update(test_key, value="2", timeout=None)
314
336
  ttl = redis_client.ttl(key_str)
315
337
  assert ttl == -1 # No expiration
316
338
 
317
339
  def test_decoder_assertion(self, redis_client_raw: Redis):
318
340
  """Test that the RedisLock constructor asserts that decode_responses is enabled."""
319
341
  # redis_client_raw is a client with decode_responses=False
320
- with pytest.raises(AssertionError, match="Redis must be configured to decode responses"):
342
+ with pytest.raises(
343
+ AssertionError, match="Redis must be configured to decode responses"
344
+ ):
321
345
  RedisLock(redis_client_raw, "test")
322
346
 
347
+ def test_multi_thread_lock_competition(self, redis_lock: RedisLock):
348
+ """Test that only one thread can acquire the same lock at a time."""
349
+ lock_key = "multi-thread-test-key"
350
+ num_threads = 10
351
+ success_count = 0
352
+ lock_holder = None
353
+ threads_completed = 0
354
+ thread_lock = threading.Lock() # Thread lock to protect shared variables
355
+
356
+ # Initial time
357
+ current_time = datetime.datetime(2023, 1, 1, 12, 0, 0)
358
+
359
+ def worker():
360
+ nonlocal success_count, lock_holder, threads_completed
361
+ thread_id = threading.get_ident()
362
+ # Try to acquire the lock multiple times with a small delay
363
+ for _ in range(5): # Try 5 times
364
+ if redis_lock.lock(lock_key, value=str(thread_id), timeout=100):
365
+ # This thread acquired the lock
366
+ with thread_lock: # Protect the shared counter
367
+ success_count += 1
368
+ lock_holder = thread_id
369
+ # Wait a bit before retrying
370
+ time.sleep(0.1)
371
+
372
+ with thread_lock: # Protect the shared counter
373
+ threads_completed += 1
374
+
375
+ # Start multiple threads to compete for the lock
376
+ threads = []
377
+ for _ in range(num_threads):
378
+ thread = threading.Thread(target=worker)
379
+ thread.start()
380
+ threads.append(thread)
381
+
382
+ # Use busy waiting with freezegun to simulate time passing
383
+ with freeze_time(current_time) as frozen_time:
384
+ # Wait for all threads to complete using busy waiting
385
+ while any(thread.is_alive() for thread in threads):
386
+ # Advance time by 0.05 seconds to accelerate sleep
387
+ frozen_time.tick(0.05)
388
+ time.sleep(0.001) # Short real sleep to prevent CPU hogging
389
+
390
+ # Verify only one thread got the lock
391
+ assert success_count == 1
392
+ assert lock_holder is not None
393
+ assert threads_completed == num_threads
394
+
395
+ def test_lock_timeout_expiry(self, redis_lock: RedisLock):
396
+ """Test that a lock can be acquired after the previous holder's timeout expires."""
397
+ lock_key = "timeout-test-key"
398
+ lock_timeout = 60 # 60 seconds timeout
399
+
400
+ # Set the starting time
401
+ initial_time = datetime.datetime(2023, 1, 1, 12, 0, 0)
402
+
403
+ with freeze_time(initial_time) as frozen_time:
404
+ # First thread acquires the lock
405
+ thread1_id = "thread-1"
406
+ assert redis_lock.lock(lock_key, value=thread1_id, timeout=lock_timeout)
407
+
408
+ # Verify the lock is held by thread 1
409
+ assert redis_lock.is_locked(lock_key)
410
+ assert redis_lock.lock_value(lock_key) == thread1_id
411
+
412
+ # Thread 2 tries to acquire the same lock and fails
413
+ thread2_id = "thread-2"
414
+ assert not redis_lock.lock(lock_key, value=thread2_id)
415
+
416
+ # Advance time to just before timeout
417
+ frozen_time.tick(lock_timeout - 1)
418
+
419
+ # Thread 2 tries again and still fails
420
+ assert not redis_lock.lock(lock_key, value=thread2_id)
421
+
422
+ # Advance time past the timeout
423
+ frozen_time.tick(2)
424
+
425
+ # Thread 2 tries again and succeeds because the lock has expired
426
+ assert redis_lock.lock(lock_key, value=thread2_id)
427
+
428
+ # Verify the lock is now held by thread 2
429
+ assert redis_lock.is_locked(lock_key)
430
+ assert redis_lock.lock_value(lock_key) == thread2_id
431
+
432
+ def test_lock_update_prevents_timeout(self, redis_lock: RedisLock, redis_client: Redis):
433
+ """Test that updating a lock prevents it from timing out."""
434
+ lock_key = "update-test-key"
435
+ lock_timeout = 60 # 60 seconds timeout
436
+
437
+ # Set the starting time
438
+ initial_time = datetime.datetime(2023, 1, 1, 12, 0, 0)
439
+
440
+ with freeze_time(initial_time) as frozen_time:
441
+ # First thread acquires the lock
442
+ thread1_id = "thread-1"
443
+ assert redis_lock.lock(lock_key, value=thread1_id, timeout=lock_timeout)
444
+
445
+ # Advance time to just before timeout
446
+ frozen_time.tick(lock_timeout - 10)
447
+
448
+ # Thread 1 updates the lock
449
+ redis_lock.update(lock_key, value=thread1_id, timeout=lock_timeout)
450
+
451
+ # Advance time past the original timeout
452
+ frozen_time.tick(20)
453
+
454
+ # Thread 2 tries to acquire the lock and fails because thread 1 updated it
455
+ thread2_id = "thread-2"
456
+ assert not redis_lock.lock(lock_key, value=thread2_id)
457
+
458
+ # Verify the lock is still held by thread 1
459
+ assert redis_lock.is_locked(lock_key)
460
+ assert redis_lock.lock_value(lock_key) == thread1_id
461
+
462
+ # Advance time past the new timeout
463
+ frozen_time.tick(lock_timeout)
464
+
465
+ # Thread 2 tries again and succeeds because the updated lock has expired
466
+ assert redis_lock.lock(lock_key, value=thread2_id)
467
+
468
+ # Verify the lock is now held by thread 2
469
+ assert redis_lock.is_locked(lock_key)
470
+ assert redis_lock.lock_value(lock_key) == thread2_id
471
+
472
+ def test_rlock_same_thread_different_thread(self, redis_lock: RedisLock, redis_client: Redis):
473
+ """Test that the same thread can rlock itself but different threads cannot."""
474
+ lock_key = "rlock-test-key"
475
+ thread1_id = "thread-1"
476
+ thread2_id = "thread-2"
477
+
478
+ # Thread 1 acquires the lock
479
+ assert redis_lock.lock(lock_key, value=thread1_id)
480
+
481
+ # Thread 1 can reacquire its own lock with rlock
482
+ assert redis_lock.rlock(lock_key, value=thread1_id)
483
+
484
+ # Thread 2 cannot acquire the lock with either lock or rlock
485
+ assert not redis_lock.lock(lock_key, value=thread2_id)
486
+ assert not redis_lock.rlock(lock_key, value=thread2_id)
487
+
488
+ # Thread 1 releases the lock
489
+ redis_lock.unlock(lock_key)
490
+
491
+ # Thread 2 can now acquire the lock
492
+ assert redis_lock.lock(lock_key, value=thread2_id)
493
+
323
494
 
324
495
  class TestRedisLockPool:
325
496
  """Tests for the RedisLockPool class."""
@@ -327,94 +498,94 @@ class TestRedisLockPool:
327
498
  def test_keys(self, redis_lock_pool: RedisLockPool):
328
499
  """Test the keys method."""
329
500
  # Add some keys to the pool
330
- redis_lock_pool.assign(['key1', 'key2'])
501
+ redis_lock_pool.assign(["key1", "key2"])
331
502
  keys = redis_lock_pool.keys()
332
- assert sorted(list(keys)) == ['key1', 'key2']
503
+ assert sorted(list(keys)) == ["key1", "key2"]
333
504
 
334
- def test_extend(self, redis_lock_pool: RedisLockPool, redis_client: Redis):
505
+ def test_extend(self, redis_lock_pool: RedisLockPool):
335
506
  """Test the extend method with keys."""
336
- redis_lock_pool.extend(['key1', 'key2'])
337
-
507
+ redis_lock_pool.extend(["key1", "key2"])
508
+
338
509
  # Verify keys were added
339
- assert 'key1' in redis_lock_pool
340
- assert 'key2' in redis_lock_pool
510
+ assert "key1" in redis_lock_pool
511
+ assert "key2" in redis_lock_pool
341
512
 
342
- def test_shrink(self, redis_lock_pool: RedisLockPool, redis_client: Redis):
513
+ def test_shrink(self, redis_lock_pool: RedisLockPool):
343
514
  """Test the shrink method."""
344
515
  # First add keys
345
- redis_lock_pool.extend(['key1', 'key2', 'key3'])
346
-
516
+ redis_lock_pool.extend(["key1", "key2", "key3"])
517
+
347
518
  # Then shrink
348
- redis_lock_pool.shrink(['key1', 'key2'])
519
+ redis_lock_pool.shrink(["key1", "key2"])
349
520
 
350
521
  # 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
522
+ assert "key1" not in redis_lock_pool
523
+ assert "key2" not in redis_lock_pool
524
+ assert "key3" in redis_lock_pool
354
525
 
355
- def test_clear(self, redis_lock_pool: RedisLockPool, redis_client: Redis):
526
+ def test_clear(self, redis_lock_pool: RedisLockPool):
356
527
  """Test the clear method."""
357
528
  # First add keys
358
- redis_lock_pool.extend(['key1', 'key2'])
359
-
529
+ redis_lock_pool.extend(["key1", "key2"])
530
+
360
531
  # Then clear
361
532
  redis_lock_pool.clear()
362
-
533
+
363
534
  # Verify all keys were removed
364
535
  assert len(redis_lock_pool) == 0
365
536
 
366
- def test_assign(self, redis_lock_pool: RedisLockPool, redis_client: Redis):
537
+ def test_assign(self, redis_lock_pool: RedisLockPool):
367
538
  """Test the assign method."""
368
539
  # First add some initial keys
369
- redis_lock_pool.extend(['old1', 'old2'])
370
-
540
+ redis_lock_pool.extend(["old1", "old2"])
541
+
371
542
  # Then assign new keys
372
- redis_lock_pool.assign(['key1', 'key2'])
373
-
543
+ redis_lock_pool.assign(["key1", "key2"])
544
+
374
545
  # 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
546
+ assert "old1" not in redis_lock_pool
547
+ assert "old2" not in redis_lock_pool
548
+ assert "key1" in redis_lock_pool
549
+ assert "key2" in redis_lock_pool
379
550
 
380
- def test_contains(self, redis_lock_pool: RedisLockPool, redis_client: Redis):
551
+ def test_contains(self, redis_lock_pool: RedisLockPool):
381
552
  """Test the __contains__ method."""
382
553
  # Add a key
383
- redis_lock_pool.extend(['key1'])
384
-
554
+ redis_lock_pool.extend(["key1"])
555
+
385
556
  # Test __contains__
386
- assert 'key1' in redis_lock_pool
387
- assert 'key2' not in redis_lock_pool
557
+ assert "key1" in redis_lock_pool
558
+ assert "key2" not in redis_lock_pool
388
559
 
389
- def test_len(self, redis_lock_pool: RedisLockPool, redis_client: Redis):
560
+ def test_len(self, redis_lock_pool: RedisLockPool):
390
561
  """Test the __len__ method."""
391
562
  # Add some keys
392
- redis_lock_pool.extend(['key1', 'key2', 'key3'])
393
-
563
+ redis_lock_pool.extend(["key1", "key2", "key3"])
564
+
394
565
  # Test __len__
395
566
  assert len(redis_lock_pool) == 3
396
567
 
397
- def test_get_set_del_item(self, redis_lock_pool: RedisLockPool, redis_client: Redis):
568
+ def test_get_set_del_item(self, redis_lock_pool: RedisLockPool):
398
569
  """Test the __getitem__, __setitem__, and __delitem__ methods."""
399
570
  # First test extend to add a key
400
- redis_lock_pool.extend(['key1'])
401
-
571
+ redis_lock_pool.extend(["key1"])
572
+
402
573
  # Test __getitem__
403
- assert 'key1' in redis_lock_pool
404
-
574
+ assert "key1" in redis_lock_pool
575
+
405
576
  # Test shrink to remove key
406
- redis_lock_pool.shrink(['key1'])
407
- assert 'key1' not in redis_lock_pool
577
+ redis_lock_pool.shrink(["key1"])
578
+ assert "key1" not in redis_lock_pool
408
579
 
409
- def test_health_check(self, redis_lock_pool: RedisLockPool, redis_client: Redis):
580
+ def test_health_check(self, redis_lock_pool: RedisLockPool):
410
581
  """Test the health_check method."""
411
582
  # Add some keys
412
- redis_lock_pool.extend(['key1', 'key2', 'key3'])
413
-
583
+ redis_lock_pool.extend(["key1", "key2", "key3"])
584
+
414
585
  # Lock some keys
415
- redis_lock_pool.lock('key1')
416
- redis_lock_pool.lock('key2')
417
-
586
+ redis_lock_pool.lock("key1")
587
+ redis_lock_pool.lock("key2")
588
+
418
589
  # Check health
419
590
  locked, free = redis_lock_pool.health_check()
420
591
  assert locked == 2
@@ -424,7 +595,7 @@ class TestRedisLockPool:
424
595
  """Test health_check on an empty pool."""
425
596
  # Clear the pool first
426
597
  redis_lock_pool.clear()
427
-
598
+
428
599
  # Check health of empty pool
429
600
  locked, free = redis_lock_pool.health_check()
430
601
  assert locked == 0
@@ -434,11 +605,11 @@ class TestRedisLockPool:
434
605
  """Test extending the pool with an empty list of keys."""
435
606
  # Clear the pool first
436
607
  redis_lock_pool.clear()
437
-
608
+
438
609
  # Extend with empty list - should not change anything
439
610
  redis_lock_pool.extend([])
440
611
  assert len(redis_lock_pool) == 0
441
-
612
+
442
613
  # Test with None
443
614
  redis_lock_pool.extend(None)
444
615
  assert len(redis_lock_pool) == 0
@@ -446,12 +617,12 @@ class TestRedisLockPool:
446
617
  def test_shrink_empty_keys(self, redis_lock_pool: RedisLockPool):
447
618
  """Test shrinking the pool with an empty list of keys."""
448
619
  # Add some keys
449
- redis_lock_pool.extend(['key1', 'key2'])
450
-
620
+ redis_lock_pool.extend(["key1", "key2"])
621
+
451
622
  # Shrink with empty list - should not change anything
452
623
  redis_lock_pool.shrink([])
453
624
  assert len(redis_lock_pool) == 2
454
-
625
+
455
626
  # Test with None - should also not change anything
456
627
  redis_lock_pool.shrink(None)
457
628
  assert len(redis_lock_pool) == 2
@@ -460,11 +631,11 @@ class TestRedisLockPool:
460
631
  """Test assigning keys to the pool."""
461
632
  # Clear first
462
633
  redis_lock_pool.clear()
463
-
634
+
464
635
  # Test assign with actual keys
465
- keys = ['new1', 'new2', 'new3']
636
+ keys = ["new1", "new2", "new3"]
466
637
  redis_lock_pool.assign(keys)
467
-
638
+
468
639
  # Verify all keys were added
469
640
  assert len(redis_lock_pool) == 3
470
641
  for key in keys:
@@ -473,30 +644,30 @@ class TestRedisLockPool:
473
644
  def test_iterate_pool(self, redis_lock_pool: RedisLockPool):
474
645
  """Test iterating over the keys in the pool."""
475
646
  # Add some keys
476
- test_keys = ['key1', 'key2', 'key3']
647
+ test_keys = ["key1", "key2", "key3"]
477
648
  redis_lock_pool.extend(test_keys)
478
-
649
+
479
650
  # Iterate and collect keys
480
651
  iterated_keys = []
481
652
  for key in redis_lock_pool:
482
653
  iterated_keys.append(key)
483
-
654
+
484
655
  # Verify all keys were iterated
485
656
  assert sorted(iterated_keys) == sorted(test_keys)
486
657
 
487
658
  def test_assign_empty(self, redis_lock_pool: RedisLockPool):
488
659
  """Test assigning an empty list of keys to the pool."""
489
660
  # First add some keys
490
- redis_lock_pool.extend(['old1', 'old2'])
491
-
661
+ redis_lock_pool.extend(["old1", "old2"])
662
+
492
663
  # Then assign empty list
493
664
  redis_lock_pool.assign([])
494
-
665
+
495
666
  # Verify all keys were removed
496
667
  assert len(redis_lock_pool) == 0
497
-
668
+
498
669
  # Assign None
499
- redis_lock_pool.extend(['old1'])
670
+ redis_lock_pool.extend(["old1"])
500
671
  redis_lock_pool.assign(None)
501
672
  assert len(redis_lock_pool) == 0
502
673
 
@@ -506,163 +677,172 @@ class TestThreadLock:
506
677
 
507
678
  def test_lock(self, thread_lock: ThreadLock):
508
679
  """Test the lock method."""
509
- assert thread_lock.lock('test-key')
510
- assert thread_lock.is_locked('test-key')
680
+ assert thread_lock.lock("test-key")
681
+ assert thread_lock.is_locked("test-key")
511
682
 
512
683
  def test_unlock(self, thread_lock: ThreadLock):
513
684
  """Test the unlock method."""
514
685
  # First set the lock
515
- thread_lock.update('test-key')
516
- assert thread_lock.is_locked('test-key')
686
+ thread_lock.update("test-key")
687
+ assert thread_lock.is_locked("test-key")
517
688
  # Then unlock
518
- thread_lock.unlock('test-key')
519
- assert not thread_lock.is_locked('test-key')
689
+ thread_lock.unlock("test-key")
690
+ assert not thread_lock.is_locked("test-key")
520
691
 
521
692
  def test_update(self, thread_lock: ThreadLock):
522
693
  """Test the update method."""
523
- thread_lock.update('test-key', value='2', timeout=60)
524
- assert thread_lock.lock_value('test-key') == '2'
694
+ thread_lock.update("test-key", value="2", timeout=60)
695
+ assert thread_lock.lock_value("test-key") == "2"
525
696
  # TTL should be close to 60
526
- assert thread_lock.key_status('test-key', timeout=60) == LockStatus.LOCKED
697
+ assert thread_lock.key_status("test-key", timeout=60) == LockStatus.LOCKED
527
698
 
528
699
  def test_lock_value(self, thread_lock: ThreadLock):
529
700
  """Test the lock_value method."""
530
- thread_lock.update('test-key', value='120')
531
- assert thread_lock.lock_value('test-key') == '120'
701
+ thread_lock.update("test-key", value="120")
702
+ assert thread_lock.lock_value("test-key") == "120"
532
703
 
533
704
  def test_is_locked(self, thread_lock: ThreadLock):
534
705
  """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')
706
+ assert not thread_lock.is_locked("test-key")
707
+ thread_lock.update("test-key", value="300")
708
+ assert thread_lock.is_locked("test-key")
709
+ assert not thread_lock.is_locked("non-existent-key")
539
710
 
540
711
  def test_key_status(self, thread_lock: ThreadLock):
541
712
  """Test the key_status method."""
542
713
  # Test FREE status
543
- assert thread_lock.key_status('free-key') == LockStatus.FREE
544
-
714
+ assert thread_lock.key_status("free-key") == LockStatus.FREE
715
+
545
716
  # Test LOCKED status
546
- thread_lock.update('locked-key', value='1', timeout=60)
547
- assert thread_lock.key_status('locked-key') == LockStatus.LOCKED
548
-
717
+ thread_lock.update("locked-key", value="1", timeout=60)
718
+ assert thread_lock.key_status("locked-key") == LockStatus.LOCKED
719
+
549
720
  # 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
721
+ thread_lock.update("unavailable-key", value="1", timeout=200)
722
+ assert thread_lock.key_status("unavailable-key", timeout=100) == LockStatus.UNAVAILABLE
552
723
 
553
724
  def test_rlock(self, thread_lock: ThreadLock):
554
725
  """Test the rlock method."""
555
726
  # 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
-
727
+ assert thread_lock.rlock("test-key", value="1") is True
728
+ assert thread_lock.lock_value("test-key") == "1"
729
+
559
730
  # Test acquiring the same lock with the same value
560
- assert thread_lock.rlock('test-key', value='1') is True
561
-
731
+ assert thread_lock.rlock("test-key", value="1") is True
732
+
562
733
  # Test acquiring the same lock with a different value
563
- assert thread_lock.rlock('test-key', value='2') is False
564
-
734
+ assert thread_lock.rlock("test-key", value="2") is False
735
+
565
736
  # Verify the original value wasn't changed
566
- assert thread_lock.lock_value('test-key') == '1'
737
+ assert thread_lock.lock_value("test-key") == "1"
567
738
 
568
739
  def test_conditional_setdel_operations(self, thread_lock: ThreadLock):
569
740
  """Test the conditional set/del operations."""
570
741
  # Set initial value
571
- test_key = 'cond-key'
572
- thread_lock.update(test_key, value='10')
573
-
742
+ test_key = "cond-key"
743
+ thread_lock.update(test_key, value="10")
744
+
574
745
  # Test setgt (greater than)
575
746
  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
-
747
+ assert thread_lock.lock_value(test_key) == "15" # Value should be updated to 15
748
+
578
749
  # 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
-
750
+ assert not thread_lock.setgt(
751
+ key=test_key, value=5
752
+ ) # 5 < 15, should not succeed
753
+ assert thread_lock.lock_value(test_key) == "15" # Value should remain 15
754
+
582
755
  # Test setlt (less than)
583
756
  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
-
757
+ assert thread_lock.lock_value(test_key) == "5" # Value should be updated to 5
758
+
586
759
  # 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
-
760
+ assert not thread_lock.setlt(
761
+ key=test_key, value=10
762
+ ) # 10 > 5, should not succeed
763
+ assert thread_lock.lock_value(test_key) == "5" # Value should remain 5
764
+
590
765
  # Test setge (greater than or equal)
591
766
  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
-
767
+ assert thread_lock.lock_value(test_key) == "5" # Value should still be 5
768
+
594
769
  # Test setge with unsuccessful condition
595
770
  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
-
771
+ assert thread_lock.lock_value(test_key) == "5" # Value should remain 5
772
+
598
773
  # Test setle (less than or equal)
599
774
  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
-
775
+ assert thread_lock.lock_value(test_key) == "5" # Value should remain 5
776
+
602
777
  # Test setle with unsuccessful condition
603
778
  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
-
779
+ assert thread_lock.lock_value(test_key) == "5" # Value should remain 5
780
+
606
781
  # Test seteq (equal)
607
782
  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
-
783
+ assert thread_lock.lock_value(test_key) == "5" # Value should remain 5
784
+
610
785
  # 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
-
786
+ assert not thread_lock.seteq(
787
+ key=test_key, value=6
788
+ ) # 6 != 5, should not succeed
789
+ assert thread_lock.lock_value(test_key) == "5" # Value should remain 5
790
+
614
791
  # Test setne (not equal)
615
792
  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
-
793
+ assert thread_lock.lock_value(test_key) == "10" # Value should be updated to 10
794
+
618
795
  # 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
-
796
+ assert not thread_lock.setne(
797
+ key=test_key, value=10
798
+ ) # 10 == 10, should not succeed
799
+ assert thread_lock.lock_value(test_key) == "10" # Value should remain 10
800
+
622
801
  # Test delgt (delete if greater than)
623
- thread_lock.update(test_key, value='10')
802
+ thread_lock.update(test_key, value="10")
624
803
  assert thread_lock.delgt(key=test_key, value=15) # 15 > 10, should delete
625
804
  assert not thread_lock.is_locked(test_key) # Key should be deleted
626
-
805
+
627
806
  # Test dellt (delete if less than)
628
- thread_lock.update(test_key, value='10')
807
+ thread_lock.update(test_key, value="10")
629
808
  assert thread_lock.dellt(key=test_key, value=5) # 5 < 10, should delete
630
809
  assert not thread_lock.is_locked(test_key) # Key should be deleted
631
-
810
+
632
811
  # Test delge (delete if greater than or equal)
633
- thread_lock.update(test_key, value='10')
812
+ thread_lock.update(test_key, value="10")
634
813
  assert thread_lock.delge(key=test_key, value=10) # 10 >= 10, should delete
635
814
  assert not thread_lock.is_locked(test_key) # Key should be deleted
636
-
815
+
637
816
  # Test delle (delete if less than or equal)
638
- thread_lock.update(test_key, value='10')
817
+ thread_lock.update(test_key, value="10")
639
818
  assert thread_lock.delle(key=test_key, value=10) # 10 <= 10, should delete
640
819
  assert not thread_lock.is_locked(test_key) # Key should be deleted
641
-
820
+
642
821
  # Test deleq (delete if equal)
643
- thread_lock.update(test_key, value='10')
822
+ thread_lock.update(test_key, value="10")
644
823
  assert thread_lock.deleq(key=test_key, value=10) # 10 == 10, should delete
645
824
  assert not thread_lock.is_locked(test_key) # Key should be deleted
646
-
825
+
647
826
  # Test delne (delete if not equal)
648
- thread_lock.update(test_key, value='10')
827
+ thread_lock.update(test_key, value="10")
649
828
  assert thread_lock.delne(key=test_key, value=5) # 5 != 10, should delete
650
829
  assert not thread_lock.is_locked(test_key) # Key should be deleted
651
830
 
652
831
  def test_thread_safety(self, thread_lock: ThreadLock):
653
832
  """Test thread safety of ThreadLock."""
833
+
654
834
  def worker(key):
655
835
  if thread_lock.lock(key):
656
836
  time.sleep(0.1) # Simulate some work
657
837
  thread_lock.unlock(key)
658
838
  return True
659
839
  return False
660
-
840
+
661
841
  # Test multiple threads trying to acquire the same lock
662
842
  with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor:
663
- futures = [executor.submit(worker, 'test-key') for _ in range(10)]
843
+ futures = [executor.submit(worker, "test-key") for _ in range(10)]
664
844
  results = [f.result() for f in futures]
665
-
845
+
666
846
  # Only one thread should have successfully acquired the lock
667
847
  assert sum(results) == 1
668
848
 
@@ -674,23 +854,179 @@ class TestThreadLock:
674
854
  def test_timeout_types(self, thread_lock: ThreadLock):
675
855
  """Test different timeout types."""
676
856
  from datetime import timedelta, datetime
677
-
857
+
678
858
  # Test with timedelta
679
- test_key = 'timeout-key'
680
-
859
+ test_key = "timeout-key"
860
+
681
861
  # Use timedelta for timeout (30 seconds)
682
- thread_lock.update(test_key, value='1', timeout=timedelta(seconds=30))
862
+ thread_lock.update(test_key, value="1", timeout=timedelta(seconds=30))
683
863
  assert thread_lock.is_locked(test_key)
684
864
  assert thread_lock.key_status(test_key) == LockStatus.LOCKED
685
-
865
+
686
866
  # Use None for timeout (should use a very long timeout)
687
- thread_lock.update(test_key, value='2', timeout=None)
867
+ thread_lock.update(test_key, value="2", timeout=None)
688
868
  assert thread_lock.is_locked(test_key)
689
869
  # The TTL should be very high (set to 2099)
690
870
  future_time = datetime(2099, 1, 1).timestamp() - time.time()
691
871
  # Allow 10 seconds margin in the test
692
872
  assert thread_lock._get_ttl(test_key) > future_time - 10
693
873
 
874
+ def test_multi_thread_lock_competition(self, thread_lock: ThreadLock):
875
+ """Test that only one thread can acquire the same lock at a time."""
876
+ lock_key = "multi-thread-test-key"
877
+ num_threads = 10
878
+ success_count = 0
879
+ lock_holder = None
880
+ threads_completed = 0
881
+ thread_protect_lock = threading.Lock() # Thread lock to protect shared variables
882
+
883
+ # Initial time
884
+ current_time = datetime.datetime(2023, 1, 1, 12, 0, 0)
885
+
886
+ def worker():
887
+ nonlocal success_count, lock_holder, threads_completed
888
+ thread_id = threading.get_ident()
889
+
890
+ # Try to acquire the lock multiple times with a small delay
891
+ for _ in range(5): # Try 5 times
892
+ if thread_lock.lock(lock_key, value=str(thread_id), timeout=100):
893
+ # This thread acquired the lock
894
+ with thread_protect_lock: # Protect the shared counter
895
+ success_count += 1
896
+ lock_holder = thread_id
897
+
898
+ # Hold the lock for a short time
899
+ time.sleep(0.1)
900
+
901
+ # Release the lock
902
+ # thread_lock.unlock(lock_key)
903
+ break
904
+
905
+ # Wait a bit before retrying
906
+ time.sleep(0.1)
907
+
908
+ with thread_protect_lock: # Protect the shared counter
909
+ threads_completed += 1
910
+
911
+ # Start multiple threads to compete for the lock
912
+ threads = []
913
+ for _ in range(num_threads):
914
+ thread = threading.Thread(target=worker)
915
+ thread.start()
916
+ threads.append(thread)
917
+
918
+ # Use busy waiting with freezegun to simulate time passing
919
+ with freeze_time(current_time) as frozen_time:
920
+ # Wait for all threads to complete using busy waiting
921
+ while any(thread.is_alive() for thread in threads):
922
+ # Advance time by 0.05 seconds to accelerate sleeps
923
+ frozen_time.tick(0.05)
924
+ time.sleep(0.001) # Short real sleep to prevent CPU hogging
925
+
926
+ # Verify only one thread got the lock
927
+ assert success_count == 1
928
+ assert lock_holder is not None
929
+ assert threads_completed == num_threads
930
+
931
+ def test_lock_timeout_expiry(self, thread_lock: ThreadLock):
932
+ """Test that a lock can be acquired after the previous holder's timeout expires."""
933
+ lock_key = "timeout-test-key"
934
+ lock_timeout = 60 # 60 seconds timeout
935
+
936
+ # Set the starting time
937
+ initial_time = datetime.datetime(2023, 1, 1, 12, 0, 0)
938
+
939
+ with freeze_time(initial_time) as frozen_time:
940
+ # First thread acquires the lock
941
+ thread1_id = "thread-1"
942
+ assert thread_lock.lock(lock_key, value=thread1_id, timeout=lock_timeout)
943
+
944
+ # Verify the lock is held by thread 1
945
+ assert thread_lock.is_locked(lock_key)
946
+ assert thread_lock.lock_value(lock_key) == thread1_id
947
+
948
+ # Thread 2 tries to acquire the same lock and fails
949
+ thread2_id = "thread-2"
950
+ assert not thread_lock.lock(lock_key, value=thread2_id)
951
+
952
+ # Advance time to just before timeout
953
+ frozen_time.move_to(initial_time + datetime.timedelta(seconds=lock_timeout - 1))
954
+
955
+ # Thread 2 tries again and still fails
956
+ assert not thread_lock.lock(lock_key, value=thread2_id)
957
+
958
+ # Advance time past the timeout
959
+ frozen_time.move_to(initial_time + datetime.timedelta(seconds=lock_timeout + 1))
960
+
961
+ # Thread 2 tries again and succeeds because the lock has expired
962
+ assert thread_lock.lock(lock_key, value=thread2_id)
963
+
964
+ # Verify the lock is now held by thread 2
965
+ assert thread_lock.is_locked(lock_key)
966
+ assert thread_lock.lock_value(lock_key) == thread2_id
967
+
968
+ def test_lock_update_prevents_timeout(self, thread_lock: ThreadLock):
969
+ """Test that updating a lock prevents it from timing out."""
970
+ lock_key = "update-test-key"
971
+ lock_timeout = 60 # 60 seconds timeout
972
+
973
+ # Set the starting time
974
+ initial_time = datetime.datetime(2023, 1, 1, 12, 0, 0)
975
+
976
+ with freeze_time(initial_time) as frozen_time:
977
+ # First thread acquires the lock
978
+ thread1_id = "thread-1"
979
+ assert thread_lock.lock(lock_key, value=thread1_id, timeout=lock_timeout)
980
+
981
+ # Advance time to just before timeout
982
+ frozen_time.move_to(initial_time + datetime.timedelta(seconds=lock_timeout - 10))
983
+
984
+ # Thread 1 updates the lock
985
+ thread_lock.update(lock_key, value=thread1_id, timeout=lock_timeout)
986
+
987
+ # Advance time past the original timeout
988
+ frozen_time.move_to(initial_time + datetime.timedelta(seconds=lock_timeout + 10))
989
+
990
+ # Thread 2 tries to acquire the lock and fails because thread 1 updated it
991
+ thread2_id = "thread-2"
992
+ assert not thread_lock.lock(lock_key, value=thread2_id)
993
+
994
+ # Verify the lock is still held by thread 1
995
+ assert thread_lock.is_locked(lock_key)
996
+ assert thread_lock.lock_value(lock_key) == thread1_id
997
+
998
+ # Advance time past the new timeout
999
+ frozen_time.move_to(initial_time + datetime.timedelta(seconds=2 * lock_timeout + 10))
1000
+
1001
+ # Thread 2 tries again and succeeds because the updated lock has expired
1002
+ assert thread_lock.lock(lock_key, value=thread2_id)
1003
+
1004
+ # Verify the lock is now held by thread 2
1005
+ assert thread_lock.is_locked(lock_key)
1006
+ assert thread_lock.lock_value(lock_key) == thread2_id
1007
+
1008
+ def test_rlock_same_thread_different_thread(self, thread_lock: ThreadLock):
1009
+ """Test that the same thread can rlock itself but different threads cannot."""
1010
+ lock_key = "rlock-test-key"
1011
+ thread1_id = "thread-1"
1012
+ thread2_id = "thread-2"
1013
+
1014
+ # Thread 1 acquires the lock
1015
+ assert thread_lock.lock(lock_key, value=thread1_id)
1016
+
1017
+ # Thread 1 can reacquire its own lock with rlock
1018
+ assert thread_lock.rlock(lock_key, value=thread1_id)
1019
+
1020
+ # Thread 2 cannot acquire the lock with either lock or rlock
1021
+ assert not thread_lock.lock(lock_key, value=thread2_id)
1022
+ assert not thread_lock.rlock(lock_key, value=thread2_id)
1023
+
1024
+ # Thread 1 releases the lock
1025
+ thread_lock.unlock(lock_key)
1026
+
1027
+ # Thread 2 can now acquire the lock
1028
+ assert thread_lock.lock(lock_key, value=thread2_id)
1029
+
694
1030
 
695
1031
  class TestThreadLockPool:
696
1032
  """Tests for the ThreadLockPool class."""
@@ -698,86 +1034,86 @@ class TestThreadLockPool:
698
1034
  def test_keys(self, thread_lock_pool: ThreadLockPool):
699
1035
  """Test the keys method."""
700
1036
  # Add some keys to the pool
701
- thread_lock_pool.assign(['key1', 'key2'])
1037
+ thread_lock_pool.assign(["key1", "key2"])
702
1038
  keys = thread_lock_pool.keys()
703
- assert sorted(list(keys)) == ['key1', 'key2']
1039
+ assert sorted(list(keys)) == ["key1", "key2"]
704
1040
 
705
1041
  def test_extend(self, thread_lock_pool: ThreadLockPool):
706
1042
  """Test the extend method with keys."""
707
- thread_lock_pool.extend(['key1', 'key2'])
708
-
1043
+ thread_lock_pool.extend(["key1", "key2"])
1044
+
709
1045
  # Verify keys were added
710
- assert 'key1' in thread_lock_pool
711
- assert 'key2' in thread_lock_pool
1046
+ assert "key1" in thread_lock_pool
1047
+ assert "key2" in thread_lock_pool
712
1048
 
713
1049
  def test_shrink(self, thread_lock_pool: ThreadLockPool):
714
1050
  """Test the shrink method."""
715
1051
  # First add keys
716
- thread_lock_pool.extend(['key1', 'key2', 'key3'])
717
-
1052
+ thread_lock_pool.extend(["key1", "key2", "key3"])
1053
+
718
1054
  # Then shrink
719
- thread_lock_pool.shrink(['key1', 'key2'])
1055
+ thread_lock_pool.shrink(["key1", "key2"])
720
1056
 
721
1057
  # 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
1058
+ assert "key1" not in thread_lock_pool
1059
+ assert "key2" not in thread_lock_pool
1060
+ assert "key3" in thread_lock_pool
725
1061
 
726
1062
  def test_clear(self, thread_lock_pool: ThreadLockPool):
727
1063
  """Test the clear method."""
728
1064
  # First add keys
729
- thread_lock_pool.extend(['key1', 'key2'])
730
-
1065
+ thread_lock_pool.extend(["key1", "key2"])
1066
+
731
1067
  # Then clear
732
1068
  thread_lock_pool.clear()
733
-
1069
+
734
1070
  # Verify all keys were removed
735
1071
  assert len(thread_lock_pool) == 0
736
1072
 
737
1073
  def test_assign(self, thread_lock_pool: ThreadLockPool):
738
1074
  """Test the assign method."""
739
1075
  # First add some initial keys
740
- thread_lock_pool.extend(['old1', 'old2'])
741
-
1076
+ thread_lock_pool.extend(["old1", "old2"])
1077
+
742
1078
  # Then assign new keys
743
- thread_lock_pool.assign(['key1', 'key2'])
744
-
1079
+ thread_lock_pool.assign(["key1", "key2"])
1080
+
745
1081
  # 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
1082
+ assert "old1" not in thread_lock_pool
1083
+ assert "old2" not in thread_lock_pool
1084
+ assert "key1" in thread_lock_pool
1085
+ assert "key2" in thread_lock_pool
750
1086
 
751
1087
  def test_contains(self, thread_lock_pool: ThreadLockPool):
752
1088
  """Test the __contains__ method."""
753
1089
  # Add a key
754
- thread_lock_pool.extend(['key1'])
755
-
1090
+ thread_lock_pool.extend(["key1"])
1091
+
756
1092
  # Test __contains__
757
- assert 'key1' in thread_lock_pool
758
- assert 'key2' not in thread_lock_pool
1093
+ assert "key1" in thread_lock_pool
1094
+ assert "key2" not in thread_lock_pool
759
1095
 
760
1096
  def test_set_del_item(self, thread_lock_pool: ThreadLockPool):
761
1097
  """Test the __setitem__ and __delitem__ methods."""
762
1098
  # First test extend to add a key
763
- thread_lock_pool.extend(['key1'])
764
-
1099
+ thread_lock_pool.extend(["key1"])
1100
+
765
1101
  # Test __getitem__
766
- assert 'key1' in thread_lock_pool
767
-
1102
+ assert "key1" in thread_lock_pool
1103
+
768
1104
  # Test shrink to remove key
769
- thread_lock_pool.shrink(['key1'])
770
- assert 'key1' not in thread_lock_pool
1105
+ thread_lock_pool.shrink(["key1"])
1106
+ assert "key1" not in thread_lock_pool
771
1107
 
772
1108
  def test_health_check(self, thread_lock_pool: ThreadLockPool):
773
1109
  """Test the health_check method."""
774
1110
  # Add some keys
775
- thread_lock_pool.extend(['key1', 'key2', 'key3'])
776
-
1111
+ thread_lock_pool.extend(["key1", "key2", "key3"])
1112
+
777
1113
  # Lock some keys
778
- thread_lock_pool.lock('key1')
779
- thread_lock_pool.lock('key2')
780
-
1114
+ thread_lock_pool.lock("key1")
1115
+ thread_lock_pool.lock("key2")
1116
+
781
1117
  # Check health
782
1118
  locked, free = thread_lock_pool.health_check()
783
1119
  assert locked == 2
@@ -785,21 +1121,22 @@ class TestThreadLockPool:
785
1121
 
786
1122
  def test_thread_safety(self, thread_lock_pool: ThreadLockPool):
787
1123
  """Test thread safety of ThreadLockPool."""
1124
+
788
1125
  def worker(key):
789
1126
  if thread_lock_pool.lock(key):
790
1127
  time.sleep(0.1) # Simulate some work
791
1128
  thread_lock_pool.unlock(key)
792
1129
  return True
793
1130
  return False
794
-
1131
+
795
1132
  # Add keys to the pool
796
- thread_lock_pool.extend(['test-key'])
797
-
1133
+ thread_lock_pool.extend(["test-key"])
1134
+
798
1135
  # Test multiple threads trying to acquire the same lock
799
1136
  with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor:
800
- futures = [executor.submit(worker, 'test-key') for _ in range(10)]
1137
+ futures = [executor.submit(worker, "test-key") for _ in range(10)]
801
1138
  results = [f.result() for f in futures]
802
-
1139
+
803
1140
  # Only one thread should have successfully acquired the lock
804
1141
  assert sum(results) == 1
805
1142
 
@@ -807,7 +1144,7 @@ class TestThreadLockPool:
807
1144
  """Test health_check on an empty pool."""
808
1145
  # Clear the pool first
809
1146
  thread_lock_pool.clear()
810
-
1147
+
811
1148
  # Check health of empty pool
812
1149
  locked, free = thread_lock_pool.health_check()
813
1150
  assert locked == 0
@@ -818,15 +1155,15 @@ class TestThreadLockPool:
818
1155
  # Test clear on already empty pool
819
1156
  thread_lock_pool.clear()
820
1157
  assert len(thread_lock_pool) == 0
821
-
1158
+
822
1159
  # Test shrink on empty pool
823
- thread_lock_pool.shrink(['nonexistent'])
1160
+ thread_lock_pool.shrink(["nonexistent"])
824
1161
  assert len(thread_lock_pool) == 0
825
-
1162
+
826
1163
  # Test extend with None
827
1164
  thread_lock_pool.extend(None)
828
1165
  assert len(thread_lock_pool) == 0
829
-
1166
+
830
1167
  # Test assign with None
831
1168
  thread_lock_pool.assign(None)
832
1169
  assert len(thread_lock_pool) == 0
@@ -834,14 +1171,14 @@ class TestThreadLockPool:
834
1171
  def test_iterate_pool(self, thread_lock_pool: ThreadLockPool):
835
1172
  """Test iterating over the keys in the pool."""
836
1173
  # Add some keys
837
- test_keys = ['key1', 'key2', 'key3']
1174
+ test_keys = ["key1", "key2", "key3"]
838
1175
  thread_lock_pool.extend(test_keys)
839
-
1176
+
840
1177
  # Iterate and collect keys
841
1178
  iterated_keys = []
842
1179
  for key in thread_lock_pool:
843
1180
  iterated_keys.append(key)
844
-
1181
+
845
1182
  # Verify all keys were iterated
846
1183
  assert sorted(iterated_keys) == sorted(test_keys)
847
1184