Skip to content

Commit 55a50a2

Browse files
authored
Truncate pipeline exception message to a sane size (#3530)
Fixes #20234.
1 parent 052e8ee commit 55a50a2

File tree

9 files changed

+92
-5
lines changed

9 files changed

+92
-5
lines changed

Diff for: redis/asyncio/client.py

+5-1
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@
7777
get_lib_version,
7878
safe_str,
7979
str_if_bytes,
80+
truncate_text,
8081
)
8182

8283
if TYPE_CHECKING and SSL_AVAILABLE:
@@ -1513,7 +1514,10 @@ def annotate_exception(
15131514
self, exception: Exception, number: int, command: Iterable[object]
15141515
) -> None:
15151516
cmd = " ".join(map(safe_str, command))
1516-
msg = f"Command # {number} ({cmd}) of pipeline caused error: {exception.args}"
1517+
msg = (
1518+
f"Command # {number} ({truncate_text(cmd)}) "
1519+
"of pipeline caused error: {exception.args}"
1520+
)
15171521
exception.args = (msg,) + exception.args[1:]
15181522

15191523
async def parse_response(

Diff for: redis/asyncio/cluster.py

+4-2
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@
7171
get_lib_version,
7272
safe_str,
7373
str_if_bytes,
74+
truncate_text,
7475
)
7576

7677
if SSL_AVAILABLE:
@@ -1633,8 +1634,9 @@ async def _execute(
16331634
if isinstance(result, Exception):
16341635
command = " ".join(map(safe_str, cmd.args))
16351636
msg = (
1636-
f"Command # {cmd.position + 1} ({command}) of pipeline "
1637-
f"caused error: {result.args}"
1637+
f"Command # {cmd.position + 1} "
1638+
f"({truncate_text(command)}) "
1639+
f"of pipeline caused error: {result.args}"
16381640
)
16391641
result.args = (msg,) + result.args[1:]
16401642
raise result

Diff for: redis/client.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@
6161
get_lib_version,
6262
safe_str,
6363
str_if_bytes,
64+
truncate_text,
6465
)
6566

6667
if TYPE_CHECKING:
@@ -1524,7 +1525,8 @@ def raise_first_error(self, commands, response):
15241525
def annotate_exception(self, exception, number, command):
15251526
cmd = " ".join(map(safe_str, command))
15261527
msg = (
1527-
f"Command # {number} ({cmd}) of pipeline caused error: {exception.args[0]}"
1528+
f"Command # {number} ({truncate_text(cmd)}) of pipeline "
1529+
f"caused error: {exception.args[0]}"
15281530
)
15291531
exception.args = (msg,) + exception.args[1:]
15301532

Diff for: redis/cluster.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
merge_result,
4848
safe_str,
4949
str_if_bytes,
50+
truncate_text,
5051
)
5152

5253

