@@ -122,3 +122,37 @@ def test_connect_timeout_error_without_retry(self):
122
122
assert conn ._connect .call_count == 1
123
123
assert str (e .value ) == "Timeout connecting to server"
124
124
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