Skip to content

Commit 582df33

Browse files
Reset the asyncio connect lock when the background event loop is torn down
Connection.close() nulled the loop and thread but left _connect_lock pointing at an asyncio.Lock bound to the loop that had just been closed. The lazy-create branch in _get_async_connection only rebuilds the lock when it finds None, so a future path that re-enters after close() would reuse a primitive attached to a dead loop. Set _connect_lock back to None alongside _loop/_thread inside the same critical section so the invariant "asyncio primitive is either None or attached to the live loop" holds uniformly with the async connection's lazy-init contract. The public API still guards against use-after-close via the _closed flag; this is the defensive half of the same contract. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent bd353e3 commit 582df33

File tree

2 files changed

+26
-0
lines changed

2 files changed

+26
-0
lines changed

src/dqlitedbapi/connection.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,11 @@ def close(self) -> None:
282282
self._loop.close()
283283
self._loop = None
284284
self._thread = None
285+
# Drop the asyncio.Lock bound to the loop we just closed;
286+
# the lazy-create branch in _get_async_connection rebuilds it
287+
# against the next loop so the primitive never outlives its
288+
# owning event loop.
289+
self._connect_lock = None
285290

286291
async def _close_async(self) -> None:
287292
"""Async implementation of close -- runs on event loop thread."""

tests/test_cursor.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,3 +130,24 @@ def test_scroll_raises_not_supported(self) -> None:
130130
cursor = Cursor(conn)
131131
with pytest.raises(NotSupportedError):
132132
cursor.scroll(0)
133+
134+
135+
class TestConnectionCloseResetsLock:
136+
def test_close_nulls_connect_lock(self) -> None:
137+
"""After close(), the asyncio connect lock must be reset so it
138+
doesn't outlive its owning loop (symmetry with the async side)."""
139+
import asyncio
140+
141+
conn = Connection("localhost:9001")
142+
# Simulate the state a lazy _get_async_connection would have left:
143+
# a background loop running and an asyncio.Lock created on it.
144+
loop = conn._ensure_loop()
145+
146+
async def _make_lock() -> asyncio.Lock:
147+
return asyncio.Lock()
148+
149+
conn._connect_lock = asyncio.run_coroutine_threadsafe(_make_lock(), loop).result(timeout=5)
150+
assert conn._connect_lock is not None
151+
152+
conn.close()
153+
assert conn._connect_lock is None

0 commit comments

Comments
 (0)