Skip to content

Commit 1e10d3c

Browse files
Wait briefly for the cancelled coroutine to unwind on sync timeout
Connection._run_sync called future.cancel() on sync-side timeout and returned the OperationalError to the caller immediately. The coroutine was still unwinding on the loop thread, possibly still holding the underlying DqliteConnection's _in_use flag — so the caller's very next sync method (commit, close, another execute) observed "another operation is in progress" until the prior coroutine actually caught CancelledError and finished its finally blocks. Bound a 1-second wait for the cancellation to land before raising; _invalidate stays as fire-and-forget for the genuinely-stuck case. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent d777f65 commit 1e10d3c

File tree

1 file changed

+14
-0
lines changed

1 file changed

+14
-0
lines changed

src/dqlitedbapi/connection.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""PEP 249 Connection implementation for dqlite."""
22

33
import asyncio
4+
import concurrent.futures
45
import contextlib
56
import math
67
import threading
@@ -223,6 +224,19 @@ def _run_sync(self, coro: Any) -> Any:
223224
self._async_conn._invalidate,
224225
OperationalError(f"sync timeout after {self._timeout}s"),
225226
)
227+
# Wait a bounded time for the cancelled coroutine to
228+
# unwind. Without this, the next sync call can race the
229+
# still-running prior coroutine — both want the
230+
# underlying DqliteConnection's ``_in_use`` flag, and the
231+
# new op sees ``already in use`` even though from the
232+
# caller's perspective the previous operation already
233+
# raised. The 1s cap is enough for normal cancellation
234+
# to land; ``_invalidate`` above is the safety net for
235+
# a genuinely stuck coroutine.
236+
with contextlib.suppress(
237+
concurrent.futures.CancelledError, concurrent.futures.TimeoutError, Exception
238+
):
239+
future.result(timeout=1.0)
226240
raise OperationalError(f"Operation timed out after {self._timeout} seconds") from e
227241

228242
async def _get_async_connection(self) -> DqliteConnection:

0 commit comments

Comments
 (0)