Skip to content

Commit f121cf2

Browse files
dvora-hkristjanvalurchayim
authored
Add support for CLIENT SETINFO (#2857)
Co-authored-by: Kristján Valur Jónsson <sweskman@gmail.com> Co-authored-by: Chayim <chayim@users.noreply.github.com> Co-authored-by: Chayim I. Kirshen <c@kirshen.com>
1 parent d5c2d1d commit f121cf2

18 files changed

+181
-25
lines changed

.flake8

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,12 @@ exclude =
1515
whitelist.py,
1616
tasks.py
1717
ignore =
18+
E126
19+
E203
1820
F405
21+
N801
22+
N802
23+
N803
24+
N806
25+
N815
1926
W503
20-
E203
21-
E126

redis/_parsers/helpers.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -629,8 +629,7 @@ def parse_client_info(value):
629629
"key1=value1 key2=value2 key3=value3"
630630
"""
631631
client_info = {}
632-
infos = str_if_bytes(value).split(" ")
633-
for info in infos:
632+
for info in str_if_bytes(value).strip().split():
634633
key, value = info.split("=")
635634
client_info[key] = value
636635

@@ -700,6 +699,7 @@ def string_keys_to_dict(key_string, callback):
700699
"CLIENT KILL": parse_client_kill,
701700
"CLIENT LIST": parse_client_list,
702701
"CLIENT PAUSE": bool_ok,
702+
"CLIENT SETINFO": bool_ok,
703703
"CLIENT SETNAME": bool_ok,
704704
"CLIENT UNBLOCK": bool,
705705
"CLUSTER ADDSLOTS": bool_ok,

redis/asyncio/client.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,13 @@
6262
WatchError,
6363
)
6464
from redis.typing import ChannelT, EncodableT, KeyT
65-
from redis.utils import HIREDIS_AVAILABLE, _set_info_logger, safe_str, str_if_bytes
65+
from redis.utils import (
66+
HIREDIS_AVAILABLE,
67+
_set_info_logger,
68+
get_lib_version,
69+
safe_str,
70+
str_if_bytes,
71+
)
6672

6773
PubSubHandler = Callable[[Dict[str, str]], Awaitable[None]]
6874
_KeyT = TypeVar("_KeyT", bound=KeyT)
@@ -190,6 +196,8 @@ def __init__(
190196
single_connection_client: bool = False,
191197
health_check_interval: int = 0,
192198
client_name: Optional[str] = None,
199+
lib_name: Optional[str] = "redis-py",
200+
lib_version: Optional[str] = get_lib_version(),
193201
username: Optional[str] = None,
194202
retry: Optional[Retry] = None,
195203
auto_close_connection_pool: bool = True,
@@ -232,6 +240,8 @@ def __init__(
232240
"max_connections": max_connections,
233241
"health_check_interval": health_check_interval,
234242
"client_name": client_name,
243+
"lib_name": lib_name,
244+
"lib_version": lib_version,
235245
"redis_connect_func": redis_connect_func,
236246
"protocol": protocol,
237247
}

redis/asyncio/cluster.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@
6262
TryAgainError,
6363
)
6464
from redis.typing import AnyKeyT, EncodableT, KeyT
65-
from redis.utils import dict_merge, safe_str, str_if_bytes
65+
from redis.utils import dict_merge, get_lib_version, safe_str, str_if_bytes
6666

6767
TargetNodesT = TypeVar(
6868
"TargetNodesT", str, "ClusterNode", List["ClusterNode"], Dict[Any, "ClusterNode"]
@@ -237,6 +237,8 @@ def __init__(
237237
username: Optional[str] = None,
238238
password: Optional[str] = None,
239239
client_name: Optional[str] = None,
240+
lib_name: Optional[str] = "redis-py",
241+
lib_version: Optional[str] = get_lib_version(),
240242
# Encoding related kwargs
241243
encoding: str = "utf-8",
242244
encoding_errors: str = "strict",
@@ -288,6 +290,8 @@ def __init__(
288290
"username": username,
289291
"password": password,
290292
"client_name": client_name,
293+
"lib_name": lib_name,
294+
"lib_version": lib_version,
291295
# Encoding related kwargs
292296
"encoding": encoding,
293297
"encoding_errors": encoding_errors,

redis/asyncio/connection.py

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@
4949
TimeoutError,
5050
)
5151
from redis.typing import EncodableT
52-
from redis.utils import HIREDIS_AVAILABLE, str_if_bytes
52+
from redis.utils import HIREDIS_AVAILABLE, get_lib_version, str_if_bytes
5353

5454
from .._parsers import (
5555
BaseParser,
@@ -101,6 +101,8 @@ class AbstractConnection:
101101
"db",
102102
"username",
103103
"client_name",
104+
"lib_name",
105+
"lib_version",
104106
"credential_provider",
105107
"password",
106108
"socket_timeout",
@@ -140,6 +142,8 @@ def __init__(
140142
socket_read_size: int = 65536,
141143
health_check_interval: float = 0,
142144
client_name: Optional[str] = None,
145+
lib_name: Optional[str] = "redis-py",
146+
lib_version: Optional[str] = get_lib_version(),
143147
username: Optional[str] = None,
144148
retry: Optional[Retry] = None,
145149
redis_connect_func: Optional[ConnectCallbackT] = None,
@@ -157,6 +161,8 @@ def __init__(
157161
self.pid = os.getpid()
158162
self.db = db
159163
self.client_name = client_name
164+
self.lib_name = lib_name
165+
self.lib_version = lib_version
160166
self.credential_provider = credential_provider
161167
self.password = password
162168
self.username = username
@@ -347,9 +353,23 @@ async def on_connect(self) -> None:
347353
if str_if_bytes(await self.read_response()) != "OK":
348354
raise ConnectionError("Error setting client name")
349355

350-
# if a database is specified, switch to it
356+
# set the library name and version, pipeline for lower startup latency
357+
if self.lib_name:
358+
await self.send_command("CLIENT", "SETINFO", "LIB-NAME", self.lib_name)
359+
if self.lib_version:
360+
await self.send_command("CLIENT", "SETINFO", "LIB-VER", self.lib_version)
361+
# if a database is specified, switch to it. Also pipeline this
351362
if self.db:
352363
await self.send_command("SELECT", self.db)
364+
365+
# read responses from pipeline
366+
for _ in (sent for sent in (self.lib_name, self.lib_version) if sent):
367+
try:
368+
await self.read_response()
369+
except ResponseError:
370+
pass
371+
372+
if self.db:
353373
if str_if_bytes(await self.read_response()) != "OK":
354374
raise ConnectionError("Invalid Database")
355375

redis/client.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,13 @@
3131
)
3232
from redis.lock import Lock
3333
from redis.retry import Retry
34-
from redis.utils import HIREDIS_AVAILABLE, _set_info_logger, safe_str, str_if_bytes
34+
from redis.utils import (
35+
HIREDIS_AVAILABLE,
36+
_set_info_logger,
37+
get_lib_version,
38+
safe_str,
39+
str_if_bytes,
40+
)
3541

3642
SYM_EMPTY = b""
3743
EMPTY_RESPONSE = "EMPTY_RESPONSE"
@@ -171,6 +177,8 @@ def __init__(
171177
single_connection_client=False,
172178
health_check_interval=0,
173179
client_name=None,
180+
lib_name="redis-py",
181+
lib_version=get_lib_version(),
174182
username=None,
175183
retry=None,
176184
redis_connect_func=None,
@@ -222,6 +230,8 @@ def __init__(
222230
"max_connections": max_connections,
223231
"health_check_interval": health_check_interval,
224232
"client_name": client_name,
233+
"lib_name": lib_name,
234+
"lib_version": lib_version,
225235
"redis_connect_func": redis_connect_func,
226236
"credential_provider": credential_provider,
227237
"protocol": protocol,

redis/cluster.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,8 @@ def parse_cluster_myshardid(resp, **options):
143143
"encoding_errors",
144144
"errors",
145145
"host",
146+
"lib_name",
147+
"lib_version",
146148
"max_connections",
147149
"nodes_flag",
148150
"redis_connect_func",
@@ -225,6 +227,7 @@ class AbstractRedisCluster:
225227
"ACL WHOAMI",
226228
"AUTH",
227229
"CLIENT LIST",
230+
"CLIENT SETINFO",
228231
"CLIENT SETNAME",
229232
"CLIENT GETNAME",
230233
"CONFIG SET",

redis/commands/core.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -708,6 +708,13 @@ def client_setname(self, name: str, **kwargs) -> ResponseT:
708708
"""
709709
return self.execute_command("CLIENT SETNAME", name, **kwargs)
710710

711+
def client_setinfo(self, attr: str, value: str, **kwargs) -> ResponseT:
712+
"""
713+
Sets the current connection library name or version
714+
For mor information see https://redis.io/commands/client-setinfo
715+
"""
716+
return self.execute_command("CLIENT SETINFO", attr, value, **kwargs)
717+
711718
def client_unblock(
712719
self, client_id: int, error: bool = False, **kwargs
713720
) -> ResponseT:

redis/connection.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
HIREDIS_AVAILABLE,
3232
HIREDIS_PACK_AVAILABLE,
3333
SSL_AVAILABLE,
34+
get_lib_version,
3435
str_if_bytes,
3536
)
3637

