Skip to content

Commit b1fc2ae

Browse files
committed
ongoing
1 parent cd4c17d commit b1fc2ae

File tree

2 files changed

+117
-136
lines changed

2 files changed

+117
-136
lines changed

packages/service-library/src/servicelib/redis/_client.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -114,10 +114,12 @@ def is_healthy(self) -> bool:
114114
return self._is_healthy
115115

116116
def create_lock(
117-
self, lock_name: str, *, ttl: datetime.timedelta = DEFAULT_LOCK_TTL
117+
self, lock_name: str, *, ttl: datetime.timedelta | None = DEFAULT_LOCK_TTL
118118
) -> Lock:
119119
return self._client.lock(
120-
name=lock_name, timeout=ttl.total_seconds(), blocking=False
120+
name=lock_name,
121+
timeout=ttl.total_seconds() if ttl is not None else None,
122+
blocking=False,
121123
)
122124

123125
async def lock_value(self, lock_name: str) -> str | None:

packages/service-library/tests/redis/test_client.py

Lines changed: 113 additions & 134 deletions
Original file line numberDiff line numberDiff line change
@@ -8,20 +8,13 @@
88
import datetime
99
from collections.abc import AsyncIterator, Callable
1010
from contextlib import AbstractAsyncContextManager
11-
from typing import Final
1211

1312
import pytest
1413
from faker import Faker
1514
from pytest_mock import MockerFixture
1615
from redis.exceptions import LockError, LockNotOwnedError
17-
from servicelib.redis import (
18-
CouldNotAcquireLockError,
19-
RedisClientSDK,
20-
RedisClientsManager,
21-
RedisManagerDBConfig,
22-
)
16+
from servicelib.redis import RedisClientSDK, RedisClientsManager, RedisManagerDBConfig
2317
from servicelib.redis import _constants as redis_constants
24-
from servicelib.utils import limited_gather
2518
from settings_library.redis import RedisDatabase, RedisSettings
2619
from tenacity import (
2720
AsyncRetrying,
@@ -39,11 +32,6 @@
3932
]
4033

4134

