summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJordan Cook <jordan.cook.git@proton.me>2022-09-29 15:32:56 -0500
committerJordan Cook <jordan.cook.git@proton.me>2022-09-30 16:00:16 -0500
commite1de1742fc8f91229fff069b12c3ce0b23a43d16 (patch)
treeae258ff31fef5ceeba907b426a08971b8b05e382
parente06d915d12f97a72b932be7dc67ccf4b80077324 (diff)
parent823398b25b48421c94c3aba61566837ee78545e0 (diff)
downloadrequests-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.md2
-rw-r--r--docs/user_guide/backends/redis.md12
-rw-r--r--requests_cache/backends/redis.py24
-rw-r--r--tests/integration/test_redis.py25
4 files changed, 51 insertions, 12 deletions
diff --git a/HISTORY.md b/HISTORY.md
index 919c1fc..2a381d9 100644
--- a/HISTORY.md
+++ b/HISTORY.md
@@ -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()