Skip to content

Commit 7231d15

Browse files
committed
Add regression tests and fixes for issue redis#1128
1 parent 74c251a commit 7231d15

File tree

4 files changed

+87
-4
lines changed

4 files changed

+87
-4
lines changed

redis/asyncio/client.py

+11-2
Original file line numberDiff line numberDiff line change
@@ -477,8 +477,17 @@ async def _send_command_parse_response(self, conn, command_name, *args, **option
477477
"""
478478
Send a command and parse the response
479479
"""
480-
await conn.send_command(*args)
481-
return await self.parse_response(conn, command_name, **options)
480+
try:
481+
await conn.send_command(*args)
482+
return await self.parse_response(conn, command_name, **options)
483+
except BaseException:
484+
# An exception while reading or writing leaves the connection in
485+
# an unknown state. There may be un-written or un-read data
486+
# so we cannot re-use it for a subsequent command/response transaction.
487+
# This may be a TimeoutError or any other error not handled by the
488+
# Connection object itself.
489+
await conn.disconnect(nowait=True)
490+
raise
482491

483492
async def _disconnect_raise(self, conn: Connection, error: Exception):
484493
"""

redis/client.py

+12-2
Original file line numberDiff line numberDiff line change
@@ -1231,8 +1231,18 @@ def _send_command_parse_response(self, conn, command_name, *args, **options):
12311231
"""
12321232
Send a command and parse the response
12331233
"""
1234-
conn.send_command(*args)
1235-
return self.parse_response(conn, command_name, **options)
1234+
try:
1235+
conn.send_command(*args)
1236+
return self.parse_response(conn, command_name, **options)
1237+
except BaseException:
1238+
# An exception while reading or writing leaves the connection in
1239+
# an unknown state. There may be un-written or un-read data
1240+
# so we cannot re-use it for a subsequent command/response transaction.
1241+
# This can be any error not handled by the Connection itself, such
1242+
# as BaseExceptions may have been used to interrupt IO, when using
1243+
# gevent.
1244+
conn.disconnect()
1245+
raise
12361246

12371247
def _disconnect_raise(self, conn, error):
12381248
"""

tests/test_asyncio/test_commands.py

+32
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
"""
22
Tests async overrides of commands from their mixins
33
"""
4+
import asyncio
45
import binascii
56
import datetime
67
import re
78
from string import ascii_letters
9+
import async_timeout
810

911
import pytest
1012
import pytest_asyncio
@@ -2999,6 +3001,36 @@ async def test_module_list(self, r: redis.Redis):
29993001
for x in await r.module_list():
30003002
assert isinstance(x, dict)
30013003

3004+
@pytest.mark.onlynoncluster
3005+
async def test_interrupted_command(self, r: redis.Redis):
3006+
"""
3007+
Regression test for issue #1128: An Un-handled BaseException
3008+
will leave the socket with un-read response to a previous
3009+
command.
3010+
"""
3011+
ready = asyncio.Event()
3012+
3013+
async def helper():
3014+
with pytest.raises(asyncio.CancelledError):
3015+
# blocking pop
3016+
ready.set()
3017+
await r.brpop(["nonexist"])
3018+
# if all is well, we can continue. The following should not hang.
3019+
await r.set("status", "down")
3020+
return "done"
3021+
3022+
task = asyncio.create_task(helper())
3023+
await ready.wait()
3024+
await asyncio.sleep(0.01)
3025+
# the task is now sleeping, lets send it an exception
3026+
task.cancel()
3027+
try:
3028+
async with async_timeout.timeout(0.1):
3029+
assert await task == "done"
3030+
except asyncio.TimeoutError:
3031+
task.cancel()
3032+
await task
3033+
30023034

30033035
@pytest.mark.onlynoncluster
30043036
class TestBinarySave:

tests/test_commands.py

+32
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
import binascii
22
import datetime
33
import re
4+
import threading
45
import time
56
from string import ascii_letters
7+
from asyncio import CancelledError
68
from unittest import mock
9+
from unittest.mock import patch
10+
import socket
711

812
import pytest
913

@@ -4719,6 +4723,34 @@ def test_psync(self, r):
47194723
res = r2.psync(r2.client_id(), 1)
47204724
assert b"FULLRESYNC" in res
47214725

4726+
@pytest.mark.onlynoncluster
4727+
def test_interrupted_command(self, r: redis.Redis):
4728+
"""
4729+
Regression test for issue #1128: An Un-handled BaseException
4730+
will leave the socket with un-read response to a previous
4731+
command.
4732+
"""
4733+
print("start")
4734+
4735+
def helper():
4736+
try:
4737+
# blocking pop
4738+
with patch.object(
4739+
socket.socket, "recv_into", side_effect=CancelledError
4740+
) as mock_recv:
4741+
r.brpop(["nonexist"])
4742+
except CancelledError:
4743+
print("canc")
4744+
pass # we got some BaseException.
4745+
# if all is well, we can continue.
4746+
r.set("status", "down") # should not hang
4747+
return "done"
4748+
4749+
thread = threading.Thread(target=helper)
4750+
thread.start()
4751+
thread.join(0.1)
4752+
assert not thread.is_alive()
4753+
47224754

47234755
@pytest.mark.onlynoncluster
47244756
class TestBinarySave:

0 commit comments

Comments
 (0)