Skip to content

Commit 75a4f2b

Browse files
Reset async connection locks on close
AsyncConnection.close() sets _closed and drops the underlying DqliteConnection but leaves _connect_lock / _op_lock bound to the loop they were first touched on. A subsequent fixture or SQLAlchemy glue path that re-uses the object across loops would observe stale primitives. Null both attributes in close() so the invariant "an asyncio primitive is None or bound to the live loop" holds across both sync and async Connection classes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent be5724c commit 75a4f2b

2 files changed

Lines changed: 31 additions & 0 deletions

File tree

src/dqlitedbapi/aio/connection.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,12 +123,24 @@ async def close(self) -> None:
123123
# in-flight op (if any) under the lock.
124124
self._closed = True
125125
if self._async_conn is None:
126+
# Null the lazy locks so a subsequent fixture or
127+
# SQLAlchemy-glue reuse of the object in a different event
128+
# loop cannot observe a primitive bound to the dead loop.
129+
# Parity with the sync close() reset established for
130+
# connect_lock.
131+
self._connect_lock = None
132+
self._op_lock = None
126133
return
127134
_, op_lock = self._ensure_locks()
128135
async with op_lock:
129136
if self._async_conn is not None:
130137
await self._async_conn.close()
131138
self._async_conn = None
139+
# Reset the locks *after* closing so any task that was parked on
140+
# ``op_lock`` observes the "_closed -> raise InterfaceError"
141+
# re-check before it touches the now-None primitive.
142+
self._connect_lock = None
143+
self._op_lock = None
132144

133145
async def commit(self) -> None:
134146
"""Commit any pending transaction.

tests/test_async_lock_binding.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,3 +46,22 @@ async def touch_locks() -> None:
4646
conn._connect_lock = None
4747
conn._op_lock = None
4848
asyncio.run(touch_locks())
49+
50+
51+
class TestAsyncCloseResetsLocks:
52+
"""close() nulls the lazily-created locks so a subsequent re-use
53+
from a new event loop cannot observe primitives bound to the dead
54+
loop. Parity with the sync close() connect_lock reset.
55+
"""
56+
57+
def test_close_without_ever_connecting_nulls_locks(self) -> None:
58+
async def scenario() -> None:
59+
conn = AsyncConnection("localhost:19001", database="x")
60+
# No cursor/execute has run; _async_conn is None but the
61+
# caller calls close() anyway (matches the docstring
62+
# promise that close is safe even on unused connections).
63+
await conn.close()
64+
assert conn._connect_lock is None
65+
assert conn._op_lock is None
66+
67+
asyncio.run(scenario())

0 commit comments

Comments
 (0)