redis 5.3.0b4__py3-none-any.whl → 6.0.0b1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- redis/__init__.py +2 -11
- redis/_parsers/base.py +14 -2
- redis/asyncio/client.py +21 -13
- redis/asyncio/cluster.py +79 -56
- redis/asyncio/connection.py +40 -11
- redis/asyncio/lock.py +26 -5
- redis/asyncio/sentinel.py +9 -1
- redis/asyncio/utils.py +1 -1
- redis/auth/token.py +6 -2
- redis/backoff.py +15 -0
- redis/client.py +80 -59
- redis/cluster.py +114 -52
- redis/commands/cluster.py +1 -11
- redis/commands/core.py +218 -206
- redis/commands/helpers.py +0 -70
- redis/commands/redismodules.py +0 -20
- redis/commands/search/aggregation.py +3 -1
- redis/commands/search/commands.py +41 -14
- redis/commands/search/dialect.py +3 -0
- redis/commands/search/profile_information.py +14 -0
- redis/commands/search/query.py +5 -1
- redis/connection.py +48 -23
- redis/exceptions.py +4 -1
- redis/lock.py +24 -4
- redis/ocsp.py +2 -1
- redis/sentinel.py +1 -1
- redis/typing.py +1 -1
- redis/utils.py +107 -1
- {redis-5.3.0b4.dist-info → redis-6.0.0b1.dist-info}/METADATA +57 -23
- {redis-5.3.0b4.dist-info → redis-6.0.0b1.dist-info}/RECORD +33 -40
- {redis-5.3.0b4.dist-info → redis-6.0.0b1.dist-info}/WHEEL +1 -2
- redis/commands/graph/__init__.py +0 -263
- redis/commands/graph/commands.py +0 -313
- redis/commands/graph/edge.py +0 -91
- redis/commands/graph/exceptions.py +0 -3
- redis/commands/graph/execution_plan.py +0 -211
- redis/commands/graph/node.py +0 -88
- redis/commands/graph/path.py +0 -78
- redis/commands/graph/query_result.py +0 -588
- redis-5.3.0b4.dist-info/top_level.txt +0 -1
- /redis/commands/search/{indexDefinition.py → index_definition.py} +0 -0
- {redis-5.3.0b4.dist-info → redis-6.0.0b1.dist-info/licenses}/LICENSE +0 -0
redis/commands/redismodules.py
CHANGED
|
@@ -72,16 +72,6 @@ class RedisModuleCommands:
|
|
|
72
72
|
tdigest = TDigestBloom(client=self)
|
|
73
73
|
return tdigest
|
|
74
74
|
|
|
75
|
-
def graph(self, index_name="idx"):
|
|
76
|
-
"""Access the graph namespace, providing support for
|
|
77
|
-
redis graph data.
|
|
78
|
-
"""
|
|
79
|
-
|
|
80
|
-
from .graph import Graph
|
|
81
|
-
|
|
82
|
-
g = Graph(client=self, name=index_name)
|
|
83
|
-
return g
|
|
84
|
-
|
|
85
75
|
|
|
86
76
|
class AsyncRedisModuleCommands(RedisModuleCommands):
|
|
87
77
|
def ft(self, index_name="idx"):
|
|
@@ -91,13 +81,3 @@ class AsyncRedisModuleCommands(RedisModuleCommands):
|
|
|
91
81
|
|
|
92
82
|
s = AsyncSearch(client=self, index_name=index_name)
|
|
93
83
|
return s
|
|
94
|
-
|
|
95
|
-
def graph(self, index_name="idx"):
|
|
96
|
-
"""Access the graph namespace, providing support for
|
|
97
|
-
redis graph data.
|
|
98
|
-
"""
|
|
99
|
-
|
|
100
|
-
from .graph import AsyncGraph
|
|
101
|
-
|
|
102
|
-
g = AsyncGraph(client=self, name=index_name)
|
|
103
|
-
return g
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
from typing import List, Union
|
|
2
2
|
|
|
3
|
+
from redis.commands.search.dialect import DEFAULT_DIALECT
|
|
4
|
+
|
|
3
5
|
FIELDNAME = object()
|
|
4
6
|
|
|
5
7
|
|
|
@@ -110,7 +112,7 @@ class AggregateRequest:
|
|
|
110
112
|
self._with_schema = False
|
|
111
113
|
self._verbatim = False
|
|
112
114
|
self._cursor = []
|
|
113
|
-
self._dialect =
|
|
115
|
+
self._dialect = DEFAULT_DIALECT
|
|
114
116
|
self._add_scores = False
|
|
115
117
|
self._scorer = "TFIDF"
|
|
116
118
|
|
|
@@ -5,12 +5,13 @@ from typing import Dict, List, Optional, Union
|
|
|
5
5
|
from redis.client import NEVER_DECODE, Pipeline
|
|
6
6
|
from redis.utils import deprecated_function
|
|
7
7
|
|
|
8
|
-
from ..helpers import get_protocol_version
|
|
8
|
+
from ..helpers import get_protocol_version
|
|
9
9
|
from ._util import to_string
|
|
10
10
|
from .aggregation import AggregateRequest, AggregateResult, Cursor
|
|
11
11
|
from .document import Document
|
|
12
12
|
from .field import Field
|
|
13
|
-
from .
|
|
13
|
+
from .index_definition import IndexDefinition
|
|
14
|
+
from .profile_information import ProfileInformation
|
|
14
15
|
from .query import Query
|
|
15
16
|
from .result import Result
|
|
16
17
|
from .suggestion import SuggestionParser
|
|
@@ -67,7 +68,7 @@ class SearchCommands:
|
|
|
67
68
|
|
|
68
69
|
def _parse_results(self, cmd, res, **kwargs):
|
|
69
70
|
if get_protocol_version(self.client) in ["3", 3]:
|
|
70
|
-
return res
|
|
71
|
+
return ProfileInformation(res) if cmd == "FT.PROFILE" else res
|
|
71
72
|
else:
|
|
72
73
|
return self._RESP2_MODULE_CALLBACKS[cmd](res, **kwargs)
|
|
73
74
|
|
|
@@ -101,7 +102,7 @@ class SearchCommands:
|
|
|
101
102
|
with_scores=query._with_scores,
|
|
102
103
|
)
|
|
103
104
|
|
|
104
|
-
return result,
|
|
105
|
+
return result, ProfileInformation(res[1])
|
|
105
106
|
|
|
106
107
|
def _parse_spellcheck(self, res, **kwargs):
|
|
107
108
|
corrections = {}
|
|
@@ -254,8 +255,18 @@ class SearchCommands:
|
|
|
254
255
|
|
|
255
256
|
For more information see `FT.DROPINDEX <https://redis.io/commands/ft.dropindex>`_.
|
|
256
257
|
""" # noqa
|
|
257
|
-
|
|
258
|
-
|
|
258
|
+
args = [DROPINDEX_CMD, self.index_name]
|
|
259
|
+
|
|
260
|
+
delete_str = (
|
|
261
|
+
"DD"
|
|
262
|
+
if isinstance(delete_documents, bool) and delete_documents is True
|
|
263
|
+
else ""
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
if delete_str:
|
|
267
|
+
args.append(delete_str)
|
|
268
|
+
|
|
269
|
+
return self.execute_command(*args)
|
|
259
270
|
|
|
260
271
|
def _add_document(
|
|
261
272
|
self,
|
|
@@ -499,7 +510,7 @@ class SearchCommands:
|
|
|
499
510
|
For more information see `FT.SEARCH <https://redis.io/commands/ft.search>`_.
|
|
500
511
|
""" # noqa
|
|
501
512
|
args, query = self._mk_query_args(query, query_params=query_params)
|
|
502
|
-
st = time.
|
|
513
|
+
st = time.monotonic()
|
|
503
514
|
|
|
504
515
|
options = {}
|
|
505
516
|
if get_protocol_version(self.client) not in ["3", 3]:
|
|
@@ -511,7 +522,7 @@ class SearchCommands:
|
|
|
511
522
|
return res
|
|
512
523
|
|
|
513
524
|
return self._parse_results(
|
|
514
|
-
SEARCH_CMD, res, query=query, duration=(time.
|
|
525
|
+
SEARCH_CMD, res, query=query, duration=(time.monotonic() - st) * 1000.0
|
|
515
526
|
)
|
|
516
527
|
|
|
517
528
|
def explain(
|
|
@@ -585,7 +596,7 @@ class SearchCommands:
|
|
|
585
596
|
|
|
586
597
|
def profile(
|
|
587
598
|
self,
|
|
588
|
-
query: Union[
|
|
599
|
+
query: Union[Query, AggregateRequest],
|
|
589
600
|
limited: bool = False,
|
|
590
601
|
query_params: Optional[Dict[str, Union[str, int, float]]] = None,
|
|
591
602
|
):
|
|
@@ -595,13 +606,13 @@ class SearchCommands:
|
|
|
595
606
|
|
|
596
607
|
### Parameters
|
|
597
608
|
|
|
598
|
-
**query**: This can be either an `AggregateRequest
|
|
609
|
+
**query**: This can be either an `AggregateRequest` or `Query`.
|
|
599
610
|
**limited**: If set to True, removes details of reader iterator.
|
|
600
611
|
**query_params**: Define one or more value parameters.
|
|
601
612
|
Each parameter has a name and a value.
|
|
602
613
|
|
|
603
614
|
"""
|
|
604
|
-
st = time.
|
|
615
|
+
st = time.monotonic()
|
|
605
616
|
cmd = [PROFILE_CMD, self.index_name, ""]
|
|
606
617
|
if limited:
|
|
607
618
|
cmd.append("LIMITED")
|
|
@@ -620,7 +631,7 @@ class SearchCommands:
|
|
|
620
631
|
res = self.execute_command(*cmd)
|
|
621
632
|
|
|
622
633
|
return self._parse_results(
|
|
623
|
-
PROFILE_CMD, res, query=query, duration=(time.
|
|
634
|
+
PROFILE_CMD, res, query=query, duration=(time.monotonic() - st) * 1000.0
|
|
624
635
|
)
|
|
625
636
|
|
|
626
637
|
def spellcheck(self, query, distance=None, include=None, exclude=None):
|
|
@@ -691,6 +702,10 @@ class SearchCommands:
|
|
|
691
702
|
cmd = [DICT_DUMP_CMD, name]
|
|
692
703
|
return self.execute_command(*cmd)
|
|
693
704
|
|
|
705
|
+
@deprecated_function(
|
|
706
|
+
version="8.0.0",
|
|
707
|
+
reason="deprecated since Redis 8.0, call config_set from core module instead",
|
|
708
|
+
)
|
|
694
709
|
def config_set(self, option: str, value: str) -> bool:
|
|
695
710
|
"""Set runtime configuration option.
|
|
696
711
|
|
|
@@ -705,6 +720,10 @@ class SearchCommands:
|
|
|
705
720
|
raw = self.execute_command(*cmd)
|
|
706
721
|
return raw == "OK"
|
|
707
722
|
|
|
723
|
+
@deprecated_function(
|
|
724
|
+
version="8.0.0",
|
|
725
|
+
reason="deprecated since Redis 8.0, call config_get from core module instead",
|
|
726
|
+
)
|
|
708
727
|
def config_get(self, option: str) -> str:
|
|
709
728
|
"""Get runtime configuration option value.
|
|
710
729
|
|
|
@@ -931,7 +950,7 @@ class AsyncSearchCommands(SearchCommands):
|
|
|
931
950
|
For more information see `FT.SEARCH <https://redis.io/commands/ft.search>`_.
|
|
932
951
|
""" # noqa
|
|
933
952
|
args, query = self._mk_query_args(query, query_params=query_params)
|
|
934
|
-
st = time.
|
|
953
|
+
st = time.monotonic()
|
|
935
954
|
|
|
936
955
|
options = {}
|
|
937
956
|
if get_protocol_version(self.client) not in ["3", 3]:
|
|
@@ -943,7 +962,7 @@ class AsyncSearchCommands(SearchCommands):
|
|
|
943
962
|
return res
|
|
944
963
|
|
|
945
964
|
return self._parse_results(
|
|
946
|
-
SEARCH_CMD, res, query=query, duration=(time.
|
|
965
|
+
SEARCH_CMD, res, query=query, duration=(time.monotonic() - st) * 1000.0
|
|
947
966
|
)
|
|
948
967
|
|
|
949
968
|
async def aggregate(
|
|
@@ -1006,6 +1025,10 @@ class AsyncSearchCommands(SearchCommands):
|
|
|
1006
1025
|
|
|
1007
1026
|
return self._parse_results(SPELLCHECK_CMD, res)
|
|
1008
1027
|
|
|
1028
|
+
@deprecated_function(
|
|
1029
|
+
version="8.0.0",
|
|
1030
|
+
reason="deprecated since Redis 8.0, call config_set from core module instead",
|
|
1031
|
+
)
|
|
1009
1032
|
async def config_set(self, option: str, value: str) -> bool:
|
|
1010
1033
|
"""Set runtime configuration option.
|
|
1011
1034
|
|
|
@@ -1020,6 +1043,10 @@ class AsyncSearchCommands(SearchCommands):
|
|
|
1020
1043
|
raw = await self.execute_command(*cmd)
|
|
1021
1044
|
return raw == "OK"
|
|
1022
1045
|
|
|
1046
|
+
@deprecated_function(
|
|
1047
|
+
version="8.0.0",
|
|
1048
|
+
reason="deprecated since Redis 8.0, call config_get from core module instead",
|
|
1049
|
+
)
|
|
1023
1050
|
async def config_get(self, option: str) -> str:
|
|
1024
1051
|
"""Get runtime configuration option value.
|
|
1025
1052
|
|
redis/commands/search/query.py
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
from typing import List, Optional, Union
|
|
2
2
|
|
|
3
|
+
from redis.commands.search.dialect import DEFAULT_DIALECT
|
|
4
|
+
|
|
3
5
|
|
|
4
6
|
class Query:
|
|
5
7
|
"""
|
|
@@ -40,7 +42,7 @@ class Query:
|
|
|
40
42
|
self._highlight_fields: List = []
|
|
41
43
|
self._language: Optional[str] = None
|
|
42
44
|
self._expander: Optional[str] = None
|
|
43
|
-
self._dialect:
|
|
45
|
+
self._dialect: int = DEFAULT_DIALECT
|
|
44
46
|
|
|
45
47
|
def query_string(self) -> str:
|
|
46
48
|
"""Return the query string of this query only."""
|
|
@@ -177,6 +179,8 @@ class Query:
|
|
|
177
179
|
Use a different scoring function to evaluate document relevance.
|
|
178
180
|
Default is `TFIDF`.
|
|
179
181
|
|
|
182
|
+
Since Redis 8.0 default was changed to BM25STD.
|
|
183
|
+
|
|
180
184
|
:param scorer: The scoring function to use
|
|
181
185
|
(e.g. `TFIDF.DOCNORM` or `BM25`)
|
|
182
186
|
"""
|
redis/connection.py
CHANGED
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
import copy
|
|
2
2
|
import os
|
|
3
3
|
import socket
|
|
4
|
-
import ssl
|
|
5
4
|
import sys
|
|
6
5
|
import threading
|
|
6
|
+
import time
|
|
7
7
|
import weakref
|
|
8
8
|
from abc import abstractmethod
|
|
9
9
|
from itertools import chain
|
|
10
10
|
from queue import Empty, Full, LifoQueue
|
|
11
|
-
from
|
|
12
|
-
from typing import Any, Callable, Dict, List, Optional, Type, Union
|
|
11
|
+
from typing import Any, Callable, Dict, List, Optional, Type, TypeVar, Union
|
|
13
12
|
from urllib.parse import parse_qs, unquote, urlparse
|
|
14
13
|
|
|
15
14
|
from redis.cache import (
|
|
@@ -42,12 +41,18 @@ from .utils import (
|
|
|
42
41
|
HIREDIS_AVAILABLE,
|
|
43
42
|
SSL_AVAILABLE,
|
|
44
43
|
compare_versions,
|
|
44
|
+
deprecated_args,
|
|
45
45
|
ensure_string,
|
|
46
46
|
format_error_message,
|
|
47
47
|
get_lib_version,
|
|
48
48
|
str_if_bytes,
|
|
49
49
|
)
|
|
50
50
|
|
|
51
|
+
if SSL_AVAILABLE:
|
|
52
|
+
import ssl
|
|
53
|
+
else:
|
|
54
|
+
ssl = None
|
|
55
|
+
|
|
51
56
|
if HIREDIS_AVAILABLE:
|
|
52
57
|
import hiredis
|
|
53
58
|
|
|
@@ -439,7 +444,11 @@ class AbstractConnection(ConnectionInterface):
|
|
|
439
444
|
self._parser.on_connect(self)
|
|
440
445
|
if len(auth_args) == 1:
|
|
441
446
|
auth_args = ["default", auth_args[0]]
|
|
442
|
-
|
|
447
|
+
# avoid checking health here -- PING will fail if we try
|
|
448
|
+
# to check the health prior to the AUTH
|
|
449
|
+
self.send_command(
|
|
450
|
+
"HELLO", self.protocol, "AUTH", *auth_args, check_health=False
|
|
451
|
+
)
|
|
443
452
|
self.handshake_metadata = self.read_response()
|
|
444
453
|
# if response.get(b"proto") != self.protocol and response.get(
|
|
445
454
|
# "proto"
|
|
@@ -537,7 +546,7 @@ class AbstractConnection(ConnectionInterface):
|
|
|
537
546
|
|
|
538
547
|
def check_health(self):
|
|
539
548
|
"""Check the health of the connection with a PING/PONG"""
|
|
540
|
-
if self.health_check_interval and time() > self.next_health_check:
|
|
549
|
+
if self.health_check_interval and time.monotonic() > self.next_health_check:
|
|
541
550
|
self.retry.call_with_retry(self._send_ping, self._ping_failed)
|
|
542
551
|
|
|
543
552
|
def send_packed_command(self, command, check_health=True):
|
|
@@ -617,9 +626,7 @@ class AbstractConnection(ConnectionInterface):
|
|
|
617
626
|
except OSError as e:
|
|
618
627
|
if disconnect_on_error:
|
|
619
628
|
self.disconnect()
|
|
620
|
-
raise ConnectionError(
|
|
621
|
-
f"Error while reading from {host_error}" f" : {e.args}"
|
|
622
|
-
)
|
|
629
|
+
raise ConnectionError(f"Error while reading from {host_error} : {e.args}")
|
|
623
630
|
except BaseException:
|
|
624
631
|
# Also by default close in case of BaseException. A lot of code
|
|
625
632
|
# relies on this behaviour when doing Command/Response pairs.
|
|
@@ -629,7 +636,7 @@ class AbstractConnection(ConnectionInterface):
|
|
|
629
636
|
raise
|
|
630
637
|
|
|
631
638
|
if self.health_check_interval:
|
|
632
|
-
self.next_health_check = time() + self.health_check_interval
|
|
639
|
+
self.next_health_check = time.monotonic() + self.health_check_interval
|
|
633
640
|
|
|
634
641
|
if isinstance(response, ResponseError):
|
|
635
642
|
try:
|
|
@@ -672,7 +679,7 @@ class AbstractConnection(ConnectionInterface):
|
|
|
672
679
|
output.append(SYM_EMPTY.join(pieces))
|
|
673
680
|
return output
|
|
674
681
|
|
|
675
|
-
def get_protocol(self) -> int
|
|
682
|
+
def get_protocol(self) -> Union[int, str]:
|
|
676
683
|
return self.protocol
|
|
677
684
|
|
|
678
685
|
@property
|
|
@@ -904,9 +911,11 @@ class CacheProxyConnection(ConnectionInterface):
|
|
|
904
911
|
and self._cache.get(self._current_command_cache_key).status
|
|
905
912
|
!= CacheEntryStatus.IN_PROGRESS
|
|
906
913
|
):
|
|
907
|
-
|
|
914
|
+
res = copy.deepcopy(
|
|
908
915
|
self._cache.get(self._current_command_cache_key).cache_value
|
|
909
916
|
)
|
|
917
|
+
self._current_command_cache_key = None
|
|
918
|
+
return res
|
|
910
919
|
|
|
911
920
|
response = self._conn.read_response(
|
|
912
921
|
disable_decoding=disable_decoding,
|
|
@@ -932,6 +941,8 @@ class CacheProxyConnection(ConnectionInterface):
|
|
|
932
941
|
cache_entry.cache_value = response
|
|
933
942
|
self._cache.set(cache_entry)
|
|
934
943
|
|
|
944
|
+
self._current_command_cache_key = None
|
|
945
|
+
|
|
935
946
|
return response
|
|
936
947
|
|
|
937
948
|
def pack_command(self, *args):
|
|
@@ -1031,7 +1042,7 @@ class SSLConnection(Connection):
|
|
|
1031
1042
|
if ssl_cert_reqs is None:
|
|
1032
1043
|
ssl_cert_reqs = ssl.CERT_NONE
|
|
1033
1044
|
elif isinstance(ssl_cert_reqs, str):
|
|
1034
|
-
CERT_REQS = {
|
|
1045
|
+
CERT_REQS = { # noqa: N806
|
|
1035
1046
|
"none": ssl.CERT_NONE,
|
|
1036
1047
|
"optional": ssl.CERT_OPTIONAL,
|
|
1037
1048
|
"required": ssl.CERT_REQUIRED,
|
|
@@ -1259,6 +1270,9 @@ def parse_url(url):
|
|
|
1259
1270
|
return kwargs
|
|
1260
1271
|
|
|
1261
1272
|
|
|
1273
|
+
_CP = TypeVar("_CP", bound="ConnectionPool")
|
|
1274
|
+
|
|
1275
|
+
|
|
1262
1276
|
class ConnectionPool:
|
|
1263
1277
|
"""
|
|
1264
1278
|
Create a connection pool. ``If max_connections`` is set, then this
|
|
@@ -1274,7 +1288,7 @@ class ConnectionPool:
|
|
|
1274
1288
|
"""
|
|
1275
1289
|
|
|
1276
1290
|
@classmethod
|
|
1277
|
-
def from_url(cls, url, **kwargs):
|
|
1291
|
+
def from_url(cls: Type[_CP], url: str, **kwargs) -> _CP:
|
|
1278
1292
|
"""
|
|
1279
1293
|
Return a connection pool configured from the given URL.
|
|
1280
1294
|
|
|
@@ -1374,6 +1388,7 @@ class ConnectionPool:
|
|
|
1374
1388
|
# will notice the first thread already did the work and simply
|
|
1375
1389
|
# release the lock.
|
|
1376
1390
|
self._fork_lock = threading.Lock()
|
|
1391
|
+
self._lock = threading.Lock()
|
|
1377
1392
|
self.reset()
|
|
1378
1393
|
|
|
1379
1394
|
def __repr__(self) -> (str, str):
|
|
@@ -1391,7 +1406,6 @@ class ConnectionPool:
|
|
|
1391
1406
|
return self.connection_kwargs.get("protocol", None)
|
|
1392
1407
|
|
|
1393
1408
|
def reset(self) -> None:
|
|
1394
|
-
self._lock = threading.Lock()
|
|
1395
1409
|
self._created_connections = 0
|
|
1396
1410
|
self._available_connections = []
|
|
1397
1411
|
self._in_use_connections = set()
|
|
@@ -1454,8 +1468,14 @@ class ConnectionPool:
|
|
|
1454
1468
|
finally:
|
|
1455
1469
|
self._fork_lock.release()
|
|
1456
1470
|
|
|
1457
|
-
|
|
1471
|
+
@deprecated_args(
|
|
1472
|
+
args_to_warn=["*"],
|
|
1473
|
+
reason="Use get_connection() without args instead",
|
|
1474
|
+
version="5.0.3",
|
|
1475
|
+
)
|
|
1476
|
+
def get_connection(self, command_name=None, *keys, **options) -> "Connection":
|
|
1458
1477
|
"Get a connection from the pool"
|
|
1478
|
+
|
|
1459
1479
|
self._checkpid()
|
|
1460
1480
|
with self._lock:
|
|
1461
1481
|
try:
|
|
@@ -1474,7 +1494,7 @@ class ConnectionPool:
|
|
|
1474
1494
|
try:
|
|
1475
1495
|
if connection.can_read() and self.cache is None:
|
|
1476
1496
|
raise ConnectionError("Connection has data")
|
|
1477
|
-
except (ConnectionError, OSError):
|
|
1497
|
+
except (ConnectionError, TimeoutError, OSError):
|
|
1478
1498
|
connection.disconnect()
|
|
1479
1499
|
connection.connect()
|
|
1480
1500
|
if connection.can_read():
|
|
@@ -1518,7 +1538,7 @@ class ConnectionPool:
|
|
|
1518
1538
|
except KeyError:
|
|
1519
1539
|
# Gracefully fail when a connection is returned to this pool
|
|
1520
1540
|
# that the pool doesn't actually own
|
|
1521
|
-
|
|
1541
|
+
return
|
|
1522
1542
|
|
|
1523
1543
|
if self.owns_connection(connection):
|
|
1524
1544
|
self._available_connections.append(connection)
|
|
@@ -1526,10 +1546,10 @@ class ConnectionPool:
|
|
|
1526
1546
|
AfterConnectionReleasedEvent(connection)
|
|
1527
1547
|
)
|
|
1528
1548
|
else:
|
|
1529
|
-
#
|
|
1530
|
-
# to the pool
|
|
1531
|
-
#
|
|
1532
|
-
|
|
1549
|
+
# Pool doesn't own this connection, do not add it back
|
|
1550
|
+
# to the pool.
|
|
1551
|
+
# The created connections count should not be changed,
|
|
1552
|
+
# because the connection was not created by the pool.
|
|
1533
1553
|
connection.disconnect()
|
|
1534
1554
|
return
|
|
1535
1555
|
|
|
@@ -1676,7 +1696,12 @@ class BlockingConnectionPool(ConnectionPool):
|
|
|
1676
1696
|
self._connections.append(connection)
|
|
1677
1697
|
return connection
|
|
1678
1698
|
|
|
1679
|
-
|
|
1699
|
+
@deprecated_args(
|
|
1700
|
+
args_to_warn=["*"],
|
|
1701
|
+
reason="Use get_connection() without args instead",
|
|
1702
|
+
version="5.0.3",
|
|
1703
|
+
)
|
|
1704
|
+
def get_connection(self, command_name=None, *keys, **options):
|
|
1680
1705
|
"""
|
|
1681
1706
|
Get a connection, blocking for ``self.timeout`` until a connection
|
|
1682
1707
|
is available from the pool.
|
|
@@ -1716,7 +1741,7 @@ class BlockingConnectionPool(ConnectionPool):
|
|
|
1716
1741
|
try:
|
|
1717
1742
|
if connection.can_read():
|
|
1718
1743
|
raise ConnectionError("Connection has data")
|
|
1719
|
-
except (ConnectionError, OSError):
|
|
1744
|
+
except (ConnectionError, TimeoutError, OSError):
|
|
1720
1745
|
connection.disconnect()
|
|
1721
1746
|
connection.connect()
|
|
1722
1747
|
if connection.can_read():
|
redis/exceptions.py
CHANGED
|
@@ -79,6 +79,7 @@ class ModuleError(ResponseError):
|
|
|
79
79
|
|
|
80
80
|
class LockError(RedisError, ValueError):
|
|
81
81
|
"Errors acquiring or releasing a lock"
|
|
82
|
+
|
|
82
83
|
# NOTE: For backwards compatibility, this class derives from ValueError.
|
|
83
84
|
# This was originally chosen to behave like threading.Lock.
|
|
84
85
|
|
|
@@ -88,12 +89,14 @@ class LockError(RedisError, ValueError):
|
|
|
88
89
|
|
|
89
90
|
|
|
90
91
|
class LockNotOwnedError(LockError):
|
|
91
|
-
"Error trying to extend or release a lock that is (
|
|
92
|
+
"Error trying to extend or release a lock that is not owned (anymore)"
|
|
93
|
+
|
|
92
94
|
pass
|
|
93
95
|
|
|
94
96
|
|
|
95
97
|
class ChildDeadlockedError(Exception):
|
|
96
98
|
"Error indicating that a child process is deadlocked after a fork()"
|
|
99
|
+
|
|
97
100
|
pass
|
|
98
101
|
|
|
99
102
|
|
redis/lock.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import logging
|
|
1
2
|
import threading
|
|
2
3
|
import time as mod_time
|
|
3
4
|
import uuid
|
|
@@ -7,6 +8,8 @@ from typing import Optional, Type
|
|
|
7
8
|
from redis.exceptions import LockError, LockNotOwnedError
|
|
8
9
|
from redis.typing import Number
|
|
9
10
|
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
10
13
|
|
|
11
14
|
class Lock:
|
|
12
15
|
"""
|
|
@@ -82,6 +85,7 @@ class Lock:
|
|
|
82
85
|
blocking: bool = True,
|
|
83
86
|
blocking_timeout: Optional[Number] = None,
|
|
84
87
|
thread_local: bool = True,
|
|
88
|
+
raise_on_release_error: bool = True,
|
|
85
89
|
):
|
|
86
90
|
"""
|
|
87
91
|
Create a new Lock instance named ``name`` using the Redis client
|
|
@@ -125,6 +129,11 @@ class Lock:
|
|
|
125
129
|
thread-1 would see the token value as "xyz" and would be
|
|
126
130
|
able to successfully release the thread-2's lock.
|
|
127
131
|
|
|
132
|
+
``raise_on_release_error`` indicates whether to raise an exception when
|
|
133
|
+
the lock is no longer owned when exiting the context manager. By default,
|
|
134
|
+
this is True, meaning an exception will be raised. If False, the warning
|
|
135
|
+
will be logged and the exception will be suppressed.
|
|
136
|
+
|
|
128
137
|
In some use cases it's necessary to disable thread local storage. For
|
|
129
138
|
example, if you have code where one thread acquires a lock and passes
|
|
130
139
|
that lock instance to a worker thread to release later. If thread
|
|
@@ -140,6 +149,7 @@ class Lock:
|
|
|
140
149
|
self.blocking = blocking
|
|
141
150
|
self.blocking_timeout = blocking_timeout
|
|
142
151
|
self.thread_local = bool(thread_local)
|
|
152
|
+
self.raise_on_release_error = raise_on_release_error
|
|
143
153
|
self.local = threading.local() if self.thread_local else SimpleNamespace()
|
|
144
154
|
self.local.token = None
|
|
145
155
|
self.register_scripts()
|
|
@@ -168,7 +178,14 @@ class Lock:
|
|
|
168
178
|
exc_value: Optional[BaseException],
|
|
169
179
|
traceback: Optional[TracebackType],
|
|
170
180
|
) -> None:
|
|
171
|
-
|
|
181
|
+
try:
|
|
182
|
+
self.release()
|
|
183
|
+
except LockError:
|
|
184
|
+
if self.raise_on_release_error:
|
|
185
|
+
raise
|
|
186
|
+
logger.warning(
|
|
187
|
+
"Lock was unlocked or no longer owned when exiting context manager."
|
|
188
|
+
)
|
|
172
189
|
|
|
173
190
|
def acquire(
|
|
174
191
|
self,
|
|
@@ -251,7 +268,10 @@ class Lock:
|
|
|
251
268
|
"""
|
|
252
269
|
expected_token = self.local.token
|
|
253
270
|
if expected_token is None:
|
|
254
|
-
raise LockError(
|
|
271
|
+
raise LockError(
|
|
272
|
+
"Cannot release a lock that's not owned or is already unlocked.",
|
|
273
|
+
lock_name=self.name,
|
|
274
|
+
)
|
|
255
275
|
self.local.token = None
|
|
256
276
|
self.do_release(expected_token)
|
|
257
277
|
|
|
@@ -264,7 +284,7 @@ class Lock:
|
|
|
264
284
|
lock_name=self.name,
|
|
265
285
|
)
|
|
266
286
|
|
|
267
|
-
def extend(self, additional_time:
|
|
287
|
+
def extend(self, additional_time: Number, replace_ttl: bool = False) -> bool:
|
|
268
288
|
"""
|
|
269
289
|
Adds more time to an already acquired lock.
|
|
270
290
|
|
|
@@ -281,7 +301,7 @@ class Lock:
|
|
|
281
301
|
raise LockError("Cannot extend a lock with no timeout", lock_name=self.name)
|
|
282
302
|
return self.do_extend(additional_time, replace_ttl)
|
|
283
303
|
|
|
284
|
-
def do_extend(self, additional_time:
|
|
304
|
+
def do_extend(self, additional_time: Number, replace_ttl: bool) -> bool:
|
|
285
305
|
additional_time = int(additional_time * 1000)
|
|
286
306
|
if not bool(
|
|
287
307
|
self.lua_extend(
|
redis/ocsp.py
CHANGED
|
@@ -15,6 +15,7 @@ from cryptography.hazmat.primitives.asymmetric.rsa import RSAPublicKey
|
|
|
15
15
|
from cryptography.hazmat.primitives.hashes import SHA1, Hash
|
|
16
16
|
from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat
|
|
17
17
|
from cryptography.x509 import ocsp
|
|
18
|
+
|
|
18
19
|
from redis.exceptions import AuthorizationError, ConnectionError
|
|
19
20
|
|
|
20
21
|
|
|
@@ -56,7 +57,7 @@ def _check_certificate(issuer_cert, ocsp_bytes, validate=True):
|
|
|
56
57
|
if ocsp_response.response_status == ocsp.OCSPResponseStatus.SUCCESSFUL:
|
|
57
58
|
if ocsp_response.certificate_status != ocsp.OCSPCertStatus.GOOD:
|
|
58
59
|
raise ConnectionError(
|
|
59
|
-
f
|
|
60
|
+
f"Received an {str(ocsp_response.certificate_status).split('.')[1]} "
|
|
60
61
|
"ocsp certificate status"
|
|
61
62
|
)
|
|
62
63
|
else:
|
redis/sentinel.py
CHANGED
|
@@ -273,7 +273,7 @@ class Sentinel(SentinelCommands):
|
|
|
273
273
|
)
|
|
274
274
|
return (
|
|
275
275
|
f"<{type(self).__module__}.{type(self).__name__}"
|
|
276
|
-
f
|
|
276
|
+
f"(sentinels=[{','.join(sentinel_addresses)}])>"
|
|
277
277
|
)
|
|
278
278
|
|
|
279
279
|
def check_master_state(self, state, service_name):
|
redis/typing.py
CHANGED