Skip to content

Commit 9beabe8

Browse files
Pin interrupt drain ResultResponse early-return for EXEC-INTERRUPT race
``DqliteProtocol.interrupt``'s drain loop has an explicit ``isinstance(response, ResultResponse): return`` arm to handle the EXEC-side terminal: when an EXEC's done-callback has emitted ``RESULT`` before the INTERRUPT took effect, the response queue holds the RESULT before the EmptyResponse acknowledgement. Without the arm, the loop would hit the "Expected EmptyResponse" branch and poison the wire. The line was uncovered. Add a unit test feeding a ``ResultResponse`` to the drain loop and asserting clean return. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent f6b79d6 commit 9beabe8

1 file changed

Lines changed: 20 additions & 0 deletions

File tree

tests/test_interrupt.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,26 @@ async def test_interrupt_drains_to_empty_response(self, protocol: DqliteProtocol
4343
# Write was issued.
4444
protocol._writer.write.assert_called() # type: ignore[attr-defined]
4545

46+
async def test_interrupt_drains_in_flight_result_response(
47+
self, protocol: DqliteProtocol
48+
) -> None:
49+
"""Pin the EXEC-side terminal: an INTERRUPT landing on an
50+
EXEC / EXEC_SQL whose done-callback has already emitted
51+
``RESULT`` before the interrupt took effect leaves a
52+
``ResultResponse`` in the response queue ahead of the
53+
``EmptyResponse`` acknowledgement. The drain loop must
54+
treat ``ResultResponse`` as equivalent to
55+
``EmptyResponse`` (wire is coherent, interrupt has been
56+
honoured); without this, the "Expected EmptyResponse"
57+
arm would fire and poison the wire.
58+
"""
59+
from dqlitewire.messages import ResultResponse
60+
61+
result = ResultResponse(last_insert_id=42, rows_affected=1).encode()
62+
protocol._reader.read = AsyncMock(side_effect=[result, b""])
63+
# Must NOT raise — ResultResponse is the EXEC-side terminal.
64+
await protocol.interrupt(db_id=1)
65+
4666
async def test_interrupt_swallows_in_flight_rows(self, protocol: DqliteProtocol) -> None:
4767
"""A RowsResponse landing after INTERRUPT (in-flight from before
4868
the server processed the interrupt) is dropped; the drain loop

0 commit comments

Comments
 (0)