@@ -2125,7 +2126,8 @@ def annotate_exception(self, exception, number, command):
21252126
"""
21262127
cmd = " ".join(map(safe_str, command))
21272128
msg = (
2128-
f"Command # {number} ({cmd}) of pipeline caused error: {exception.args[0]}"
2129+
f"Command # {number} ({truncate_text(cmd)}) of pipeline "
2130+
f"caused error: {exception.args[0]}"
21292131
)
21302132
exception.args = (msg,) + exception.args[1:]
21312133

Diff for: redis/utils.py

+7
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import datetime
22
import logging
3+
import textwrap
34
from contextlib import contextmanager
45
from functools import wraps
56
from typing import Any, Dict, List, Mapping, Optional, Union
@@ -298,3 +299,9 @@ def extract_expire_flags(
298299
exp_options.extend(["PXAT", pxat])
299300

300301
return exp_options
302+
303+
304+
def truncate_text(txt, max_length=100):
305+
return textwrap.shorten(
306+
text=txt, width=max_length, placeholder="...", break_long_words=True
307+
)

Diff for: tests/test_asyncio/test_cluster.py

+19
Original file line numberDiff line numberDiff line change
@@ -2905,6 +2905,25 @@ async def test_asking_error(self, r: RedisCluster) -> None:
29052905
assert ask_node._free.pop().read_response.await_count
29062906
assert res == ["MOCK_OK"]
29072907

2908+
async def test_error_is_truncated(self, r) -> None:
2909+
"""
2910+
Test that an error from the pipeline is truncated correctly.
2911+
"""
2912+
key = "a" * 50
2913+
a_value = "a" * 20
2914+
b_value = "b" * 20
2915+
2916+
async with r.pipeline() as pipe:
2917+
pipe.set(key, 1)
2918+
pipe.hset(key, mapping={"field_a": a_value, "field_b": b_value})
2919+
pipe.expire(key, 100)
2920+
2921+
with pytest.raises(Exception) as ex:
2922+
await pipe.execute()
2923+
2924+
expected = f"Command # 2 (HSET {key} field_a {a_value} field_b...) of pipeline caused error: "
2925+
assert str(ex.value).startswith(expected)
2926+
29082927
async def test_moved_redirection_on_slave_with_default(
29092928
self, r: RedisCluster
29102929
) -> None:

Diff for: tests/test_asyncio/test_pipeline.py

+16
Original file line numberDiff line numberDiff line change
@@ -368,6 +368,22 @@ async def test_exec_error_in_no_transaction_pipeline_unicode_command(self, r):
368368

369369
assert await r.get(key) == b"1"
370370

371+
async def test_exec_error_in_pipeline_truncated(self, r):
372+
key = "a" * 50
373+
a_value = "a" * 20
374+
b_value = "b" * 20
375+
376+
await r.set(key, 1)
377+
async with r.pipeline(transaction=False) as pipe:
378+
pipe.hset(key, mapping={"field_a": a_value, "field_b": b_value})
379+
pipe.expire(key, 100)
380+
381+
with pytest.raises(redis.ResponseError) as ex:
382+
await pipe.execute()
383+
384+
expected = f"Command # 1 (HSET {key} field_a {a_value} field_b...) of pipeline caused error: "
385+
assert str(ex.value).startswith(expected)
386+
371387
async def test_pipeline_with_bitfield(self, r):
372388
async with r.pipeline() as pipe:
373389
pipe.set("a", "1")

Diff for: tests/test_cluster.py

+19
Original file line numberDiff line numberDiff line change
@@ -3315,6 +3315,25 @@ def raise_ask_error():
33153315
assert ask_node.redis_connection.connection.read_response.called
33163316
assert res == ["MOCK_OK"]
33173317

3318+
def test_error_is_truncated(self, r):
3319+
"""
3320+
Test that an error from the pipeline is truncated correctly.
3321+
"""
3322+
key = "a" * 50
3323+
a_value = "a" * 20
3324+
b_value = "b" * 20
3325+
3326+
with r.pipeline() as pipe:
3327+
pipe.set(key, 1)
3328+
pipe.hset(key, mapping={"field_a": a_value, "field_b": b_value})
3329+
pipe.expire(key, 100)
3330+
3331+
with pytest.raises(Exception) as ex:
3332+
pipe.execute()
3333+
3334+
expected = f"Command # 2 (HSET {key} field_a {a_value} field_b...) of pipeline caused error: "
3335+
assert str(ex.value).startswith(expected)
3336+
33183337
def test_return_previously_acquired_connections(self, r):
33193338
# in order to ensure that a pipeline will make use of connections
33203339
# from different nodes

Diff for: tests/test_pipeline.py

+16
Original file line numberDiff line numberDiff line change
@@ -369,6 +369,22 @@ def test_exec_error_in_no_transaction_pipeline_unicode_command(self, r):
369369

370370
assert r[key] == b"1"
371371

372+
def test_exec_error_in_pipeline_truncated(self, r):
373+
key = "a" * 50
374+
a_value = "a" * 20
375+
b_value = "b" * 20
376+
377+
r[key] = 1
378+
with r.pipeline(transaction=False) as pipe:
379+
pipe.hset(key, mapping={"field_a": a_value, "field_b": b_value})
380+
pipe.expire(key, 100)
381+
382+
with pytest.raises(redis.ResponseError) as ex:
383+
pipe.execute()
384+
385+
expected = f"Command # 1 (HSET {key} field_a {a_value} field_b...) of pipeline caused error: "
386+
assert str(ex.value).startswith(expected)
387+
372388
def test_pipeline_with_bitfield(self, r):
373389
with r.pipeline() as pipe:
374390
pipe.set("a", "1")

0 commit comments

Comments
 (0)