Skip to content

Commit 066f35f

Browse files
authored
Merge branch 'master' into ps_improve_jsontype_definition
2 parents da0aaad + 06fac1d commit 066f35f

File tree

8 files changed

+273
-66
lines changed

8 files changed

+273
-66
lines changed

.github/actions/run-tests/action.yml

Lines changed: 21 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ runs:
4646
CLIENT_LIBS_TEST_IMAGE_TAG: ${{ inputs.redis-version }}
4747
run: |
4848
set -e
49-
49+
5050
echo "::group::Installing dependencies"
5151
pip install -r dev_requirements.txt
5252
pip uninstall -y redis # uninstall Redis package installed via redis-entraid
@@ -57,80 +57,79 @@ runs:
5757
pip install -e ./hiredis-py
5858
else
5959
pip install "hiredis${{inputs.hiredis-version}}"
60-
fi
60+
fi
6161
echo "PARSER_BACKEND=$(echo "${{inputs.parser-backend}}_${{inputs.hiredis-version}}" | sed 's/[^a-zA-Z0-9]/_/g')" >> $GITHUB_ENV
6262
else
6363
echo "PARSER_BACKEND=${{inputs.parser-backend}}" >> $GITHUB_ENV
6464
fi
6565
echo "::endgroup::"
66-
66+
6767
echo "::group::Starting Redis servers"
6868
redis_major_version=$(echo "$REDIS_VERSION" | grep -oP '^\d+')
6969
echo "REDIS_MAJOR_VERSION=${redis_major_version}" >> $GITHUB_ENV
70-
70+
7171
if (( redis_major_version < 8 )); then
7272
echo "Using redis-stack for module tests"
73-
74-
# Mapping of redis version to stack version
73+
74+
# Mapping of redis version to stack version
7575
declare -A redis_stack_version_mapping=(
7676
["7.4.2"]="rs-7.4.0-v2"
7777
["7.2.7"]="rs-7.2.0-v14"
78-
["6.2.17"]="rs-6.2.6-v18"
7978
)
80-
79+
8180
if [[ -v redis_stack_version_mapping[$REDIS_VERSION] ]]; then
8281
export CLIENT_LIBS_TEST_STACK_IMAGE_TAG=${redis_stack_version_mapping[$REDIS_VERSION]}
8382
echo "REDIS_MOD_URL=redis://127.0.0.1:6479/0" >> $GITHUB_ENV
8483
else
8584
echo "Version not found in the mapping."
8685
exit 1
8786
fi
88-
87+
8988
if (( redis_major_version < 7 )); then
9089
export REDIS_STACK_EXTRA_ARGS="--tls-auth-clients optional --save ''"
91-
export REDIS_EXTRA_ARGS="--tls-auth-clients optional --save ''"
90+
export REDIS_EXTRA_ARGS="--tls-auth-clients optional --save ''"
9291
fi
93-
92+
9493
invoke devenv --endpoints=all-stack
9594
else
9695
echo "Using redis CE for module tests"
9796
echo "REDIS_MOD_URL=redis://127.0.0.1:6379" >> $GITHUB_ENV
9897
invoke devenv --endpoints all
99-
fi
100-
98+
fi
99+
101100
sleep 10 # time to settle
102101
echo "::endgroup::"
103102
shell: bash
104103

105104
- name: Run tests
106105
run: |
107106
set -e
108-
107+
109108
run_tests() {
110109
local protocol=$1
111110
local eventloop=""
112-
111+
113112
if [ "${{inputs.event-loop}}" == "uvloop" ]; then
114113
eventloop="--uvloop"
115114
fi
116-
115+
117116
echo "::group::RESP${protocol} standalone tests"
118117
echo "REDIS_MOD_URL=${REDIS_MOD_URL}"
119-
118+
120119
if (( $REDIS_MAJOR_VERSION < 7 )) && [ "$protocol" == "3" ]; then
121120
echo "Skipping module tests: Modules doesn't support RESP3 for Redis versions < 7"
122121
invoke standalone-tests --redis-mod-url=${REDIS_MOD_URL} $eventloop --protocol="${protocol}" --extra-markers="not redismod and not cp_integration"
123-
else
122+
else
124123
invoke standalone-tests --redis-mod-url=${REDIS_MOD_URL} $eventloop --protocol="${protocol}"
125124
fi
126-
125+
127126
echo "::endgroup::"
128-
127+
129128
echo "::group::RESP${protocol} cluster tests"
130129
invoke cluster-tests $eventloop --protocol=${protocol}
131-
echo "::endgroup::"
130+
echo "::endgroup::"
132131
}
133-
132+
134133
run_tests 2 "${{inputs.event-loop}}"
135134
run_tests 3 "${{inputs.event-loop}}"
136135
shell: bash

.github/workflows/integration.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ jobs:
7474
max-parallel: 15
7575
fail-fast: false
7676
matrix:
77-
redis-version: ['8.0.1-pre', '${{ needs.redis_version.outputs.CURRENT }}', '7.2.7', '6.2.17']
77+
redis-version: ['8.0.1-pre', '${{ needs.redis_version.outputs.CURRENT }}', '7.2.7']
7878
python-version: ['3.9', '3.13']
7979
parser-backend: ['plain']
8080
event-loop: ['asyncio']

.github/workflows/spellcheck.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ jobs:
88
- name: Checkout
99
uses: actions/checkout@v4
1010
- name: Check Spelling
11-
uses: rojopolis/spellcheck-github-actions@0.48.0
11+
uses: rojopolis/spellcheck-github-actions@0.49.0
1212
with:
1313
config_path: .github/spellcheck-settings.yml
1414
task_name: Markdown

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"

0 commit comments

Comments
 (0)