From 40bed9f2be263fdfe84b407781e946fa3eb8abca Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Tue, 24 Mar 2026 22:04:50 -0400 Subject: [PATCH] fix txns for postgres MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The real issue: psycopg2 operates in "implicit transaction" mode by default. Every query — even a simple SELECT — starts a transaction. So after runWithConnection runs a SELECT, the connection is "in a transaction" even though nothing needs committing. Twisted's ConnectionPool.runWithConnection handled this by calling rollback() after each runWithConnection call to close the implicit transaction. The fix should be: commit/rollback after runWithConnection completes (not before the next call), matching Twisted's behavior: --- synapse/storage/native_database.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/synapse/storage/native_database.py b/synapse/storage/native_database.py index 7cbf49ad87..f7338bb7e7 100644 --- a/synapse/storage/native_database.py +++ b/synapse/storage/native_database.py @@ -186,7 +186,15 @@ class NativeConnectionPool: # Get connection inside the executor thread so that # _thread_local resolves to this thread's connection. conn = self._get_connection() - return func(conn, *args, **kwargs) + try: + return func(conn, *args, **kwargs) + finally: + # psycopg2 auto-starts a transaction on every query, even + # SELECTs. Roll back to close the implicit transaction, + # matching Twisted's ConnectionPool.runWithConnection + # which did the same to keep connections clean. + if self._engine.in_transaction(conn): + conn.rollback() loop = asyncio.get_running_loop() return await loop.run_in_executor(self._executor, _inner)