Skip to content

Commit cb2c330

Browse files
authored
Merge branch 'redis:master' into hscan-no-values
2 parents 8d588ca + ebb6171 commit cb2c330

File tree

15 files changed

+251
-46
lines changed

15 files changed

+251
-46
lines changed

.github/workflows/integration.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ jobs:
8888
path: '${{matrix.test-type}}*results.xml'
8989

9090
- name: Upload codecov coverage
91-
uses: codecov/codecov-action@v3
91+
uses: codecov/codecov-action@v4
9292
with:
9393
fail_ci_if_error: false
9494

CHANGES

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
* Allow to control the minimum SSL version
12
* Add an optional lock_name attribute to LockError.
23
* Fix return types for `get`, `set_path` and `strappend` in JSONCommands
34
* Connection.register_connect_callback() is made public.

docs/examples/asyncio_examples.ipynb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,7 @@
201201
"\n",
202202
"async def reader(channel: redis.client.PubSub):\n",
203203
" while True:\n",
204-
" message = await channel.get_message(ignore_subscribe_messages=True)\n",
204+
" message = await channel.get_message(ignore_subscribe_messages=True, timeout=None)\n",
205205
" if message is not None:\n",
206206
" print(f\"(Reader) Message Received: {message}\")\n",
207207
" if message[\"data\"].decode() == STOPWORD:\n",
@@ -264,7 +264,7 @@
264264
"\n",
265265
"async def reader(channel: redis.client.PubSub):\n",
266266
" while True:\n",
267-
" message = await channel.get_message(ignore_subscribe_messages=True)\n",
267+
" message = await channel.get_message(ignore_subscribe_messages=True, timeout=None)\n",
268268
" if message is not None:\n",
269269
" print(f\"(Reader) Message Received: {message}\")\n",
270270
" if message[\"data\"].decode() == STOPWORD:\n",

