|
10 | 10 | from redis.asyncio.connection import (
|
11 | 11 | BaseParser,
|
12 | 12 | Connection,
|
| 13 | + HiredisParser, |
13 | 14 | PythonParser,
|
14 | 15 | UnixDomainSocketConnection,
|
15 | 16 | )
|
16 | 17 | from redis.asyncio.retry import Retry
|
17 | 18 | from redis.backoff import NoBackoff
|
18 | 19 | from redis.exceptions import ConnectionError, InvalidResponse, TimeoutError
|
| 20 | +from redis.utils import HIREDIS_AVAILABLE |
19 | 21 | from tests.conftest import skip_if_server_version_lt
|
20 | 22 |
|
21 | 23 | from .compat import mock
|
@@ -191,3 +193,83 @@ async def test_connection_parse_response_resume(r: redis.Redis):
|
191 | 193 | pytest.fail("didn't receive a response")
|
192 | 194 | assert response
|
193 | 195 | assert i > 0
|
| 196 | + |
| 197 | + |
| 198 | +@pytest.mark.onlynoncluster |
| 199 | +@pytest.mark.parametrize( |
| 200 | + "parser_class", [PythonParser, HiredisParser], ids=["PythonParser", "HiredisParser"] |
| 201 | +) |
| 202 | +async def test_connection_disconect_race(parser_class): |
| 203 | + """ |
| 204 | + This test reproduces the case in issue #2349 |
| 205 | + where a connection is closed while the parser is reading to feed the |
| 206 | + internal buffer.The stream `read()` will succeed, but when it returns, |
| 207 | + another task has already called `disconnect()` and is waiting for |
| 208 | + close to finish. When we attempts to feed the buffer, we will fail |
| 209 | + since the buffer is no longer there. |
| 210 | +
|
| 211 | + This test verifies that a read in progress can finish even |
| 212 | + if the `disconnect()` method is called. |
| 213 | + """ |
| 214 | + if parser_class == PythonParser: |
| 215 | + pytest.xfail("doesn't work yet with PythonParser") |
| 216 | + if parser_class == HiredisParser and not HIREDIS_AVAILABLE: |
| 217 | + pytest.skip("Hiredis not available") |
| 218 | + |
| 219 | + args = {} |
| 220 | + args["parser_class"] = parser_class |
| 221 | + |
| 222 | + conn = Connection(**args) |
| 223 | + |
| 224 | + cond = asyncio.Condition() |
| 225 | + # 0 == initial |
| 226 | + # 1 == reader is reading |
| 227 | + # 2 == closer has closed and is waiting for close to finish |
| 228 | + state = 0 |
| 229 | + |
| 230 | + # Mock read function, which wait for a close to happen before returning |
| 231 | + # Can either be invoked as two `read()` calls (HiredisParser) |
| 232 | + # or as a `readline()` followed by `readexact()` (PythonParser) |
| 233 | + chunks = [b"$13\r\n", b"Hello, World!\r\n"] |
| 234 | + |
| 235 | + async def read(_=None): |
| 236 | + nonlocal state |
| 237 | + async with cond: |
| 238 | + if state == 0: |
| 239 | + state = 1 # we are reading |
| 240 | + cond.notify() |
| 241 | + # wait until the closing task has done |
| 242 | + await cond.wait_for(lambda: state == 2) |
| 243 | + return chunks.pop(0) |
| 244 | + |
| 245 | + # function closes the connection while reader is still blocked reading |
| 246 | + async def do_close(): |
| 247 | + nonlocal state |
| 248 | + async with cond: |
| 249 | + await cond.wait_for(lambda: state == 1) |
| 250 | + state = 2 |
| 251 | + cond.notify() |
| 252 | + await conn.disconnect() |
| 253 | + |
| 254 | + async def do_read(): |
| 255 | + return await conn.read_response() |
| 256 | + |
| 257 | + reader = mock.AsyncMock() |
| 258 | + writer = mock.AsyncMock() |
| 259 | + writer.transport = mock.Mock() |
| 260 | + writer.transport.get_extra_info.side_effect = None |
| 261 | + |
| 262 | + # for HiredisParser |
| 263 | + reader.read.side_effect = read |
| 264 | + # for PythonParser |
| 265 | + reader.readline.side_effect = read |
| 266 | + reader.readexactly.side_effect = read |
| 267 | + |
| 268 | + async def open_connection(*args, **kwargs): |
| 269 | + return reader, writer |
| 270 | + |
| 271 | + with patch.object(asyncio, "open_connection", open_connection): |
| 272 | + await conn.connect() |
| 273 | + |
| 274 | + vals = await asyncio.gather(do_read(), do_close()) |
| 275 | + assert vals == [b"Hello, World!", None] |
0 commit comments