enhancement
major
major
major
#29838
ClusterManager: a database connection that failed in a cluster transaction is returned to the pool and keeps failing every refetch
Observation
On a PostgreSQL installation running in cluster mode, the periodic ClusterManagerRefetch failed permanently after some event on the database side, while ordinary knowledge base commits went through:
INFO RowLevelLockingSequenceManager - Retry 2 times to get sequence number for 'cm_request' 'ERROR: cannot execute SELECT FOR UPDATE in a read-only transaction' ... ERROR ClusterManager - Failed to refetch cluster manager. org.postgresql.util.PSQLException: ERROR: cannot execute SELECT FOR UPDATE in a read-only transaction at com.top_logic.knowledge.service.db2.RowLevelLockingSequenceManager.nextSequenceNumber(RowLevelLockingSequenceManager.java:94) at com.top_logic.base.cluster.ClusterManager$ClusterManagerDBExecutor.nextSequenceNumber(ClusterManager.java:1400) at com.top_logic.base.cluster.ClusterManager$ClusterManagerDBExecutor.refetch(ClusterManager.java:1601) ...
The read-only state is not set by TopLogic: write connections are created with setReadOnly(false) and PooledConnectionImpl.cleanup() restores the JDBC flag on release. The state comes from the server or the session (e.g. a physical connection bound to a node in recovery after a fail-over, or default_transaction_read_only). Such a state is invisible to the pool: the JDBC flag is a client-side value, and the validation ping (SELECT 1) succeeds on a read-only server as well.
Cause
The two users of the write pool handle a failed transaction differently:
- A knowledge base commit (DefaultDBContext.close(false)) invalidates its connection after a failure, so a broken physical connection leaves the pool with the first failing commit.
- The ClusterManager works through a SimpleDBExecutor, whose closeTransaction() always returns the connection to the pool with releaseWriteConnection(), also after a failure.
A process in which only the cluster manager writes (e.g. a cluster node without user sessions) therefore never gets rid of such a connection: with the LIFO pool the next refetch borrows the same connection again and fails again.
Open question: in the reported case commits did go through at the same time. Within a single process they would have hit the same connection and removed it, so the commits and the failing refetch presumably ran in different processes. This is not yet confirmed.
Solution
SimpleDBExecutor.closeTransaction() invalidates the connection unless the transaction was completed by commitTransaction() or rollbackTransaction(). A transaction aborted by an exception thus drops its physical connection, and the next cluster transaction gets a fresh connection, as a knowledge base commit does.
API change
The unused SimpleDBExecutor methods beginTransaction(CommitHandler), releaseCommitContext() and beginWithDefaultKB() are removed. They took the transaction connection from a commit handler instead of the connection pool the executor was created with. A SimpleDBExecutor now only works with its own transaction started by beginTransaction().