Skip to content

Commit 6c9c8f9

Browse files
Invalidate underlying connection on sync-timeout
ISSUE-12 — _run_sync now schedules _async_conn._invalidate() on the loop thread before raising OperationalError on sync-side timeout. The coroutine may have already written bytes to the socket before observing the cancel; without invalidation the next operation reuses a wire in unknown state, causing mysterious protocol errors later. Fire-and-forget via call_soon_threadsafe so we don't need to wait for the event loop. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 83fdc76 commit 6c9c8f9

File tree

1 file changed

+17
-0
lines changed

1 file changed

+17
-0
lines changed

src/dqlitedbapi/connection.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,12 @@ def _run_sync(self, coro: Any) -> Any:
7171
and blocks until the result is available. The operation lock
7272
ensures only one operation runs at a time, preventing wire
7373
protocol corruption from concurrent access.
74+
75+
On sync-side timeout we cancel the future AND invalidate the
76+
underlying connection. The coroutine may have already written
77+
partial bytes to the socket before observing the cancel;
78+
invalidation poisons the wire stream so the next operation
79+
reconnects instead of reusing a torn protocol state.
7480
"""
7581
with self._op_lock:
7682
loop = self._ensure_loop()
@@ -81,6 +87,17 @@ def _run_sync(self, coro: Any) -> Any:
8187
return future.result(timeout=self._timeout)
8288
except TimeoutError as e:
8389
future.cancel()
90+
# Poison the underlying connection. The coroutine may have
91+
# half-written a request; the wire is in unknown state.
92+
# Fire-and-forget on the loop thread (don't await).
93+
if self._async_conn is not None:
94+
try:
95+
loop.call_soon_threadsafe(
96+
self._async_conn._invalidate,
97+
OperationalError(f"sync timeout after {self._timeout}s"),
98+
)
99+
except RuntimeError:
100+
pass # loop already shutting down
84101
raise OperationalError(f"Operation timed out after {self._timeout} seconds") from e
85102

86103
async def _get_async_connection(self) -> DqliteConnection:

0 commit comments

Comments
 (0)