From 627b9558978b486629e81fb3e736a90cda4ea945 Mon Sep 17 00:00:00 2001 From: yumoqing Date: Sat, 27 Jun 2026 14:40:06 +0800 Subject: [PATCH] Fix CancelledError handling in connection pool to prevent connection leaks Problem: High-concurrency streaming requests with client disconnects trigger asyncio.CancelledError (BaseException in Python 3.8+), but exception handlers only caught Exception, causing: - yielded_sqlor.used permanently stays True - sqlor.exit() never called - Connection slots permanently consumed, eventual pool exhaustion Fix: 1. SqlorPool.context(): Use try/finally instead of try/except to always reset yielded_sqlor.used=False, even on CancelledError 2. DBPools.sqlorContext(): Change except Exception to except BaseException, add finally block to always call sqlor.exit(), suppress CancelledError logging noise Verification: Ad-hoc tests confirm connections properly reset on cancellation and can be reused. Normal exceptions still propagate correctly. --- sqlor/dbpools.py | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/sqlor/dbpools.py b/sqlor/dbpools.py index d93913b..cf11f18 100644 --- a/sqlor/dbpools.py +++ b/sqlor/dbpools.py @@ -126,13 +126,10 @@ class SqlorPool: yielded_sqlor.use_at = time.time() try: yield yielded_sqlor.sqlor + finally: + # Always reset state, even on CancelledError (BaseException in Python 3.8+) yielded_sqlor.used = False yielded_sqlor.use_at = time.time() - return - except Exception as e: - yielded_sqlor.used = False - yielded_sqlor.use_at = time.time() - raise e @SingletonDecorator @@ -178,17 +175,23 @@ class DBPools(EventDispatcher): yield sqlor if sqlor and sqlor.dataChanged: await sqlor.commit() - await sqlor.exit() - except Exception as e: + except BaseException as e: self.e_except = e - cb = format_exc() - exception(f'sqlorContext():EXCEPTION{e}, {cb}') + if not isinstance(e, asyncio.CancelledError): + cb = format_exc() + exception(f'sqlorContext():EXCEPTION{e}, {cb}') try: await sqlor.rollback() except: pass - await sqlor.exit() - raise e + raise + finally: + # Always call exit(), even on CancelledError (BaseException in Python 3.8+) + if sqlor: + try: + await sqlor.exit() + except: + pass def get_exception(self): return self.e_except