Skip to content

Commit 714adf9

Browse files
authored
[4.6.1] Supporting setting CLIENT INFO via CLIENT SETINFO (#2875)
1 parent 9f50357 commit 714adf9

File tree

13 files changed

+121
-91
lines changed

13 files changed

+121
-91
lines changed

redis/asyncio/connection.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@
5757
TimeoutError,
5858
)
5959
from redis.typing import EncodableT, EncodedT
60-
from redis.utils import HIREDIS_AVAILABLE, str_if_bytes
60+
from redis.utils import HIREDIS_AVAILABLE, get_lib_version, str_if_bytes
6161

6262
hiredis = None
6363
if HIREDIS_AVAILABLE:
@@ -453,6 +453,8 @@ class AbstractConnection:
453453
"db",
454454
"username",
455455
"client_name",
456+
"lib_name",
457+
"lib_version",
456458
"credential_provider",
457459
"password",
458460
"socket_timeout",
@@ -491,6 +493,8 @@ def __init__(
491493
socket_read_size: int = 65536,
492494
health_check_interval: float = 0,
493495
client_name: Optional[str] = None,
496+
lib_name: Optional[str] = "redis-py",
497+
lib_version: Optional[str] = get_lib_version(),
494498
username: Optional[str] = None,
495499
retry: Optional[Retry] = None,
496500
redis_connect_func: Optional[ConnectCallbackT] = None,
@@ -507,6 +511,8 @@ def __init__(
507511
self.pid = os.getpid()
508512
self.db = db
509513
self.client_name = client_name
514+
self.lib_name = lib_name
515+
self.lib_version = lib_version
510516
self.credential_provider = credential_provider
511517
self.password = password
512518
self.username = username
@@ -654,6 +660,18 @@ async def on_connect(self) -> None:
654660
if str_if_bytes(await self.read_response()) != "OK":
655661
raise ConnectionError("Error setting client name")
656662

663+
try:
664+
# set the library name and version
665+
if self.lib_name:
666+
await self.send_command("CLIENT", "SETINFO", "LIB-NAME", self.lib_name)
667+
await self.read_response()
668+
if self.lib_version:
669+
await self.send_command(
670+
"CLIENT", "SETINFO", "LIB-VER", self.lib_version
671+
)
672+
await self.read_response()
673+
except ResponseError:
674+
pass
657675
# if a database is specified, switch to it
658676
if self.db:
659677
await self.send_command("SELECT", self.db)

redis/client.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
)
2828
from redis.lock import Lock
2929
from redis.retry import Retry
30-
from redis.utils import safe_str, str_if_bytes
30+
from redis.utils import get_lib_version, safe_str, str_if_bytes
3131

3232
SYM_EMPTY = b""
3333
EMPTY_RESPONSE = "EMPTY_RESPONSE"
@@ -643,7 +643,11 @@ def parse_client_info(value):
643643
"key1=value1 key2=value2 key3=value3"
644644
"""
645645
client_info = {}
646+
value = str_if_bytes(value)
647+
if value[-1] == "\n":
648+
value = value[:-1]
646649
infos = str_if_bytes(value).split(" ")
650+
infos = value.split(" ")
647651
for info in infos:
648652
key, value = info.split("=")
649653
client_info[key] = value
@@ -754,6 +758,7 @@ class AbstractRedis:
754758
"CLIENT SETNAME": bool_ok,
755759
"CLIENT UNBLOCK": lambda r: r and int(r) == 1 or False,
756760
"CLIENT PAUSE": bool_ok,
761+
"CLIENT SETINFO": bool_ok,
757762
"CLIENT GETREDIR": int,
758763
"CLIENT TRACKINGINFO": lambda r: list(map(str_if_bytes, r)),
759764
"CLUSTER ADDSLOTS": bool_ok,
@@ -949,6 +954,8 @@ def __init__(
949954
single_connection_client=False,
950955
health_check_interval=0,
951956
client_name=None,
957+
lib_name="redis-py",
958+
lib_version=get_lib_version(),
952959
username=None,
953960
retry=None,
954961
redis_connect_func=None,
@@ -999,6 +1006,8 @@ def __init__(
9991006
"max_connections": max_connections,
10001007
"health_check_interval": health_check_interval,
10011008
"client_name": client_name,
1009+
"lib_name": lib_name,
1010+
"lib_version": lib_version,
10021011
"redis_connect_func": redis_connect_func,
10031012
"credential_provider": credential_provider,
10041013
}

redis/cluster.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,8 @@ def parse_cluster_myshardid(resp, **options):
137137
"encoding_errors",
138138
"errors",
139139
"host",
140+
"lib_name",
141+
"lib_version",
140142
"max_connections",
141143
"nodes_flag",
142144
"redis_connect_func",
@@ -220,6 +222,7 @@ class AbstractRedisCluster:
220222
"CLIENT LIST",
221223
"CLIENT SETNAME",
222224
"CLIENT GETNAME",
225+
"CLIENT SETINFO",
223226
"CONFIG SET",
224227
"CONFIG REWRITE",
225228
"CONFIG RESETSTAT",

redis/commands/core.py

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

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

redis/connection.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
CRYPTOGRAPHY_AVAILABLE,
4040
HIREDIS_AVAILABLE,
4141
HIREDIS_PACK_AVAILABLE,
42+
get_lib_version,
4243
str_if_bytes,
4344
)
4445

@@ -605,6 +606,8 @@ def __init__(
605606
socket_read_size=65536,
606607
health_check_interval=0,
607608
client_name=None,
609+
lib_name="redis-py",
610+
lib_version=get_lib_version(),
608611
username=None,
609612
retry=None,
610613
redis_connect_func=None,
@@ -628,6 +631,8 @@ def __init__(
628631
self.pid = os.getpid()
629632
self.db = db
630633
self.client_name = client_name
634+
self.lib_name = lib_name
635+
self.lib_version = lib_version
631636
self.credential_provider = credential_provider
632637
self.password = password
633638
self.username = username

redis/utils.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import sys
12
from contextlib import contextmanager
23
from functools import wraps
34
from typing import Any, Dict, Mapping, Union
@@ -12,6 +13,10 @@
1213
HIREDIS_AVAILABLE = False
1314
HIREDIS_PACK_AVAILABLE = False
1415

16+
if sys.version_info >= (3, 8):
17+
from importlib import metadata
18+
else:
19+
import importlib_metadata as metadata
1520
try:
1621
import cryptography # noqa
1722

@@ -110,3 +115,11 @@ def wrapper(*args, **kwargs):
110115
return wrapper
111116

112117
return decorator
118+
119+
120+
def get_lib_version():
121+
try:
122+
libver = metadata.version("redis")
123+
except metadata.PackageNotFoundError:
124+
libver = "99.99.99"
125+
return libver

tests/conftest.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,7 @@ def pytest_sessionstart(session):
141141
enterprise = info["enterprise"]
142142
except redis.ConnectionError:
143143
# provide optimistic defaults
144+
info = {}
144145
version = "10.0.0"
145146
arch_bits = 64
146147
cluster_enabled = False
@@ -157,9 +158,7 @@ def pytest_sessionstart(session):
157158
redismod_url = session.config.getoption("--redismod-url")
158159
info = _get_info(redismod_url)
159160
REDIS_INFO["modules"] = info["modules"]
160-
except redis.exceptions.ConnectionError:
161-
pass
162-
except KeyError:
161+
except (KeyError, redis.exceptions.ConnectionError):
163162
pass
164163

165164
if cluster_enabled:

tests/test_asyncio/conftest.py

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

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

tests/test_asyncio/test_cluster.py

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

21882188
assert isinstance(await r.acl_log(target_nodes=node), list)
2189-
assert len(await r.acl_log(target_nodes=node)) == 2
2189+
assert len(await r.acl_log(target_nodes=node)) == 3
21902190
assert len(await r.acl_log(count=1, target_nodes=node)) == 1
21912191
assert isinstance((await r.acl_log(target_nodes=node))[0], dict)
21922192
assert "client-info" in (await r.acl_log(count=1, target_nodes=node))[0]

tests/test_asyncio/test_commands.py

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ async def test_acl_deluser(self, r_teardown):
112112
username = "redis-py-user"
113113
r = r_teardown(username)
114114

115-
assert await r.acl_deluser(username) == 0
115+
assert await r.acl_deluser(username) in [0, 1]
116116
assert await r.acl_setuser(username, enabled=False, reset=True)
117117
assert await r.acl_deluser(username) == 1
118118

@@ -268,7 +268,7 @@ async def test_acl_log(self, r_teardown, create_redis):
268268
await user_client.hset("cache:0", "hkey", "hval")
269269

270270
assert isinstance(await r.acl_log(), list)
271-
assert len(await r.acl_log()) == 2
271+
assert len(await r.acl_log()) == 3
272272
assert len(await r.acl_log(count=1)) == 1
273273
assert isinstance((await r.acl_log())[0], dict)
274274
assert "client-info" in (await r.acl_log(count=1))[0]
@@ -347,6 +347,27 @@ async def test_client_setname(self, r: redis.Redis):
347347
assert await r.client_setname("redis_py_test")
348348
assert await r.client_getname() == "redis_py_test"
349349

350+
@skip_if_server_version_lt("7.2.0")
351+
async def test_client_setinfo(self, r: redis.Redis):
352+
await r.ping()
353+
info = await r.client_info()
354+
assert info["lib-name"] == "redis-py"
355+
assert info["lib-ver"] == redis.__version__
356+
assert await r.client_setinfo("lib-name", "test")
357+
assert await r.client_setinfo("lib-ver", "123")
358+
359+
info = await r.client_info()
360+
assert info["lib-name"] == "test"
361+
assert info["lib-ver"] == "123"
362+
r2 = redis.asyncio.Redis(lib_name="test2", lib_version="1234")
363+
info = await r2.client_info()
364+
assert info["lib-name"] == "test2"
365+
assert info["lib-ver"] == "1234"
366+
r3 = redis.asyncio.Redis(lib_name=None, lib_version=None)
367+
info = await r3.client_info()
368+
assert info["lib-name"] == ""
369+
assert info["lib-ver"] == ""
370+
350371
@skip_if_server_version_lt("2.6.9")
351372
@pytest.mark.onlynoncluster
352373
async def test_client_kill(self, r: redis.Redis, r2):

tests/test_asyncio/test_connection.py

Lines changed: 4 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,12 @@
1010
from redis.asyncio.connection import (
1111
BaseParser,
1212
Connection,
13-
HiredisParser,
1413
PythonParser,
1514
UnixDomainSocketConnection,
1615
)
1716
from redis.asyncio.retry import Retry
1817
from redis.backoff import NoBackoff
1918
from redis.exceptions import ConnectionError, InvalidResponse, TimeoutError
20-
from redis.utils import HIREDIS_AVAILABLE
2119
from tests.conftest import skip_if_server_version_lt
2220

2321
from .compat import mock
@@ -126,9 +124,11 @@ async def test_can_run_concurrent_commands(r):
126124
assert all(await asyncio.gather(*(r.ping() for _ in range(10))))
127125

128126

129-
async def test_connect_retry_on_timeout_error():
127+
async def test_connect_retry_on_timeout_error(connect_args):
130128
"""Test that the _connect function is retried in case of a timeout"""
131-
conn = Connection(retry_on_timeout=True, retry=Retry(NoBackoff(), 3))
129+
conn = Connection(
130+
retry_on_timeout=True, retry=Retry(NoBackoff(), 3), **connect_args
131+
)
132132
origin_connect = conn._connect
133133
conn._connect = mock.AsyncMock()
134134

@@ -195,84 +195,6 @@ async def test_connection_parse_response_resume(r: redis.Redis):
195195
assert i > 0
196196

197197

198-
@pytest.mark.onlynoncluster
199-
@pytest.mark.parametrize(
200-
"parser_class", [PythonParser, HiredisParser], ids=["PythonParser", "HiredisParser"]
201-
)
202-
async def test_connection_disconect_race(parser_class):
203-
"""
204-
This test reproduces the case in issue #2349
205-
where a connection is closed while the parser is reading to feed the
206-
internal buffer.The stream `read()` will succeed, but when it returns,
207-
another task has already called `disconnect()` and is waiting for
208-
close to finish. When we attempts to feed the buffer, we will fail
209-
since the buffer is no longer there.
210-
211-
This test verifies that a read in progress can finish even
212-
if the `disconnect()` method is called.
213-
"""
214-
if parser_class == HiredisParser and not HIREDIS_AVAILABLE:
215-
pytest.skip("Hiredis not available")
216-
217-
args = {}
218-
args["parser_class"] = parser_class
219-
220-
conn = Connection(**args)
221-
222-
cond = asyncio.Condition()
223-
# 0 == initial
224-
# 1 == reader is reading
225-
# 2 == closer has closed and is waiting for close to finish
226-
state = 0
227-
228-
# Mock read function, which wait for a close to happen before returning
229-
# Can either be invoked as two `read()` calls (HiredisParser)
230-
# or as a `readline()` followed by `readexact()` (PythonParser)
231-
chunks = [b"$13\r\n", b"Hello, World!\r\n"]
232-
233-
async def read(_=None):
234-
nonlocal state
235-
async with cond:
236-
if state == 0:
237-
state = 1 # we are reading
238-
cond.notify()
239-
# wait until the closing task has done
240-
await cond.wait_for(lambda: state == 2)
241-
return chunks.pop(0)
242-
243-
# function closes the connection while reader is still blocked reading
244-
async def do_close():
245-
nonlocal state
246-
async with cond:
247-
await cond.wait_for(lambda: state == 1)
248-
state = 2
249-
cond.notify()
250-
await conn.disconnect()
251-
252-
async def do_read():
253-
return await conn.read_response()
254-
255-
reader = mock.AsyncMock()
256-
writer = mock.AsyncMock()
257-
writer.transport = mock.Mock()
258-
writer.transport.get_extra_info.side_effect = None
259-
260-
# for HiredisParser
261-
reader.read.side_effect = read
262-
# for PythonParser
263-
reader.readline.side_effect = read
264-
reader.readexactly.side_effect = read
265-
266-
async def open_connection(*args, **kwargs):
267-
return reader, writer
268-
269-
with patch.object(asyncio, "open_connection", open_connection):
270-
await conn.connect()
271-
272-
vals = await asyncio.gather(do_read(), do_close())
273-
assert vals == [b"Hello, World!", None]
274-
275-
276198
@pytest.mark.onlynoncluster
277199
def test_create_single_connection_client_from_url():
278200
client = Redis.from_url("redis://localhost:6379/0?", single_connection_client=True)

0 commit comments

Comments
 (0)