Skip to content

Commit 24e9c32

Browse files
Cancel-and-detach prior pending_drain on _invalidate; clear cause on close; guard create_task on closed loop
Three client-side cleanup follow-ups: - ``_invalidate`` overwrote ``self._pending_drain`` with a fresh bounded-drain task whenever it fired. If a prior _invalidate had already published one, that orphan kept running but was no longer reachable through ``self`` — close() awaited only the second task, and the first triggered "Task was destroyed but it is pending" at GC, recreating the exact warning ``_pending_drain`` was added to suppress. Cancel-and-detach the prior task before the assignment, mirroring ``connect()``'s pending-retire idiom. - ``_invalidate``'s ``loop.create_task(_bounded_drain())`` was unguarded against ``RuntimeError("Event loop is closed")`` — realistic during interpreter shutdown / engine.dispose() races. The unhandled raise replaced the original cancel/cause that drove invalidation, breaking SA's is_disconnect substring classifier downstream. Wrap the create_task in try/except RuntimeError, log debug, leave _pending_drain as None. - ``_close_impl`` did not clear ``self._invalidation_cause``. The cached exception holds a traceback chain (frames + locals + globals) that pinned a large object graph across close → re-connect cycles, even though every other piece of post-failure state was scrubbed. Add the one-line clear to the existing scrub block. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent e3a2ab3 commit 24e9c32

1 file changed

Lines changed: 36 additions & 1 deletion

File tree

src/dqliteclient/connection.py

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1247,6 +1247,13 @@ async def _close_impl(self) -> None:
12471247
self._savepoint_stack.clear()
12481248
self._savepoint_implicit_begin = False
12491249
self._has_untracked_savepoint = False
1250+
# Drop the cached invalidation cause. The traceback chain
1251+
# holds frame globals / locals (potentially including the
1252+
# bind-parameter list of a failed executemany), pinning a
1253+
# large object graph across close → re-connect cycles.
1254+
# ``connect()``'s success path already clears it; this
1255+
# close-path clear handles the no-reconnect case.
1256+
self._invalidation_cause = None
12501257
# Clear the loop binding so a subsequent ``connect()`` on a
12511258
# different event loop is accepted by ``_check_in_use``. The
12521259
# failed-connect path already clears this in ``connect()``'s
@@ -1431,9 +1438,37 @@ async def _bounded_drain() -> None:
14311438
with contextlib.suppress(Exception):
14321439
await asyncio.wait_for(proto.wait_closed(), timeout=self._close_timeout)
14331440

1441+
# If a prior ``_invalidate`` already published a
1442+
# pending-drain task, cancel-and-detach it before
1443+
# overwriting. The first orphan would otherwise keep
1444+
# running but be unreachable through ``self``,
1445+
# triggering "Task was destroyed but it is pending"
1446+
# at GC even though ``close()`` awaited the SECOND
1447+
# task. ``connect()``'s pending-retire path uses the
1448+
# same cancel-and-detach idiom; this completes the
1449+
# parity.
1450+
prior = self._pending_drain
1451+
if prior is not None and not prior.done():
1452+
prior.cancel()
14341453
# Strong-ref on self so the task is not GC'd before
14351454
# close() awaits it.
1436-
self._pending_drain = loop.create_task(_bounded_drain())
1455+
try:
1456+
self._pending_drain = loop.create_task(_bounded_drain())
1457+
except RuntimeError as cause:
1458+
# ``loop.create_task`` raises
1459+
# RuntimeError("Event loop is closed") if the
1460+
# loop has been stopped — a real shape during
1461+
# interpreter shutdown / engine.dispose() races.
1462+
# Don't replace the original cancel/cause with a
1463+
# bare RuntimeError; log and move on with no
1464+
# pending drain.
1465+
logger.debug(
1466+
"Connection._invalidate: loop.create_task raised %s "
1467+
"while scheduling _bounded_drain; original cause preserved",
1468+
type(cause).__name__,
1469+
exc_info=True,
1470+
)
1471+
self._pending_drain = None
14371472
self._protocol = None
14381473
self._db_id = None
14391474
self._in_use = False

0 commit comments

Comments
 (0)