@@ -122,3 +122,37 @@ def test_connect_timeout_error_without_retry(self):
122122 assert conn ._connect .call_count == 1
123123 assert str (e .value ) == "Timeout connecting to server"
124124 self .clear (conn )
125+
126+ @pytest .mark .parametrize ('exc_type' , [Exception , BaseException ])
127+ def test_read_response__interrupt_does_not_corrupt (self , exc_type ):
128+ conn = Connection ()
129+
130+ # A note on BaseException:
131+ # While socket.recv is not supposed to raise BaseException, gevent's version of socket
132+ # (which, when using gevent + redis-py, one would monkey-patch in) can raise BaseException
133+ # on a timer elapse, since `gevent.Timeout` derives from BaseException. This design suggests
134+ # that a timeout should not be suppressed but rather allowed to propagate.
135+ # asyncio.exceptions.CancelledError also derives from BaseException for same reason.
136+ #
137+ # The notion that one should never `expect:` or `expect BaseException`, however, is misguided.
138+ # It's idiomatic to handle it, to provide for exception safety, as long as you re-raise.
139+ #
140+ # with gevent.Timeout(5):
141+ # res = client.exists('my_key')
142+
143+ conn .send_command ("GET non_existent_key" )
144+ resp = conn .read_response ()
145+ assert resp is None
146+
147+ with pytest .raises (exc_type ):
148+ conn .send_command ("EXISTS non_existent_key" )
149+ # due to the interrupt, the integer '0' result of EXISTS will remain on the socket's buffer
150+ with patch .object (socket .socket , "recv" , side_effect = exc_type ) as mock_recv :
151+ _ = conn .read_response ()
152+ mock_recv .assert_called_once ()
153+
154+ conn .send_command ("GET non_existent_key" )
155+ resp = conn .read_response ()
156+ # If working properly, this will get a None.
157+ # If not, it will get a zero (the integer result of the previous EXISTS command).
158+ assert resp is None
0 commit comments