Skip to content

Commit cdff719

Browse files
Schedule writer.close on loop thread in Connection.close best-effort arm
connection.py:1441-1448 — Connection.close()'s finally block called writer.close() directly on the calling thread while self._loop is still running on a different thread (loop.stop is queued via call_soon_threadsafe several lines later). The sibling force_close_transport at line 1554-1555 already schedules the same call via loop.call_soon_threadsafe(_safe_writer_close, writer) precisely because StreamWriter.close() mutates transport state (_remove_reader on the selector) and is not thread-safe per stdlib. Asymmetric discipline within the same file. The current contextlib.suppress(Exception) masked the symptoms of the race. Switch to call_soon_threadsafe(_safe_writer_close) when the loop is alive; fall back to the synchronous call only when the loop is already closed / unavailable. FIFO ready-queue discipline with the subsequent loop.stop queue ensures FIN goes out before run_forever exits — exactly the pattern force_close_transport already uses. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent d04837f commit cdff719

1 file changed

Lines changed: 21 additions & 1 deletion

File tree

src/dqlitedbapi/connection.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1442,7 +1442,27 @@ def close(self) -> None:
14421442
inner = self._async_conn
14431443
proto = getattr(inner, "_protocol", None)
14441444
writer = getattr(proto, "_writer", None) if proto is not None else None
1445-
if writer is not None:
1445+
if writer is not None and self._loop is not None and not self._loop.is_closed():
1446+
# ``StreamWriter.close()`` mutates transport
1447+
# state (calls ``self._loop._remove_reader(...)``
1448+
# under the hood) and is documented as not
1449+
# thread-safe by stdlib asyncio — touching
1450+
# the selector from the calling thread while
1451+
# the loop is still running on its own thread
1452+
# races with the selector's transport-state
1453+
# bookkeeping. Schedule via
1454+
# ``call_soon_threadsafe`` like the sibling
1455+
# ``force_close_transport`` does (see
1456+
# ``connection.py:1554-1555``). FIFO discipline
1457+
# of the ready queue with the subsequent
1458+
# ``loop.stop`` queue ensures FIN goes out
1459+
# before ``run_forever`` exits.
1460+
with contextlib.suppress(RuntimeError):
1461+
self._loop.call_soon_threadsafe(_safe_writer_close, writer)
1462+
elif writer is not None:
1463+
# Loop is closed / unavailable — the
1464+
# ``_safe_writer_close`` synchronous call
1465+
# is the best we can do to flush FIN.
14461466
with contextlib.suppress(Exception):
14471467
writer.close()
14481468
self._async_conn = None

0 commit comments

Comments
 (0)