@@ -122,3 +122,40 @@ 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
132
+ # of socket (which, when using gevent + redis-py, one would monkey-patch in)
133
+ # can raise BaseException on a timer elapse, since `gevent.Timeout` derives
134
+ # from BaseException. This design suggests that a timeout should
135
+ # not be suppressed but rather allowed to propagate.
136
+ # asyncio.exceptions.CancelledError also derives from BaseException
137
+ # for same reason.
138
+ #
139
+ # The notion that one should never `expect:` or `expect BaseException`,
140
+ # however, is misguided. It's idiomatic to handle it, to provide
141
+ # for exception safety, as long as you re-raise.
142
+ #
143
+ # with gevent.Timeout(5):
144
+ # res = client.exists('my_key')
145
+
146
+ conn .send_command ("GET non_existent_key" )
147
+ resp = conn .read_response ()
148
+ assert resp is None
149
+
150
+ with pytest .raises (exc_type ):
151
+ conn .send_command ("EXISTS non_existent_key" )
152
+ # due to the interrupt, the integer '0' result of EXISTS will remain on the socket's buffer
153
+ with patch .object (socket .socket , "recv" , side_effect = exc_type ) as mock_recv :
154
+ _ = conn .read_response ()
155
+ mock_recv .assert_called_once ()
156
+
157
+ conn .send_command ("GET non_existent_key" )
158
+ resp = conn .read_response ()
159
+ # If working properly, this will get a None.
160
+ # If not, it will get a zero (the integer result of the previous EXISTS command).
161
+ assert resp is None
0 commit comments