Skip to content

Commit eaeb1e0

Browse files
Document the owning-loop cursor-cascade cost on force_close_transport
When AsyncConnection.force_close_transport is invoked from a coroutine on the owning loop with many open cursors, the cursor- cascade walk runs inline — synchronous attribute writes plus a weakref.proxy swap per cursor. For a connection with hundreds of open cursors that's measurable loop CPU before the method returns. The documented intent of the method is loop-already-dead / GC / atexit / SA-do_terminate paths where the cascade-on-owning-loop case does not arise, so the behaviour is correct. Surface the constraint in the docstring so an in-loop caller with many open cursors picks the cooperative close() instead. Add a docstring-lint test that pins the wording so a future refactor cannot silently drop the constraint.
1 parent de66887 commit eaeb1e0

2 files changed

Lines changed: 46 additions & 0 deletions

File tree

src/dqlitedbapi/aio/connection.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1192,6 +1192,18 @@ def force_close_transport(self) -> None:
11921192
runs directly; on a foreign thread with a live loop the cancel
11931193
is scheduled via ``call_soon_threadsafe`` so the ready-queue
11941194
is not mutated cross-thread.
1195+
1196+
Owning-loop note: when invoked from a coroutine on the
1197+
owning loop (e.g. an in-loop test fixture explicitly
1198+
terminating a connection with many open cursors), the
1199+
cursor-cascade walk runs inline without yielding — synchronous
1200+
attribute writes on each cursor plus a ``weakref.proxy`` swap.
1201+
For a connection with N open cursors that is O(N) loop CPU
1202+
before this method returns. Documented intent is the
1203+
loop-already-dead / GC / atexit / SA-do_terminate paths where
1204+
the cascade-on-owning-loop case does not arise; an in-loop
1205+
caller with hundreds of open cursors who wants cooperative
1206+
teardown should await ``close()`` instead.
11951207
"""
11961208
# PEP 249 §6.4 + project discipline: every public Connection
11971209
# method clears ``messages`` as the first statement.
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
"""Pin: ``AsyncConnection.force_close_transport``'s docstring
2+
documents the owning-loop cursor-cascade cost.
3+
4+
When invoked from a coroutine on the owning loop, the cursor-cascade
5+
walk runs inline (no yield) — synchronous per-cursor attribute writes
6+
plus a ``weakref.proxy`` swap. For a connection with hundreds of
7+
open cursors that's measurable loop CPU. The documented intent of
8+
the method is loop-already-dead / GC / atexit / SA-do_terminate
9+
paths where the cascade-on-owning-loop case doesn't arise, so the
10+
behaviour is correct; the docstring just needs to surface the
11+
constraint so an in-loop caller can pick the right method (``close()``).
12+
"""
13+
14+
from __future__ import annotations
15+
16+
from dqlitedbapi.aio.connection import AsyncConnection
17+
18+
19+
def test_force_close_transport_docstring_mentions_owning_loop_cascade() -> None:
20+
"""The docstring must surface the owning-loop O(N_cursors)
21+
cascade cost so an in-loop caller picks ``close()`` instead."""
22+
doc = AsyncConnection.force_close_transport.__doc__ or ""
23+
assert "owning loop" in doc.lower(), (
24+
"force_close_transport docstring must mention the owning-loop "
25+
"case so in-loop callers can pick the cooperative ``close()``"
26+
)
27+
assert "cursor" in doc.lower(), (
28+
"force_close_transport docstring must mention cursor-cascade "
29+
"cost on the owning-loop call shape"
30+
)
31+
assert "close()" in doc, (
32+
"force_close_transport docstring must point at ``close()`` "
33+
"as the cooperative alternative for in-loop callers"
34+
)

0 commit comments

Comments
 (0)