diff options
author | Jordan Cook <jordan.cook.git@proton.me> | 2022-09-29 15:32:56 -0500 |
---|---|---|
committer | Jordan Cook <jordan.cook.git@proton.me> | 2022-09-30 16:00:16 -0500 |
commit | e1de1742fc8f91229fff069b12c3ce0b23a43d16 (patch) | |
tree | ae258ff31fef5ceeba907b426a08971b8b05e382 | |
parent | e06d915d12f97a72b932be7dc67ccf4b80077324 (diff) | |
parent | 823398b25b48421c94c3aba61566837ee78545e0 (diff) | |
download | requests-cache-e1de1742fc8f91229fff069b12c3ce0b23a43d16.tar.gz |
Merge pull request #698 from requests-cache/redis-ttl-offset
Add ttl_offset argument for Redis backend
-rw-r--r-- | HISTORY.md | 2 | ||||
-rw-r--r-- | docs/user_guide/backends/redis.md | 12 | ||||
-rw-r--r-- | requests_cache/backends/redis.py | 24 | ||||
-rw-r--r-- | tests/integration/test_redis.py | 25 |
4 files changed, 51 insertions, 12 deletions
@@ -35,6 +35,8 @@ * Add `size()` method to get estimated size of the database (including in-memory databases) * Add `sorted()` method with sorting and other query options * Add `wal` parameter to enable write-ahead logging +* Redis: + * Add `ttl_offset` argument to add a delay between cache expiration and deletion * MongoDB: * Store responses in plain (human-readable) document format instead of fully serialized binary * Add optional integration with MongoDB TTL to improve performance for removing expired responses diff --git a/docs/user_guide/backends/redis.md b/docs/user_guide/backends/redis.md index 141834c..56df69a 100644 --- a/docs/user_guide/backends/redis.md +++ b/docs/user_guide/backends/redis.md @@ -46,12 +46,16 @@ or disabled entirely. See [Redis Persistence](https://redis.io/topics/persistenc ## Expiration Redis natively supports TTL on a per-key basis, and can automatically remove expired responses from the cache. This will be set by by default, according to normal {ref}`expiration settings <expiration>`. - -Expired items are not removed immediately, but will never be returned from the cache. See -[Redis: EXPIRE](https://redis.io/commands/expire/) docs for more details. +See [Redis: EXPIRE](https://redis.io/commands/expire/) docs for more details on internal TTL behavior. If you intend to reuse expired responses, e.g. with {ref}`conditional-requests` or `stale_if_error`, -you can disable this behavior with the `ttl` argument: +you can use the `ttl_offset` argument to add additional time before deletion (default: 1 hour). +In other words, this makes backend expiration longer than cache expiration: +```python +>>> backend = RedisCache(ttl_offset=3600) +``` + +Alternatively, you can disable TTL completely with the `ttl` argument: ```python >>> backend = RedisCache(ttl=False) ``` diff --git a/requests_cache/backends/redis.py b/requests_cache/backends/redis.py index 95b44b5..259b858 100644 --- a/requests_cache/backends/redis.py +++ b/requests_cache/backends/redis.py @@ -14,26 +14,33 @@ from ..cache_keys import decode, encode from ..serializers import utf8_encoder from . import BaseCache, BaseStorage +DEFAULT_TTL_OFFSET = 3600 logger = getLogger(__name__) -# TODO: TTL tests -# TODO: Option to set a TTL offset, for longer expiration than expire_after class RedisCache(BaseCache): """Redis cache backend. Args: namespace: Redis namespace connection: Redis connection instance to use instead of creating a new one - ttl: Use Redis TTL to automatically remove expired items + ttl: Use Redis TTL to automatically delete expired items + ttl_offset: Additional time to wait before deleting expired items, in seconds kwargs: Additional keyword arguments for :py:class:`redis.client.Redis` """ def __init__( - self, namespace='http_cache', connection: Redis = None, ttl: bool = True, **kwargs + self, + namespace='http_cache', + connection: Redis = None, + ttl: bool = True, + ttl_offset: int = DEFAULT_TTL_OFFSET, + **kwargs, ): super().__init__(cache_name=namespace, **kwargs) - self.responses = RedisDict(namespace, connection=connection, ttl=ttl, **kwargs) + self.responses = RedisDict( + namespace, connection=connection, ttl=ttl, ttl_offset=ttl_offset, **kwargs + ) kwargs.pop('serializer', None) self.redirects = RedisHashDict( namespace, @@ -58,6 +65,7 @@ class RedisDict(BaseStorage): collection_name: str = None, connection=None, ttl: bool = True, + ttl_offset: int = DEFAULT_TTL_OFFSET, **kwargs, ): @@ -66,6 +74,7 @@ class RedisDict(BaseStorage): self.connection = connection or StrictRedis(**connection_kwargs) self.namespace = namespace self.ttl = ttl + self.ttl_offset = ttl_offset def _bkey(self, key: str) -> bytes: """Get a full hash key as bytes""" @@ -86,8 +95,9 @@ class RedisDict(BaseStorage): def __setitem__(self, key, item): """Save an item to the cache, optionally with TTL""" expires_delta = getattr(item, 'expires_delta', None) - if self.ttl and (expires_delta or 0) > 0: - self.connection.setex(self._bkey(key), expires_delta, self.serialize(item)) + ttl_seconds = (expires_delta or 0) + self.ttl_offset + if self.ttl and ttl_seconds > 0: + self.connection.setex(self._bkey(key), ttl_seconds, self.serialize(item)) else: self.connection.set(self._bkey(key), self.serialize(item)) diff --git a/tests/integration/test_redis.py b/tests/integration/test_redis.py index 83a15d5..4c74d68 100644 --- a/tests/integration/test_redis.py +++ b/tests/integration/test_redis.py @@ -1,9 +1,10 @@ from unittest.mock import patch import pytest +from redis import StrictRedis from requests_cache.backends import RedisCache, RedisDict, RedisHashDict -from tests.conftest import fail_if_no_connection +from tests.conftest import fail_if_no_connection, httpbin from tests.integration.base_cache_test import BaseCacheTest from tests.integration.base_storage_test import BaseStorageTest @@ -36,3 +37,25 @@ class TestRedisHashDict(TestRedisDict): class TestRedisCache(BaseCacheTest): backend_class = RedisCache + + @patch.object(StrictRedis, 'setex') + def test_ttl(self, mock_setex): + session = self.init_session(expire_after=60) + session.get(httpbin('get')) + call_args = mock_setex.mock_calls[0][1] + assert call_args[1] == 3660 # Should be expiration + default offset + + @patch.object(StrictRedis, 'setex') + def test_ttl__offset(self, mock_setex): + session = self.init_session(expire_after=60, ttl_offset=500) + session.get(httpbin('get')) + call_args = mock_setex.mock_calls[0][1] + assert call_args[1] == 560 # Should be expiration + custom offset + + @patch.object(StrictRedis, 'setex') + @patch.object(StrictRedis, 'set') + def test_ttl__disabled(self, mock_set, mock_setex): + session = self.init_session(expire_after=60, ttl=False) + session.get(httpbin('get')) + mock_setex.assert_not_called() + mock_set.assert_called() |