Merge "Enforce lagged-slave read-only mode on the DB layer"
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Wed, 7 Oct 2015 01:40:34 +0000 (01:40 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Wed, 7 Oct 2015 01:40:34 +0000 (01:40 +0000)
1  2 
autoload.php
includes/db/Database.php

diff --combined autoload.php
@@@ -287,10 -287,10 +287,11 @@@ $wgAutoloadLocalClasses = array
        'DBLockManager' => __DIR__ . '/includes/filebackend/lockmanager/DBLockManager.php',
        'DBMasterPos' => __DIR__ . '/includes/db/DatabaseUtility.php',
        'DBQueryError' => __DIR__ . '/includes/db/DatabaseError.php',
+       'DBReadOnlyError' => __DIR__ . '/includes/db/DatabaseError.php',
        'DBSiteStore' => __DIR__ . '/includes/site/DBSiteStore.php',
        'DBUnexpectedError' => __DIR__ . '/includes/db/DatabaseError.php',
        'DataUpdate' => __DIR__ . '/includes/deferred/DataUpdate.php',
 +      'Database' => __DIR__ . '/includes/db/Database.php',
        'DatabaseBase' => __DIR__ . '/includes/db/Database.php',
        'DatabaseInstaller' => __DIR__ . '/includes/installer/DatabaseInstaller.php',
        'DatabaseLag' => __DIR__ . '/maintenance/lag.php',
        'MemcachedBagOStuff' => __DIR__ . '/includes/objectcache/MemcachedBagOStuff.php',
        'MemcachedPeclBagOStuff' => __DIR__ . '/includes/objectcache/MemcachedPeclBagOStuff.php',
        'MemcachedPhpBagOStuff' => __DIR__ . '/includes/objectcache/MemcachedPhpBagOStuff.php',
 +      'MemoizedCallable' => __DIR__ . '/includes/libs/MemoizedCallable.php',
        'MemoryFileBackend' => __DIR__ . '/includes/filebackend/MemoryFileBackend.php',
        'MergeHistoryPager' => __DIR__ . '/includes/specials/SpecialMergeHistory.php',
        'MergeLogFormatter' => __DIR__ . '/includes/logging/MergeLogFormatter.php',
        'PostgresBlob' => __DIR__ . '/includes/db/DatabasePostgres.php',
        'PostgresField' => __DIR__ . '/includes/db/DatabasePostgres.php',
        'PostgresInstaller' => __DIR__ . '/includes/installer/PostgresInstaller.php',
 -      'PostgresTransactionState' => __DIR__ . '/includes/db/DatabasePostgres.php',
        'PostgresUpdater' => __DIR__ . '/includes/installer/PostgresUpdater.php',
        'Preferences' => __DIR__ . '/includes/Preferences.php',
        'PreferencesForm' => __DIR__ . '/includes/Preferences.php',
diff --combined includes/db/Database.php
@@@ -45,9 -45,6 +45,9 @@@ abstract class DatabaseBase implements 
  
        protected $mServer, $mUser, $mPassword, $mDBname;
  
 +      /** @var BagOStuff APC cache */
 +      protected $srvCache;
 +
        /** @var resource Database connection */
        protected $mConn = null;
        protected $mOpened = false;
@@@ -99,9 -96,6 +99,9 @@@
         */
        private $mTrxTimestamp = null;
  
 +      /** @var float Lag estimate at the time of BEGIN */
 +      private $mTrxSlaveLag = null;
 +
        /**
         * Remembers the function name given for starting the most recent transaction via begin().
         * Used to provide additional context for error reporting.
         *   - DBO_PERSISTENT: use persistant database connection
         */
        public function setFlag( $flag ) {
 -              global $wgDebugDBTransactions;
                $this->mFlags |= $flag;
 -              if ( ( $flag & DBO_TRX ) && $wgDebugDBTransactions ) {
 -                      wfDebug( "Implicit transactions are now enabled.\n" );
 -              }
        }
  
        /**
         *   - DBO_PERSISTENT: use persistant database connection
         */
        public function clearFlag( $flag ) {
 -              global $wgDebugDBTransactions;
                $this->mFlags &= ~$flag;
 -              if ( ( $flag & DBO_TRX ) && $wgDebugDBTransactions ) {
 -                      wfDebug( "Implicit transactions are now disabled.\n" );
 -              }
        }
  
        /**
         * @param array $params Parameters passed from DatabaseBase::factory()
         */
        function __construct( array $params ) {
 -              global $wgDBprefix, $wgDBmwschema, $wgCommandLineMode, $wgDebugDBTransactions;
 +              global $wgDBprefix, $wgDBmwschema, $wgCommandLineMode;
 +
 +              $this->mTrxAtomicLevels = new SplStack;
 +              $this->srvCache = ObjectCache::newAccelerator( 'hash' );
  
                $server = $params['host'];
                $user = $params['user'];
                if ( $this->mFlags & DBO_DEFAULT ) {
                        if ( $wgCommandLineMode ) {
                                $this->mFlags &= ~DBO_TRX;
 -                              if ( $wgDebugDBTransactions ) {
 -                                      wfDebug( "Implicit transaction open disabled.\n" );
 -                              }
                        } else {
                                $this->mFlags |= DBO_TRX;
 -                              if ( $wgDebugDBTransactions ) {
 -                                      wfDebug( "Implicit transaction open enabled.\n" );
 -                              }
                        }
                }
  
         *     for a successful read query, or false on failure if $tempIgnore set
         */
        public function query( $sql, $fname = __METHOD__, $tempIgnore = false ) {
 -              global $wgUser, $wgDebugDBTransactions, $wgDebugDumpSqlLength;
 +              global $wgUser;
  
                $this->mLastQuery = $sql;
  
                $isWriteQuery = $this->isWriteQuery( $sql );
                if ( $isWriteQuery ) {
-                       if ( !$this->mDoneWrites ) {
-                               wfDebug( __METHOD__ . ': Writes done: ' .
-                                       DatabaseBase::generalizeSQL( $sql ) . "\n" );
+                       $reason = $this->getLBInfo( 'readOnlyReason' );
+                       if ( is_string( $reason ) ) {
+                               throw new DBReadOnlyError( $this, "Database is read-only: $reason" );
                        }
                        # Set a flag indicating that writes have been done
                        $this->mDoneWrites = microtime( true );
                $commentedSql = preg_replace( '/\s|$/', " /* $fname $userName */ ", $sql, 1 );
  
                if ( !$this->mTrxLevel && $this->getFlag( DBO_TRX ) && $this->isTransactableQuery( $sql ) ) {
 -                      if ( $wgDebugDBTransactions ) {
 -                              wfDebug( "Implicit transaction start.\n" );
 -                      }
                        $this->begin( __METHOD__ . " ($fname)" );
                        $this->mTrxAutomatic = true;
                }
                }
  
                if ( $this->debug() ) {
 -                      static $cnt = 0;
 -
 -                      $cnt++;
 -                      $sqlx = $wgDebugDumpSqlLength ? substr( $commentedSql, 0, $wgDebugDumpSqlLength )
 -                              : $commentedSql;
 -                      $sqlx = strtr( $sqlx, "\t\n", '  ' );
 -
 -                      $master = $isMaster ? 'master' : 'slave';
 -                      wfDebug( "Query {$this->mDBname} ($cnt) ($master): $sqlx\n" );
 +                      wfDebugLog( 'queries', sprintf( "%s: %s", $this->mDBname, $sql ) );
                }
  
                $queryId = MWDebug::query( $sql, $fname, $isMaster );
         *
         * @return string
         */
 -      static function generalizeSQL( $sql ) {
 +      protected static function generalizeSQL( $sql ) {
                # This does the same as the regexp below would do, but in such a way
                # as to avoid crashing php on some large strings.
                # $sql = preg_replace( "/'([^\\\\']|\\\\.)*'|\"([^\\\\\"]|\\\\.)*\"/", "'X'", $sql );
         * @throws DBError
         */
        final public function begin( $fname = __METHOD__ ) {
 -              global $wgDebugDBTransactions;
 -
                if ( $this->mTrxLevel ) { // implicit commit
                        if ( $this->mTrxAtomicLevels ) {
                                // If the current transaction was an automatic atomic one, then we definitely have
                                        ) )
                                );
                        } else {
 -                              // if the transaction was automatic and has done write operations,
 -                              // log it if $wgDebugDBTransactions is enabled.
 -                              if ( $this->mTrxDoneWrites && $wgDebugDBTransactions ) {
 +                              // if the transaction was automatic and has done write operations
 +                              if ( $this->mTrxDoneWrites ) {
                                        wfDebug( "$fname: Automatic transaction with writes in progress" .
                                                " (from {$this->mTrxFname}), performing implicit commit!\n"
                                        );
                $this->mTrxPreCommitCallbacks = array();
                $this->mTrxShortId = wfRandomString( 12 );
                $this->mTrxWriteDuration = 0.0;
 +              // First SELECT after BEGIN will establish the snapshot in REPEATABLE-READ.
 +              // Get an estimate of the slave lag before then, treating estimate staleness
 +              // as lag itself just to be safe
 +              $status = $this->getApproximateLagStatus();
 +              $this->mTrxSlaveLag = $status['lag'] + ( microtime( true ) - $status['since'] );
        }
  
        /**
                return true;
        }
  
 +      /**
 +       * Get the slave lag when the current transaction started
 +       * or a general lag estimate if not transaction is active
 +       *
 +       * This is useful when transactions might use snapshot isolation
 +       * (e.g. REPEATABLE-READ in innodb), so the "real" lag of that data
 +       * is this lag plus transaction duration. If they don't, it is still
 +       * safe to be pessimistic. In AUTO-COMMIT mode, this still gives an
 +       * indication of the staleness of subsequent reads.
 +       *
 +       * @return array ('lag': seconds, 'since': UNIX timestamp of BEGIN)
 +       * @since 1.27
 +       */
 +      public function getSessionLagStatus() {
 +              return $this->getTransactionLagStatus() ?: $this->getApproximateLagStatus();
 +      }
 +
 +      /**
 +       * Get the slave 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
 +       * is this lag plus transaction duration. If they don't, it is still
 +       * safe to be pessimistic. This returns null if there is no transaction.
 +       *
 +       * @return array|null ('lag': seconds, 'since': UNIX timestamp of BEGIN)
 +       * @since 1.27
 +       */
 +      public function getTransactionLagStatus() {
 +              return $this->mTrxLevel
 +                      ? array( 'lag' => $this->mTrxSlaveLag, 'since' => $this->trxTimestamp() )
 +                      : null;
 +      }
 +
 +      /**
 +       * Get a slave lag estimate for this server
 +       *
 +       * @return array ('lag': seconds, 'since': UNIX timestamp of estimate)
 +       * @since 1.27
 +       */
 +      public function getApproximateLagStatus() {
 +              return array(
 +                      'lag'   => $this->getLBInfo( 'slave' ) ? $this->getLag() : 0,
 +                      'since' => microtime( true )
 +              );
 +      }
 +
 +      /**
 +       * Merge the result of getSessionLagStatus() for several DBs
 +       * using the most pessimistic values to estimate the lag of
 +       * any data derived from them in combination
 +       *
 +       * This is information is useful for caching modules
 +       *
 +       * @see WANObjectCache::set()
 +       * @see WANObjectCache::getWithSetCallback()
 +       *
 +       * @param IDatabase $db1
 +       * @param IDatabase ...
 +       * @return array ('lag': highest lag, 'since': lowest estimate UNIX timestamp)
 +       * @since 1.27
 +       */
 +      public static function getCacheSetOptions( IDatabase $db1 ) {
 +              $res = array( 'lag' => 0, 'since' => INF );
 +              foreach ( func_get_args() as $db ) {
 +                      /** @var IDatabase $db */
 +                      $status = $db->getSessionLagStatus();
 +                      $res['lag'] = max( $res['lag'], $status['lag'] );
 +                      $res['since'] = min( $res['since'], $status['since'] );
 +              }
 +
 +              return $res;
 +      }
 +
        /**
         * Get slave lag. Currently supported only by MySQL.
         *
                }
        }
  }
 +
 +/**
 + * @since 1.27
 + */
 +abstract class Database extends DatabaseBase {
 +      // B/C until nothing type hints for DatabaseBase
 +      // @TODO: finish renaming DatabaseBase => Database
 +}