Skip to content

Commit 06fac1d

Browse files
Fixing sentinel command execution to allow returning of actual responses when meaningful - behaviour controlled by 'return_responses' argument. (#3191)
* Fixing sentinel command response * Linter check pass * Applying review comments and fixing some issues. * Fix several spelling mistakes * Adding new sentinel integration tests. Fixing the boolean return value type for sentinel's execute_command * Replacing usage of reduce in the boolean return types with python's built-in 'all' function for better performance and improved readability. * Changing the default behavior to be as before, returning of the responces is now optional. Fixed the pydocs to reflect the actual method behavior. --------- Co-authored-by: petyaslavova <[email protected]>
1 parent 5977e38 commit 06fac1d

File tree

5 files changed

+250
-42
lines changed

5 files changed

+250
-42
lines changed

redis/asyncio/sentinel.py

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -223,19 +223,31 @@ async def execute_command(self, *args, **kwargs):
223223
once - If set to True, then execute the resulting command on a single
224224
node at random, rather than across the entire sentinel cluster.
225225
"""
226-
once = bool(kwargs.get("once", False))
227-
if "once" in kwargs.keys():
228-
kwargs.pop("once")
226+
once = bool(kwargs.pop("once", False))
227+
228+
# Check if command is supposed to return the original
229+
# responses instead of boolean value.
230+
return_responses = bool(kwargs.pop("return_responses", False))
229231

230232
if once:
231-
await random.choice(self.sentinels).execute_command(*args, **kwargs)
232-
else:
233-
tasks = [
234-
asyncio.Task(sentinel.execute_command(*args, **kwargs))
235-
for sentinel in self.sentinels
236-
]
237-
await asyncio.gather(*tasks)
238-
return True
233+
response = await random.choice(self.sentinels).execute_command(
234+
*args, **kwargs
235+
)
236+
if return_responses:
237+
return [response]
238+
else:
239+
return True if response else False
240+
241+
tasks = [
242+
asyncio.Task(sentinel.execute_command(*args, **kwargs))
243+
for sentinel in self.sentinels
244+
]
245+
responses = await asyncio.gather(*tasks)
246+
247+
if return_responses:
248+
return responses
249+
250+
return all(responses)
239251

240252
def __repr__(self):
241253
sentinel_addresses = []

redis/commands/sentinel.py

Lines changed: 42 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,35 @@ def sentinel(self, *args):
1111
"""Redis Sentinel's SENTINEL command."""
1212
warnings.warn(DeprecationWarning("Use the individual sentinel_* methods"))
1313

14-
def sentinel_get_master_addr_by_name(self, service_name):
15-
"""Returns a (host, port) pair for the given ``service_name``"""
16-
return self.execute_command("SENTINEL GET-MASTER-ADDR-BY-NAME", service_name)
17-
18-
def sentinel_master(self, service_name):
19-
"""Returns a dictionary containing the specified masters state."""
20-
return self.execute_command("SENTINEL MASTER", service_name)
14+
def sentinel_get_master_addr_by_name(self, service_name, return_responses=False):
15+
"""
16+
Returns a (host, port) pair for the given ``service_name`` when return_responses is True,
17+
otherwise returns a boolean value that indicates if the command was successful.
18+
"""
19+
return self.execute_command(
20+
"SENTINEL GET-MASTER-ADDR-BY-NAME",
21+
service_name,
22+
once=True,
23+
return_responses=return_responses,
24+
)
25+
26+
def sentinel_master(self, service_name, return_responses=False):
27+
"""
28+
Returns a dictionary containing the specified masters state, when return_responses is True,
29+
otherwise returns a boolean value that indicates if the command was successful.
30+
"""
31+
return self.execute_command(
32+
"SENTINEL MASTER", service_name, return_responses=return_responses
33+
)
2134

2235
def sentinel_masters(self):
23-
"""Returns a list of dictionaries containing each master's state."""
36+
"""
37+
Returns a list of dictionaries containing each master's state.
38+
39+
Important: This function is called by the Sentinel implementation and is
40+
called directly on the Redis standalone client for sentinels,
41+
so it doesn't support the "once" and "return_responses" options.
42+
"""
2443
return self.execute_command("SENTINEL MASTERS")
2544

2645
def sentinel_monitor(self, name, ip, port, quorum):
@@ -31,16 +50,27 @@ def sentinel_remove(self, name):
3150
"""Remove a master from Sentinel's monitoring"""
3251
return self.execute_command("SENTINEL REMOVE", name)
3352

34-
def sentinel_sentinels(self, service_name):
35-
"""Returns a list of sentinels for ``service_name``"""
36-
return self.execute_command("SENTINEL SENTINELS", service_name)
53+
def sentinel_sentinels(self, service_name, return_responses=False):
54+
"""
55+
Returns a list of sentinels for ``service_name``, when return_responses is True,
56+
otherwise returns a boolean value that indicates if the command was successful.
57+
"""
58+
return self.execute_command(
59+
"SENTINEL SENTINELS", service_name, return_responses=return_responses
60+
)
3761

3862
def sentinel_set(self, name, option, value):
3963
"""Set Sentinel monitoring parameters for a given master"""
4064
return self.execute_command("SENTINEL SET", name, option, value)
4165

4266
def sentinel_slaves(self, service_name):
43-
"""Returns a list of slaves for ``service_name``"""
67+
"""
68+
Returns a list of slaves for ``service_name``
69+
70+
Important: This function is called by the Sentinel implementation and is
71+
called directly on the Redis standalone client for sentinels,
72+
so it doesn't support the "once" and "return_responses" options.
73+
"""
4474
return self.execute_command("SENTINEL SLAVES", service_name)
4575

4676
def sentinel_reset(self, pattern):

redis/sentinel.py

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -254,16 +254,27 @@ def execute_command(self, *args, **kwargs):
254254
once - If set to True, then execute the resulting command on a single
255255
node at random, rather than across the entire sentinel cluster.
256256
"""
257-
once = bool(kwargs.get("once", False))
258-
if "once" in kwargs.keys():
259-
kwargs.pop("once")
257+
once = bool(kwargs.pop("once", False))
258+
259+
# Check if command is supposed to return the original
260+
# responses instead of boolean value.
261+
return_responses = bool(kwargs.pop("return_responses", False))
260262

261263
if once:
262-
random.choice(self.sentinels).execute_command(*args, **kwargs)
263-
else:
264-
for sentinel in self.sentinels:
265-
sentinel.execute_command(*args, **kwargs)
266-
return True
264+
response = random.choice(self.sentinels).execute_command(*args, **kwargs)
265+
if return_responses:
266+
return [response]
267+
else:
268+
return True if response else False
269+
270+
responses = []
271+
for sentinel in self.sentinels:
272+
responses.append(sentinel.execute_command(*args, **kwargs))
273+
274+
if return_responses:
275+
return responses
276+
277+
return all(responses)
267278

268279
def __repr__(self):
269280
sentinel_addresses = []

tests/test_asyncio/test_sentinel.py

Lines changed: 84 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,35 @@ def sentinel(request, cluster):
8484
return Sentinel([("foo", 26379), ("bar", 26379)])
8585

8686

87+
@pytest.fixture()
88+
async def deployed_sentinel(request):
89+
sentinel_ips = request.config.getoption("--sentinels")
90+
sentinel_endpoints = [
91+
(ip.strip(), int(port.strip()))
92+
for ip, port in (endpoint.split(":") for endpoint in sentinel_ips.split(","))
93+
]
94+
kwargs = {}
95+
decode_responses = True
96+
97+
sentinel_kwargs = {"decode_responses": decode_responses}
98+
force_master_ip = "localhost"
99+
100+
protocol = request.config.getoption("--protocol", 2)
101+
102+
sentinel = Sentinel(
103+
sentinel_endpoints,
104+
force_master_ip=force_master_ip,
105+
sentinel_kwargs=sentinel_kwargs,
106+
socket_timeout=0.1,
107+
protocol=protocol,
108+
decode_responses=decode_responses,
109+
**kwargs,
110+
)
111+
yield sentinel
112+
for s in sentinel.sentinels:
113+
await s.close()
114+
115+
87116
@pytest.mark.onlynoncluster
88117
async def test_discover_master(sentinel, master_ip):
89118
address = await sentinel.discover_master("mymaster")
@@ -226,19 +255,22 @@ async def test_slave_round_robin(cluster, sentinel, master_ip):
226255

227256

228257
@pytest.mark.onlynoncluster
229-
async def test_ckquorum(cluster, sentinel):
230-
assert await sentinel.sentinel_ckquorum("mymaster")
258+
async def test_ckquorum(sentinel):
259+
resp = await sentinel.sentinel_ckquorum("mymaster")
260+
assert resp is True
231261

232262

233263
@pytest.mark.onlynoncluster
234-
async def test_flushconfig(cluster, sentinel):
235-
assert await sentinel.sentinel_flushconfig()
264+
async def test_flushconfig(sentinel):
265+
resp = await sentinel.sentinel_flushconfig()
266+
assert resp is True
236267

237268

238269
@pytest.mark.onlynoncluster
239270
async def test_reset(cluster, sentinel):
240271
cluster.master["is_odown"] = True
241-
assert await sentinel.sentinel_reset("mymaster")
272+
resp = await sentinel.sentinel_reset("mymaster")
273+
assert resp is True
242274

243275

244276
@pytest.mark.onlynoncluster
@@ -284,3 +316,50 @@ async def test_repr_correctly_represents_connection_object(sentinel):
284316
str(connection)
285317
== "<redis.asyncio.sentinel.SentinelManagedConnection,host=127.0.0.1,port=6379)>" # noqa: E501
286318
)
319+
320+
321+
# Tests against real sentinel instances
322+
@pytest.mark.onlynoncluster
323+
async def test_get_sentinels(deployed_sentinel):
324+
resps = await deployed_sentinel.sentinel_sentinels(
325+
"redis-py-test", return_responses=True
326+
)
327+
328+
# validate that the original command response is returned
329+
assert isinstance(resps, list)
330+
331+
# validate that the command has been executed against all sentinels
332+
# each response from each sentinel is returned
333+
assert len(resps) > 1
334+
335+
# validate default behavior
336+
resps = await deployed_sentinel.sentinel_sentinels("redis-py-test")
337+
assert isinstance(resps, bool)
338+
339+
340+
@pytest.mark.onlynoncluster
341+
async def test_get_master_addr_by_name(deployed_sentinel):
342+
resps = await deployed_sentinel.sentinel_get_master_addr_by_name(
343+
"redis-py-test",
344+
return_responses=True,
345+
)
346+
347+
# validate that the original command response is returned
348+
assert isinstance(resps, list)
349+
350+
# validate that the command has been executed just once
351+
# when executed once, only one response element is returned
352+
assert len(resps) == 1
353+
354+
assert isinstance(resps[0], tuple)
355+
356+
# validate default behavior
357+
resps = await deployed_sentinel.sentinel_get_master_addr_by_name("redis-py-test")
358+
assert isinstance(resps, bool)
359+
360+
361+
@pytest.mark.onlynoncluster
362+
async def test_redis_master_usage(deployed_sentinel):
363+
r = await deployed_sentinel.master_for("redis-py-test", db=0)
364+
await r.set("foo", "bar")
365+
assert (await r.get("foo")) == "bar"

tests/test_sentinel.py

Lines changed: 82 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,35 @@ def sentinel(request, cluster):
8686
return Sentinel([("foo", 26379), ("bar", 26379)])
8787

8888

89+
@pytest.fixture()
90+
def deployed_sentinel(request):
91+
sentinel_ips = request.config.getoption("--sentinels")
92+
sentinel_endpoints = [
93+
(ip.strip(), int(port.strip()))
94+
for ip, port in (endpoint.split(":") for endpoint in sentinel_ips.split(","))
95+
]
96+
kwargs = {}
97+
decode_responses = True
98+
99+
sentinel_kwargs = {"decode_responses": decode_responses}
100+
force_master_ip = "localhost"
101+
102+
protocol = request.config.getoption("--protocol", 2)
103+
104+
sentinel = Sentinel(
105+
sentinel_endpoints,
106+
force_master_ip=force_master_ip,
107+
sentinel_kwargs=sentinel_kwargs,
108+
socket_timeout=0.1,
109+
protocol=protocol,
110+
decode_responses=decode_responses,
111+
**kwargs,
112+
)
113+
yield sentinel
114+
for s in sentinel.sentinels:
115+
s.close()
116+
117+
89118
@pytest.mark.onlynoncluster
90119
def test_discover_master(sentinel, master_ip):
91120
address = sentinel.discover_master("mymaster")
@@ -184,7 +213,7 @@ def test_discover_slaves(cluster, sentinel):
184213

185214

186215
@pytest.mark.onlynoncluster
187-
def test_master_for(cluster, sentinel, master_ip):
216+
def test_master_for(sentinel, master_ip):
188217
master = sentinel.master_for("mymaster", db=9)
189218
assert master.ping()
190219
assert master.connection_pool.master_address == (master_ip, 6379)
@@ -228,19 +257,22 @@ def test_slave_round_robin(cluster, sentinel, master_ip):
228257

229258

230259
@pytest.mark.onlynoncluster
231-
def test_ckquorum(cluster, sentinel):
232-
assert sentinel.sentinel_ckquorum("mymaster")
260+
def test_ckquorum(sentinel):
261+
resp = sentinel.sentinel_ckquorum("mymaster")
262+
assert resp is True
233263

234264

235265
@pytest.mark.onlynoncluster
236-
def test_flushconfig(cluster, sentinel):
237-
assert sentinel.sentinel_flushconfig()
266+
def test_flushconfig(sentinel):
267+
resp = sentinel.sentinel_flushconfig()
268+
assert resp is True
238269

239270

240271
@pytest.mark.onlynoncluster
241272
def test_reset(cluster, sentinel):
242273
cluster.master["is_odown"] = True
243-
assert sentinel.sentinel_reset("mymaster")
274+
resp = sentinel.sentinel_reset("mymaster")
275+
assert resp is True
244276

245277

246278
@pytest.mark.onlynoncluster
@@ -266,3 +298,47 @@ def mock_disconnect():
266298

267299
assert calls == 1
268300
pool.disconnect()
301+
302+
303+
# Tests against real sentinel instances
304+
@pytest.mark.onlynoncluster
305+
def test_get_sentinels(deployed_sentinel):
306+
resps = deployed_sentinel.sentinel_sentinels("redis-py-test", return_responses=True)
307+
308+
# validate that the original command response is returned
309+
assert isinstance(resps, list)
310+
311+
# validate that the command has been executed against all sentinels
312+
# each response from each sentinel is returned
313+
assert len(resps) > 1
314+
315+
# validate default behavior
316+
resps = deployed_sentinel.sentinel_sentinels("redis-py-test")
317+
assert isinstance(resps, bool)
318+
319+
320+
@pytest.mark.onlynoncluster
321+
def test_get_master_addr_by_name(deployed_sentinel):
322+
resps = deployed_sentinel.sentinel_get_master_addr_by_name(
323+
"redis-py-test", return_responses=True
324+
)
325+
326+
# validate that the original command response is returned
327+
assert isinstance(resps, list)
328+
329+
# validate that the command has been executed just once
330+
# when executed once, only one response element is returned
331+
assert len(resps) == 1
332+
333+
assert isinstance(resps[0], tuple)
334+
335+
# validate default behavior
336+
resps = deployed_sentinel.sentinel_get_master_addr_by_name("redis-py-test")
337+
assert isinstance(resps, bool)
338+
339+
340+
@pytest.mark.onlynoncluster
341+
def test_redis_master_usage(deployed_sentinel):
342+
r = deployed_sentinel.master_for("redis-py-test", db=0)
343+
r.set("foo", "bar")
344+
assert r.get("foo") == "bar"

0 commit comments

Comments
 (0)