Merge "add IGNORE INDEX option to mysql database handler"
[lhc/web/wiklou.git] / includes / db / Database.php
index 874e9c4..0a1774d 100644 (file)
@@ -39,6 +39,11 @@ abstract class DatabaseBase implements IDatabase {
 
        /** How long before it is worth doing a dummy query to test the connection */
        const PING_TTL = 1.0;
+       const PING_QUERY = 'SELECT 1 AS ping';
+
+       const TINY_WRITE_SEC = .010;
+       const SLOW_WRITE_SEC = .500;
+       const SMALL_WRITE_ROWS = 100;
 
        /** @var string SQL query */
        protected $mLastQuery = '';
@@ -54,6 +59,8 @@ abstract class DatabaseBase implements IDatabase {
        protected $mPassword;
        /** @var string */
        protected $mDBname;
+       /** @var bool */
+       protected $cliMode;
 
        /** @var BagOStuff APC cache */
        protected $srvCache;
@@ -69,8 +76,10 @@ abstract class DatabaseBase implements IDatabase {
        protected $mTrxPreCommitCallbacks = [];
        /** @var array[] List of (callable, method name) */
        protected $mTrxEndCallbacks = [];
-       /** @var bool Whether to suppress triggering of post-commit callbacks */
-       protected $suppressPostCommitCallbacks = false;
+       /** @var array[] Map of (name => (callable, method name)) */
+       protected $mTrxRecurringCallbacks = [];
+       /** @var bool Whether to suppress triggering of transaction end callbacks */
+       protected $mTrxEndCallbacksSuppressed = false;
 
        /** @var string */
        protected $mTablePrefix;
@@ -102,7 +111,6 @@ abstract class DatabaseBase implements IDatabase {
         * @var int
         */
        protected $mTrxLevel = 0;
-
        /**
         * Either a short hexidecimal string if a transaction is active or ""
         *
@@ -110,7 +118,6 @@ abstract class DatabaseBase implements IDatabase {
         * @see DatabaseBase::mTrxLevel
         */
        protected $mTrxShortId = '';
-
        /**
         * The UNIX time that the transaction started. Callers can assume that if
         * snapshot isolation is used, then the data is *at least* up to date to that
@@ -120,10 +127,8 @@ abstract class DatabaseBase implements IDatabase {
         * @see DatabaseBase::mTrxLevel
         */
        private $mTrxTimestamp = null;
-
        /** @var float Lag estimate at the time of BEGIN */
-       private $mTrxSlaveLag = null;
-
+       private $mTrxReplicaLag = null;
        /**
         * Remembers the function name given for starting the most recent transaction via begin().
         * Used to provide additional context for error reporting.
@@ -132,7 +137,6 @@ abstract class DatabaseBase implements IDatabase {
         * @see DatabaseBase::mTrxLevel
         */
        private $mTrxFname = null;
-
        /**
         * Record if possible write queries were done in the last transaction started
         *
@@ -140,7 +144,6 @@ abstract class DatabaseBase implements IDatabase {
         * @see DatabaseBase::mTrxLevel
         */
        private $mTrxDoneWrites = false;
-
        /**
         * Record if the current transaction was started implicitly due to DBO_TRX being set.
         *
@@ -148,34 +151,44 @@ abstract class DatabaseBase implements IDatabase {
         * @see DatabaseBase::mTrxLevel
         */
        private $mTrxAutomatic = false;
-
        /**
         * Array of levels of atomicity within transactions
         *
         * @var array
         */
        private $mTrxAtomicLevels = [];
-
        /**
         * Record if the current transaction was started implicitly by DatabaseBase::startAtomic
         *
         * @var bool
         */
        private $mTrxAutomaticAtomic = false;
-
        /**
         * Track the write query callers of the current transaction
         *
         * @var string[]
         */
        private $mTrxWriteCallers = [];
-
        /**
-        * Track the seconds spent in write queries for the current transaction
-        *
-        * @var float
+        * @var float Seconds spent in write queries for the current transaction
         */
        private $mTrxWriteDuration = 0.0;
+       /**
+        * @var integer Number of write queries for the current transaction
+        */
+       private $mTrxWriteQueryCount = 0;
+       /**
+        * @var float Like mTrxWriteQueryCount but excludes lock-bound, easy to replicate, queries
+        */
+       private $mTrxWriteAdjDuration = 0.0;
+       /**
+        * @var integer Number of write queries counted in mTrxWriteAdjDuration
+        */
+       private $mTrxWriteAdjQueryCount = 0;
+       /**
+        * @var float RTT time estimate
+        */
+       private $mRTTEstimate = 0.0;
 
        /** @var array Map of (name => 1) for locks obtained via lock() */
        private $mNamedLocksHeld = [];
@@ -419,8 +432,26 @@ abstract class DatabaseBase implements IDatabase {
                );
        }
 
-       public function pendingWriteQueryDuration() {
-               return $this->mTrxLevel ? $this->mTrxWriteDuration : false;
+       public function pendingWriteQueryDuration( $type = self::ESTIMATE_TOTAL ) {
+               if ( !$this->mTrxLevel ) {
+                       return false;
+               } elseif ( !$this->mTrxDoneWrites ) {
+                       return 0.0;
+               }
+
+               switch ( $type ) {
+                       case self::ESTIMATE_DB_APPLY:
+                               $this->ping( $rtt );
+                               $rttAdjTotal = $this->mTrxWriteAdjQueryCount * $rtt;
+                               $applyTime = max( $this->mTrxWriteAdjDuration - $rttAdjTotal, 0 );
+                               // For omitted queries, make them count as something at least
+                               $omitted = $this->mTrxWriteQueryCount - $this->mTrxWriteAdjQueryCount;
+                               $applyTime += self::TINY_WRITE_SEC * $omitted;
+
+                               return $applyTime;
+                       default: // everything
+                               return $this->mTrxWriteDuration;
+               }
        }
 
        public function pendingWriteCallers() {
@@ -541,7 +572,7 @@ abstract class DatabaseBase implements IDatabase {
         * @param array $params Parameters passed from DatabaseBase::factory()
         */
        function __construct( array $params ) {
-               global $wgDBprefix, $wgDBmwschema, $wgCommandLineMode;
+               global $wgDBprefix, $wgDBmwschema;
 
                $this->srvCache = ObjectCache::getLocalServerInstance( 'hash' );
 
@@ -554,9 +585,13 @@ abstract class DatabaseBase implements IDatabase {
                $schema = $params['schema'];
                $foreign = $params['foreign'];
 
+               $this->cliMode = isset( $params['cliMode'] )
+                       ? $params['cliMode']
+                       : ( PHP_SAPI === 'cli' );
+
                $this->mFlags = $flags;
                if ( $this->mFlags & DBO_DEFAULT ) {
-                       if ( $wgCommandLineMode ) {
+                       if ( $this->cliMode ) {
                                $this->mFlags &= ~DBO_TRX;
                        } else {
                                $this->mFlags |= DBO_TRX;
@@ -627,6 +662,8 @@ abstract class DatabaseBase implements IDatabase {
         * @return DatabaseBase|null DatabaseBase subclass or null
         */
        final public static function factory( $dbType, $p = [] ) {
+               global $wgCommandLineMode;
+
                $canonicalDBTypes = [
                        'mysql' => [ 'mysqli', 'mysql' ],
                        'postgres' => [],
@@ -684,6 +721,7 @@ abstract class DatabaseBase implements IDatabase {
                                $p['schema'] = isset( $defaultSchemas[$dbType] ) ? $defaultSchemas[$dbType] : null;
                        }
                        $p['foreign'] = isset( $p['foreign'] ) ? $p['foreign'] : false;
+                       $p['cliMode'] = $wgCommandLineMode;
 
                        return new $class( $p );
                } else {
@@ -808,7 +846,16 @@ abstract class DatabaseBase implements IDatabase {
         * @return bool
         */
        protected function isWriteQuery( $sql ) {
-               return !preg_match( '/^(?:SELECT|BEGIN|ROLLBACK|COMMIT|SET|SHOW|EXPLAIN|\(SELECT)\b/i', $sql );
+               return !preg_match(
+                       '/^(?:SELECT|BEGIN|ROLLBACK|COMMIT|SET|SHOW|EXPLAIN|\(SELECT)\b/i', $sql );
+       }
+
+       /**
+        * @param $sql
+        * @return string|null
+        */
+       protected function getQueryVerb( $sql ) {
+               return preg_match( '/^\s*([a-z]+)/i', $sql, $m ) ? strtoupper( $m[1] ) : null;
        }
 
        /**
@@ -821,8 +868,8 @@ abstract class DatabaseBase implements IDatabase {
         * @return bool
         */
        protected function isTransactableQuery( $sql ) {
-               $verb = substr( $sql, 0, strcspn( $sql, " \t\r\n" ) );
-               return !in_array( $verb, [ 'BEGIN', 'COMMIT', 'ROLLBACK', 'SHOW', 'SET' ] );
+               $verb = $this->getQueryVerb( $sql );
+               return !in_array( $verb, [ 'BEGIN', 'COMMIT', 'ROLLBACK', 'SHOW', 'SET' ], true );
        }
 
        public function query( $sql, $fname = __METHOD__, $tempIgnore = false ) {
@@ -945,18 +992,22 @@ abstract class DatabaseBase implements IDatabase {
                $this->profiler->profileIn( $queryProf );
                $ret = $this->doQuery( $commentedSql );
                $this->profiler->profileOut( $queryProf );
-               $queryRuntime = microtime( true ) - $startTime;
+               $queryRuntime = max( microtime( true ) - $startTime, 0.0 );
 
                unset( $queryProfSection ); // profile out (if set)
 
                if ( $ret !== false ) {
                        $this->lastPing = $startTime;
                        if ( $isWrite && $this->mTrxLevel ) {
-                               $this->mTrxWriteDuration += $queryRuntime;
+                               $this->updateTrxWriteQueryTime( $sql, $queryRuntime );
                                $this->mTrxWriteCallers[] = $fname;
                        }
                }
 
+               if ( $sql === self::PING_QUERY ) {
+                       $this->mRTTEstimate = $queryRuntime;
+               }
+
                $this->getTransactionProfiler()->recordQueryCompletion(
                        $queryProf, $startTime, $isWrite, $this->affectedRows()
                );
@@ -965,6 +1016,38 @@ abstract class DatabaseBase implements IDatabase {
                return $ret;
        }
 
+       /**
+        * Update the estimated run-time of a query, not counting large row lock times
+        *
+        * LoadBalancer can be set to rollback transactions that will create huge replication
+        * lag. It bases this estimate off of pendingWriteQueryDuration(). Certain simple
+        * queries, like inserting a row can take a long time due to row locking. This method
+        * uses some simple heuristics to discount those cases.
+        *
+        * @param string $sql A SQL write query
+        * @param float $runtime Total runtime, including RTT
+        */
+       private function updateTrxWriteQueryTime( $sql, $runtime ) {
+               // Whether this is indicative of replica DB runtime (except for RBR or ws_repl)
+               $indicativeOfReplicaRuntime = true;
+               if ( $runtime > self::SLOW_WRITE_SEC ) {
+                       $verb = $this->getQueryVerb( $sql );
+                       // insert(), upsert(), replace() are fast unless bulky in size or blocked on locks
+                       if ( $verb === 'INSERT' ) {
+                               $indicativeOfReplicaRuntime = $this->affectedRows() > self::SMALL_WRITE_ROWS;
+                       } elseif ( $verb === 'REPLACE' ) {
+                               $indicativeOfReplicaRuntime = $this->affectedRows() > self::SMALL_WRITE_ROWS / 2;
+                       }
+               }
+
+               $this->mTrxWriteDuration += $runtime;
+               $this->mTrxWriteQueryCount += 1;
+               if ( $indicativeOfReplicaRuntime ) {
+                       $this->mTrxWriteAdjDuration += $runtime;
+                       $this->mTrxWriteAdjQueryCount += 1;
+               }
+       }
+
        private function canRecoverFromDisconnect( $sql, $priorWritesPending ) {
                # Transaction dropped; this can mean lost writes, or REPEATABLE-READ snapshots.
                # Dropped connections also mean that named locks are automatically released.
@@ -992,6 +1075,7 @@ abstract class DatabaseBase implements IDatabase {
                try {
                        // Handle callbacks in mTrxEndCallbacks
                        $this->runOnTransactionIdleCallbacks( self::TRIGGER_ROLLBACK );
+                       $this->runTransactionListenerCallbacks( self::TRIGGER_ROLLBACK );
                        return null;
                } catch ( Exception $e ) {
                        // Already logged; move on...
@@ -1260,8 +1344,13 @@ abstract class DatabaseBase implements IDatabase {
                } else {
                        $useIndex = '';
                }
+               if ( isset( $options['IGNORE INDEX'] ) && is_string( $options['IGNORE INDEX'] ) ) {
+                       $ignoreIndex = $this->ignoreIndexClause( $options['IGNORE INDEX'] );
+               } else {
+                       $ignoreIndex = '';
+               }
 
-               return [ $startOpts, $useIndex, $preLimitTail, $postLimitTail ];
+               return [ $startOpts, $useIndex, $preLimitTail, $postLimitTail, $ignoreIndex ];
        }
 
        /**
@@ -1329,31 +1418,34 @@ abstract class DatabaseBase implements IDatabase {
                $useIndexes = ( isset( $options['USE INDEX'] ) && is_array( $options['USE INDEX'] ) )
                        ? $options['USE INDEX']
                        : [];
+               $ignoreIndexes = ( isset( $options['IGNORE INDEX'] ) && is_array( $options['IGNORE INDEX'] ) )
+                       ? $options['IGNORE INDEX']
+                       : [];
 
                if ( is_array( $table ) ) {
                        $from = ' FROM ' .
-                               $this->tableNamesWithUseIndexOrJOIN( $table, $useIndexes, $join_conds );
+                               $this->tableNamesWithIndexClauseOrJOIN( $table, $useIndexes, $ignoreIndexes, $join_conds );
                } elseif ( $table != '' ) {
                        if ( $table[0] == ' ' ) {
                                $from = ' FROM ' . $table;
                        } else {
                                $from = ' FROM ' .
-                                       $this->tableNamesWithUseIndexOrJOIN( [ $table ], $useIndexes, [] );
+                                       $this->tableNamesWithIndexClauseOrJOIN( [ $table ], $useIndexes, $ignoreIndexes, [] );
                        }
                } else {
                        $from = '';
                }
 
-               list( $startOpts, $useIndex, $preLimitTail, $postLimitTail ) =
+               list( $startOpts, $useIndex, $preLimitTail, $postLimitTail, $ignoreIndex ) =
                        $this->makeSelectOptions( $options );
 
                if ( !empty( $conds ) ) {
                        if ( is_array( $conds ) ) {
                                $conds = $this->makeList( $conds, LIST_AND );
                        }
-                       $sql = "SELECT $startOpts $vars $from $useIndex WHERE $conds $preLimitTail";
+                       $sql = "SELECT $startOpts $vars $from $useIndex $ignoreIndex WHERE $conds $preLimitTail";
                } else {
-                       $sql = "SELECT $startOpts $vars $from $useIndex $preLimitTail";
+                       $sql = "SELECT $startOpts $vars $from $useIndex $ignoreIndex $preLimitTail";
                }
 
                if ( isset( $options['LIMIT'] ) ) {
@@ -1726,6 +1818,15 @@ abstract class DatabaseBase implements IDatabase {
                return '(' . $this->selectSQLText( $table, $fld, $conds, null, [], $join_conds ) . ')';
        }
 
+       /**
+        * @param string $field Field or column to cast
+        * @return string
+        * @since 1.28
+        */
+       public function buildStringCast( $field ) {
+               return $field;
+       }
+
        public function selectDB( $db ) {
                # Stub. Shouldn't cause serious problems if it's not overridden, but
                # if your database engine supports a concept similar to MySQL's
@@ -1955,19 +2056,21 @@ abstract class DatabaseBase implements IDatabase {
 
        /**
         * Get the aliased table name clause for a FROM clause
-        * which might have a JOIN and/or USE INDEX clause
+        * which might have a JOIN and/or USE INDEX or IGNORE INDEX clause
         *
         * @param array $tables ( [alias] => table )
         * @param array $use_index Same as for select()
+        * @param array $ignore_index Same as for select()
         * @param array $join_conds Same as for select()
         * @return string
         */
-       protected function tableNamesWithUseIndexOrJOIN(
-               $tables, $use_index = [], $join_conds = []
+       protected function tableNamesWithIndexClauseOrJOIN(
+               $tables, $use_index = [], $ignore_index = [], $join_conds = []
        ) {
                $ret = [];
                $retJOIN = [];
                $use_index = (array)$use_index;
+               $ignore_index = (array)$ignore_index;
                $join_conds = (array)$join_conds;
 
                foreach ( $tables as $alias => $table ) {
@@ -1986,6 +2089,12 @@ abstract class DatabaseBase implements IDatabase {
                                                $tableClause .= ' ' . $use;
                                        }
                                }
+                               if ( isset( $ignore_index[$alias] ) ) { // has IGNORE INDEX?
+                                       $ignore = $this->ignoreIndexClause( implode( ',', (array)$ignore_index[$alias] ) );
+                                       if ( $ignore != '' ) {
+                                               $tableClause .= ' ' . $ignore;
+                                       }
+                               }
                                $on = $this->makeList( (array)$conds, LIST_AND );
                                if ( $on != '' ) {
                                        $tableClause .= ' ON (' . $on . ')';
@@ -1999,6 +2108,14 @@ abstract class DatabaseBase implements IDatabase {
                                        implode( ',', (array)$use_index[$alias] )
                                );
 
+                               $ret[] = $tableClause;
+                       } elseif ( isset( $ignore_index[$alias] ) ) {
+                               // Is there an INDEX clause for this table?
+                               $tableClause = $this->tableNameWithAlias( $table, $alias );
+                               $tableClause .= ' ' . $this->ignoreIndexClause(
+                                       implode( ',', (array)$ignore_index[$alias] )
+                               );
+
                                $ret[] = $tableClause;
                        } else {
                                $tableClause = $this->tableNameWithAlias( $table, $alias );
@@ -2131,6 +2248,20 @@ abstract class DatabaseBase implements IDatabase {
                return '';
        }
 
+       /**
+        * IGNORE INDEX clause. Unlikely to be useful for anything but MySQL. This
+        * is only needed because a) MySQL must be as efficient as possible due to
+        * its use on Wikipedia, and b) MySQL 4.0 is kind of dumb sometimes about
+        * which index to pick. Anyway, other databases might have different
+        * indexes on a given table. So don't bother overriding this unless you're
+        * MySQL.
+        * @param string $index
+        * @return string
+        */
+       public function ignoreIndexClause( $index ) {
+               return '';
+       }
+
        public function replace( $table, $uniqueIndexes, $rows, $fname = __METHOD__ ) {
                $quotedTable = $this->tableName( $table );
 
@@ -2256,12 +2387,12 @@ abstract class DatabaseBase implements IDatabase {
                        $ok = $this->insert( $table, $rows, $fname, [ 'IGNORE' ] ) && $ok;
                } catch ( Exception $e ) {
                        if ( $useTrx ) {
-                               $this->rollback( $fname );
+                               $this->rollback( $fname, self::FLUSHING_INTERNAL );
                        }
                        throw $e;
                }
                if ( $useTrx ) {
-                       $this->commit( $fname, self::TRANSACTION_INTERNAL );
+                       $this->commit( $fname, self::FLUSHING_INTERNAL );
                }
 
                return $ok;
@@ -2340,7 +2471,46 @@ abstract class DatabaseBase implements IDatabase {
                return $this->query( $sql, $fname );
        }
 
-       public function insertSelect( $destTable, $srcTable, $varMap, $conds,
+       public function insertSelect(
+               $destTable, $srcTable, $varMap, $conds,
+               $fname = __METHOD__, $insertOptions = [], $selectOptions = []
+       ) {
+               if ( $this->cliMode ) {
+                       // For massive migrations with downtime, we don't want to select everything
+                       // into memory and OOM, so do all this native on the server side if possible.
+                       return $this->nativeInsertSelect(
+                               $destTable,
+                               $srcTable,
+                               $varMap,
+                               $conds,
+                               $fname,
+                               $insertOptions,
+                               $selectOptions
+                       );
+               }
+
+               // For web requests, do a locking SELECT and then INSERT. This puts the SELECT burden
+               // on only the master (without needing row-based-replication). It also makes it easy to
+               // know how big the INSERT is going to be.
+               $fields = [];
+               foreach ( $varMap as $dstColumn => $sourceColumnOrSql ) {
+                       $fields[] = $this->fieldNameWithAlias( $sourceColumnOrSql, $dstColumn );
+               }
+               $selectOptions[] = 'FOR UPDATE';
+               $res = $this->select( $srcTable, implode( ',', $fields ), $conds, $fname, $selectOptions );
+               if ( !$res ) {
+                       return false;
+               }
+
+               $rows = [];
+               foreach ( $res as $row ) {
+                       $rows[] = (array)$row;
+               }
+
+               return $this->insert( $destTable, $rows, $fname, $insertOptions );
+       }
+
+       public function nativeInsertSelect( $destTable, $srcTable, $varMap, $conds,
                $fname = __METHOD__,
                $insertOptions = [], $selectOptions = []
        ) {
@@ -2356,7 +2526,8 @@ abstract class DatabaseBase implements IDatabase {
                        $selectOptions = [ $selectOptions ];
                }
 
-               list( $startOpts, $useIndex, $tailOpts ) = $this->makeSelectOptions( $selectOptions );
+               list( $startOpts, $useIndex, $tailOpts, $ignoreIndex ) = $this->makeSelectOptions(
+                       $selectOptions );
 
                if ( is_array( $srcTable ) ) {
                        $srcTable = implode( ',', array_map( [ &$this, 'tableName' ], $srcTable ) );
@@ -2366,7 +2537,7 @@ abstract class DatabaseBase implements IDatabase {
 
                $sql = "INSERT $insertOptions INTO $destTable (" . implode( ',', array_keys( $varMap ) ) . ')' .
                        " SELECT $startOpts " . implode( ',', $varMap ) .
-                       " FROM $srcTable $useIndex ";
+                       " FROM $srcTable $useIndex $ignoreIndex ";
 
                if ( $conds != '*' ) {
                        if ( is_array( $conds ) ) {
@@ -2560,27 +2731,35 @@ abstract class DatabaseBase implements IDatabase {
                        $this->mTrxPreCommitCallbacks[] = [ $callback, wfGetCaller() ];
                } else {
                        // If no transaction is active, then make one for this callback
-                       $this->begin( __METHOD__, self::TRANSACTION_INTERNAL );
+                       $this->startAtomic( __METHOD__ );
                        try {
                                call_user_func( $callback );
-                               $this->commit( __METHOD__ );
+                               $this->endAtomic( __METHOD__ );
                        } catch ( Exception $e ) {
-                               $this->rollback( __METHOD__ );
+                               $this->rollback( __METHOD__, self::FLUSHING_INTERNAL );
                                throw $e;
                        }
                }
        }
 
+       final public function setTransactionListener( $name, callable $callback = null ) {
+               if ( $callback ) {
+                       $this->mTrxRecurringCallbacks[$name] = [ $callback, wfGetCaller() ];
+               } else {
+                       unset( $this->mTrxRecurringCallbacks[$name] );
+               }
+       }
+
        /**
-        * Whether to disable running of post-commit callbacks
+        * Whether to disable running of post-COMMIT/ROLLBACK callbacks
         *
         * This method should not be used outside of Database/LoadBalancer
         *
         * @param bool $suppress
         * @since 1.28
         */
-       final public function setPostCommitCallbackSupression( $suppress ) {
-               $this->suppressPostCommitCallbacks = $suppress;
+       final public function setTrxEndCallbackSuppression( $suppress ) {
+               $this->mTrxEndCallbacksSuppressed = $suppress;
        }
 
        /**
@@ -2593,7 +2772,7 @@ abstract class DatabaseBase implements IDatabase {
         * @throws Exception
         */
        public function runOnTransactionIdleCallbacks( $trigger ) {
-               if ( $this->suppressPostCommitCallbacks ) {
+               if ( $this->mTrxEndCallbacksSuppressed ) {
                        return;
                }
 
@@ -2623,7 +2802,7 @@ abstract class DatabaseBase implements IDatabase {
                                        // Some callbacks may use startAtomic/endAtomic, so make sure
                                        // their transactions are ended so other callbacks don't fail
                                        if ( $this->trxLevel() ) {
-                                               $this->rollback( __METHOD__ );
+                                               $this->rollback( __METHOD__, self::FLUSHING_INTERNAL );
                                        }
                                }
                        }
@@ -2663,10 +2842,41 @@ abstract class DatabaseBase implements IDatabase {
                }
        }
 
+       /**
+        * Actually run any "transaction listener" callbacks.
+        *
+        * This method should not be used outside of Database/LoadBalancer
+        *
+        * @param integer $trigger IDatabase::TRIGGER_* constant
+        * @throws Exception
+        * @since 1.20
+        */
+       public function runTransactionListenerCallbacks( $trigger ) {
+               if ( $this->mTrxEndCallbacksSuppressed ) {
+                       return;
+               }
+
+               /** @var Exception $e */
+               $e = null; // first exception
+
+               foreach ( $this->mTrxRecurringCallbacks as $callback ) {
+                       try {
+                               list( $phpCallback ) = $callback;
+                               $phpCallback( $trigger, $this );
+                       } catch ( Exception $ex ) {
+                               MWExceptionHandler::logException( $ex );
+                               $e = $e ?: $ex;
+                       }
+               }
+
+               if ( $e instanceof Exception ) {
+                       throw $e; // re-throw any first exception
+               }
+       }
+
        final public function startAtomic( $fname = __METHOD__ ) {
                if ( !$this->mTrxLevel ) {
                        $this->begin( $fname, self::TRANSACTION_INTERNAL );
-                       $this->mTrxAutomatic = true;
                        // If DBO_TRX is set, a series of startAtomic/endAtomic pairs will result
                        // in all changes being in one transaction to keep requests transactional.
                        if ( !$this->getFlag( DBO_TRX ) ) {
@@ -2697,7 +2907,7 @@ abstract class DatabaseBase implements IDatabase {
                try {
                        $res = call_user_func_array( $callback, [ $this, $fname ] );
                } catch ( Exception $e ) {
-                       $this->rollback( $fname );
+                       $this->rollback( $fname, self::FLUSHING_INTERNAL );
                        throw $e;
                }
                $this->endAtomic( $fname );
@@ -2719,11 +2929,14 @@ abstract class DatabaseBase implements IDatabase {
                                // @TODO: make this an exception at some point
                                $msg = "$fname: Implicit transaction already active (from {$this->mTrxFname}).";
                                wfLogDBError( $msg );
+                               wfWarn( $msg );
                                return; // join the main transaction set
                        }
                } elseif ( $this->getFlag( DBO_TRX ) && $mode !== self::TRANSACTION_INTERNAL ) {
                        // @TODO: make this an exception at some point
-                       wfLogDBError( "$fname: Implicit transaction expected (DBO_TRX set)." );
+                       $msg = "$fname: Implicit transaction expected (DBO_TRX set).";
+                       wfLogDBError( $msg );
+                       wfWarn( $msg );
                        return; // let any writes be in the main transaction
                }
 
@@ -2734,17 +2947,20 @@ abstract class DatabaseBase implements IDatabase {
                $this->mTrxTimestamp = microtime( true );
                $this->mTrxFname = $fname;
                $this->mTrxDoneWrites = false;
-               $this->mTrxAutomatic = false;
+               $this->mTrxAutomatic = ( $mode === self::TRANSACTION_INTERNAL );
                $this->mTrxAutomaticAtomic = false;
                $this->mTrxAtomicLevels = [];
                $this->mTrxShortId = wfRandomString( 12 );
                $this->mTrxWriteDuration = 0.0;
+               $this->mTrxWriteQueryCount = 0;
+               $this->mTrxWriteAdjDuration = 0.0;
+               $this->mTrxWriteAdjQueryCount = 0;
                $this->mTrxWriteCallers = [];
                // First SELECT after BEGIN will establish the snapshot in REPEATABLE-READ.
-               // Get an estimate of the slave lag before then, treating estimate staleness
+               // Get an estimate of the replica DB lag before then, treating estimate staleness
                // as lag itself just to be safe
                $status = $this->getApproximateLagStatus();
-               $this->mTrxSlaveLag = $status['lag'] + ( microtime( true ) - $status['since'] );
+               $this->mTrxReplicaLag = $status['lag'] + ( microtime( true ) - $status['since'] );
        }
 
        /**
@@ -2783,7 +2999,9 @@ abstract class DatabaseBase implements IDatabase {
                                return; // nothing to do
                        } elseif ( $this->mTrxAutomatic ) {
                                // @TODO: make this an exception at some point
-                               wfLogDBError( "$fname: Explicit commit of implicit transaction." );
+                               $msg = "$fname: Explicit commit of implicit transaction.";
+                               wfLogDBError( $msg );
+                               wfWarn( $msg );
                                return; // wait for the main transaction set commit round
                        }
                }
@@ -2792,7 +3010,7 @@ abstract class DatabaseBase implements IDatabase {
                $this->assertOpen();
 
                $this->runOnTransactionPreCommitCallbacks();
-               $writeTime = $this->pendingWriteQueryDuration();
+               $writeTime = $this->pendingWriteQueryDuration( self::ESTIMATE_DB_APPLY );
                $this->doCommit( $fname );
                if ( $this->mTrxDoneWrites ) {
                        $this->mDoneWrites = microtime( true );
@@ -2801,6 +3019,7 @@ abstract class DatabaseBase implements IDatabase {
                }
 
                $this->runOnTransactionIdleCallbacks( self::TRIGGER_COMMIT );
+               $this->runTransactionListenerCallbacks( self::TRIGGER_COMMIT );
        }
 
        /**
@@ -2846,6 +3065,7 @@ abstract class DatabaseBase implements IDatabase {
                $this->mTrxIdleCallbacks = []; // clear
                $this->mTrxPreCommitCallbacks = []; // clear
                $this->runOnTransactionIdleCallbacks( self::TRIGGER_ROLLBACK );
+               $this->runTransactionListenerCallbacks( self::TRIGGER_ROLLBACK );
        }
 
        /**
@@ -2863,6 +3083,18 @@ abstract class DatabaseBase implements IDatabase {
                }
        }
 
+       public function flushSnapshot( $fname = __METHOD__ ) {
+               if ( $this->writesOrCallbacksPending() || $this->explicitTrxActive() ) {
+                       // This only flushes transactions to clear snapshots, not to write data
+                       throw new DBUnexpectedError(
+                               $this,
+                               "$fname: Cannot COMMIT to clear snapshot because writes are pending."
+                       );
+               }
+
+               $this->commit( $fname, self::FLUSHING_INTERNAL );
+       }
+
        public function explicitTrxActive() {
                return $this->mTrxLevel && ( $this->mTrxAtomicLevels || !$this->mTrxAutomatic );
        }
@@ -2967,17 +3199,24 @@ abstract class DatabaseBase implements IDatabase {
                }
        }
 
-       public function ping() {
+       public function ping( &$rtt = null ) {
+               // Avoid hitting the server if it was hit recently
                if ( $this->isOpen() && ( microtime( true ) - $this->lastPing ) < self::PING_TTL ) {
-                       return true;
+                       if ( !func_num_args() || $this->mRTTEstimate > 0 ) {
+                               $rtt = $this->mRTTEstimate;
+                               return true; // don't care about $rtt
+                       }
                }
 
-               $ignoreErrors = true;
-               $this->clearFlag( DBO_TRX, self::REMEMBER_PRIOR );
                // This will reconnect if possible or return false if not
-               $ok = (bool)$this->query( "SELECT 1 AS ping", __METHOD__, $ignoreErrors );
+               $this->clearFlag( DBO_TRX, self::REMEMBER_PRIOR );
+               $ok = ( $this->query( self::PING_QUERY, __METHOD__, true ) !== false );
                $this->restoreFlags( self::RESTORE_PRIOR );
 
+               if ( $ok ) {
+                       $rtt = $this->mRTTEstimate;
+               }
+
                return $ok;
        }
 
@@ -3004,7 +3243,7 @@ abstract class DatabaseBase implements IDatabase {
        }
 
        /**
-        * Get the slave lag when the current transaction started
+        * Get the replica DB lag when the current transaction started
         *
         * This is useful when transactions might use snapshot isolation
         * (e.g. REPEATABLE-READ in innodb), so the "real" lag of that data
@@ -3016,19 +3255,19 @@ abstract class DatabaseBase implements IDatabase {
         */
        public function getTransactionLagStatus() {
                return $this->mTrxLevel
-                       ? [ 'lag' => $this->mTrxSlaveLag, 'since' => $this->trxTimestamp() ]
+                       ? [ 'lag' => $this->mTrxReplicaLag, 'since' => $this->trxTimestamp() ]
                        : null;
        }
 
        /**
-        * Get a slave lag estimate for this server
+        * Get a replica DB lag estimate for this server
         *
         * @return array ('lag': seconds or false on error, 'since': UNIX timestamp of estimate)
         * @since 1.27
         */
        public function getApproximateLagStatus() {
                return [
-                       'lag'   => $this->getLBInfo( 'slave' ) ? $this->getLag() : 0,
+                       'lag'   => $this->getLBInfo( 'replica' ) ? $this->getLag() : 0,
                        'since' => microtime( true )
                ];
        }