From 36aa3a5aede7cdccc5d64a7c7ddccca4f2de0b4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Krier?= Date: Fri, 4 Aug 2023 15:59:35 +0200 Subject: [PATCH] Discard disconnected connection from the pool When a connection in the pool is disconnected, it stays in the pool and it may still be served. Using such connection raise an OperationalError and so the request is retried but before that the connection is put back to the pool. So we may get it back again. It is not possible to distinct between an OperationalError due to a disconnection or due to a isolation error for example. So with the current design of Transaction API, an OperationalError always put back the connection to the pool. This main that we can only ensure that the connections served are still connected by executing a simple query. And close connections that are failing. Closes #12452 (grafted from f8aaf62a0c7508e3f3398e47dc2b86e6f8f06cc0) --HG-- branch : 6.0 --- .../trytond/backend/postgresql/database.py | 37 +++++++++++-------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/trytond/trytond/backend/postgresql/database.py b/trytond/trytond/backend/postgresql/database.py index 87575dc2ee..1797042486 100644 --- a/trytond/trytond/backend/postgresql/database.py +++ b/trytond/trytond/backend/postgresql/database.py @@ -257,10 +257,10 @@ class Database(DatabaseInterface): return self def get_connection(self, autocommit=False, readonly=False): - for count in range(config.getint('database', 'retry'), -1, -1): + retry = max(config.getint('database', 'retry'), _maxconn) + for count in range(retry, -1, -1): try: conn = self._connpool.getconn() - break except PoolError: if count and not self._connpool.closed: logger.info('waiting a connection') @@ -271,19 +271,26 @@ class Database(DatabaseInterface): logger.error( 'connection to "%s" failed', self.name, exc_info=True) raise - # We do not use set_session because psycopg2 < 2.7 and psycopg2cffi - # change the default_transaction_* attributes which breaks external - # pooling at the transaction level. - if autocommit: - conn.set_isolation_level(ISOLATION_LEVEL_AUTOCOMMIT) - else: - conn.set_isolation_level(ISOLATION_LEVEL_REPEATABLE_READ) - # psycopg2cffi does not have the readonly property - if hasattr(conn, 'readonly'): - conn.readonly = readonly - elif not autocommit and readonly: - cursor = conn.cursor() - cursor.execute('SET TRANSACTION READ ONLY') + try: + # We do not use set_session because psycopg2 < 2.7 and + # psycopg2cffi change the default_transaction_* attributes + # which breaks external pooling at the transaction level. + if autocommit: + conn.set_isolation_level(ISOLATION_LEVEL_AUTOCOMMIT) + else: + conn.set_isolation_level(ISOLATION_LEVEL_REPEATABLE_READ) + # psycopg2cffi does not have the readonly property + if hasattr(conn, 'readonly'): + conn.readonly = readonly + elif not autocommit and readonly: + cursor = conn.cursor() + cursor.execute('SET TRANSACTION READ ONLY') + # Detect disconnection + conn.cursor().execute('SELECT 1') + except DatabaseOperationalError: + self._connpool.putconn(conn, close=True) + continue + break return conn def put_connection(self, connection, close=False):