@@ -1000,6 +1000,40 @@ async def _initialize_close_unqueued(
10001000 exc_info = True ,
10011001 )
10021002
1003+ async def _close_best_effort (self , conn : DqliteConnection , site : str ) -> None :
1004+ """Best-effort shielded close that logs ``_POOL_CLEANUP_EXCEPTIONS``
1005+ at DEBUG with ``exc_info=True`` rather than silently swallowing
1006+ them.
1007+
1008+ ``site`` is a short identifier appended to the log key
1009+ (``"pool.<site>: close error"``) so operators can locate which
1010+ call site emitted the record. Mirrors the
1011+ ``_initialize_close_unqueued`` discipline — both helpers now log
1012+ through ``exc_info=True`` so the suppressed-exception chain is
1013+ recoverable at DEBUG verbosity instead of being silently
1014+ dropped by ``contextlib.suppress`` (which has no logging
1015+ surface). The asymmetry between the initialize-time helper
1016+ (logged) and the release-time sites (silent) was a residual
1017+ diagnostic gap after the prior ``_POOL_CLEANUP_EXCEPTIONS``
1018+ narrowing pass; routing all release/drain close sites through
1019+ this helper closes it.
1020+
1021+ Cancel-safety: ``asyncio.shield`` runs the close to completion
1022+ even if an outer cancel is delivered during the await; the
1023+ CancelledError still propagates to the caller after the close
1024+ finishes. The CancelledError arm is intentionally NOT caught
1025+ here — preserves the pre-existing behaviour of the sites this
1026+ helper replaces (``contextlib.suppress(*_POOL_CLEANUP_EXCEPTIONS)``
1027+ suppressed only the cleanup-exception tuple; CancelledError
1028+ propagated to the surrounding frame). Sites that need to keep
1029+ running through cancel (like ``_initialize_close_unqueued``'s
1030+ per-conn walk) handle that arm explicitly.
1031+ """
1032+ try :
1033+ await asyncio .shield (conn .close ())
1034+ except _POOL_CLEANUP_EXCEPTIONS :
1035+ logger .debug ("pool.%s: close error" , site , exc_info = True )
1036+
10031037 async def _create_connection (self ) -> DqliteConnection :
10041038 """Create a new connection to the leader.
10051039
@@ -1060,20 +1094,22 @@ async def _put_back_or_release_late_winner(self, conn: DqliteConnection) -> None
10601094 # had happened. Mirrors the ``_drain_idle`` discipline.
10611095 conn ._pool_released = False
10621096 try :
1063- # Suppress the canonical ``_POOL_CLEANUP_EXCEPTIONS``
1064- # tuple (OSError + DqliteConnectionError + ProtocolError
1065- # + OperationalError + InterfaceError), not just
1066- # ``OSError``. A late-winner conn whose transport was
1067- # broken by a peer reset between checkout and close
1068- # raises one of the broader categories from
1069- # ``close()``; pre-fix that exception escaped, the
1097+ # Route through ``_close_best_effort`` so the canonical
1098+ # ``_POOL_CLEANUP_EXCEPTIONS`` tuple (OSError +
1099+ # DqliteConnectionError + ProtocolError +
1100+ # OperationalError + InterfaceError) is absorbed at
1101+ # DEBUG with ``exc_info=True`` (matches the sibling
1102+ # ``_initialize_close_unqueued`` discipline) rather
1103+ # than silently dropped. A late-winner conn whose
1104+ # transport was broken by a peer reset between
1105+ # checkout and close raises one of the broader
1106+ # categories from ``close()``; the helper logs them
1107+ # and proceeds so ``_release_reservation`` still runs
1108+ # below — pre-fix-narrow those exceptions escaped, the
10701109 # ``finally`` restored the flag, and
1071- # ``_release_reservation`` was NEVER reached — the
1072- # reservation slot leaked permanently. Matches the
1073- # canonical ``_release`` discipline at the sibling
1074- # close-and-track path.
1075- with contextlib .suppress (* _POOL_CLEANUP_EXCEPTIONS ):
1076- await asyncio .shield (conn .close ())
1110+ # ``_release_reservation`` was NEVER reached, leaking
1111+ # the reservation slot permanently.
1112+ await self ._close_best_effort (conn , "acquire-late-winner-closed" )
10771113 finally :
10781114 # Restore the flag for contract symmetry with
10791115 # ``_drain_idle``: any stale-reference second close()
@@ -1092,18 +1128,19 @@ async def _put_back_or_release_late_winner(self, conn: DqliteConnection) -> None
10921128 # the reference would leak a live reader task and a
10931129 # socket. Close explicitly and adjust the reservation
10941130 # count so the pool shrinks cleanly instead of leaking.
1095- # Suppress the canonical ``_POOL_CLEANUP_EXCEPTIONS``
1096- # tuple — a half-torn-down transport raises
1097- # ``DqliteConnectionError`` / ``InterfaceError`` /
1098- # ``ProtocolError`` / ``OperationalError`` in addition to
1099- # plain ``OSError``; pre-fix the broader categories
1100- # escaped and the slot leaked. Flip ``_pool_released``
1101- # to ``False`` first so the close actually runs (see
1102- # the closed-pool arm above for the rationale).
1131+ # Route through ``_close_best_effort`` so the canonical
1132+ # ``_POOL_CLEANUP_EXCEPTIONS`` tuple — a half-torn-down
1133+ # transport raises ``DqliteConnectionError`` /
1134+ # ``InterfaceError`` / ``ProtocolError`` /
1135+ # ``OperationalError`` in addition to plain ``OSError`` —
1136+ # is absorbed at DEBUG with ``exc_info=True``; pre-fix-
1137+ # narrow the broader categories escaped and the slot
1138+ # leaked. Flip ``_pool_released`` to ``False`` first so
1139+ # the close actually runs (see the closed-pool arm above
1140+ # for the rationale).
11031141 conn ._pool_released = False
11041142 try :
1105- with contextlib .suppress (* _POOL_CLEANUP_EXCEPTIONS ):
1106- await asyncio .shield (conn .close ())
1143+ await self ._close_best_effort (conn , "acquire-late-winner-queuefull" )
11071144 finally :
11081145 conn ._pool_released = True
11091146 # Route through the helper so the counter stays
@@ -1735,8 +1772,7 @@ async def acquire(self) -> AsyncIterator[DqliteConnection]:
17351772 conn ._pool_released = False
17361773 try :
17371774 try :
1738- with contextlib .suppress (* _POOL_CLEANUP_EXCEPTIONS ):
1739- await asyncio .shield (conn .close ())
1775+ await self ._close_best_effort (conn , "acquire-drain-stale-conn" )
17401776 except RuntimeError :
17411777 # "Event loop is closed" during racing
17421778 # ``engine.dispose()``, or cross-loop
@@ -2173,21 +2209,21 @@ async def _release(self, conn: DqliteConnection) -> None:
21732209 returned_to_queue = False
21742210 try :
21752211 if self ._closed :
2176- # Shield the close so an outer cancel mid-cleanup
2177- # (e.g. ``asyncio.timeout`` around ``engine.dispose()``)
2178- # cannot abort the close mid-``wait_closed`` and orphan
2179- # the StreamReader task. ``_POOL_CLEANUP_EXCEPTIONS``
2180- # does not include ``CancelledError``; the shield is
2181- # the load-bearing guard. Symmetric with every other
2182- # close site in this module.
2183- with contextlib .suppress (* _POOL_CLEANUP_EXCEPTIONS ):
2184- await asyncio .shield (conn .close ())
2212+ # Shield (via ``_close_best_effort``) so an outer cancel
2213+ # mid-cleanup (e.g. ``asyncio.timeout`` around
2214+ # ``engine.dispose()``) cannot abort the close mid-
2215+ # ``wait_closed`` and orphan the StreamReader task.
2216+ # ``_POOL_CLEANUP_EXCEPTIONS`` does not include
2217+ # ``CancelledError``; the shield is the load-bearing
2218+ # guard. ``_POOL_CLEANUP_EXCEPTIONS`` errors are logged
2219+ # at DEBUG with ``exc_info=True`` (mirrors
2220+ # ``_initialize_close_unqueued``).
2221+ await self ._close_best_effort (conn , "release-closed" )
21852222 conn ._pool_released = True
21862223 return
21872224
21882225 if not await self ._reset_connection (conn ):
2189- with contextlib .suppress (* _POOL_CLEANUP_EXCEPTIONS ):
2190- await asyncio .shield (conn .close ())
2226+ await self ._close_best_effort (conn , "release-reset-rolled-back" )
21912227 conn ._pool_released = True
21922228 return
21932229
@@ -2199,8 +2235,7 @@ async def _release(self, conn: DqliteConnection) -> None:
21992235 # the conn is unreachable). Mirrors the close-vs-initialize
22002236 # symmetry fix.
22012237 if self ._closed :
2202- with contextlib .suppress (* _POOL_CLEANUP_EXCEPTIONS ):
2203- await asyncio .shield (conn .close ())
2238+ await self ._close_best_effort (conn , "release-post-reset-closed" )
22042239 conn ._pool_released = True
22052240 return
22062241
@@ -2223,8 +2258,7 @@ async def _release(self, conn: DqliteConnection) -> None:
22232258 try :
22242259 self ._pool .put_nowait (conn )
22252260 except asyncio .QueueFull :
2226- with contextlib .suppress (* _POOL_CLEANUP_EXCEPTIONS ):
2227- await asyncio .shield (conn .close ())
2261+ await self ._close_best_effort (conn , "release-queuefull" )
22282262 conn ._pool_released = True
22292263 else :
22302264 conn ._pool_released = True
0 commit comments