@@ -140,6 +141,8 @@ def __init__(
140141
socket_read_size=65536,
141142
health_check_interval=0,
142143
client_name=None,
144+
lib_name="redis-py",
145+
lib_version=get_lib_version(),
143146
username=None,
144147
retry=None,
145148
redis_connect_func=None,
@@ -164,6 +167,8 @@ def __init__(
164167
self.pid = os.getpid()
165168
self.db = db
166169
self.client_name = client_name
170+
self.lib_name = lib_name
171+
self.lib_version = lib_version
167172
self.credential_provider = credential_provider
168173
self.password = password
169174
self.username = username
@@ -360,6 +365,21 @@ def on_connect(self):
360365
if str_if_bytes(self.read_response()) != "OK":
361366
raise ConnectionError("Error setting client name")
362367

368+
try:
369+
# set the library name and version
370+
if self.lib_name:
371+
self.send_command("CLIENT", "SETINFO", "LIB-NAME", self.lib_name)
372+
self.read_response()
373+
except ResponseError:
374+
pass
375+
376+
try:
377+
if self.lib_version:
378+
self.send_command("CLIENT", "SETINFO", "LIB-VER", self.lib_version)
379+
self.read_response()
380+
except ResponseError:
381+
pass
382+
363383
# if a database is specified, switch to it
364384
if self.db:
365385
self.send_command("SELECT", self.db)

redis/utils.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import logging
2+
import sys
23
from contextlib import contextmanager
34
from functools import wraps
45
from typing import Any, Dict, Mapping, Union
@@ -27,6 +28,11 @@
2728
except ImportError:
2829
CRYPTOGRAPHY_AVAILABLE = False
2930

31+
if sys.version_info >= (3, 8):
32+
from importlib import metadata
33+
else:
34+
import importlib_metadata as metadata
35+
3036

3137
def from_url(url, **kwargs):
3238
"""
@@ -131,3 +137,11 @@ def _set_info_logger():
131137
handler = logging.StreamHandler()
132138
handler.setLevel(logging.INFO)
133139
logger.addHandler(handler)
140+
141+
142+
def get_lib_version():
143+
try:
144+
libver = metadata.version("redis")
145+
except metadata.PackageNotFoundError:
146+
libver = "99.99.99"
147+
return libver

tests/conftest.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ def pytest_sessionstart(session):
131131
enterprise = info["enterprise"]
132132
except redis.ConnectionError:
133133
# provide optimistic defaults
134+
info = {}
134135
version = "10.0.0"
135136
arch_bits = 64
136137
cluster_enabled = False
@@ -145,9 +146,7 @@ def pytest_sessionstart(session):
145146
# module info
146147
try:
147148
REDIS_INFO["modules"] = info["modules"]
148-
except redis.exceptions.ConnectionError:
149-
pass
150-
except KeyError:
149+
except (KeyError, redis.exceptions.ConnectionError):
151150
pass
152151

153152
if cluster_enabled:

tests/test_asyncio/conftest.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,3 +253,15 @@ async def __aexit__(self, exc_type, exc_inst, tb):
253253

254254
def asynccontextmanager(func):
255255
return _asynccontextmanager(func)
256+
257+
258+
# helpers to get the connection arguments for this run
259+
@pytest.fixture()
260+
def redis_url(request):
261+
return request.config.getoption("--redis-url")
262+
263+
264+
@pytest.fixture()
265+
def connect_args(request):
266+
url = request.config.getoption("--redis-url")
267+
return parse_url(url)

tests/test_asyncio/test_cluster.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2294,7 +2294,7 @@ async def test_acl_log(
22942294
await user_client.hset("{cache}:0", "hkey", "hval")
22952295

22962296
assert isinstance(await r.acl_log(target_nodes=node), list)
2297-
assert len(await r.acl_log(target_nodes=node)) == 2
2297+
assert len(await r.acl_log(target_nodes=node)) == 3
22982298
assert len(await r.acl_log(count=1, target_nodes=node)) == 1
22992299
assert isinstance((await r.acl_log(target_nodes=node))[0], dict)
23002300
assert "client-info" in (await r.acl_log(count=1, target_nodes=node))[0]

tests/test_asyncio/test_commands.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -273,7 +273,7 @@ async def test_acl_log(self, r_teardown, create_redis):
273273
await user_client.hset("cache:0", "hkey", "hval")
274274

275275
assert isinstance(await r.acl_log(), list)
276-
assert len(await r.acl_log()) == 2
276+
assert len(await r.acl_log()) == 3
277277
assert len(await r.acl_log(count=1)) == 1
278278
assert isinstance((await r.acl_log())[0], dict)
279279
expected = (await r.acl_log(count=1))[0]
@@ -355,6 +355,26 @@ async def test_client_setname(self, r: redis.Redis):
355355
r, await r.client_getname(), "redis_py_test", b"redis_py_test"
356356
)
357357

358+
@skip_if_server_version_lt("7.2.0")
359+
async def test_client_setinfo(self, r: redis.Redis):
360+
await r.ping()
361+
info = await r.client_info()
362+
assert info["lib-name"] == "redis-py"
363+
assert info["lib-ver"] == redis.__version__
364+
assert await r.client_setinfo("lib-name", "test")
365+
assert await r.client_setinfo("lib-ver", "123")
366+
info = await r.client_info()
367+
assert info["lib-name"] == "test"
368+
assert info["lib-ver"] == "123"
369+
r2 = redis.asyncio.Redis(lib_name="test2", lib_version="1234")
370+
info = await r2.client_info()
371+
assert info["lib-name"] == "test2"
372+
assert info["lib-ver"] == "1234"
373+
r3 = redis.asyncio.Redis(lib_name=None, lib_version=None)
374+
info = await r3.client_info()
375+
assert info["lib-name"] == ""
376+
assert info["lib-ver"] == ""
377+
358378
@skip_if_server_version_lt("2.6.9")
359379
@pytest.mark.onlynoncluster
360380
async def test_client_kill(self, r: redis.Redis, r2):

0 commit comments

Comments
 (0)