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.
- redis_allocator/__init__.py +5 -1
- redis_allocator/_version.py +1 -0
- redis_allocator/allocator.py +819 -280
- redis_allocator/lock.py +66 -17
- redis_allocator/task_queue.py +81 -57
- redis_allocator-0.3.2.dist-info/METADATA +529 -0
- redis_allocator-0.3.2.dist-info/RECORD +15 -0
- {redis_allocator-0.0.1.dist-info → redis_allocator-0.3.2.dist-info}/licenses/LICENSE +21 -21
- tests/conftest.py +150 -46
- tests/test_allocator.py +461 -488
- tests/test_lock.py +675 -338
- tests/test_task_queue.py +136 -136
- redis_allocator-0.0.1.dist-info/METADATA +0 -229
- redis_allocator-0.0.1.dist-info/RECORD +0 -14
- {redis_allocator-0.0.1.dist-info → redis_allocator-0.3.2.dist-info}/WHEEL +0 -0
- {redis_allocator-0.0.1.dist-info → redis_allocator-0.3.2.dist-info}/top_level.txt +0 -0
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
|
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(
|
15
|
-
assert redis_client.exists(redis_lock._key_str(
|
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(
|
21
|
-
assert redis_client.exists(redis_lock._key_str(
|
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(
|
24
|
-
assert not redis_client.exists(redis_lock._key_str(
|
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(
|
29
|
-
assert redis_client.get(redis_lock._key_str(
|
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(
|
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
|
44
|
+
def test_lock_value(self, redis_lock: RedisLock):
|
35
45
|
"""Test the lock_value method."""
|
36
|
-
|
37
|
-
assert redis_lock.lock_value(
|
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(
|
42
|
-
redis_client.set(redis_lock._key_str(
|
43
|
-
assert redis_lock.is_locked(
|
44
|
-
assert not redis_lock.is_locked(
|
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(
|
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(
|
53
|
-
assert redis_lock.key_status(
|
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(
|
57
|
-
assert redis_lock.key_status(
|
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(
|
61
|
-
assert redis_lock.key_status(
|
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(
|
67
|
-
assert redis_client.get(redis_lock._key_str(
|
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(
|
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(
|
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(
|
86
|
+
assert redis_client.get(redis_lock._key_str("test-key")) == "1"
|
77
87
|
|
78
|
-
def test_conditional_setdel_operations(
|
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 =
|
96
|
+
test_key = "cond-key"
|
85
97
|
key_str = redis_lock._key_str(test_key)
|
86
|
-
redis_client.set(key_str,
|
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) ==
|
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) ==
|
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) ==
|
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(
|
102
|
-
|
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) ==
|
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) ==
|
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) ==
|
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) ==
|
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) ==
|
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) ==
|
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) ==
|
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(
|
134
|
-
|
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,
|
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,
|
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,
|
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,
|
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,
|
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,
|
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,
|
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 =
|
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,
|
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) ==
|
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) ==
|
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) ==
|
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) ==
|
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,
|
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) ==
|
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) ==
|
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) ==
|
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) ==
|
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) ==
|
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,
|
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,
|
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,
|
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,
|
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,
|
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,
|
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,
|
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(
|
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(
|
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(
|
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,
|
280
|
-
lock2 = RedisLock(redis_client,
|
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,
|
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 =
|
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=
|
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=
|
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(
|
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([
|
501
|
+
redis_lock_pool.assign(["key1", "key2"])
|
331
502
|
keys = redis_lock_pool.keys()
|
332
|
-
assert sorted(list(keys)) == [
|
503
|
+
assert sorted(list(keys)) == ["key1", "key2"]
|
333
504
|
|
334
|
-
def test_extend(self, redis_lock_pool: RedisLockPool
|
505
|
+
def test_extend(self, redis_lock_pool: RedisLockPool):
|
335
506
|
"""Test the extend method with keys."""
|
336
|
-
redis_lock_pool.extend([
|
337
|
-
|
507
|
+
redis_lock_pool.extend(["key1", "key2"])
|
508
|
+
|
338
509
|
# Verify keys were added
|
339
|
-
assert
|
340
|
-
assert
|
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
|
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([
|
346
|
-
|
516
|
+
redis_lock_pool.extend(["key1", "key2", "key3"])
|
517
|
+
|
347
518
|
# Then shrink
|
348
|
-
redis_lock_pool.shrink([
|
519
|
+
redis_lock_pool.shrink(["key1", "key2"])
|
349
520
|
|
350
521
|
# Verify keys were removed
|
351
|
-
assert
|
352
|
-
assert
|
353
|
-
assert
|
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
|
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([
|
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
|
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([
|
370
|
-
|
540
|
+
redis_lock_pool.extend(["old1", "old2"])
|
541
|
+
|
371
542
|
# Then assign new keys
|
372
|
-
redis_lock_pool.assign([
|
373
|
-
|
543
|
+
redis_lock_pool.assign(["key1", "key2"])
|
544
|
+
|
374
545
|
# Verify old keys were removed and new keys were added
|
375
|
-
assert
|
376
|
-
assert
|
377
|
-
assert
|
378
|
-
assert
|
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
|
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([
|
384
|
-
|
554
|
+
redis_lock_pool.extend(["key1"])
|
555
|
+
|
385
556
|
# Test __contains__
|
386
|
-
assert
|
387
|
-
assert
|
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
|
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([
|
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
|
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([
|
401
|
-
|
571
|
+
redis_lock_pool.extend(["key1"])
|
572
|
+
|
402
573
|
# Test __getitem__
|
403
|
-
assert
|
404
|
-
|
574
|
+
assert "key1" in redis_lock_pool
|
575
|
+
|
405
576
|
# Test shrink to remove key
|
406
|
-
redis_lock_pool.shrink([
|
407
|
-
assert
|
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
|
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([
|
413
|
-
|
583
|
+
redis_lock_pool.extend(["key1", "key2", "key3"])
|
584
|
+
|
414
585
|
# Lock some keys
|
415
|
-
redis_lock_pool.lock(
|
416
|
-
redis_lock_pool.lock(
|
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([
|
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 = [
|
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 = [
|
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([
|
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([
|
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(
|
510
|
-
assert thread_lock.is_locked(
|
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(
|
516
|
-
assert thread_lock.is_locked(
|
686
|
+
thread_lock.update("test-key")
|
687
|
+
assert thread_lock.is_locked("test-key")
|
517
688
|
# Then unlock
|
518
|
-
thread_lock.unlock(
|
519
|
-
assert not thread_lock.is_locked(
|
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(
|
524
|
-
assert thread_lock.lock_value(
|
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(
|
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(
|
531
|
-
assert thread_lock.lock_value(
|
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(
|
536
|
-
thread_lock.update(
|
537
|
-
assert thread_lock.is_locked(
|
538
|
-
assert not thread_lock.is_locked(
|
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(
|
544
|
-
|
714
|
+
assert thread_lock.key_status("free-key") == LockStatus.FREE
|
715
|
+
|
545
716
|
# Test LOCKED status
|
546
|
-
thread_lock.update(
|
547
|
-
assert thread_lock.key_status(
|
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(
|
551
|
-
assert thread_lock.key_status(
|
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(
|
557
|
-
assert thread_lock.lock_value(
|
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(
|
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(
|
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(
|
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 =
|
572
|
-
thread_lock.update(test_key, value=
|
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) ==
|
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(
|
580
|
-
|
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) ==
|
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(
|
588
|
-
|
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) ==
|
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) ==
|
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) ==
|
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) ==
|
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) ==
|
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(
|
612
|
-
|
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) ==
|
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(
|
620
|
-
|
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=
|
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=
|
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=
|
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=
|
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=
|
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=
|
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,
|
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 =
|
680
|
-
|
859
|
+
test_key = "timeout-key"
|
860
|
+
|
681
861
|
# Use timedelta for timeout (30 seconds)
|
682
|
-
thread_lock.update(test_key, value=
|
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=
|
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([
|
1037
|
+
thread_lock_pool.assign(["key1", "key2"])
|
702
1038
|
keys = thread_lock_pool.keys()
|
703
|
-
assert sorted(list(keys)) == [
|
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([
|
708
|
-
|
1043
|
+
thread_lock_pool.extend(["key1", "key2"])
|
1044
|
+
|
709
1045
|
# Verify keys were added
|
710
|
-
assert
|
711
|
-
assert
|
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([
|
717
|
-
|
1052
|
+
thread_lock_pool.extend(["key1", "key2", "key3"])
|
1053
|
+
|
718
1054
|
# Then shrink
|
719
|
-
thread_lock_pool.shrink([
|
1055
|
+
thread_lock_pool.shrink(["key1", "key2"])
|
720
1056
|
|
721
1057
|
# Verify keys were removed
|
722
|
-
assert
|
723
|
-
assert
|
724
|
-
assert
|
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([
|
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([
|
741
|
-
|
1076
|
+
thread_lock_pool.extend(["old1", "old2"])
|
1077
|
+
|
742
1078
|
# Then assign new keys
|
743
|
-
thread_lock_pool.assign([
|
744
|
-
|
1079
|
+
thread_lock_pool.assign(["key1", "key2"])
|
1080
|
+
|
745
1081
|
# Verify old keys were removed and new keys were added
|
746
|
-
assert
|
747
|
-
assert
|
748
|
-
assert
|
749
|
-
assert
|
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([
|
755
|
-
|
1090
|
+
thread_lock_pool.extend(["key1"])
|
1091
|
+
|
756
1092
|
# Test __contains__
|
757
|
-
assert
|
758
|
-
assert
|
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([
|
764
|
-
|
1099
|
+
thread_lock_pool.extend(["key1"])
|
1100
|
+
|
765
1101
|
# Test __getitem__
|
766
|
-
assert
|
767
|
-
|
1102
|
+
assert "key1" in thread_lock_pool
|
1103
|
+
|
768
1104
|
# Test shrink to remove key
|
769
|
-
thread_lock_pool.shrink([
|
770
|
-
assert
|
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([
|
776
|
-
|
1111
|
+
thread_lock_pool.extend(["key1", "key2", "key3"])
|
1112
|
+
|
777
1113
|
# Lock some keys
|
778
|
-
thread_lock_pool.lock(
|
779
|
-
thread_lock_pool.lock(
|
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([
|
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,
|
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([
|
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 = [
|
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
|
|