Skip to content

Commit b4b950c

Browse files
author
m.mazaherifard
committed
pool: prevent trimming the last idle connection under load
Previously, the inactivity timer could terminate idle connections even when doing so left the pool effectively empty. Under heavy load this forced the pool to create new connections, causing extra overhead and occasional TimeoutErrors during acquire(). This change adds a guard in PoolConnectionHolder so that idle deactivation only happens when it is safe: - never below pool min_size - never if there are waiters - never removing the last idle connection This ensures the pool retains at least one ready connection and avoids spurious connection churn under load.
1 parent 5b14653 commit b4b950c

File tree

1 file changed

+47
-0
lines changed

1 file changed

+47
-0
lines changed

asyncpg/pool.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,12 +287,59 @@ def _maybe_cancel_inactive_callback(self) -> None:
287287
self._inactive_callback.cancel()
288288
self._inactive_callback = None
289289

290+
def _can_deactivate_inactive_connection(self) -> bool:
291+
"""Return True if an idle connection may be deactivated (trimmed).
292+
293+
Constraints:
294+
- Do not trim if there are waiters in the pool queue.
295+
- Do not trim below pool min size.
296+
- Keep at least one idle connection available.
297+
"""
298+
pool = getattr(self, '_pool', None)
299+
if pool is None:
300+
return True
301+
302+
# If the pool is closing, avoid racing the explicit close path.
303+
if getattr(pool, '_closing', False):
304+
return False
305+
306+
holders = list(getattr(pool, '_holders', []) or [])
307+
total = len(holders)
308+
minsize = int(getattr(pool, '_minsize', 0) or 0)
309+
310+
# Number of tasks waiting to acquire a connection.
311+
q = getattr(pool, '_queue', None)
312+
try:
313+
waiters = q.qsize() if q is not None else 0
314+
except Exception:
315+
# on error, assume no waiters.
316+
waiters = 0
317+
318+
# Count currently idle holders that have a live connection.
319+
idle = sum(
320+
1 for h in holders
321+
if getattr(h, "_in_use", None) is None and getattr(h, "_con", None) is not None
322+
)
323+
324+
return (
325+
waiters == 0
326+
and idle >= 2
327+
and (total - 1) >= minsize
328+
)
329+
290330
def _deactivate_inactive_connection(self) -> None:
291331
if self._in_use is not None:
292332
raise exceptions.InternalClientError(
293333
'attempting to deactivate an acquired connection')
294334

295335
if self._con is not None:
336+
# Only deactivate if doing so respects pool size and demand constraints.
337+
if not self._can_deactivate_inactive_connection():
338+
# Still mark this holder as available and keep the connection.
339+
# Re-arm the inactivity timer so we can reevaluate later.
340+
self._setup_inactive_callback()
341+
return
342+
296343
# The connection is idle and not in use, so it's fine to
297344
# use terminate() instead of close().
298345
self._con.terminate()

0 commit comments

Comments
 (0)