Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft: Add support for NX, XX, GT and LT to expire and pexpire #762

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions django_redis/client/default.py
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,10 @@ def expire(
timeout: ExpiryT,
version: Optional[int] = None,
client: Optional[Redis] = None,
nx: bool = False,
xx: bool = False,
gt: bool = False,
lt: bool = False,
) -> bool:
if timeout is DEFAULT_TIMEOUT:
timeout = self._backend.default_timeout # type: ignore
Expand All @@ -326,14 +330,18 @@ def expire(

key = self.make_key(key, version=version)

return client.expire(key, timeout)
return client.expire(key, timeout, nx, xx, gt, lt)

def pexpire(
self,
key: KeyT,
timeout: ExpiryT,
version: Optional[int] = None,
client: Optional[Redis] = None,
nx: bool = False,
xx: bool = False,
gt: bool = False,
lt: bool = False,
) -> bool:
if timeout is DEFAULT_TIMEOUT:
timeout = self._backend.default_timeout # type: ignore
Expand All @@ -343,7 +351,7 @@ def pexpire(

key = self.make_key(key, version=version)

return bool(client.pexpire(key, timeout))
return bool(client.pexpire(key, timeout, nx, xx, gt, lt))

def pexpire_at(
self,
Expand Down
46 changes: 42 additions & 4 deletions django_redis/client/sharded.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,19 +171,57 @@

return super().persist(key=key, version=version, client=client)

def expire(self, key, timeout, version=None, client=None):
def expire(

Check warning on line 174 in django_redis/client/sharded.py

View check run for this annotation

Codecov / codecov/patch

django_redis/client/sharded.py#L174

Added line #L174 was not covered by tests
self,
key,
timeout,
version=None,
client=None,
nx=False,
xx=False,
gt=False,
lt=False,

Check warning on line 183 in django_redis/client/sharded.py

View check run for this annotation

Codecov / codecov/patch

django_redis/client/sharded.py#L178-L183

Added lines #L178 - L183 were not covered by tests
):
if client is None:
key = self.make_key(key, version=version)
client = self.get_server(key)

return super().expire(key=key, timeout=timeout, version=version, client=client)
return super().expire(
key=key,
timeout=timeout,
version=version,
client=client,
nx=nx,
xx=xx,
gt=gt,
lt=lt,

Check warning on line 197 in django_redis/client/sharded.py

View check run for this annotation

Codecov / codecov/patch

django_redis/client/sharded.py#L189-L197

Added lines #L189 - L197 were not covered by tests
)

def pexpire(self, key, timeout, version=None, client=None):
def pexpire(

Check warning on line 200 in django_redis/client/sharded.py

View check run for this annotation

Codecov / codecov/patch

django_redis/client/sharded.py#L200

Added line #L200 was not covered by tests
self,
key,
timeout,
version=None,
client=None,
nx=False,
xx=False,
gt=False,
lt=False,

Check warning on line 209 in django_redis/client/sharded.py

View check run for this annotation

Codecov / codecov/patch

django_redis/client/sharded.py#L204-L209

Added lines #L204 - L209 were not covered by tests
):
if client is None:
key = self.make_key(key, version=version)
client = self.get_server(key)

return super().pexpire(key=key, timeout=timeout, version=version, client=client)
return super().pexpire(
key=key,
timeout=timeout,
version=version,
client=client,
nx=nx,
xx=xx,
gt=gt,
lt=lt,

Check warning on line 223 in django_redis/client/sharded.py

View check run for this annotation

Codecov / codecov/patch

django_redis/client/sharded.py#L215-L223

Added lines #L215 - L223 were not covered by tests
)

def pexpire_at(self, key, when: Union[datetime, int], version=None, client=None):
"""
Expand Down
172 changes: 154 additions & 18 deletions tests/test_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from django.test import override_settings
from pytest_django.fixtures import SettingsWrapper
from pytest_mock import MockerFixture
from redis.exceptions import ResponseError

from django_redis.cache import RedisCache
from django_redis.client import ShardClient, herd
Expand Down Expand Up @@ -600,30 +601,165 @@
assert ttl is None
assert cache.persist("not-existent-key") is False

def test_expire(self, cache: RedisCache):
cache.set("foo", "bar", timeout=None)
assert cache.expire("foo", 20) is True
ttl = cache.ttl("foo")
assert pytest.approx(ttl) == 20
assert cache.expire("not-existent-key", 20) is False
@pytest.mark.parametrize(
"initial_timeout, new_timeout, params, expected_result, expected_ttl",
[
# Basic expire functionality (existing test case)
(None, 20, {}, True, 20),
# NX tests (only set if key has no expiry)
# Should work - key has no expiry
(None, 30, {"nx": True}, True, 30),
# Should fail - key already has expiry
(20, 30, {"nx": True}, False, 20),
# XX tests (only set if key has existing expiry)
# Should work - key has expiry
(20, 30, {"xx": True}, True, 30),
# Should fail - key has no expiry
(None, 30, {"xx": True}, False, None),
# GT tests (only set if new expiry is greater than current)
# Should work - new timeout > current
(20, 30, {"gt": True}, True, 30),
# Should fail - new timeout < current
(30, 20, {"gt": True}, False, 30),
# LT tests (only set if new expiry is less than current)
# Should work - new timeout < current
(30, 20, {"lt": True}, True, 20),
# Should fail - new timeout > current
(20, 30, {"lt": True}, False, 20),
],
)
def test_expire_with_conditions(

Check warning on line 631 in tests/test_backend.py

View check run for this annotation

Codecov / codecov/patch

tests/test_backend.py#L631

Added line #L631 was not covered by tests
self,
cache: RedisCache,
initial_timeout,
new_timeout,
params,
expected_result,
expected_ttl,
):
cache.set("foo", "bar", timeout=initial_timeout)
result = cache.expire("foo", new_timeout, **params)

Check warning on line 641 in tests/test_backend.py

View check run for this annotation

Codecov / codecov/patch

tests/test_backend.py#L640-L641

Added lines #L640 - L641 were not covered by tests

assert result is expected_result
if expected_ttl is not None:
assert pytest.approx(cache.ttl("foo")) == expected_ttl

Check warning on line 645 in tests/test_backend.py

View check run for this annotation

Codecov / codecov/patch

tests/test_backend.py#L643-L645

Added lines #L643 - L645 were not covered by tests
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you need to add a higher tolerance have a look at the docs


def test_expire_with_default_timeout(self, cache: RedisCache):
def test_expire_combinations(self, cache: RedisCache):

Check warning on line 647 in tests/test_backend.py

View check run for this annotation

Codecov / codecov/patch

tests/test_backend.py#L647

Added line #L647 was not covered by tests
# Test that incompatible combinations raise redis.exceptions.ResponseError
cache.set("foo", "bar", timeout=20)

Check warning on line 649 in tests/test_backend.py

View check run for this annotation

Codecov / codecov/patch

tests/test_backend.py#L649

Added line #L649 was not covered by tests

# NX and XX are mutually exclusive
with pytest.raises(ResponseError):
cache.expire("foo", 30, nx=True, xx=True)

Check warning on line 653 in tests/test_backend.py

View check run for this annotation

Codecov / codecov/patch

tests/test_backend.py#L653

Added line #L653 was not covered by tests

# GT and LT are mutually exclusive)
with pytest.raises(ResponseError):
cache.expire("foo", 30, gt=True, lt=True)

Check warning on line 657 in tests/test_backend.py

View check run for this annotation

Codecov / codecov/patch

tests/test_backend.py#L657

Added line #L657 was not covered by tests

def test_expire_on_non_existent_key(self, cache: RedisCache):

Check warning on line 659 in tests/test_backend.py

View check run for this annotation

Codecov / codecov/patch

tests/test_backend.py#L659

Added line #L659 was not covered by tests
# Test expire with conditions on non-existent key
assert cache.expire("non-existent", 20) is False
assert cache.expire("non-existent", 20, nx=True) is False
assert cache.expire("non-existent", 20, xx=True) is False
assert cache.expire("non-existent", 20, gt=True) is False
assert cache.expire("non-existent", 20, lt=True) is False

Check warning on line 665 in tests/test_backend.py

View check run for this annotation

Codecov / codecov/patch

tests/test_backend.py#L661-L665

Added lines #L661 - L665 were not covered by tests

def test_expire_with_default_timeout_and_conditions(self, cache: RedisCache):

Check warning on line 667 in tests/test_backend.py

View check run for this annotation

Codecov / codecov/patch

tests/test_backend.py#L667

Added line #L667 was not covered by tests
cache.set("foo", "bar", timeout=None)
assert cache.expire("foo", DEFAULT_TIMEOUT) is True
assert cache.expire("not-existent-key", DEFAULT_TIMEOUT) is False
assert cache.expire("foo", DEFAULT_TIMEOUT, nx=True) is True

Check warning on line 669 in tests/test_backend.py

View check run for this annotation

Codecov / codecov/patch

tests/test_backend.py#L669

Added line #L669 was not covered by tests

cache.set("foo2", "bar", timeout=20)
assert cache.expire("foo2", DEFAULT_TIMEOUT, xx=True) is True

Check warning on line 672 in tests/test_backend.py

View check run for this annotation

Codecov / codecov/patch

tests/test_backend.py#L671-L672

Added lines #L671 - L672 were not covered by tests

@pytest.mark.parametrize(
"initial_timeout, new_timeout, params, expected_result, expected_pttl",
[
# Basic pexpire functionality (existing test case)
(None, 20500, {}, True, 20500),
# NX tests (only set if key has no expiry)
# Should work - key has no expiry
(None, 30500, {"nx": True}, True, 30500),
# Should fail - key already has expiry
(20500, 30500, {"nx": True}, False, 20500),
# XX tests (only set if key has existing expiry)
# Should work - key has expiry
(20500, 30500, {"xx": True}, True, 30500),
# Should fail - key has no expiry
(None, 30500, {"xx": True}, False, None),
# GT tests (only set if new expiry is greater than current)
# Should work - new timeout > current
(20500, 30500, {"gt": True}, True, 30500),
# Should fail - new timeout < current
(30500, 20500, {"gt": True}, False, 30500),
# LT tests (only set if new expiry is less than current)
# Should work - new timeout < current
(30500, 20500, {"lt": True}, True, 20500),
# Should fail - new timeout > current
(20500, 30500, {"lt": True}, False, 20500),
],
)
def test_pexpire_with_conditions(

Check warning on line 701 in tests/test_backend.py

View check run for this annotation

Codecov / codecov/patch

tests/test_backend.py#L701

Added line #L701 was not covered by tests
self,
cache: RedisCache,
initial_timeout,
new_timeout,
params,
expected_result,
expected_pttl,
):
cache.set(
"foo", "bar", timeout=initial_timeout / 1000 if initial_timeout else None

Check warning on line 711 in tests/test_backend.py

View check run for this annotation

Codecov / codecov/patch

tests/test_backend.py#L710-L711

Added lines #L710 - L711 were not covered by tests
)
result = cache.pexpire("foo", new_timeout, **params)

Check warning on line 713 in tests/test_backend.py

View check run for this annotation

Codecov / codecov/patch

tests/test_backend.py#L713

Added line #L713 was not covered by tests

assert result is expected_result
if expected_pttl is not None:

Check warning on line 716 in tests/test_backend.py

View check run for this annotation

Codecov / codecov/patch

tests/test_backend.py#L715-L716

Added lines #L715 - L716 were not covered by tests
# Using a delta of 10ms for approximate comparison due to timing precision
assert pytest.approx(cache.pttl("foo"), abs=10) == expected_pttl

Check warning on line 718 in tests/test_backend.py

View check run for this annotation

Codecov / codecov/patch

tests/test_backend.py#L718

Added line #L718 was not covered by tests

def test_pexpire_combinations(self, cache: RedisCache):

Check warning on line 720 in tests/test_backend.py

View check run for this annotation

Codecov / codecov/patch

tests/test_backend.py#L720

Added line #L720 was not covered by tests
# Test that incompatible combinations raise redis.exceptions.ResponseError
cache.set("foo", "bar", timeout=20)

Check warning on line 722 in tests/test_backend.py

View check run for this annotation

Codecov / codecov/patch

tests/test_backend.py#L722

Added line #L722 was not covered by tests

# NX and XX are mutually exclusive
with pytest.raises(ResponseError):
cache.pexpire("foo", 30500, nx=True, xx=True)

Check warning on line 726 in tests/test_backend.py

View check run for this annotation

Codecov / codecov/patch

tests/test_backend.py#L726

Added line #L726 was not covered by tests

# GT and LT are mutually exclusive
with pytest.raises(ResponseError):
cache.pexpire("foo", 30500, gt=True, lt=True)

Check warning on line 730 in tests/test_backend.py

View check run for this annotation

Codecov / codecov/patch

tests/test_backend.py#L730

Added line #L730 was not covered by tests

def test_pexpire(self, cache: RedisCache):
def test_pexpire_on_non_existent_key(self, cache: RedisCache):

Check warning on line 732 in tests/test_backend.py

View check run for this annotation

Codecov / codecov/patch

tests/test_backend.py#L732

Added line #L732 was not covered by tests
# Test pexpire with conditions on non-existent key
assert cache.pexpire("non-existent", 20500) is False
assert cache.pexpire("non-existent", 20500, nx=True) is False
assert cache.pexpire("non-existent", 20500, xx=True) is False
assert cache.pexpire("non-existent", 20500, gt=True) is False
assert cache.pexpire("non-existent", 20500, lt=True) is False

Check warning on line 738 in tests/test_backend.py

View check run for this annotation

Codecov / codecov/patch

tests/test_backend.py#L734-L738

Added lines #L734 - L738 were not covered by tests

def test_pexpire_with_default_timeout_and_conditions(self, cache: RedisCache):

Check warning on line 740 in tests/test_backend.py

View check run for this annotation

Codecov / codecov/patch

tests/test_backend.py#L740

Added line #L740 was not covered by tests
# Test with DEFAULT_TIMEOUT
cache.set("foo", "bar", timeout=None)
assert cache.pexpire("foo", 20500) is True
ttl = cache.pttl("foo")
# delta is set to 10 as precision error causes tests to fail
assert pytest.approx(ttl, 10) == 20500
assert cache.pexpire("not-existent-key", 20500) is False
assert cache.pexpire("foo", DEFAULT_TIMEOUT, nx=True) is True

Check warning on line 743 in tests/test_backend.py

View check run for this annotation

Codecov / codecov/patch

tests/test_backend.py#L743

Added line #L743 was not covered by tests

def test_pexpire_with_default_timeout(self, cache: RedisCache):
# Set a specific timeout and test with XX condition
cache.set("foo2", "bar", timeout=20)
assert cache.pexpire("foo2", DEFAULT_TIMEOUT, xx=True) is True

Check warning on line 747 in tests/test_backend.py

View check run for this annotation

Codecov / codecov/patch

tests/test_backend.py#L746-L747

Added lines #L746 - L747 were not covered by tests

def test_pexpire_precision(self, cache: RedisCache):

Check warning on line 749 in tests/test_backend.py

View check run for this annotation

Codecov / codecov/patch

tests/test_backend.py#L749

Added line #L749 was not covered by tests
# Test precision with very small and large millisecond values
cache.set("foo", "bar", timeout=None)
assert cache.pexpire("foo", DEFAULT_TIMEOUT) is True
assert cache.pexpire("not-existent-key", DEFAULT_TIMEOUT) is False

# Test with small millisecond value
assert cache.pexpire("foo", 100) is True # 100ms
pttl = cache.pttl("foo")
assert pytest.approx(pttl, abs=10) == 100

Check warning on line 756 in tests/test_backend.py

View check run for this annotation

Codecov / codecov/patch

tests/test_backend.py#L754-L756

Added lines #L754 - L756 were not covered by tests

# Test with large millisecond value
cache.set("foo2", "bar", timeout=None)
assert cache.pexpire("foo2", 3600000) is True # 1 hour in ms
pttl = cache.pttl("foo2")
assert pytest.approx(pttl, abs=10) == 3600000

Check warning on line 762 in tests/test_backend.py

View check run for this annotation

Codecov / codecov/patch

tests/test_backend.py#L759-L762

Added lines #L759 - L762 were not covered by tests

def test_pexpire_at(self, cache: RedisCache):
# Test settings expiration time 1 hour ahead by datetime.
Expand Down
Loading