42-
async def _is_locked(redis_client_sdk: RedisClientSDK, lock_name: str) -> bool:
43-
lock = redis_client_sdk.redis.lock(lock_name)
44-
return await lock.locked()
45-
46-
4735
@pytest.fixture
4836
async def redis_client_sdk(
4937
get_redis_client_sdk: Callable[
@@ -66,34 +54,28 @@ def with_short_default_redis_lock_ttl(mocker: MockerFixture) -> None:
6654
)
6755

6856

69-
async def test_redis_key_encode_decode(redis_client_sdk: RedisClientSDK, faker: Faker):
70-
key = faker.pystr()
71-
value = faker.pystr()
72-
await redis_client_sdk.redis.set(key, value)
73-
val = await redis_client_sdk.redis.get(key)
74-
assert val == value
75-
await redis_client_sdk.redis.delete(key)
57+
@pytest.fixture
58+
def lock_name(faker: Faker) -> str:
59+
return faker.pystr()
7660

7761

78-
async def test_redis_lock_acquisition(redis_client_sdk: RedisClientSDK, faker: Faker):
79-
lock_name = faker.pystr()
80-
lock = redis_client_sdk.redis.lock(lock_name)
62+
async def test_redis_lock_no_ttl(redis_client_sdk: RedisClientSDK, lock_name: str):
63+
lock = redis_client_sdk.create_lock(lock_name, ttl=None)
8164
assert await lock.locked() is False
8265

83-
# Try to acquire the lock:
8466
lock_acquired = await lock.acquire(blocking=False)
8567
assert lock_acquired is True
8668
assert await lock.locked() is True
8769
assert await lock.owned() is True
8870
with pytest.raises(LockError):
89-
# a lock with no timeout cannot be reacquired
71+
# a lock with no ttl cannot be reacquired
9072
await lock.reacquire()
9173
with pytest.raises(LockError):
92-
# a lock with no timeout cannot be extended
74+
# a lock with no ttl cannot be extended
9375
await lock.extend(2)
9476

9577
# try to acquire the lock a second time
96-
same_lock = redis_client_sdk.redis.lock(lock_name)
78+
same_lock = redis_client_sdk.create_lock(lock_name, ttl=None)
9779
assert await same_lock.locked() is True
9880
assert await same_lock.owned() is False
9981
assert await same_lock.acquire(blocking=False) is False
@@ -104,11 +86,10 @@ async def test_redis_lock_acquisition(redis_client_sdk: RedisClientSDK, faker: F
10486
assert not await lock.owned()
10587

10688

107-
async def test_redis_lock_context_manager(
108-
redis_client_sdk: RedisClientSDK, faker: Faker
89+
async def test_redis_lock_context_manager_no_ttl(
90+
redis_client_sdk: RedisClientSDK, lock_name: str
10991
):
110-
lock_name = faker.pystr()
111-
lock = redis_client_sdk.redis.lock(lock_name)
92+
lock = redis_client_sdk.create_lock(lock_name, ttl=None)
11293
assert not await lock.locked()
11394

11495
async with lock:
@@ -123,7 +104,7 @@ async def test_redis_lock_context_manager(
123104
await lock.extend(2)
124105

125106
# try to acquire the lock a second time
126-
same_lock = redis_client_sdk.redis.lock(lock_name, blocking_timeout=1)
107+
same_lock = redis_client_sdk.create_lock(lock_name, ttl=None)
127108
assert await same_lock.locked()
128109
assert not await same_lock.owned()
129110
assert await same_lock.acquire() is False
@@ -134,11 +115,9 @@ async def test_redis_lock_context_manager(
134115

135116

136117
async def test_redis_lock_with_ttl(
137-
redis_client_sdk: RedisClientSDK, faker: Faker, redis_lock_ttl: datetime.timedelta
118+
redis_client_sdk: RedisClientSDK, lock_name: str, redis_lock_ttl: datetime.timedelta
138119
):
139-
ttl_lock = redis_client_sdk.redis.lock(
140-
faker.pystr(), timeout=redis_lock_ttl.total_seconds()
141-
)
120+
ttl_lock = redis_client_sdk.create_lock(lock_name, ttl=redis_lock_ttl)
142121
assert not await ttl_lock.locked()
143122

144123
with pytest.raises(LockNotOwnedError): # noqa: PT012
@@ -150,104 +129,104 @@ async def test_redis_lock_with_ttl(
150129
assert not await ttl_lock.locked()
151130

152131

153-
async def test_lock_context_with_already_locked_lock_raises(
154-
redis_client_sdk: RedisClientSDK, faker: Faker
155-
):
156-
lock_name = faker.pystr()
157-
assert await _is_locked(redis_client_sdk, lock_name) is False
158-
async with redis_client_sdk.lock_context(lock_name) as lock:
159-
assert await _is_locked(redis_client_sdk, lock_name) is True
160-
161-
assert isinstance(lock.name, str)
162-
163-
# case where gives up immediately to acquire lock without waiting
164-
with pytest.raises(CouldNotAcquireLockError):
165-
async with redis_client_sdk.lock_context(lock.name, blocking=False):
166-
...
167-
168-
# case when lock waits up to blocking_timeout_s before giving up on
169-
# lock acquisition
170-
with pytest.raises(CouldNotAcquireLockError):
171-
async with redis_client_sdk.lock_context(
172-
lock.name, blocking=True, blocking_timeout_s=0.1
173-
):
174-
...
175-
176-
assert await lock.locked() is True
177-
assert await _is_locked(redis_client_sdk, lock_name) is False
178-
179-
180-
async def test_lock_context_with_data(redis_client_sdk: RedisClientSDK, faker: Faker):
181-
lock_data = faker.text()
182-
lock_name = faker.pystr()
183-
assert await _is_locked(redis_client_sdk, lock_name) is False
184-
assert await redis_client_sdk.lock_value(lock_name) is None
185-
async with redis_client_sdk.lock_context(lock_name, lock_value=lock_data):
186-
assert await _is_locked(redis_client_sdk, lock_name) is True
187-
assert await redis_client_sdk.lock_value(lock_name) == lock_data
188-
assert await _is_locked(redis_client_sdk, lock_name) is False
189-
assert await redis_client_sdk.lock_value(lock_name) is None
190-
191-
192-
async def test_lock_context_released_after_error(
193-
redis_client_sdk: RedisClientSDK, faker: Faker
194-
):
195-
lock_name = faker.pystr()
196-
197-
assert await redis_client_sdk.lock_value(lock_name) is None
198-
199-
with pytest.raises(RuntimeError): # noqa: PT012
200-
async with redis_client_sdk.lock_context(lock_name):
201-
assert await redis_client_sdk.redis.get(lock_name) is not None
202-
msg = "Expected error"
203-
raise RuntimeError(msg)
204-
205-
assert await redis_client_sdk.lock_value(lock_name) is None
206-
207-
208-
async def test_lock_acquired_in_parallel_to_update_same_resource(
209-
with_short_default_redis_lock_ttl: None,
210-
get_redis_client_sdk: Callable[
211-
[RedisDatabase], AbstractAsyncContextManager[RedisClientSDK]
212-
],
213-
faker: Faker,
214-
):
215-
INCREASE_OPERATIONS: Final[int] = 250
216-
INCREASE_BY: Final[int] = 10
217-
218-
class RaceConditionCounter:
219-
def __init__(self):
220-
self.value: int = 0
221-
222-
async def race_condition_increase(self, by: int) -> None:
223-
current_value = self.value
224-
current_value += by
225-
# most likely situation which creates issues
226-
await asyncio.sleep(redis_constants.DEFAULT_LOCK_TTL.total_seconds() / 2)
227-
self.value = current_value
228-
229-
counter = RaceConditionCounter()
230-
lock_name: str = faker.pystr()
231-
# ensures it does nto time out before acquiring the lock
232-
time_for_all_inc_counter_calls_to_finish_s: float = (
233-
redis_constants.DEFAULT_LOCK_TTL.total_seconds() * INCREASE_OPERATIONS * 10
234-
)
235-
236-
async def _inc_counter() -> None:
237-
async with get_redis_client_sdk( # noqa: SIM117
238-
RedisDatabase.RESOURCES
239-
) as redis_client_sdk:
240-
async with redis_client_sdk.lock_context(
241-
lock_key=lock_name,
242-
blocking=True,
243-
blocking_timeout_s=time_for_all_inc_counter_calls_to_finish_s,
244-
):
245-
await counter.race_condition_increase(INCREASE_BY)
246-
247-
await limited_gather(
248-
*(_inc_counter() for _ in range(INCREASE_OPERATIONS)), limit=15
249-
)
250-
assert counter.value == INCREASE_BY * INCREASE_OPERATIONS
132+
# async def test_lock_context_with_already_locked_lock_raises(
133+
# redis_client_sdk: RedisClientSDK, faker: Faker
134+
# ):
135+
# lock_name = faker.pystr()
136+
# assert await _is_locked(redis_client_sdk, lock_name) is False
137+
# async with redis_client_sdk.lock_context(lock_name) as lock:
138+
# assert await _is_locked(redis_client_sdk, lock_name) is True
139+
140+
# assert isinstance(lock.name, str)
141+
142+
# # case where gives up immediately to acquire lock without waiting
143+
# with pytest.raises(CouldNotAcquireLockError):
144+
# async with redis_client_sdk.lock_context(lock.name, blocking=False):
145+
# ...
146+
147+
# # case when lock waits up to blocking_timeout_s before giving up on
148+
# # lock acquisition
149+
# with pytest.raises(CouldNotAcquireLockError):
150+
# async with redis_client_sdk.lock_context(
151+
# lock.name, blocking=True, blocking_timeout_s=0.1
152+
# ):
153+
# ...
154+
155+
# assert await lock.locked() is True
156+
# assert await _is_locked(redis_client_sdk, lock_name) is False
157+
158+
159+
# async def test_lock_context_with_data(redis_client_sdk: RedisClientSDK, faker: Faker):
160+
# lock_data = faker.text()
161+
# lock_name = faker.pystr()
162+
# assert await _is_locked(redis_client_sdk, lock_name) is False
163+
# assert await redis_client_sdk.lock_value(lock_name) is None
164+
# async with redis_client_sdk.lock_context(lock_name, lock_value=lock_data):
165+
# assert await _is_locked(redis_client_sdk, lock_name) is True
166+
# assert await redis_client_sdk.lock_value(lock_name) == lock_data
167+
# assert await _is_locked(redis_client_sdk, lock_name) is False
168+
# assert await redis_client_sdk.lock_value(lock_name) is None
169+
170+
171+
# async def test_lock_context_released_after_error(
172+
# redis_client_sdk: RedisClientSDK, faker: Faker
173+
# ):
174+
# lock_name = faker.pystr()
175+
176+
# assert await redis_client_sdk.lock_value(lock_name) is None
177+
178+
# with pytest.raises(RuntimeError): # noqa: PT012
179+
# async with redis_client_sdk.lock_context(lock_name):
180+
# assert await redis_client_sdk.redis.get(lock_name) is not None
181+
# msg = "Expected error"
182+
# raise RuntimeError(msg)
183+
184+
# assert await redis_client_sdk.lock_value(lock_name) is None
185+
186+
187+
# async def test_lock_acquired_in_parallel_to_update_same_resource(
188+
# with_short_default_redis_lock_ttl: None,
189+
# get_redis_client_sdk: Callable[
190+
# [RedisDatabase], AbstractAsyncContextManager[RedisClientSDK]
191+
# ],
192+
# faker: Faker,
193+
# ):
194+
# INCREASE_OPERATIONS: Final[int] = 250
195+
# INCREASE_BY: Final[int] = 10
196+
197+
# class RaceConditionCounter:
198+
# def __init__(self):
199+
# self.value: int = 0
200+
201+
# async def race_condition_increase(self, by: int) -> None:
202+
# current_value = self.value
203+
# current_value += by
204+
# # most likely situation which creates issues
205+
# await asyncio.sleep(redis_constants.DEFAULT_LOCK_TTL.total_seconds() / 2)
206+
# self.value = current_value
207+
208+
# counter = RaceConditionCounter()
209+
# lock_name: str = faker.pystr()
210+
# # ensures it does nto time out before acquiring the lock
211+
# time_for_all_inc_counter_calls_to_finish_s: float = (
212+
# redis_constants.DEFAULT_LOCK_TTL.total_seconds() * INCREASE_OPERATIONS * 10
213+
# )
214+
215+
# async def _inc_counter() -> None:
216+
# async with get_redis_client_sdk( # noqa: SIM117
217+
# RedisDatabase.RESOURCES
218+
# ) as redis_client_sdk:
219+
# async with redis_client_sdk.lock_context(
220+
# lock_key=lock_name,
221+
# blocking=True,
222+
# blocking_timeout_s=time_for_all_inc_counter_calls_to_finish_s,
223+
# ):
224+
# await counter.race_condition_increase(INCREASE_BY)
225+
226+
# await limited_gather(
227+
# *(_inc_counter() for _ in range(INCREASE_OPERATIONS)), limit=15
228+
# )
229+
# assert counter.value == INCREASE_BY * INCREASE_OPERATIONS
251230

252231

253232
async def test_redis_client_sdks_manager(

0 commit comments

Comments
 (0)