docs/examples/ssl_connection_examples.ipynb

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,42 @@
7676
"ssl_connection.ping()"
7777
]
7878
},
79+
{
80+
"cell_type": "markdown",
81+
"metadata": {},
82+
"source": [
83+
"## Connecting to a Redis instance via SSL, while specifying a minimum TLS version"
84+
]
85+
},
86+
{
87+
"cell_type": "code",
88+
"execution_count": null,
89+
"metadata": {},
90+
"outputs": [
91+
{
92+
"data": {
93+
"text/plain": [
94+
"True"
95+
]
96+
},
97+
"execution_count": 6,
98+
"metadata": {},
99+
"output_type": "execute_result"
100+
}
101+
],
102+
"source": [
103+
"import redis\n",
104+
"import ssl\n",
105+
"\n",
106+
"ssl_conn = redis.Redis(\n",
107+
" host=\"localhost\",\n",
108+
" port=6666,\n",
109+
" ssl=True,\n",
110+
" ssl_min_version=ssl.TLSVersion.TLSv1_3,\n",
111+
")\n",
112+
"ssl_conn.ping()"
113+
]
114+
},
79115
{
80116
"cell_type": "markdown",
81117
"metadata": {},

redis/asyncio/client.py

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import copy
33
import inspect
44
import re
5+
import ssl
56
import warnings
67
from typing import (
78
TYPE_CHECKING,
@@ -226,6 +227,7 @@ def __init__(
226227
ssl_ca_certs: Optional[str] = None,
227228
ssl_ca_data: Optional[str] = None,
228229
ssl_check_hostname: bool = False,
230+
ssl_min_version: Optional[ssl.TLSVersion] = None,
229231
max_connections: Optional[int] = None,
230232
single_connection_client: bool = False,
231233
health_check_interval: int = 0,
@@ -332,6 +334,7 @@ def __init__(
332334
"ssl_ca_certs": ssl_ca_certs,
333335
"ssl_ca_data": ssl_ca_data,
334336
"ssl_check_hostname": ssl_check_hostname,
337+
"ssl_min_version": ssl_min_version,
335338
}
336339
)
337340
# This arg only used if no pool is passed in
@@ -924,11 +927,15 @@ async def connect(self):
924927
async def _disconnect_raise_connect(self, conn, error):
925928
"""
926929
Close the connection and raise an exception
927-
if retry_on_timeout is not set or the error
928-
is not a TimeoutError. Otherwise, try to reconnect
930+
if retry_on_error is not set or the error is not one
931+
of the specified error types. Otherwise, try to
932+
reconnect
929933
"""
930934
await conn.disconnect()
931-
if not (conn.retry_on_timeout and isinstance(error, TimeoutError)):
935+
if (
936+
conn.retry_on_error is None
937+
or isinstance(error, tuple(conn.retry_on_error)) is False
938+
):
932939
raise error
933940
await conn.connect()
934941

@@ -1341,8 +1348,8 @@ async def _disconnect_reset_raise(self, conn, error):
13411348
"""
13421349
Close the connection, reset watching state and
13431350
raise an exception if we were watching,
1344-
retry_on_timeout is not set,
1345-
or the error is not a TimeoutError
1351+
if retry_on_error is not set or the error is not one
1352+
of the specified error types.
13461353
"""
13471354
await conn.disconnect()
13481355
# if we were already watching a variable, the watch is no longer
@@ -1353,9 +1360,12 @@ async def _disconnect_reset_raise(self, conn, error):
13531360
raise WatchError(
13541361
"A ConnectionError occurred on while watching one or more keys"
13551362
)
1356-
# if retry_on_timeout is not set, or the error is not
1357-
# a TimeoutError, raise it
1358-
if not (conn.retry_on_timeout and isinstance(error, TimeoutError)):
1363+
# if retry_on_error is not set or the error is not one
1364+
# of the specified error types, raise it
1365+
if (
1366+
conn.retry_on_error is None
1367+
or isinstance(error, tuple(conn.retry_on_error)) is False
1368+
):
13591369
await self.aclose()
13601370
raise
13611371

@@ -1530,8 +1540,8 @@ async def load_scripts(self):
15301540
async def _disconnect_raise_reset(self, conn: Connection, error: Exception):
15311541
"""
15321542
Close the connection, raise an exception if we were watching,
1533-
and raise an exception if retry_on_timeout is not set,
1534-
or the error is not a TimeoutError
1543+
and raise an exception if retry_on_error is not set or the
1544+
error is not one of the specified error types.
15351545
"""
15361546
await conn.disconnect()
15371547
# if we were watching a variable, the watch is no longer valid
@@ -1541,9 +1551,12 @@ async def _disconnect_raise_reset(self, conn: Connection, error: Exception):
15411551
raise WatchError(
15421552
"A ConnectionError occurred on while watching one or more keys"
15431553
)
1544-
# if retry_on_timeout is not set, or the error is not
1545-
# a TimeoutError, raise it
1546-
if not (conn.retry_on_timeout and isinstance(error, TimeoutError)):
1554+
# if retry_on_error is not set or the error is not one
1555+
# of the specified error types, raise it
1556+
if (
1557+
conn.retry_on_error is None
1558+
or isinstance(error, tuple(conn.retry_on_error)) is False
1559+
):
15471560
await self.reset()
15481561
raise
15491562

redis/asyncio/cluster.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import collections
33
import random
44
import socket
5+
import ssl
56
import warnings
67
from typing import (
78
Any,
@@ -271,6 +272,7 @@ def __init__(
271272
ssl_certfile: Optional[str] = None,
272273
ssl_check_hostname: bool = False,
273274
ssl_keyfile: Optional[str] = None,
275+
ssl_min_version: Optional[ssl.TLSVersion] = None,
274276
protocol: Optional[int] = 2,
275277
address_remap: Optional[Callable[[str, int], Tuple[str, int]]] = None,
276278
cache_enabled: bool = False,
@@ -344,6 +346,7 @@ def __init__(
344346
"ssl_certfile": ssl_certfile,
345347
"ssl_check_hostname": ssl_check_hostname,
346348
"ssl_keyfile": ssl_keyfile,
349+
"ssl_min_version": ssl_min_version,
347350
}
348351
)
349352

redis/asyncio/connection.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -823,6 +823,7 @@ def __init__(
823823
ssl_ca_certs: Optional[str] = None,
824824
ssl_ca_data: Optional[str] = None,
825825
ssl_check_hostname: bool = False,
826+
ssl_min_version: Optional[ssl.TLSVersion] = None,
826827
**kwargs,
827828
):
828829
self.ssl_context: RedisSSLContext = RedisSSLContext(
@@ -832,6 +833,7 @@ def __init__(
832833
ca_certs=ssl_ca_certs,
833834
ca_data=ssl_ca_data,
834835
check_hostname=ssl_check_hostname,
836+
min_version=ssl_min_version,
835837
)
836838
super().__init__(**kwargs)
837839

@@ -864,6 +866,10 @@ def ca_data(self):
864866
def check_hostname(self):
865867
return self.ssl_context.check_hostname
866868

869+
@property
870+
def min_version(self):
871+
return self.ssl_context.min_version
872+
867873

868874
class RedisSSLContext:
869875
__slots__ = (
@@ -874,6 +880,7 @@ class RedisSSLContext:
874880
"ca_data",
875881
"context",
876882
"check_hostname",
883+
"min_version",
877884
)
878885

879886
def __init__(
@@ -884,6 +891,7 @@ def __init__(
884891
ca_certs: Optional[str] = None,
885892
ca_data: Optional[str] = None,
886893
check_hostname: bool = False,
894+
min_version: Optional[ssl.TLSVersion] = None,
887895
):
888896
self.keyfile = keyfile
889897
self.certfile = certfile
@@ -903,6 +911,7 @@ def __init__(
903911
self.ca_certs = ca_certs
904912
self.ca_data = ca_data
905913
self.check_hostname = check_hostname
914+
self.min_version = min_version
906915
self.context: Optional[ssl.SSLContext] = None
907916

908917
def get(self) -> ssl.SSLContext:
@@ -914,6 +923,8 @@ def get(self) -> ssl.SSLContext:
914923
context.load_cert_chain(certfile=self.certfile, keyfile=self.keyfile)
915924
if self.ca_certs or self.ca_data:
916925
context.load_verify_locations(cafile=self.ca_certs, cadata=self.ca_data)
926+
if self.min_version is not None:
927+
context.minimum_version = self.min_version
917928
self.context = context
918929
return self.context
919930

@@ -1280,7 +1291,7 @@ class BlockingConnectionPool(ConnectionPool):
12801291
connection from the pool when all of connections are in use, rather than
12811292
raising a :py:class:`~redis.ConnectionError` (as the default
12821293
:py:class:`~redis.asyncio.ConnectionPool` implementation does), it
1283-
makes blocks the current `Task` for a specified number of seconds until
1294+
blocks the current `Task` for a specified number of seconds until
12841295
a connection becomes available.
12851296
12861297
Use ``max_connections`` to increase / decrease the pool size::

redis/client.py

Lines changed: 36 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,12 @@
2525
SentinelCommands,
2626
list_or_args,
2727
)
28-
from redis.connection import ConnectionPool, SSLConnection, UnixDomainSocketConnection
28+
from redis.connection import (
29+
AbstractConnection,
30+
ConnectionPool,
31+
SSLConnection,
32+
UnixDomainSocketConnection,
33+
)
2934
from redis.credentials import CredentialProvider
3035
from redis.exceptions import (
3136
ConnectionError,
@@ -198,6 +203,7 @@ def __init__(
198203
ssl_validate_ocsp_stapled=False,
199204
ssl_ocsp_context=None,
200205
ssl_ocsp_expected_cert=None,
206+
ssl_min_version=None,
201207
max_connections=None,
202208
single_connection_client=False,
203209
health_check_interval=0,
@@ -311,6 +317,7 @@ def __init__(
311317
"ssl_validate_ocsp": ssl_validate_ocsp,
312318
"ssl_ocsp_context": ssl_ocsp_context,
313319
"ssl_ocsp_expected_cert": ssl_ocsp_expected_cert,
320+
"ssl_min_version": ssl_min_version,
314321
}
315322
)
316323
connection_pool = ConnectionPool(**kwargs)
@@ -837,11 +844,15 @@ def clean_health_check_responses(self) -> None:
837844
def _disconnect_raise_connect(self, conn, error) -> None:
838845
"""
839846
Close the connection and raise an exception
840-
if retry_on_timeout is not set or the error
841-
is not a TimeoutError. Otherwise, try to reconnect
847+
if retry_on_error is not set or the error is not one
848+
of the specified error types. Otherwise, try to
849+
reconnect
842850
"""
843851
conn.disconnect()
844-
if not (conn.retry_on_timeout and isinstance(error, TimeoutError)):
852+
if (
853+
conn.retry_on_error is None
854+
or isinstance(error, tuple(conn.retry_on_error)) is False
855+
):
845856
raise error
846857
conn.connect()
847858

@@ -1318,8 +1329,8 @@ def _disconnect_reset_raise(self, conn, error) -> None:
13181329
"""
13191330
Close the connection, reset watching state and
13201331
raise an exception if we were watching,
1321-
retry_on_timeout is not set,
1322-
or the error is not a TimeoutError
1332+
if retry_on_error is not set or the error is not one
1333+
of the specified error types.
13231334
"""
13241335
conn.disconnect()
13251336
# if we were already watching a variable, the watch is no longer
@@ -1330,9 +1341,12 @@ def _disconnect_reset_raise(self, conn, error) -> None:
13301341
raise WatchError(
13311342
"A ConnectionError occurred on while watching one or more keys"
13321343
)
1333-
# if retry_on_timeout is not set, or the error is not
1334-
# a TimeoutError, raise it
1335-
if not (conn.retry_on_timeout and isinstance(error, TimeoutError)):
1344+
# if retry_on_error is not set or the error is not one
1345+
# of the specified error types, raise it
1346+
if (
1347+
conn.retry_on_error is None
1348+
or isinstance(error, tuple(conn.retry_on_error)) is False
1349+
):
13361350
self.reset()
13371351
raise
13381352

@@ -1490,11 +1504,15 @@ def load_scripts(self):
14901504
if not exist:
14911505
s.sha = immediate("SCRIPT LOAD", s.script)
14921506

1493-
def _disconnect_raise_reset(self, conn: Redis, error: Exception) -> None:
1507+
def _disconnect_raise_reset(
1508+
self,
1509+
conn: AbstractConnection,
1510+
error: Exception,
1511+
) -> None:
14941512
"""
14951513
Close the connection, raise an exception if we were watching,
1496-
and raise an exception if TimeoutError is not part of retry_on_error,
1497-
or the error is not a TimeoutError
1514+
and raise an exception if retry_on_error is not set or the
1515+
error is not one of the specified error types.
14981516
"""
14991517
conn.disconnect()
15001518
# if we were watching a variable, the watch is no longer valid
@@ -1504,11 +1522,13 @@ def _disconnect_raise_reset(self, conn: Redis, error: Exception) -> None:
15041522
raise WatchError(
15051523
"A ConnectionError occurred on while watching one or more keys"
15061524
)
1507-
# if TimeoutError is not part of retry_on_error, or the error
1508-
# is not a TimeoutError, raise it
1509-
if not (
1510-
TimeoutError in conn.retry_on_error and isinstance(error, TimeoutError)
1525+
# if retry_on_error is not set or the error is not one
1526+
# of the specified error types, raise it
1527+
if (
1528+
conn.retry_on_error is None
1529+
or isinstance(error, tuple(conn.retry_on_error)) is False
15111530
):
1531+
15121532
self.reset()
15131533
raise error
15141534

redis/cluster.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2143,6 +2143,8 @@ def _send_cluster_commands(
21432143
try:
21442144
connection = get_connection(redis_node, c.args)
21452145
except ConnectionError:
2146+
for n in nodes.values():
2147+
n.connection_pool.release(n.connection)
21462148
# Connection retries are being handled in the node's
21472149
# Retry object. Reinitialize the node -> slot table.
21482150
self.nodes_manager.initialize()

0 commit comments

Comments
 (0)