Skip to content

Commit 2c95068

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 or after inactivity for a few minutes, 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 after minutes of inactivity or heavy loads.
1 parent e327332 commit 2c95068

File tree

1 file changed

+36
-25
lines changed

1 file changed

+36
-25
lines changed

asyncpg/pool.py

Lines changed: 36 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -291,40 +291,51 @@ def _can_deactivate_inactive_connection(self) -> bool:
291291
"""Return True if an idle connection may be deactivated (trimmed).
292292
293293
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.
294+
- Do not trim if there are waiters in the pool queue (including acquiring).
295+
- Do not trim below pool min size (leave at least `minsize` open connections).
296+
- Keep at least one idle connection available (i.e., at least 2 idle holders so
297+
trimming one still leaves one idle).
297298
"""
298-
pool = getattr(self, '_pool', None)
299+
pool = getattr(self, "_pool", None)
299300
if pool is None:
301+
# No pool state; allow default trimming behavior.
300302
return True
301303

302-
# If the pool is closing, avoid racing the explicit close path.
303-
if getattr(pool, '_closing', False):
304-
return False
304+
# Follow original logic: if pool is closing, handle as default (allow trim).
305+
if getattr(pool, "_closing", False):
306+
return True
305307

306-
holders = list(getattr(pool, '_holders', []) or [])
307-
total = len(holders)
308-
minsize = int(getattr(pool, '_minsize', 0) or 0)
308+
minsize = int(getattr(pool, "_minsize", 0) or 0)
309309

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.
310+
# Compute the number of tasks waiting to acquire a connection.
311+
q = getattr(pool, "_queue", None)
312+
if q is not None:
313+
getters = getattr(q, "_getters", None)
314+
waiters = len(getters) if getters is not None else 0
315+
else:
316316
waiters = 0
317317

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-
318+
# Include tasks currently in the process of acquiring.
319+
waiters += int(getattr(pool, "_acquiring", 0) or 0)
320+
321+
# Count open (live) connections and how many of them are idle.
322+
open_conns = 0
323+
idle = 0
324+
holders = list(getattr(pool, "_holders", []) or [])
325+
for h in holders:
326+
if getattr(h, "_con", None) is not None:
327+
open_conns += 1
328+
if not getattr(h, "_in_use", None):
329+
idle += 1
330+
331+
# Conditions to allow trimming one idle connection:
332+
# - No waiters.
333+
# - Trimming one won't drop below minsize (so open_conns - 1 >= minsize).
334+
# - After trimming one idle, at least one idle remains (so idle >= 2).
324335
return (
325-
waiters == 0
326-
and idle >= 2
327-
and (total - 1) >= minsize
336+
waiters == 0 and
337+
(open_conns - 1) >= minsize and
338+
idle >= 2
328339
)
329340

330341
def _deactivate_inactive_connection(self) -> None:

0 commit comments

Comments
 (0)