Merge "rdbms: make Database query error handling more strict"
[lhc/web/wiklou.git] / includes / libs / rdbms / loadbalancer / LoadBalancer.php
index e95e23f..7c1b9d9 100644 (file)
@@ -54,6 +54,8 @@ class LoadBalancer implements ILoadBalancer {
        private $loadMonitorConfig;
        /** @var array[] $aliases Map of (table => (dbname, schema, prefix) map) */
        private $tableAliases = [];
+       /** @var string[] Map of (index alias => index) */
+       private $indexAliases = [];
 
        /** @var ILoadMonitor */
        private $loadMonitor;
@@ -680,6 +682,22 @@ class LoadBalancer implements ILoadBalancer {
                        $domain = false; // local connection requested
                }
 
+               if ( ( $flags & self::CONN_TRX_AUTO ) === self::CONN_TRX_AUTO ) {
+                       // Assuming all servers are of the same type (or similar), which is overwhelmingly
+                       // the case, use the master server information to get the attributes. The information
+                       // for $i cannot be used since it might be DB_REPLICA, which might require connection
+                       // attempts in order to be resolved into a real server index.
+                       $attributes = $this->getServerAttributes( $this->getWriterIndex() );
+                       if ( $attributes[Database::ATTR_DB_LEVEL_LOCKING] ) {
+                               // Callers sometimes want to (a) escape REPEATABLE-READ stateness without locking
+                               // rows (e.g. FOR UPDATE) or (b) make small commits during a larger transactions
+                               // to reduce lock contention. None of these apply for sqlite and using separate
+                               // connections just causes self-deadlocks.
+                               $flags &= ~self::CONN_TRX_AUTO;
+                               $this->connLogger->info( __METHOD__ . ': ignoring CONN_TRX_AUTO to avoid deadlocks.' );
+                       }
+               }
+
                $groups = ( $groups === false || $groups === [] )
                        ? [ false ] // check one "group": the generic pool
                        : (array)$groups;
@@ -739,7 +757,7 @@ class LoadBalancer implements ILoadBalancer {
                return $conn;
        }
 
-       public function reuseConnection( $conn ) {
+       public function reuseConnection( IDatabase $conn ) {
                $serverIndex = $conn->getLBInfo( 'serverIndex' );
                $refCount = $conn->getLBInfo( 'foreignPoolRefCount' );
                if ( $serverIndex === null || $refCount === null ) {
@@ -981,6 +999,13 @@ class LoadBalancer implements ILoadBalancer {
                return $conn;
        }
 
+       public function getServerAttributes( $i ) {
+               return Database::attributesFromType(
+                       $this->getServerType( $i ),
+                       isset( $this->servers[$i]['driver'] ) ? $this->servers[$i]['driver'] : null
+               );
+       }
+
        /**
         * Test if the specified index represents an open connection
         *
@@ -1065,6 +1090,7 @@ class LoadBalancer implements ILoadBalancer {
                        $this->getLazyConnectionRef( self::DB_MASTER, [], $db->getDomainID() )
                );
                $db->setTableAliases( $this->tableAliases );
+               $db->setIndexAliases( $this->indexAliases );
 
                if ( $server['serverIndex'] === $this->getWriterIndex() ) {
                        if ( $this->trxRoundId !== false ) {
@@ -1347,11 +1373,12 @@ class LoadBalancer implements ILoadBalancer {
                $e = null; // first exception
                $this->forEachOpenMasterConnection( function ( Database $conn ) use ( $type, &$e ) {
                        $conn->setTrxEndCallbackSuppression( false );
-                       if ( $conn->writesOrCallbacksPending() ) {
-                               // This happens if onTransactionIdle() callbacks leave callbacks on *another* DB
-                               // (which finished its callbacks already). Warn and recover in this case. Let the
-                               // callbacks run in the final commitMasterChanges() in LBFactory::shutdown().
-                               $this->queryLogger->info( __METHOD__ . ": found writes/callbacks pending." );
+                       // Callbacks run in AUTO-COMMIT mode, so make sure no transactions are pending...
+                       if ( $conn->writesPending() ) {
+                               // This happens if onTransactionIdle() callbacks write to *other* handles
+                               // (which already finished their callbacks). Let any callbacks run in the final
+                               // commitMasterChanges() in LBFactory::shutdown(), when the transaction is gone.
+                               $this->queryLogger->warning( __METHOD__ . ": found writes pending." );
                                return;
                        } elseif ( $conn->trxLevel() ) {
                                // This happens for single-DB setups where DB_REPLICA uses the master DB,
@@ -1381,9 +1408,7 @@ class LoadBalancer implements ILoadBalancer {
                $this->trxRoundId = false;
                $this->forEachOpenMasterConnection(
                        function ( IDatabase $conn ) use ( $fname, $restore ) {
-                               if ( $conn->writesOrCallbacksPending() || $conn->explicitTrxActive() ) {
-                                       $conn->rollback( $fname, $conn::FLUSHING_ALL_PEERS );
-                               }
+                               $conn->rollback( $fname, $conn::FLUSHING_ALL_PEERS );
                                if ( $restore ) {
                                        $this->undoTransactionRoundFlags( $conn );
                                }
@@ -1398,6 +1423,12 @@ class LoadBalancer implements ILoadBalancer {
        }
 
        /**
+        * Make all DB servers with DBO_DEFAULT/DBO_TRX set join the transaction round
+        *
+        * Some servers may have neither flag enabled, meaning that they opt out of such
+        * transaction rounds and remain in auto-commit mode. Such behavior might be desired
+        * when a DB server is used for something like simple key/value storage.
+        *
         * @param IDatabase $conn
         */
        private function applyTransactionRoundFlags( IDatabase $conn ) {
@@ -1409,9 +1440,10 @@ class LoadBalancer implements ILoadBalancer {
                        // DBO_TRX is controlled entirely by CLI mode presence with DBO_DEFAULT.
                        // Force DBO_TRX even in CLI mode since a commit round is expected soon.
                        $conn->setFlag( $conn::DBO_TRX, $conn::REMEMBER_PRIOR );
-                       // If config has explicitly requested DBO_TRX be either on or off by not
-                       // setting DBO_DEFAULT, then respect that. Forcing no transactions is useful
-                       // for things like blob stores (ExternalStore) which want auto-commit mode.
+               }
+
+               if ( $conn->getFlag( $conn::DBO_TRX ) ) {
+                       $conn->setLBInfo( 'trxRoundId', $this->trxRoundId );
                }
        }
 
@@ -1423,6 +1455,10 @@ class LoadBalancer implements ILoadBalancer {
                        return; // transaction rounds do not apply to these connections
                }
 
+               if ( $conn->getFlag( $conn::DBO_TRX ) ) {
+                       $conn->setLBInfo( 'trxRoundId', false );
+               }
+
                if ( $conn->getFlag( $conn::DBO_DEFAULT ) ) {
                        $conn->restoreFlags( $conn::RESTORE_PRIOR );
                }
@@ -1734,6 +1770,10 @@ class LoadBalancer implements ILoadBalancer {
                $this->tableAliases = $aliases;
        }
 
+       public function setIndexAliases( array $aliases ) {
+               $this->indexAliases = $aliases;
+       }
+
        public function setDomainPrefix( $prefix ) {
                // Find connections to explicit foreign domains still marked as in-use...
                $domainsInUse = [];