From 3c2c605705ea124ae8d27e5734d6e4d1766cd1d4 Mon Sep 17 00:00:00 2001 From: Elvis Pranskevichus Date: Fri, 2 Oct 2026 12:49:02 -0700 Subject: [PATCH] Test pool min_size backoff on target attribute mismatch Add a test for the pool's `min_size` maintenance loop when reconnecting keeps failing with `TargetServerAttributeNotMatched`. Retries must back off exponentially (1, 2, 4, ... seconds, capped at 60) and log a warning for each failed attempt. --- tests/test_pool.py | 49 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/tests/test_pool.py b/tests/test_pool.py index bc6180e0..fe56e107 100644 --- a/tests/test_pool.py +++ b/tests/test_pool.py @@ -1009,6 +1009,55 @@ async def connect(*args, **kwargs): await self.wait_for_pool_size(pool, 1) self.assertEqual(await pool.fetchval('SELECT 42'), 42) + async def test_pool_min_size_backs_off_on_target_attribute_mismatch(self): + offline = False + attempts = 0 + delays = asyncio.Queue() + resume = asyncio.Queue() + real_sleep = asyncio.sleep + + async def connect(*args, **kwargs): + nonlocal attempts + if offline: + attempts += 1 + raise asyncpg.TargetServerAttributeNotMatched( + 'primary temporarily unavailable') + return await pg_connection.connect(*args, **kwargs) + + async def retry_sleep(delay): + if asyncio.current_task() is not pool._maintenance_task: + return await real_sleep(delay) + delays.put_nowait(delay) + await resume.get() + + async with self.create_pool( + init_size=1, min_size=1, max_size=1, connect=connect, + target_session_attrs='primary', + ) as pool: + offline = True + with self.assertLogs('asyncpg.pool', level='WARNING') as logs, \ + mock.patch.object(pg_pool.asyncio, 'sleep', retry_sleep): + pool._holders[0]._con.terminate() + # Other tasks keep using the real sleep while maintenance + # retries are controlled by the test. + await asyncio.wait_for(asyncio.sleep(0), 5) + for i, expected in enumerate((1, 2, 4, 8, 16, 32, 60, 60), 1): + delay = await asyncio.wait_for(delays.get(), 5) + self.assertEqual(delay, expected) + self.assertEqual(attempts, i) + self.assertEqual(pool.get_size(), 0) + self.assertEqual(pool._queue.qsize(), 1) + if i < 8: + resume.put_nowait(None) + + offline = False + maintenance = pool._maintenance_task + resume.put_nowait(None) + await asyncio.wait_for(maintenance, 5) + self.assertEqual(len(logs.records), 8) + self.assertEqual(pool.get_size(), 1) + self.assertEqual(await pool.fetchval('SELECT 42'), 42) + async def test_pool_floor_reconnect_honors_expired_generation(self): started = asyncio.Event() resume = asyncio.Event()