Skip to content

Commit 47e6c7a

Browse files
committed
Added cache support for SSLConnection
1 parent 64fb176 commit 47e6c7a

File tree

5 files changed

+89
-6
lines changed

5 files changed

+89
-6
lines changed

redis/connection.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -863,7 +863,9 @@ def _enable_tracking_callback(self, conn: ConnectionInterface) -> None:
863863
conn._parser.set_invalidation_push_handler(self._on_invalidation_callback)
864864

865865
def _process_pending_invalidations(self):
866-
while self.can_read():
866+
while self.retry.call_with_retry_on_false(
867+
lambda: self.can_read()
868+
):
867869
self._conn.read_response(push_request=True)
868870

869871
def _on_invalidation_callback(

redis/retry.py

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import socket
2+
import time
23
from time import sleep
3-
from typing import TYPE_CHECKING, Any, Callable, Iterable, Tuple, Type, TypeVar
4+
from typing import TYPE_CHECKING, Any, Callable, Iterable, Tuple, Type, TypeVar, Optional
45

56
from redis.exceptions import ConnectionError, TimeoutError
67

@@ -68,3 +69,42 @@ def call_with_retry(
6869
backoff = self._backoff.compute(failures)
6970
if backoff > 0:
7071
sleep(backoff)
72+
73+
def call_with_retry_on_false(
74+
self,
75+
do: Callable[[], T],
76+
on_false: Optional[Callable[[], T]] = None,
77+
max_retries: Optional[int] = 3,
78+
timeout: Optional[float] = 0,
79+
exponent: Optional[int] = 2,
80+
) -> bool:
81+
"""
82+
Execute an operation that returns boolean value with retry
83+
logic in case if false value been returned.
84+
`do`: the operation to call. Expects no argument.
85+
`on_false`: Callback to be executed on retry fail.
86+
"""
87+
res = do()
88+
89+
if res:
90+
return res
91+
92+
if on_false is not None:
93+
on_false()
94+
95+
if max_retries > 0:
96+
if timeout > 0:
97+
time.sleep(timeout)
98+
99+
return self.call_with_retry_on_false(
100+
do,
101+
on_false,
102+
max_retries - 1,
103+
timeout * exponent,
104+
exponent
105+
)
106+
107+
return False
108+
109+
110+

tests/conftest.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,10 @@
1111
from packaging.version import Version
1212
from redis import Sentinel
1313
from redis.backoff import NoBackoff
14-
from redis.connection import Connection, parse_url
14+
from redis.connection import Connection, parse_url, SSLConnection
1515
from redis.exceptions import RedisClusterException
1616
from redis.retry import Retry
17+
from tests.ssl_utils import get_ssl_filename
1718

1819
REDIS_INFO = {}
1920
default_redis_url = "redis://localhost:6379/0"
@@ -323,6 +324,18 @@ def _get_client(
323324
cluster_mode = REDIS_INFO["cluster_enabled"]
324325
if not cluster_mode:
325326
url_options = parse_url(redis_url)
327+
connection_class = Connection
328+
ssl = kwargs.pop("ssl", False)
329+
if ssl:
330+
connection_class = SSLConnection
331+
kwargs["ssl_certfile"] = get_ssl_filename("client-cert.pem")
332+
kwargs["ssl_keyfile"] = get_ssl_filename("client-key.pem")
333+
# When you try to assign "required" as single string, it assigns tuple instead of string.
334+
# Probably some reserved keyword, I can't explain how does it work -_-
335+
kwargs["ssl_cert_reqs"] = "require"+"d"
336+
kwargs["ssl_ca_certs"] = get_ssl_filename("ca-cert.pem")
337+
kwargs["port"] = 6666
338+
kwargs["connection_class"] = connection_class
326339
url_options.update(kwargs)
327340
pool = redis.ConnectionPool(**url_options)
328341
client = cls(connection_pool=pool)

tests/test_cache.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,13 @@ def r(request):
1515
cache = request.param.get("cache")
1616
kwargs = request.param.get("kwargs", {})
1717
protocol = request.param.get("protocol", 3)
18+
ssl = request.param.get("ssl", False)
1819
single_connection_client = request.param.get("single_connection_client", False)
1920
with _get_client(
2021
redis.Redis,
2122
request,
2223
protocol=protocol,
24+
ssl=ssl,
2325
single_connection_client=single_connection_client,
2426
use_cache=use_cache,
2527
cache=cache,
@@ -630,3 +632,29 @@ def test_cache_clears_on_disconnect(self, master, cache):
630632
master.connection_pool.get_connection('_').disconnect()
631633
# Make sure cache is empty
632634
assert cache.currsize == 0
635+
636+
@pytest.mark.skipif(HIREDIS_AVAILABLE, reason="PythonParser only")
637+
@pytest.mark.onlynoncluster
638+
class TestSSLCache:
639+
@pytest.mark.parametrize("r", [
640+
{
641+
"cache": TTLCache(128, 300),
642+
"use_cache": True,
643+
"ssl": True,
644+
}
645+
], indirect=True)
646+
@pytest.mark.onlynoncluster
647+
def test_get_from_cache(self, r, r2, cache):
648+
r, cache = r
649+
# add key to redis
650+
r.set("foo", "bar")
651+
# get key from redis and save in local cache
652+
assert r.get("foo") == b"bar"
653+
# get key from local cache
654+
assert cache.get(("GET", "foo")) == b"bar"
655+
# change key in redis (cause invalidation)
656+
assert r2.set("foo", "barbar")
657+
# Retrieves a new value from server and cache it
658+
assert r.get("foo") == b"barbar"
659+
# Make sure that new value was cached
660+
assert cache.get(("GET", "foo")) == b"barbar"

tests/test_cluster.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -643,10 +643,10 @@ def parse_response_mock_third(connection, *args, **options):
643643
mocks["send_command"].assert_has_calls(
644644
[
645645
call("READONLY"),
646-
call("GET", "foo"),
646+
call("GET", "foo", keys=['foo']),
647647
call("READONLY"),
648-
call("GET", "foo"),
649-
call("GET", "foo"),
648+
call("GET", "foo", keys=['foo']),
649+
call("GET", "foo", keys=['foo']),
650650
]
651651
)
652652

0 commit comments

Comments
 (0)