Merge "API: Expose $wgEnableMagicLinks in meta=siteinfo"
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Wed, 14 Sep 2016 15:58:36 +0000 (15:58 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Wed, 14 Sep 2016 15:58:36 +0000 (15:58 +0000)
34 files changed:
RELEASE-NOTES-1.28
autoload.php
includes/MediaWiki.php
includes/changes/RecentChange.php
includes/db/ChronologyProtector.php
includes/db/CloneDatabase.php
includes/db/DBConnRef.php
includes/db/Database.php
includes/db/DatabaseMssql.php
includes/db/DatabaseOracle.php
includes/db/DatabaseSqlite.php
includes/db/loadbalancer/ILoadBalancer.php [new file with mode: 0644]
includes/db/loadbalancer/LBFactory.php
includes/db/loadbalancer/LBFactoryMulti.php
includes/db/loadbalancer/LBFactorySimple.php
includes/db/loadbalancer/LBFactorySingle.php
includes/db/loadbalancer/LoadBalancer.php
includes/db/loadbalancer/LoadMonitor.php [deleted file]
includes/db/loadbalancer/LoadMonitorMySQL.php [deleted file]
includes/jobqueue/JobRunner.php
includes/jobqueue/jobs/CategoryMembershipChangeJob.php
includes/libs/objectcache/MemcachedPeclBagOStuff.php [new file with mode: 0644]
includes/libs/rdbms/loadmonitor/LoadMonitor.php [new file with mode: 0644]
includes/libs/rdbms/loadmonitor/LoadMonitorMySQL.php [new file with mode: 0644]
includes/libs/rdbms/loadmonitor/LoadMonitorNull.php [new file with mode: 0644]
includes/objectcache/MemcachedPeclBagOStuff.php [deleted file]
includes/pager/ReverseChronologicalPager.php
includes/specials/SpecialChangeContentModel.php
includes/user/BotPassword.php
maintenance/refreshLinks.php
tests/parser/ParserTestRunner.php
tests/phpunit/includes/api/ApiLoginTest.php
tests/phpunit/includes/pager/ReverseChronologicalPagerTest.php
tests/phpunit/includes/user/BotPasswordTest.php

index 116dc2a..dfa482a 100644 (file)
@@ -56,9 +56,10 @@ production.
   the mw.Api instance seems to be for the local wiki.
 * After a client performs an action which alters a database that has replica databases,
   MediaWiki will wait for the replica databases to synchronize with the master database
-  while it renders the HTML output. However, if the output is a redirect, it will instead
-  alter the redirect URL to include a ?cpPosTime parameter that triggers the database
-  synchronization when the URL is followed by the client.
+  while it renders the HTML output. However, if the output is a redirect to another wiki
+  on the wiki farm with a different domain, MediaWiki will instead alter the redirect
+  URL to include a ?cpPosTime parameter that triggers the database synchronization when
+  the URL is followed by the client. The same-domain case uses a new cpPosTime cookie.
 
 === External library changes in 1.28 ===
 
index 71f1809..a71d943 100644 (file)
@@ -580,6 +580,7 @@ $wgAutoloadLocalClasses = [
        'IEUrlExtension' => __DIR__ . '/includes/libs/IEUrlExtension.php',
        'IExpiringStore' => __DIR__ . '/includes/libs/objectcache/IExpiringStore.php',
        'IJobSpecification' => __DIR__ . '/includes/jobqueue/JobSpecification.php',
+       'ILoadBalancer' => __DIR__ . '/includes/db/loadbalancer/ILoadBalancer.php',
        'IP' => __DIR__ . '/includes/utils/IP.php',
        'IPSet' => __DIR__ . '/includes/compat/IPSetCompat.php',
        'IPTC' => __DIR__ . '/includes/media/IPTC.php',
@@ -728,9 +729,9 @@ $wgAutoloadLocalClasses = [
        'ListredirectsPage' => __DIR__ . '/includes/specials/SpecialListredirects.php',
        'LoadBalancer' => __DIR__ . '/includes/db/loadbalancer/LoadBalancer.php',
        'LoadBalancerSingle' => __DIR__ . '/includes/db/loadbalancer/LBFactorySingle.php',
-       'LoadMonitor' => __DIR__ . '/includes/db/loadbalancer/LoadMonitor.php',
-       'LoadMonitorMySQL' => __DIR__ . '/includes/db/loadbalancer/LoadMonitorMySQL.php',
-       'LoadMonitorNull' => __DIR__ . '/includes/db/loadbalancer/LoadMonitor.php',
+       'LoadMonitor' => __DIR__ . '/includes/libs/rdbms/loadmonitor/LoadMonitor.php',
+       'LoadMonitorMySQL' => __DIR__ . '/includes/libs/rdbms/loadmonitor/LoadMonitorMySQL.php',
+       'LoadMonitorNull' => __DIR__ . '/includes/libs/rdbms/loadmonitor/LoadMonitorNull.php',
        'LocalFile' => __DIR__ . '/includes/filerepo/file/LocalFile.php',
        'LocalFileDeleteBatch' => __DIR__ . '/includes/filerepo/file/LocalFile.php',
        'LocalFileLockError' => __DIR__ . '/includes/filerepo/file/LocalFile.php',
@@ -910,7 +911,7 @@ $wgAutoloadLocalClasses = [
        'MemcLockManager' => __DIR__ . '/includes/filebackend/lockmanager/MemcLockManager.php',
        'MemcachedBagOStuff' => __DIR__ . '/includes/libs/objectcache/MemcachedBagOStuff.php',
        'MemcachedClient' => __DIR__ . '/includes/libs/objectcache/MemcachedClient.php',
-       'MemcachedPeclBagOStuff' => __DIR__ . '/includes/objectcache/MemcachedPeclBagOStuff.php',
+       'MemcachedPeclBagOStuff' => __DIR__ . '/includes/libs/objectcache/MemcachedPeclBagOStuff.php',
        'MemcachedPhpBagOStuff' => __DIR__ . '/includes/libs/objectcache/MemcachedPhpBagOStuff.php',
        'MemoizedCallable' => __DIR__ . '/includes/libs/MemoizedCallable.php',
        'MemoryFileBackend' => __DIR__ . '/includes/filebackend/MemoryFileBackend.php',
index 0bd183f..9bbbd35 100644 (file)
@@ -574,26 +574,44 @@ class MediaWiki {
                wfDebug( __METHOD__ . ': pre-send deferred updates completed' );
 
                // Decide when clients block on ChronologyProtector DB position writes
-               if (
+               $urlDomainDistance = (
                        $request->wasPosted() &&
                        $output->getRedirect() &&
-                       $lbFactory->hasOrMadeRecentMasterChanges( INF ) &&
-                       self::isWikiClusterURL( $output->getRedirect(), $context )
-               ) {
+                       $lbFactory->hasOrMadeRecentMasterChanges( INF )
+               ) ? self::getUrlDomainDistance( $output->getRedirect(), $context ) : false;
+
+               if ( $urlDomainDistance === 'local' || $urlDomainDistance === 'remote' ) {
                        // OutputPage::output() will be fast; $postCommitWork will not be useful for
                        // masking the latency of syncing DB positions accross all datacenters synchronously.
                        // Instead, make use of the RTT time of the client follow redirects.
                        $flags = $lbFactory::SHUTDOWN_CHRONPROT_ASYNC;
+                       $cpPosTime = microtime( true );
                        // Client's next request should see 1+ positions with this DBMasterPos::asOf() time
-                       $safeUrl = $lbFactory->appendPreShutdownTimeAsQuery(
-                               $output->getRedirect(),
-                               microtime( true )
-                       );
-                       $output->redirect( $safeUrl );
+                       if ( $urlDomainDistance === 'local' ) {
+                               // Client will stay on this domain, so set an unobtrusive cookie
+                               $expires = time() + ChronologyProtector::POSITION_TTL;
+                               $options = [ 'prefix' => '' ];
+                               $request->response()->setCookie( 'cpPosTime', $cpPosTime, $expires, $options );
+                       } else {
+                               // Cookies may not work across wiki domains, so use a URL parameter
+                               $safeUrl = $lbFactory->appendPreShutdownTimeAsQuery(
+                                       $output->getRedirect(),
+                                       $cpPosTime
+                               );
+                               $output->redirect( $safeUrl );
+                       }
                } else {
                        // OutputPage::output() is fairly slow; run it in $postCommitWork to mask
                        // the latency of syncing DB positions accross all datacenters synchronously
                        $flags = $lbFactory::SHUTDOWN_CHRONPROT_SYNC;
+                       if ( $lbFactory->hasOrMadeRecentMasterChanges( INF ) ) {
+                               $cpPosTime = microtime( true );
+                               // Set a cookie in case the DB position store cannot sync accross datacenters.
+                               // This will at least cover the common case of the user staying on the domain.
+                               $expires = time() + ChronologyProtector::POSITION_TTL;
+                               $options = [ 'prefix' => '' ];
+                               $request->response()->setCookie( 'cpPosTime', $cpPosTime, $expires, $options );
+                       }
                }
                // Record ChronologyProtector positions for DBs affected in this request at this point
                $lbFactory->shutdown( $flags, $postCommitWork );
@@ -629,9 +647,9 @@ class MediaWiki {
        /**
         * @param string $url
         * @param IContextSource $context
-        * @return bool Whether $url is to something on this wiki farm
+        * @return string|bool Either "local" or "remote" if in the farm, false otherwise
         */
-       private function isWikiClusterURL( $url, IContextSource $context ) {
+       private function getUrlDomainDistance( $url, IContextSource $context ) {
                static $relevantKeys = [ 'host' => true, 'port' => true ];
 
                $infoCandidate = wfParseUrl( $url );
@@ -647,14 +665,14 @@ class MediaWiki {
                        $context->getConfig()->get( 'LocalVirtualHosts' )
                );
 
-               foreach ( $clusterHosts as $clusterHost ) {
+               foreach ( $clusterHosts as $i => $clusterHost ) {
                        $parseUrl = wfParseUrl( $clusterHost );
                        if ( !$parseUrl ) {
                                continue;
                        }
                        $infoHost = array_intersect_key( $parseUrl, $relevantKeys );
                        if ( $infoCandidate === $infoHost ) {
-                               return true;
+                               return ( $i === 0 ) ? 'local' : 'remote';
                        }
                }
 
index a5d1fc5..8e74674 100644 (file)
@@ -336,9 +336,13 @@ class RecentChange {
                        $title = $this->getTitle();
 
                        // Never send an RC notification email about categorization changes
-                       if ( $this->mAttribs['rc_type'] != RC_CATEGORIZE ) {
-                               if ( Hooks::run( 'AbortEmailNotification', [ $editor, $title, $this ] ) ) {
-                                       # @todo FIXME: This would be better as an extension hook
+                       if (
+                               $this->mAttribs['rc_type'] != RC_CATEGORIZE &&
+                               Hooks::run( 'AbortEmailNotification', [ $editor, $title, $this ] )
+                       ) {
+                               // @FIXME: This would be better as an extension hook
+                               // Send emails or email jobs once this row is safely committed
+                               $dbw->onTransactionIdle( function () use ( $editor, $title ) {
                                        $enotif = new EmailNotification();
                                        $enotif->notifyOnPageChange(
                                                $editor,
@@ -349,7 +353,7 @@ class RecentChange {
                                                $this->mAttribs['rc_last_oldid'],
                                                $this->mExtra['pageStatus']
                                        );
-                               }
+                               } );
                        }
                }
 
index 1cdb49f..4d03bc6 100644 (file)
  * @file
  * @ingroup Database
  */
+use Psr\Log\LoggerAwareInterface;
+use Psr\Log\LoggerInterface;
+use MediaWiki\Logger\LoggerFactory;
 
 /**
  * Class for ensuring a consistent ordering of events as seen by the user, despite replication.
  * Kind of like Hawking's [[Chronology Protection Agency]].
  */
-class ChronologyProtector {
+class ChronologyProtector implements LoggerAwareInterface{
        /** @var BagOStuff */
        protected $store;
+       /** @var LoggerInterface */
+       protected $logger;
 
        /** @var string Storage key name */
        protected $key;
@@ -67,6 +72,11 @@ class ChronologyProtector {
                $this->clientId = md5( $client['ip'] . "\n" . $client['agent'] );
                $this->key = $store->makeGlobalKey( __CLASS__, $this->clientId );
                $this->waitForPosTime = $posTime;
+               $this->logger = LoggerFactory::getInstance( 'DBReplication' );
+       }
+
+       public function setLogger( LoggerInterface $logger ) {
+               $this->logger = $logger;
        }
 
        /**
@@ -106,7 +116,7 @@ class ChronologyProtector {
                $masterName = $lb->getServerName( $lb->getWriterIndex() );
                if ( !empty( $this->startupPositions[$masterName] ) ) {
                        $pos = $this->startupPositions[$masterName];
-                       wfDebugLog( 'replication', __METHOD__ . ": LB for '$masterName' set to pos $pos\n" );
+                       $this->logger->info( __METHOD__ . ": LB for '$masterName' set to pos $pos\n" );
                        $lb->waitFor( $pos );
                }
        }
@@ -129,10 +139,10 @@ class ChronologyProtector {
                $masterName = $lb->getServerName( $lb->getWriterIndex() );
                if ( $lb->getServerCount() > 1 ) {
                        $pos = $lb->getMasterPos();
-                       wfDebugLog( 'replication', __METHOD__ . ": LB for '$masterName' has pos $pos\n" );
+                       $this->logger->info( __METHOD__ . ": LB for '$masterName' has pos $pos\n" );
                        $this->shutdownPositions[$masterName] = $pos;
                } else {
-                       wfDebugLog( 'replication', __METHOD__ . ": DB '$masterName' touched\n" );
+                       $this->logger->info( __METHOD__ . ": DB '$masterName' touched\n" );
                }
                $this->shutdownTouchDBs[$masterName] = 1;
        }
@@ -165,8 +175,7 @@ class ChronologyProtector {
                        return []; // nothing to save
                }
 
-               wfDebugLog( 'replication',
-                       __METHOD__ . ": saving master pos for " .
+               $this->logger->info( __METHOD__ . ": saving master pos for " .
                        implode( ', ', array_keys( $this->shutdownPositions ) ) . "\n"
                );
 
@@ -193,16 +202,14 @@ class ChronologyProtector {
                if ( !$ok ) {
                        $bouncedPositions = $this->shutdownPositions;
                        // Raced out too many times or stash is down
-                       wfDebugLog( 'replication',
-                               __METHOD__ . ": failed to save master pos for " .
+                       $this->logger->warning( __METHOD__ . ": failed to save master pos for " .
                                implode( ', ', array_keys( $this->shutdownPositions ) ) . "\n"
                        );
                } elseif ( $mode === 'sync' &&
                        $store->getQoS( $store::ATTR_SYNCWRITES ) < $store::QOS_SYNCWRITES_BE
                ) {
                        // Positions may not be in all datacenters, force LBFactory to play it safe
-                       wfDebugLog( 'replication',
-                               __METHOD__ . ": store does not report ability to sync replicas. " );
+                       $this->logger->info( __METHOD__ . ": store may not support synchronous writes." );
                        $bouncedPositions = $this->shutdownPositions;
                } else {
                        $bouncedPositions = [];
@@ -241,7 +248,7 @@ class ChronologyProtector {
                if ( $this->wait ) {
                        // If there is an expectation to see master positions with a certain min
                        // timestamp, then block until they appear, or until a timeout is reached.
-                       if ( $this->waitForPosTime ) {
+                       if ( $this->waitForPosTime > 0.0 ) {
                                $data = null;
                                $loop = new WaitConditionLoop(
                                        function () use ( &$data ) {
@@ -267,10 +274,10 @@ class ChronologyProtector {
                        }
 
                        $this->startupPositions = $data ? $data['positions'] : [];
-                       wfDebugLog( 'replication', __METHOD__ . ": key is {$this->key} (read)\n" );
+                       $this->logger->info( __METHOD__ . ": key is {$this->key} (read)\n" );
                } else {
                        $this->startupPositions = [];
-                       wfDebugLog( 'replication', __METHOD__ . ": key is {$this->key} (unread)\n" );
+                       $this->logger->info( __METHOD__ . ": key is {$this->key} (unread)\n" );
                }
        }
 
index 577c98d..97d59d8 100644 (file)
@@ -76,7 +76,7 @@ class CloneDatabase {
                        if ( $wgSharedDB && in_array( $tbl, $wgSharedTables, true ) ) {
                                // Shared tables don't work properly when cloning due to
                                // how prefixes are handled (bug 65654)
-                               throw new MWException( "Cannot clone shared table $tbl." );
+                               throw new RuntimeException( "Cannot clone shared table $tbl." );
                        }
                        # Clean up from previous aborted run.  So that table escaping
                        # works correctly across DB engines, we need to change the pre-
@@ -93,7 +93,7 @@ class CloneDatabase {
                        ) {
                                if ( $oldTableName === $newTableName ) {
                                        // Last ditch check to avoid data loss
-                                       throw new MWException( "Not dropping new table, as '$newTableName'"
+                                       throw new LogicException( "Not dropping new table, as '$newTableName'"
                                                . " is name of both the old and the new table." );
                                }
                                $this->db->dropTable( $tbl, __METHOD__ );
@@ -129,8 +129,9 @@ class CloneDatabase {
         */
        public static function changePrefix( $prefix ) {
                global $wgDBprefix;
-               wfGetLBFactory()->forEachLB( function( $lb ) use ( $prefix ) {
-                       $lb->forEachOpenConnection( function ( $db ) use ( $prefix ) {
+               wfGetLBFactory()->forEachLB( function( LoadBalancer $lb ) use ( $prefix ) {
+                       $lb->setDomainPrefix( $prefix );
+                       $lb->forEachOpenConnection( function ( DatabaseBase $db ) use ( $prefix ) {
                                $db->tablePrefix( $prefix );
                        } );
                } );
index 9997f2c..8604295 100644 (file)
@@ -17,16 +17,22 @@ class DBConnRef implements IDatabase {
        /** @var array|null */
        private $params;
 
+       const FLD_INDEX = 0;
+       const FLD_GROUP = 1;
+       const FLD_WIKI = 2;
+
        /**
         * @param LoadBalancer $lb
-        * @param DatabaseBase|array $conn Connection or (server index, group, wiki ID) array
+        * @param DatabaseBase|array $conn Connection or (server index, group, wiki ID)
         */
        public function __construct( LoadBalancer $lb, $conn ) {
                $this->lb = $lb;
                if ( $conn instanceof DatabaseBase ) {
                        $this->conn = $conn;
-               } else {
+               } elseif ( count( $conn ) >= 3 && $conn[self::FLD_WIKI] !== false ) {
                        $this->params = $conn;
+               } else {
+                       throw new InvalidArgumentException( "Missing lazy connection arguments." );
                }
        }
 
@@ -136,6 +142,11 @@ class DBConnRef implements IDatabase {
        }
 
        public function getWikiID() {
+               if ( $this->conn === null ) {
+                       // Avoid triggering a connection
+                       return $this->params[self::FLD_WIKI];
+               }
+
                return $this->__call( __FUNCTION__, func_get_args() );
        }
 
index c41e7c7..109dbfe 100644 (file)
@@ -1,5 +1,4 @@
 <?php
-
 /**
  * @defgroup Database Database
  *
  * @file
  * @ingroup Database
  */
+use Psr\Log\LoggerAwareInterface;
+use Psr\Log\LoggerInterface;
 
 /**
  * Database abstraction object
  * @ingroup Database
  */
-abstract class DatabaseBase implements IDatabase {
+abstract class DatabaseBase implements IDatabase, LoggerAwareInterface {
        /** Number of times to re-try an operation in case of deadlock */
        const DEADLOCK_TRIES = 4;
        /** Minimum time to wait before retry, in microseconds */
@@ -64,6 +65,10 @@ abstract class DatabaseBase implements IDatabase {
 
        /** @var BagOStuff APC cache */
        protected $srvCache;
+       /** @var LoggerInterface */
+       protected $connLogger;
+       /** @var LoggerInterface */
+       protected $queryLogger;
 
        /** @var resource Database connection */
        protected $mConn = null;
@@ -219,6 +224,183 @@ abstract class DatabaseBase implements IDatabase {
        /** @var TransactionProfiler */
        protected $trxProfiler;
 
+       /**
+        * Constructor.
+        *
+        * FIXME: It is possible to construct a Database object with no associated
+        * connection object, by specifying no parameters to __construct(). This
+        * feature is deprecated and should be removed.
+        *
+        * DatabaseBase subclasses should not be constructed directly in external
+        * code. DatabaseBase::factory() should be used instead.
+        *
+        * @param array $params Parameters passed from DatabaseBase::factory()
+        */
+       function __construct( array $params ) {
+               global $wgDBprefix, $wgDBmwschema;
+
+               $this->srvCache = ObjectCache::getLocalServerInstance( 'hash' );
+
+               $server = $params['host'];
+               $user = $params['user'];
+               $password = $params['password'];
+               $dbName = $params['dbname'];
+               $flags = $params['flags'];
+               $tablePrefix = $params['tablePrefix'];
+               $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 ( $this->cliMode ) {
+                               $this->mFlags &= ~DBO_TRX;
+                       } else {
+                               $this->mFlags |= DBO_TRX;
+                       }
+               }
+
+               $this->mSessionVars = $params['variables'];
+
+               /** Get the default table prefix*/
+               if ( $tablePrefix === 'get from global' ) {
+                       $this->mTablePrefix = $wgDBprefix;
+               } else {
+                       $this->mTablePrefix = $tablePrefix;
+               }
+
+               /** Get the database schema*/
+               if ( $schema === 'get from global' ) {
+                       $this->mSchema = $wgDBmwschema;
+               } else {
+                       $this->mSchema = $schema;
+               }
+
+               $this->mForeign = $foreign;
+
+               $this->profiler = isset( $params['profiler'] )
+                       ? $params['profiler']
+                       : Profiler::instance(); // @TODO: remove global state
+               $this->trxProfiler = isset( $params['trxProfiler'] )
+                       ? $params['trxProfiler']
+                       : new TransactionProfiler();
+               $this->connLogger = isset( $params['connLogger'] )
+                       ? $params['connLogger']
+                       : new \Psr\Log\NullLogger();
+               $this->queryLogger = isset( $params['queryLogger'] )
+                       ? $params['queryLogger']
+                       : new \Psr\Log\NullLogger();
+
+               if ( $user ) {
+                       $this->open( $server, $user, $password, $dbName );
+               }
+       }
+
+       /**
+        * Given a DB type, construct the name of the appropriate child class of
+        * DatabaseBase. This is designed to replace all of the manual stuff like:
+        *    $class = 'Database' . ucfirst( strtolower( $dbType ) );
+        * as well as validate against the canonical list of DB types we have
+        *
+        * This factory function is mostly useful for when you need to connect to a
+        * database other than the MediaWiki default (such as for external auth,
+        * an extension, et cetera). Do not use this to connect to the MediaWiki
+        * database. Example uses in core:
+        * @see LoadBalancer::reallyOpenConnection()
+        * @see ForeignDBRepo::getMasterDB()
+        * @see WebInstallerDBConnect::execute()
+        *
+        * @since 1.18
+        *
+        * @param string $dbType A possible DB type
+        * @param array $p An array of options to pass to the constructor.
+        *    Valid options are: host, user, password, dbname, flags, tablePrefix, schema, driver
+        * @throws MWException If the database driver or extension cannot be found
+        * @return DatabaseBase|null DatabaseBase subclass or null
+        */
+       final public static function factory( $dbType, $p = [] ) {
+               global $wgCommandLineMode;
+
+               $canonicalDBTypes = [
+                       'mysql' => [ 'mysqli', 'mysql' ],
+                       'postgres' => [],
+                       'sqlite' => [],
+                       'oracle' => [],
+                       'mssql' => [],
+               ];
+
+               $driver = false;
+               $dbType = strtolower( $dbType );
+               if ( isset( $canonicalDBTypes[$dbType] ) && $canonicalDBTypes[$dbType] ) {
+                       $possibleDrivers = $canonicalDBTypes[$dbType];
+                       if ( !empty( $p['driver'] ) ) {
+                               if ( in_array( $p['driver'], $possibleDrivers ) ) {
+                                       $driver = $p['driver'];
+                               } else {
+                                       throw new InvalidArgumentException( __METHOD__ .
+                                               " type '$dbType' does not support driver '{$p['driver']}'" );
+                               }
+                       } else {
+                               foreach ( $possibleDrivers as $posDriver ) {
+                                       if ( extension_loaded( $posDriver ) ) {
+                                               $driver = $posDriver;
+                                               break;
+                                       }
+                               }
+                       }
+               } else {
+                       $driver = $dbType;
+               }
+               if ( $driver === false ) {
+                       throw new MWException( __METHOD__ .
+                               " no viable database extension found for type '$dbType'" );
+               }
+
+               // Determine schema defaults. Currently Microsoft SQL Server uses $wgDBmwschema,
+               // and everything else doesn't use a schema (e.g. null)
+               // Although postgres and oracle support schemas, we don't use them (yet)
+               // to maintain backwards compatibility
+               $defaultSchemas = [
+                       'mssql' => 'get from global',
+               ];
+
+               $class = 'Database' . ucfirst( $driver );
+               if ( class_exists( $class ) && is_subclass_of( $class, 'DatabaseBase' ) ) {
+                       // Resolve some defaults for b/c
+                       $p['host'] = isset( $p['host'] ) ? $p['host'] : false;
+                       $p['user'] = isset( $p['user'] ) ? $p['user'] : false;
+                       $p['password'] = isset( $p['password'] ) ? $p['password'] : false;
+                       $p['dbname'] = isset( $p['dbname'] ) ? $p['dbname'] : false;
+                       $p['flags'] = isset( $p['flags'] ) ? $p['flags'] : 0;
+                       $p['variables'] = isset( $p['variables'] ) ? $p['variables'] : [];
+                       $p['tablePrefix'] = isset( $p['tablePrefix'] ) ? $p['tablePrefix'] : 'get from global';
+                       if ( !isset( $p['schema'] ) ) {
+                               $p['schema'] = isset( $defaultSchemas[$dbType] ) ? $defaultSchemas[$dbType] : null;
+                       }
+                       $p['foreign'] = isset( $p['foreign'] ) ? $p['foreign'] : false;
+                       $p['cliMode'] = $wgCommandLineMode;
+
+                       $conn = new $class( $p );
+                       if ( isset( $p['connLogger'] ) ) {
+                               $conn->connLogger = $p['connLogger'];
+                       }
+                       if ( isset( $p['queryLogger'] ) ) {
+                               $conn->queryLogger = $p['queryLogger'];
+                       }
+               } else {
+                       $conn = null;
+               }
+
+               return $conn;
+       }
+
+       public function setLogger( LoggerInterface $logger ) {
+               $this->quertLogger = $logger;
+       }
+
        public function getServerInfo() {
                return $this->getServerVersion();
        }
@@ -552,176 +734,16 @@ abstract class DatabaseBase implements IDatabase {
         */
        abstract function strencode( $s );
 
-       /**
-        * Constructor.
-        *
-        * FIXME: It is possible to construct a Database object with no associated
-        * connection object, by specifying no parameters to __construct(). This
-        * feature is deprecated and should be removed.
-        *
-        * DatabaseBase subclasses should not be constructed directly in external
-        * code. DatabaseBase::factory() should be used instead.
-        *
-        * @param array $params Parameters passed from DatabaseBase::factory()
-        */
-       function __construct( array $params ) {
-               global $wgDBprefix, $wgDBmwschema;
-
-               $this->srvCache = ObjectCache::getLocalServerInstance( 'hash' );
-
-               $server = $params['host'];
-               $user = $params['user'];
-               $password = $params['password'];
-               $dbName = $params['dbname'];
-               $flags = $params['flags'];
-               $tablePrefix = $params['tablePrefix'];
-               $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 ( $this->cliMode ) {
-                               $this->mFlags &= ~DBO_TRX;
-                       } else {
-                               $this->mFlags |= DBO_TRX;
-                       }
-               }
-
-               $this->mSessionVars = $params['variables'];
-
-               /** Get the default table prefix*/
-               if ( $tablePrefix === 'get from global' ) {
-                       $this->mTablePrefix = $wgDBprefix;
-               } else {
-                       $this->mTablePrefix = $tablePrefix;
-               }
-
-               /** Get the database schema*/
-               if ( $schema === 'get from global' ) {
-                       $this->mSchema = $wgDBmwschema;
-               } else {
-                       $this->mSchema = $schema;
-               }
-
-               $this->mForeign = $foreign;
-
-               $this->profiler = isset( $params['profiler'] )
-                       ? $params['profiler']
-                       : Profiler::instance(); // @TODO: remove global state
-               $this->trxProfiler = isset( $params['trxProfiler'] )
-                       ? $params['trxProfiler']
-                       : new TransactionProfiler();
-
-               if ( $user ) {
-                       $this->open( $server, $user, $password, $dbName );
-               }
-
-       }
-
        /**
         * Called by serialize. Throw an exception when DB connection is serialized.
         * This causes problems on some database engines because the connection is
         * not restored on unserialize.
         */
        public function __sleep() {
-               throw new MWException( 'Database serialization may cause problems, since ' .
+               throw new RuntimeException( 'Database serialization may cause problems, since ' .
                        'the connection is not restored on wakeup.' );
        }
 
-       /**
-        * Given a DB type, construct the name of the appropriate child class of
-        * DatabaseBase. This is designed to replace all of the manual stuff like:
-        *    $class = 'Database' . ucfirst( strtolower( $dbType ) );
-        * as well as validate against the canonical list of DB types we have
-        *
-        * This factory function is mostly useful for when you need to connect to a
-        * database other than the MediaWiki default (such as for external auth,
-        * an extension, et cetera). Do not use this to connect to the MediaWiki
-        * database. Example uses in core:
-        * @see LoadBalancer::reallyOpenConnection()
-        * @see ForeignDBRepo::getMasterDB()
-        * @see WebInstallerDBConnect::execute()
-        *
-        * @since 1.18
-        *
-        * @param string $dbType A possible DB type
-        * @param array $p An array of options to pass to the constructor.
-        *    Valid options are: host, user, password, dbname, flags, tablePrefix, schema, driver
-        * @throws MWException If the database driver or extension cannot be found
-        * @return DatabaseBase|null DatabaseBase subclass or null
-        */
-       final public static function factory( $dbType, $p = [] ) {
-               global $wgCommandLineMode;
-
-               $canonicalDBTypes = [
-                       'mysql' => [ 'mysqli', 'mysql' ],
-                       'postgres' => [],
-                       'sqlite' => [],
-                       'oracle' => [],
-                       'mssql' => [],
-               ];
-
-               $driver = false;
-               $dbType = strtolower( $dbType );
-               if ( isset( $canonicalDBTypes[$dbType] ) && $canonicalDBTypes[$dbType] ) {
-                       $possibleDrivers = $canonicalDBTypes[$dbType];
-                       if ( !empty( $p['driver'] ) ) {
-                               if ( in_array( $p['driver'], $possibleDrivers ) ) {
-                                       $driver = $p['driver'];
-                               } else {
-                                       throw new MWException( __METHOD__ .
-                                               " cannot construct Database with type '$dbType' and driver '{$p['driver']}'" );
-                               }
-                       } else {
-                               foreach ( $possibleDrivers as $posDriver ) {
-                                       if ( extension_loaded( $posDriver ) ) {
-                                               $driver = $posDriver;
-                                               break;
-                                       }
-                               }
-                       }
-               } else {
-                       $driver = $dbType;
-               }
-               if ( $driver === false ) {
-                       throw new MWException( __METHOD__ .
-                               " no viable database extension found for type '$dbType'" );
-               }
-
-               // Determine schema defaults. Currently Microsoft SQL Server uses $wgDBmwschema,
-               // and everything else doesn't use a schema (e.g. null)
-               // Although postgres and oracle support schemas, we don't use them (yet)
-               // to maintain backwards compatibility
-               $defaultSchemas = [
-                       'mssql' => 'get from global',
-               ];
-
-               $class = 'Database' . ucfirst( $driver );
-               if ( class_exists( $class ) && is_subclass_of( $class, 'DatabaseBase' ) ) {
-                       // Resolve some defaults for b/c
-                       $p['host'] = isset( $p['host'] ) ? $p['host'] : false;
-                       $p['user'] = isset( $p['user'] ) ? $p['user'] : false;
-                       $p['password'] = isset( $p['password'] ) ? $p['password'] : false;
-                       $p['dbname'] = isset( $p['dbname'] ) ? $p['dbname'] : false;
-                       $p['flags'] = isset( $p['flags'] ) ? $p['flags'] : 0;
-                       $p['variables'] = isset( $p['variables'] ) ? $p['variables'] : [];
-                       $p['tablePrefix'] = isset( $p['tablePrefix'] ) ? $p['tablePrefix'] : 'get from global';
-                       if ( !isset( $p['schema'] ) ) {
-                               $p['schema'] = isset( $defaultSchemas[$dbType] ) ? $defaultSchemas[$dbType] : null;
-                       }
-                       $p['foreign'] = isset( $p['foreign'] ) ? $p['foreign'] : false;
-                       $p['cliMode'] = $wgCommandLineMode;
-
-                       return new $class( $p );
-               } else {
-                       return null;
-               }
-       }
-
        protected function installErrorHandler() {
                $this->mPHPError = false;
                $this->htmlErrors = ini_set( 'html_errors', '0' );
@@ -785,7 +807,7 @@ abstract class DatabaseBase implements IDatabase {
                        $closed = $this->closeConnection();
                        $this->mConn = false;
                } elseif ( $this->mTrxIdleCallbacks || $this->mTrxEndCallbacks ) { // sanity
-                       throw new MWException( "Transaction callbacks still pending." );
+                       throw new RuntimeException( "Transaction callbacks still pending." );
                } else {
                        $closed = true;
                }
@@ -912,7 +934,7 @@ abstract class DatabaseBase implements IDatabase {
                }
 
                if ( $this->debug() ) {
-                       wfDebugLog( 'queries', sprintf( "%s: %s", $this->mDBname, $commentedSql ) );
+                       $this->queryLogger->debug( "{$this->mDBname} {$commentedSql}" );
                }
 
                # Avoid fatals if close() was called
@@ -929,11 +951,10 @@ abstract class DatabaseBase implements IDatabase {
                        $lastErrno = $this->lastErrno();
                        # Update state tracking to reflect transaction loss due to disconnection
                        $this->handleTransactionLoss();
-                       wfDebug( "Connection lost, reconnecting...\n" );
                        if ( $this->reconnect() ) {
-                               wfDebug( "Reconnected\n" );
                                $msg = __METHOD__ . ": lost connection to {$this->getServer()}; reconnected";
-                               wfDebugLog( 'DBPerformance', "$msg:\n" . wfBacktrace( true ) );
+                               $this->connLogger->warning( $msg );
+                               $this->queryLogger->warning( "$msg:\n" . wfBacktrace( true ) );
 
                                if ( !$recoverable ) {
                                        # Callers may catch the exception and continue to use the DB
@@ -943,7 +964,8 @@ abstract class DatabaseBase implements IDatabase {
                                        $ret = $this->doProfiledQuery( $sql, $commentedSql, $isWrite, $fname );
                                }
                        } else {
-                               wfDebug( "Failed\n" );
+                               $msg = __METHOD__ . ": lost connection to {$this->getServer()} permanently";
+                               $this->connLogger->error( $msg );
                        }
                }
 
@@ -1078,7 +1100,7 @@ abstract class DatabaseBase implements IDatabase {
 
        public function reportQueryError( $error, $errno, $sql, $fname, $tempIgnore = false ) {
                if ( $this->ignoreErrors() || $tempIgnore ) {
-                       wfDebug( "SQL ERROR (ignored): $error\n" );
+                       $this->queryLogger->debug( "SQL ERROR (ignored): $error\n" );
                } else {
                        $sql1line = mb_substr( str_replace( "\n", "\\n", $sql ), 0, 5 * 1024 );
                        wfLogDBError(
@@ -1091,7 +1113,7 @@ abstract class DatabaseBase implements IDatabase {
                                        'fname' => $fname,
                                ] )
                        );
-                       wfDebug( "SQL ERROR: " . $error . "\n" );
+                       $this->queryLogger->debug( "SQL ERROR: " . $error . "\n" );
                        throw new DBQueryError( $this, $error, $errno, $sql, $fname );
                }
        }
@@ -1714,7 +1736,7 @@ abstract class DatabaseBase implements IDatabase {
                                        unset( $value[$nullKey] );
                                }
                                if ( count( $value ) == 0 && !$includeNull ) {
-                                       throw new MWException( __METHOD__ . ": empty input for field $field" );
+                                       throw new InvalidArgumentException( __METHOD__ . ": empty input for field $field" );
                                } elseif ( count( $value ) == 0 ) {
                                        // only check if $field is null
                                        $list .= "$field IS NULL";
@@ -3110,12 +3132,12 @@ abstract class DatabaseBase implements IDatabase {
        public function duplicateTableStructure( $oldName, $newName, $temporary = false,
                $fname = __METHOD__
        ) {
-               throw new MWException(
+               throw new RuntimeException(
                        'DatabaseBase::duplicateTableStructure is not implemented in descendant class' );
        }
 
        function listTables( $prefix = null, $fname = __METHOD__ ) {
-               throw new MWException( 'DatabaseBase::listTables is not implemented in descendant class' );
+               throw new RuntimeException( 'DatabaseBase::listTables is not implemented in descendant class' );
        }
 
        /**
@@ -3139,7 +3161,7 @@ abstract class DatabaseBase implements IDatabase {
         * @since 1.22
         */
        public function listViews( $prefix = null, $fname = __METHOD__ ) {
-               throw new MWException( 'DatabaseBase::listViews is not implemented in descendant class' );
+               throw new RuntimeException( 'DatabaseBase::listViews is not implemented in descendant class' );
        }
 
        /**
@@ -3151,7 +3173,7 @@ abstract class DatabaseBase implements IDatabase {
         * @since 1.22
         */
        public function isView( $name ) {
-               throw new MWException( 'DatabaseBase::isView is not implemented in descendant class' );
+               throw new RuntimeException( 'DatabaseBase::isView is not implemented in descendant class' );
        }
 
        public function timestamp( $ts = 0 ) {
@@ -3346,7 +3368,7 @@ abstract class DatabaseBase implements IDatabase {
                MediaWiki\restoreWarnings();
 
                if ( false === $fp ) {
-                       throw new MWException( "Could not open \"{$filename}\".\n" );
+                       throw new RuntimeException( "Could not open \"{$filename}\".\n" );
                }
 
                if ( !$fname ) {
index f8770d2..269a248 100644 (file)
@@ -1080,17 +1080,17 @@ class DatabaseMssql extends Database {
         */
        private function escapeIdentifier( $identifier ) {
                if ( strlen( $identifier ) == 0 ) {
-                       throw new MWException( "An identifier must not be empty" );
+                       throw new InvalidArgumentException( "An identifier must not be empty" );
                }
                if ( strlen( $identifier ) > 128 ) {
-                       throw new MWException( "The identifier '$identifier' is too long (max. 128)" );
+                       throw new InvalidArgumentException( "The identifier '$identifier' is too long (max. 128)" );
                }
                if ( ( strpos( $identifier, '[' ) !== false )
                        || ( strpos( $identifier, ']' ) !== false )
                ) {
                        // It may be allowed if you quoted with double quotation marks, but
                        // that would break if QUOTED_IDENTIFIER is OFF
-                       throw new MWException( "Square brackets are not allowed in '$identifier'" );
+                       throw new InvalidArgumentException( "Square brackets are not allowed in '$identifier'" );
                }
 
                return "[$identifier]";
index ebeb3a6..f401058 100644 (file)
@@ -369,7 +369,7 @@ class DatabaseOracle extends Database {
        protected function doQuery( $sql ) {
                wfDebug( "SQL: [$sql]\n" );
                if ( !StringUtils::isUtf8( $sql ) ) {
-                       throw new MWException( "SQL encoding is invalid\n$sql" );
+                       throw new InvalidArgumentException( "SQL encoding is invalid\n$sql" );
                }
 
                // handle some oracle specifics
index e6401b3..ef08ab0 100644 (file)
@@ -962,7 +962,7 @@ class DatabaseSqlite extends Database {
                        $this->addQuotes( $oldName ) . " AND type='table'", $fname );
                $obj = $this->fetchObject( $res );
                if ( !$obj ) {
-                       throw new MWException( "Couldn't retrieve structure for table $oldName" );
+                       throw new RuntimeException( "Couldn't retrieve structure for table $oldName" );
                }
                $sql = $obj->sql;
                $sql = preg_replace(
diff --git a/includes/db/loadbalancer/ILoadBalancer.php b/includes/db/loadbalancer/ILoadBalancer.php
new file mode 100644 (file)
index 0000000..9313ccd
--- /dev/null
@@ -0,0 +1,473 @@
+<?php
+/**
+ * Database load balancing interface
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup Database
+ * @author Aaron Schulz
+ */
+
+/**
+ * Interface for database load balancing object that manages IDatabase handles
+ *
+ * @todo: loosen up DB classes from MWException
+ * @since 1.28
+ * @ingroup Database
+ */
+interface ILoadBalancer {
+       /**
+        * @param array $params Array with keys:
+        *  - servers : Required. Array of server info structures.
+        *  - loadMonitor : Name of a class used to fetch server lag and load.
+        *  - readOnlyReason : Reason the master DB is read-only if so [optional]
+        *  - waitTimeout : Maximum time to wait for replicas for consistency [optional]
+        *  - srvCache : BagOStuff object [optional]
+        *  - wanCache : WANObjectCache object [optional]
+        * @throws MWException
+        */
+       public function __construct( array $params );
+
+       /**
+        * Get the index of the reader connection, which may be a replica DB
+        * This takes into account load ratios and lag times. It should
+        * always return a consistent index during a given invocation
+        *
+        * Side effect: opens connections to databases
+        * @param string|bool $group Query group, or false for the generic reader
+        * @param string|bool $wiki Wiki ID, or false for the current wiki
+        * @throws MWException
+        * @return bool|int|string
+        */
+       public function getReaderIndex( $group = false, $wiki = false );
+
+       /**
+        * Set the master wait position
+        * If a DB_REPLICA connection has been opened already, waits
+        * Otherwise sets a variable telling it to wait if such a connection is opened
+        * @param DBMasterPos $pos
+        */
+       public function waitFor( $pos );
+
+       /**
+        * Set the master wait position and wait for a "generic" replica DB to catch up to it
+        *
+        * This can be used a faster proxy for waitForAll()
+        *
+        * @param DBMasterPos $pos
+        * @param int $timeout Max seconds to wait; default is mWaitTimeout
+        * @return bool Success (able to connect and no timeouts reached)
+        */
+       public function waitForOne( $pos, $timeout = null );
+
+       /**
+        * Set the master wait position and wait for ALL replica DBs to catch up to it
+        * @param DBMasterPos $pos
+        * @param int $timeout Max seconds to wait; default is mWaitTimeout
+        * @return bool Success (able to connect and no timeouts reached)
+        */
+       public function waitForAll( $pos, $timeout = null );
+
+       /**
+        * Get any open connection to a given server index, local or foreign
+        * Returns false if there is no connection open
+        *
+        * @param int $i Server index
+        * @return IDatabase|bool False on failure
+        */
+       public function getAnyOpenConnection( $i );
+
+       /**
+        * Get a connection by index
+        * This is the main entry point for this class.
+        *
+        * @param int $i Server index
+        * @param array|string|bool $groups Query group(s), or false for the generic reader
+        * @param string|bool $wiki Wiki ID, or false for the current wiki
+        *
+        * @throws MWException
+        * @return IDatabase
+        */
+       public function getConnection( $i, $groups = [], $wiki = false );
+
+       /**
+        * Mark a foreign connection as being available for reuse under a different
+        * DB name or prefix. This mechanism is reference-counted, and must be called
+        * the same number of times as getConnection() to work.
+        *
+        * @param IDatabase $conn
+        * @throws MWException
+        */
+       public function reuseConnection( $conn );
+
+       /**
+        * Get a database connection handle reference
+        *
+        * The handle's methods wrap simply wrap those of a IDatabase handle
+        *
+        * @see LoadBalancer::getConnection() for parameter information
+        *
+        * @param int $db
+        * @param array|string|bool $groups Query group(s), or false for the generic reader
+        * @param string|bool $wiki Wiki ID, or false for the current wiki
+        * @return DBConnRef
+        */
+       public function getConnectionRef( $db, $groups = [], $wiki = false );
+
+       /**
+        * Get a database connection handle reference without connecting yet
+        *
+        * The handle's methods wrap simply wrap those of a IDatabase handle
+        *
+        * @see LoadBalancer::getConnection() for parameter information
+        *
+        * @param int $db
+        * @param array|string|bool $groups Query group(s), or false for the generic reader
+        * @param string|bool $wiki Wiki ID, or false for the current wiki
+        * @return DBConnRef
+        */
+       public function getLazyConnectionRef( $db, $groups = [], $wiki = false );
+
+       /**
+        * Open a connection to the server given by the specified index
+        * Index must be an actual index into the array.
+        * If the server is already open, returns it.
+        *
+        * On error, returns false, and the connection which caused the
+        * error will be available via $this->mErrorConnection.
+        *
+        * @note If disable() was called on this LoadBalancer, this method will throw a DBAccessError.
+        *
+        * @param int $i Server index
+        * @param string|bool $wiki Wiki ID, or false for the current wiki
+        * @return IDatabase|bool Returns false on errors
+        */
+       public function openConnection( $i, $wiki = false );
+
+       /**
+        * @return int
+        */
+       public function getWriterIndex();
+
+       /**
+        * Returns true if the specified index is a valid server index
+        *
+        * @param string $i
+        * @return bool
+        */
+       public function haveIndex( $i );
+
+       /**
+        * Returns true if the specified index is valid and has non-zero load
+        *
+        * @param string $i
+        * @return bool
+        */
+       public function isNonZeroLoad( $i );
+
+       /**
+        * Get the number of defined servers (not the number of open connections)
+        *
+        * @return int
+        */
+       public function getServerCount();
+
+       /**
+        * Get the host name or IP address of the server with the specified index
+        * Prefer a readable name if available.
+        * @param string $i
+        * @return string
+        */
+       public function getServerName( $i );
+
+       /**
+        * Return the server info structure for a given index, or false if the index is invalid.
+        * @param int $i
+        * @return array|bool
+        */
+       public function getServerInfo( $i );
+
+       /**
+        * Sets the server info structure for the given index. Entry at index $i
+        * is created if it doesn't exist
+        * @param int $i
+        * @param array $serverInfo
+        */
+       public function setServerInfo( $i, array $serverInfo );
+
+       /**
+        * Get the current master position for chronology control purposes
+        * @return DBMasterPos|bool Returns false if not applicable
+        */
+       public function getMasterPos();
+
+       /**
+        * Disable this load balancer. All connections are closed, and any attempt to
+        * open a new connection will result in a DBAccessError.
+        */
+       public function disable();
+
+       /**
+        * Close all open connections
+        */
+       public function closeAll();
+
+       /**
+        * Close a connection
+        *
+        * Using this function makes sure the LoadBalancer knows the connection is closed.
+        * If you use $conn->close() directly, the load balancer won't update its state.
+        *
+        * @param IDatabase $conn
+        */
+       public function closeConnection( IDatabase $conn );
+
+       /**
+        * Commit transactions on all open connections
+        * @param string $fname Caller name
+        * @throws DBExpectedError
+        */
+       public function commitAll( $fname = __METHOD__ );
+
+       /**
+        * Perform all pre-commit callbacks that remain part of the atomic transactions
+        * and disable any post-commit callbacks until runMasterPostTrxCallbacks()
+        *
+        * Use this only for mutli-database commits
+        */
+       public function finalizeMasterChanges();
+
+       /**
+        * Perform all pre-commit checks for things like replication safety
+        *
+        * Use this only for mutli-database commits
+        *
+        * @param array $options Includes:
+        *   - maxWriteDuration : max write query duration time in seconds
+        * @throws DBTransactionError
+        */
+       public function approveMasterChanges( array $options );
+
+       /**
+        * Flush any master transaction snapshots and set DBO_TRX (if DBO_DEFAULT is set)
+        *
+        * The DBO_TRX setting will be reverted to the default in each of these methods:
+        *   - commitMasterChanges()
+        *   - rollbackMasterChanges()
+        *   - commitAll()
+        * This allows for custom transaction rounds from any outer transaction scope.
+        *
+        * @param string $fname
+        * @throws DBExpectedError
+        */
+       public function beginMasterChanges( $fname = __METHOD__ );
+
+       /**
+        * Issue COMMIT on all master connections where writes where done
+        * @param string $fname Caller name
+        * @throws DBExpectedError
+        */
+       public function commitMasterChanges( $fname = __METHOD__ );
+
+       /**
+        * Issue all pending post-COMMIT/ROLLBACK callbacks
+        *
+        * Use this only for mutli-database commits
+        *
+        * @param integer $type IDatabase::TRIGGER_* constant
+        * @return Exception|null The first exception or null if there were none
+        */
+       public function runMasterPostTrxCallbacks( $type );
+
+       /**
+        * Issue ROLLBACK only on master, only if queries were done on connection
+        * @param string $fname Caller name
+        * @throws DBExpectedError
+        */
+       public function rollbackMasterChanges( $fname = __METHOD__ );
+
+       /**
+        * Suppress all pending post-COMMIT/ROLLBACK callbacks
+        *
+        * Use this only for mutli-database commits
+        *
+        * @return Exception|null The first exception or null if there were none
+        */
+       public function suppressTransactionEndCallbacks();
+
+       /**
+        * Commit all replica DB transactions so as to flush any REPEATABLE-READ or SSI snapshot
+        *
+        * @param string $fname Caller name
+        */
+       public function flushReplicaSnapshots( $fname = __METHOD__ );
+
+       /**
+        * @return bool Whether a master connection is already open
+        */
+       public function hasMasterConnection();
+
+       /**
+        * Determine if there are pending changes in a transaction by this thread
+        * @return bool
+        */
+       public function hasMasterChanges();
+
+       /**
+        * Get the timestamp of the latest write query done by this thread
+        * @return float|bool UNIX timestamp or false
+        */
+       public function lastMasterChangeTimestamp();
+
+       /**
+        * Check if this load balancer object had any recent or still
+        * pending writes issued against it by this PHP thread
+        *
+        * @param float $age How many seconds ago is "recent" [defaults to mWaitTimeout]
+        * @return bool
+        */
+       public function hasOrMadeRecentMasterChanges( $age = null );
+
+       /**
+        * Get the list of callers that have pending master changes
+        *
+        * @return string[] List of method names
+        */
+       public function pendingMasterChangeCallers();
+
+       /**
+        * @note This method will trigger a DB connection if not yet done
+        * @param string|bool $wiki Wiki ID, or false for the current wiki
+        * @return bool Whether the generic connection for reads is highly "lagged"
+        */
+       public function getLaggedReplicaMode( $wiki = false );
+
+       /**
+        * @note This method will never cause a new DB connection
+        * @return bool Whether any generic connection used for reads was highly "lagged"
+        */
+       public function laggedReplicaUsed();
+
+       /**
+        * @note This method may trigger a DB connection if not yet done
+        * @param string|bool $wiki Wiki ID, or false for the current wiki
+        * @param IDatabase|null DB master connection; used to avoid loops [optional]
+        * @return string|bool Reason the master is read-only or false if it is not
+        */
+       public function getReadOnlyReason( $wiki = false, IDatabase $conn = null );
+
+       /**
+        * Disables/enables lag checks
+        * @param null|bool $mode
+        * @return bool
+        */
+       public function allowLagged( $mode = null );
+
+       /**
+        * @return bool
+        */
+       public function pingAll();
+
+       /**
+        * Call a function with each open connection object
+        * @param callable $callback
+        * @param array $params
+        */
+       public function forEachOpenConnection( $callback, array $params = [] );
+
+       /**
+        * Call a function with each open connection object to a master
+        * @param callable $callback
+        * @param array $params
+        */
+       public function forEachOpenMasterConnection( $callback, array $params = [] );
+
+       /**
+        * Call a function with each open replica DB connection object
+        * @param callable $callback
+        * @param array $params
+        */
+       public function forEachOpenReplicaConnection( $callback, array $params = [] );
+
+       /**
+        * Get the hostname and lag time of the most-lagged replica DB
+        *
+        * This is useful for maintenance scripts that need to throttle their updates.
+        * May attempt to open connections to replica DBs on the default DB. If there is
+        * no lag, the maximum lag will be reported as -1.
+        *
+        * @param bool|string $wiki Wiki ID, or false for the default database
+        * @return array ( host, max lag, index of max lagged host )
+        */
+       public function getMaxLag( $wiki = false );
+
+       /**
+        * Get an estimate of replication lag (in seconds) for each server
+        *
+        * Results are cached for a short time in memcached/process cache
+        *
+        * Values may be "false" if replication is too broken to estimate
+        *
+        * @param string|bool $wiki
+        * @return int[] Map of (server index => float|int|bool)
+        */
+       public function getLagTimes( $wiki = false );
+
+       /**
+        * Get the lag in seconds for a given connection, or zero if this load
+        * balancer does not have replication enabled.
+        *
+        * This should be used in preference to Database::getLag() in cases where
+        * replication may not be in use, since there is no way to determine if
+        * replication is in use at the connection level without running
+        * potentially restricted queries such as SHOW SLAVE STATUS. Using this
+        * function instead of Database::getLag() avoids a fatal error in this
+        * case on many installations.
+        *
+        * @param IDatabase $conn
+        * @return int|bool Returns false on error
+        */
+       public function safeGetLag( IDatabase $conn );
+
+       /**
+        * Wait for a replica DB to reach a specified master position
+        *
+        * This will connect to the master to get an accurate position if $pos is not given
+        *
+        * @param IDatabase $conn Replica DB
+        * @param DBMasterPos|bool $pos Master position; default: current position
+        * @param integer|null $timeout Timeout in seconds [optional]
+        * @return bool Success
+        */
+       public function safeWaitForMasterPos( IDatabase $conn, $pos = false, $timeout = null );
+
+       /**
+        * Clear the cache for slag lag delay times
+        *
+        * This is only used for testing
+        */
+       public function clearLagTimeCache();
+
+       /**
+        * Set a callback via IDatabase::setTransactionListener() on
+        * all current and future master connections of this load balancer
+        *
+        * @param string $name Callback name
+        * @param callable|null $callback
+        */
+       public function setTransactionListener( $name, callable $callback = null );
+}
index 74353b4..5115fbe 100644 (file)
@@ -21,9 +21,9 @@
  * @ingroup Database
  */
 
+use Psr\Log\LoggerInterface;
 use MediaWiki\MediaWikiServices;
 use MediaWiki\Services\DestructibleService;
-use Psr\Log\LoggerInterface;
 use MediaWiki\Logger\LoggerFactory;
 
 /**
@@ -37,8 +37,12 @@ abstract class LBFactory implements DestructibleService {
        protected $trxProfiler;
        /** @var LoggerInterface */
        protected $trxLogger;
+       /** @var LoggerInterface */
+       protected $replLogger;
        /** @var BagOStuff */
        protected $srvCache;
+       /** @var BagOStuff */
+       protected $memCache;
        /** @var WANObjectCache */
        protected $wanCache;
 
@@ -67,12 +71,18 @@ abstract class LBFactory implements DestructibleService {
                $this->chronProt = $this->newChronologyProtector();
                $this->trxProfiler = Profiler::instance()->getTransactionProfiler();
                // Use APC/memcached style caching, but avoids loops with CACHE_DB (T141804)
-               $cache = ObjectCache::getLocalServerInstance();
-               if ( $cache->getQoS( $cache::ATTR_EMULATION ) > $cache::QOS_EMULATION_SQL ) {
-                       $this->srvCache = $cache;
+               $sCache = ObjectCache::getLocalServerInstance();
+               if ( $sCache->getQoS( $sCache::ATTR_EMULATION ) > $sCache::QOS_EMULATION_SQL ) {
+                       $this->srvCache = $sCache;
                } else {
                        $this->srvCache = new EmptyBagOStuff();
                }
+               $cCache = ObjectCache::getLocalClusterInstance();
+               if ( $cCache->getQoS( $cCache::ATTR_EMULATION ) > $cCache::QOS_EMULATION_SQL ) {
+                       $this->memCache = $cCache;
+               } else {
+                       $this->memCache = new EmptyBagOStuff();
+               }
                $wCache = ObjectCache::getMainWANInstance();
                if ( $wCache->getQoS( $wCache::ATTR_EMULATION ) > $wCache::QOS_EMULATION_SQL ) {
                        $this->wanCache = $wCache;
@@ -80,6 +90,7 @@ abstract class LBFactory implements DestructibleService {
                        $this->wanCache = WANObjectCache::newEmpty();
                }
                $this->trxLogger = LoggerFactory::getInstance( 'DBTransaction' );
+               $this->replLogger = LoggerFactory::getInstance( 'DBReplication' );
                $this->ticket = mt_rand();
        }
 
@@ -149,7 +160,7 @@ abstract class LBFactory implements DestructibleService {
         * @deprecated since 1.27, use LBFactory::destroy()
         */
        public static function destroyInstance() {
-               self::singleton()->destroy();
+               MediaWikiServices::getInstance()->getDBLoadBalancerFactory()->destroy();
        }
 
        /**
@@ -597,7 +608,7 @@ abstract class LBFactory implements DestructibleService {
                                'ip' => $request->getIP(),
                                'agent' => $request->getHeader( 'User-Agent' ),
                        ],
-                       $request->getFloat( 'cpPosTime', null )
+                       $request->getFloat( 'cpPosTime', $request->getCookie( 'cpPosTime', '' ) )
                );
                if ( PHP_SAPI === 'cli' ) {
                        $chronProt->setEnabled( false );
@@ -643,6 +654,25 @@ abstract class LBFactory implements DestructibleService {
                } );
        }
 
+       /**
+        * Base parameters to LoadBalancer::__construct()
+        * @return array
+        */
+       final protected function baseLoadBalancerParams() {
+               return [
+                       'localDomain' => wfWikiID(),
+                       'readOnlyReason' => $this->readOnlyReason,
+                       'srvCache' => $this->srvCache,
+                       'memCache' => $this->memCache,
+                       'wanCache' => $this->wanCache,
+                       'trxProfiler' => $this->trxProfiler,
+                       'queryLogger' => LoggerFactory::getInstance( 'DBQuery' ),
+                       'connLogger' => LoggerFactory::getInstance( 'DBConnection' ),
+                       'replLogger' => LoggerFactory::getInstance( 'DBReplication' ),
+                       'errorLogger' => [ MWExceptionHandler::class, 'logException' ]
+               ];
+       }
+
        /**
         * @param LoadBalancer $lb
         */
index e860840..dd7737b 100644 (file)
@@ -178,7 +178,7 @@ class LBFactoryMulti extends LBFactory {
 
                foreach ( $required as $key ) {
                        if ( !isset( $conf[$key] ) ) {
-                               throw new MWException( __CLASS__ . ": $key is required in configuration" );
+                               throw new InvalidArgumentException( __CLASS__ . ": $key is required in configuration" );
                        }
                        $this->$key = $conf[$key];
                }
@@ -269,7 +269,7 @@ class LBFactoryMulti extends LBFactory {
         */
        protected function newExternalLB( $cluster, $wiki = false ) {
                if ( !isset( $this->externalLoads[$cluster] ) ) {
-                       throw new MWException( __METHOD__ . ": Unknown cluster \"$cluster\"" );
+                       throw new InvalidArgumentException( __METHOD__ . ": Unknown cluster \"$cluster\"" );
                }
                $template = $this->serverTemplate;
                if ( isset( $this->externalTemplateOverrides ) ) {
@@ -311,15 +311,14 @@ class LBFactoryMulti extends LBFactory {
         * @return LoadBalancer
         */
        private function newLoadBalancer( $template, $loads, $groupLoads, $readOnlyReason ) {
-               $lb = new LoadBalancer( [
-                       'servers' => $this->makeServerArray( $template, $loads, $groupLoads ),
-                       'loadMonitor' => $this->loadMonitorClass,
-                       'readOnlyReason' => $readOnlyReason,
-                       'trxProfiler' => $this->trxProfiler,
-                       'srvCache' => $this->srvCache,
-                       'wanCache' => $this->wanCache
-               ] );
-
+               $lb = new LoadBalancer( array_merge(
+                       $this->baseLoadBalancerParams(),
+                       [
+                               'servers' => $this->makeServerArray( $template, $loads, $groupLoads ),
+                               'loadMonitor' => $this->loadMonitorClass,
+                               'readOnlyReason' => $readOnlyReason
+                       ]
+               ) );
                $this->initLoadBalancer( $lb );
 
                return $lb;
index b6fb0d2..d8590b7 100644 (file)
@@ -110,7 +110,7 @@ class LBFactorySimple extends LBFactory {
        protected function newExternalLB( $cluster, $wiki = false ) {
                global $wgExternalServers;
                if ( !isset( $wgExternalServers[$cluster] ) ) {
-                       throw new MWException( __METHOD__ . ": Unknown cluster \"$cluster\"" );
+                       throw new InvalidArgumentException( __METHOD__ . ": Unknown cluster \"$cluster\"" );
                }
 
                return $this->newLoadBalancer( $wgExternalServers[$cluster] );
@@ -131,15 +131,13 @@ class LBFactorySimple extends LBFactory {
        }
 
        private function newLoadBalancer( array $servers ) {
-               $lb = new LoadBalancer( [
-                       'servers' => $servers,
-                       'loadMonitor' => $this->loadMonitorClass,
-                       'readOnlyReason' => $this->readOnlyReason,
-                       'trxProfiler' => $this->trxProfiler,
-                       'srvCache' => $this->srvCache,
-                       'wanCache' => $this->wanCache
-               ] );
-
+               $lb = new LoadBalancer( array_merge(
+                       $this->baseLoadBalancerParams(),
+                       [
+                               'servers' => $servers,
+                               'loadMonitor' => $this->loadMonitorClass,
+                       ]
+               ) );
                $this->initLoadBalancer( $lb );
 
                return $lb;
index 14cec0e..de82a1f 100644 (file)
@@ -35,12 +35,7 @@ class LBFactorySingle extends LBFactory {
        public function __construct( array $conf ) {
                parent::__construct( $conf );
 
-               $this->lb = new LoadBalancerSingle( [
-                       'readOnlyReason' => $this->readOnlyReason,
-                       'trxProfiler' => $this->trxProfiler,
-                       'srvCache' => $this->srvCache,
-                       'wanCache' => $this->wanCache
-               ] + $conf );
+               $this->lb = new LoadBalancerSingle( array_merge( $this->baseLoadBalancerParams(), $conf ) );
        }
 
        /**
index 96ae2e7..8069cf6 100644 (file)
@@ -1,6 +1,6 @@
 <?php
 /**
- * Database load balancing.
+ * Database load balancing manager
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
  * @file
  * @ingroup Database
  */
+use Psr\Log\LoggerInterface;
 
 /**
- * Database load balancing object
+ * Database load balancing, tracking, and transaction management object
  *
- * @todo document
  * @ingroup Database
  */
-class LoadBalancer {
+class LoadBalancer implements ILoadBalancer {
        /** @var array[] Map of (server index => server config array) */
        private $mServers;
        /** @var array[] Map of (local/foreignUsed/foreignFree => server index => DatabaseBase array) */
@@ -40,19 +40,27 @@ class LoadBalancer {
        private $mAllowLagged;
        /** @var integer Seconds to spend waiting on replica DB lag to resolve */
        private $mWaitTimeout;
-       /** @var array LBFactory information */
-       private $mParentInfo;
-
        /** @var string The LoadMonitor subclass name */
        private $mLoadMonitorClass;
+
        /** @var LoadMonitor */
        private $mLoadMonitor;
        /** @var BagOStuff */
        private $srvCache;
+       /** @var BagOStuff */
+       private $memCache;
        /** @var WANObjectCache */
        private $wanCache;
        /** @var TransactionProfiler */
        protected $trxProfiler;
+       /** @var LoggerInterface */
+       protected $replLogger;
+       /** @var LoggerInterface */
+       protected $connLogger;
+       /** @var LoggerInterface */
+       protected $queryLogger;
+       /** @var LoggerInterface */
+       protected $perfLogger;
 
        /** @var bool|DatabaseBase Database connection that caused a problem */
        private $mErrorConnection;
@@ -74,6 +82,13 @@ class LoadBalancer {
        private $trxRoundId = false;
        /** @var array[] Map of (name => callable) */
        private $trxRecurringCallbacks = [];
+       /** @var string Local Wiki ID and default for selectDB() calls */
+       private $localDomain;
+       /** @var callable Exception logger */
+       private $errorLogger;
+
+       /** @var boolean */
+       private $disabled = false;
 
        /** @var integer Warn when this many connection are held */
        const CONN_HELD_WARN_THRESHOLD = 10;
@@ -84,32 +99,17 @@ class LoadBalancer {
        /** @var integer Seconds to cache master server read-only status */
        const TTL_CACHE_READONLY = 5;
 
-       /**
-        * @var boolean
-        */
-       private $disabled = false;
-
-       /**
-        * @param array $params Array with keys:
-        *  - servers : Required. Array of server info structures.
-        *  - loadMonitor : Name of a class used to fetch server lag and load.
-        *  - readOnlyReason : Reason the master DB is read-only if so [optional]
-        *  - waitTimeout : Maximum time to wait for replicas for consistency [optional]
-        *  - srvCache : BagOStuff object [optional]
-        *  - wanCache : WANObjectCache object [optional]
-        * @throws MWException
-        */
        public function __construct( array $params ) {
                if ( !isset( $params['servers'] ) ) {
-                       throw new MWException( __CLASS__ . ': missing servers parameter' );
+                       throw new InvalidArgumentException( __CLASS__ . ': missing servers parameter' );
                }
                $this->mServers = $params['servers'];
                $this->mWaitTimeout = isset( $params['waitTimeout'] )
                        ? $params['waitTimeout']
                        : self::POS_WAIT_TIMEOUT;
+               $this->localDomain = isset( $params['localDomain'] ) ? $params['localDomain'] : '';
 
                $this->mReadIndex = -1;
-               $this->mWriteIndex = -1;
                $this->mConns = [
                        'local' => [],
                        'foreignUsed' => [],
@@ -151,6 +151,11 @@ class LoadBalancer {
                } else {
                        $this->srvCache = new EmptyBagOStuff();
                }
+               if ( isset( $params['memCache'] ) ) {
+                       $this->memCache = $params['memCache'];
+               } else {
+                       $this->memCache = new EmptyBagOStuff();
+               }
                if ( isset( $params['wanCache'] ) ) {
                        $this->wanCache = $params['wanCache'];
                } else {
@@ -161,6 +166,16 @@ class LoadBalancer {
                } else {
                        $this->trxProfiler = new TransactionProfiler();
                }
+
+               $this->errorLogger = isset( $params['errorLogger'] )
+                       ? $params['errorLogger']
+                       : function ( Exception $e ) {
+                               trigger_error( E_WARNING, $e->getMessage() );
+                       };
+
+               foreach ( [ 'replLogger', 'connLogger', 'queryLogger', 'perfLogger' ] as $key ) {
+                       $this->$key = isset( $params[$key] ) ? $params[$key] : new \Psr\Log\NullLogger();
+               }
        }
 
        /**
@@ -171,7 +186,8 @@ class LoadBalancer {
        private function getLoadMonitor() {
                if ( !isset( $this->mLoadMonitor ) ) {
                        $class = $this->mLoadMonitorClass;
-                       $this->mLoadMonitor = new $class( $this );
+                       $this->mLoadMonitor = new $class( $this, $this->srvCache, $this->memCache );
+                       $this->mLoadMonitor->setLogger( $this->replLogger );
                }
 
                return $this->mLoadMonitor;
@@ -198,10 +214,10 @@ class LoadBalancer {
 
                                $host = $this->getServerName( $i );
                                if ( $lag === false && !is_infinite( $maxServerLag ) ) {
-                                       wfDebugLog( 'replication', "Server $host (#$i) is not replicating?" );
+                                       $this->replLogger->error( "Server $host (#$i) is not replicating?" );
                                        unset( $loads[$i] );
                                } elseif ( $lag > $maxServerLag ) {
-                                       wfDebugLog( 'replication', "Server $host (#$i) has >= $lag seconds of lag" );
+                                       $this->replLogger->warning( "Server $host (#$i) has >= $lag seconds of lag" );
                                        unset( $loads[$i] );
                                }
                        }
@@ -228,28 +244,10 @@ class LoadBalancer {
                return ArrayUtils::pickRandom( $loads );
        }
 
-       /**
-        * Get the index of the reader connection, which may be a replica DB
-        * This takes into account load ratios and lag times. It should
-        * always return a consistent index during a given invocation
-        *
-        * Side effect: opens connections to databases
-        * @param string|bool $group Query group, or false for the generic reader
-        * @param string|bool $wiki Wiki ID, or false for the current wiki
-        * @throws MWException
-        * @return bool|int|string
-        */
        public function getReaderIndex( $group = false, $wiki = false ) {
-               global $wgDBtype;
-
-               # @todo FIXME: For now, only go through all this for mysql databases
-               if ( $wgDBtype != 'mysql' ) {
-                       return $this->getWriterIndex();
-               }
-
                if ( count( $this->mServers ) == 1 ) {
                        # Skip the load balancing if there's only one server
-                       return 0;
+                       return $this->getWriterIndex();
                } elseif ( $group === false && $this->mReadIndex >= 0 ) {
                        # Shortcut if generic reader exists already
                        return $this->mReadIndex;
@@ -261,7 +259,7 @@ class LoadBalancer {
                                $nonErrorLoads = $this->mGroupLoads[$group];
                        } else {
                                # No loads for this group, return false and the caller can use some other group
-                               wfDebugLog( 'connect', __METHOD__ . ": no loads for group $group\n" );
+                               $this->connLogger->info( __METHOD__ . ": no loads for group $group" );
 
                                return false;
                        }
@@ -270,10 +268,10 @@ class LoadBalancer {
                }
 
                if ( !count( $nonErrorLoads ) ) {
-                       throw new MWException( "Empty server array given to LoadBalancer" );
+                       throw new InvalidArgumentException( "Empty server array given to LoadBalancer" );
                }
 
-               # Scale the configured load ratios according to the dynamic load (if the load monitor supports it)
+               # Scale the configured load ratios according to the dynamic load if supported
                $this->getLoadMonitor()->scaleLoads( $nonErrorLoads, $group, $wiki );
 
                $laggedReplicaMode = false;
@@ -302,7 +300,7 @@ class LoadBalancer {
                                }
                                if ( $i === false && count( $currentLoads ) != 0 ) {
                                        # All replica DBs lagged. Switch to read-only mode
-                                       wfDebugLog( 'replication', "All replica DBs lagged. Switch to read-only mode" );
+                                       $this->replLogger->error( "All replica DBs lagged. Switch to read-only mode" );
                                        $i = ArrayUtils::pickRandom( $currentLoads );
                                        $laggedReplicaMode = true;
                                }
@@ -312,17 +310,17 @@ class LoadBalancer {
                                # pickRandom() returned false
                                # This is permanent and means the configuration or the load monitor
                                # wants us to return false.
-                               wfDebugLog( 'connect', __METHOD__ . ": pickRandom() returned false" );
+                               $this->connLogger->debug( __METHOD__ . ": pickRandom() returned false" );
 
                                return false;
                        }
 
                        $serverName = $this->getServerName( $i );
-                       wfDebugLog( 'connect', __METHOD__ . ": Using reader #$i: $serverName..." );
+                       $this->connLogger->debug( __METHOD__ . ": Using reader #$i: $serverName..." );
 
                        $conn = $this->openConnection( $i, $wiki );
                        if ( !$conn ) {
-                               wfDebugLog( 'connect', __METHOD__ . ": Failed connecting to $i/$wiki" );
+                               $this->connLogger->warning( __METHOD__ . ": Failed connecting to $i/$wiki" );
                                unset( $nonErrorLoads[$i] );
                                unset( $currentLoads[$i] );
                                $i = false;
@@ -341,7 +339,7 @@ class LoadBalancer {
 
                # If all servers were down, quit now
                if ( !count( $nonErrorLoads ) ) {
-                       wfDebugLog( 'connect', "All servers down" );
+                       $this->connLogger->error( "All servers down" );
                }
 
                if ( $i !== false ) {
@@ -358,19 +356,13 @@ class LoadBalancer {
                                }
                        }
                        $serverName = $this->getServerName( $i );
-                       wfDebugLog( 'connect', __METHOD__ .
-                               ": using server $serverName for group '$group'\n" );
+                       $this->connLogger->debug(
+                               __METHOD__ . ": using server $serverName for group '$group'\n" );
                }
 
                return $i;
        }
 
-       /**
-        * Set the master wait position
-        * If a DB_REPLICA connection has been opened already, waits
-        * Otherwise sets a variable telling it to wait if such a connection is opened
-        * @param DBMasterPos $pos
-        */
        public function waitFor( $pos ) {
                $this->mWaitForPos = $pos;
                $i = $this->mReadIndex;
@@ -413,12 +405,6 @@ class LoadBalancer {
                return $ok;
        }
 
-       /**
-        * Set the master wait position and wait for ALL replica DBs to catch up to it
-        * @param DBMasterPos $pos
-        * @param int $timeout Max seconds to wait; default is mWaitTimeout
-        * @return bool Success (able to connect and no timeouts reached)
-        */
        public function waitForAll( $pos, $timeout = null ) {
                $this->mWaitForPos = $pos;
                $serverCount = count( $this->mServers );
@@ -433,13 +419,6 @@ class LoadBalancer {
                return $ok;
        }
 
-       /**
-        * Get any open connection to a given server index, local or foreign
-        * Returns false if there is no connection open
-        *
-        * @param int $i Server index
-        * @return DatabaseBase|bool False on failure
-        */
        public function getAnyOpenConnection( $i ) {
                foreach ( $this->mConns as $connsByServer ) {
                        if ( !empty( $connsByServer[$i] ) ) {
@@ -466,7 +445,7 @@ class LoadBalancer {
                /** @var DBMasterPos $knownReachedPos */
                $knownReachedPos = $this->srvCache->get( $key );
                if ( $knownReachedPos && $knownReachedPos->hasReached( $this->mWaitForPos ) ) {
-                       wfDebugLog( 'replication', __METHOD__ .
+                       $this->replLogger->debug( __METHOD__ .
                                ": replica DB $server known to be caught up (pos >= $knownReachedPos).\n" );
                        return true;
                }
@@ -475,13 +454,13 @@ class LoadBalancer {
                $conn = $this->getAnyOpenConnection( $index );
                if ( !$conn ) {
                        if ( !$open ) {
-                               wfDebugLog( 'replication', __METHOD__ . ": no connection open for $server\n" );
+                               $this->replLogger->debug( __METHOD__ . ": no connection open for $server\n" );
 
                                return false;
                        } else {
                                $conn = $this->openConnection( $index, '' );
                                if ( !$conn ) {
-                                       wfDebugLog( 'replication', __METHOD__ . ": failed to connect to $server\n" );
+                                       $this->replLogger->warning( __METHOD__ . ": failed to connect to $server\n" );
 
                                        return false;
                                }
@@ -491,18 +470,18 @@ class LoadBalancer {
                        }
                }
 
-               wfDebugLog( 'replication', __METHOD__ . ": Waiting for replica DB $server to catch up...\n" );
+               $this->replLogger->info( __METHOD__ . ": Waiting for replica DB $server to catch up...\n" );
                $timeout = $timeout ?: $this->mWaitTimeout;
                $result = $conn->masterPosWait( $this->mWaitForPos, $timeout );
 
                if ( $result == -1 || is_null( $result ) ) {
                        // Timed out waiting for replica DB, use master instead
                        $msg = __METHOD__ . ": Timed out waiting on $server pos {$this->mWaitForPos}";
-                       wfDebugLog( 'replication', "$msg\n" );
-                       wfDebugLog( 'DBPerformance', "$msg:\n" . wfBacktrace( true ) );
+                       $this->replLogger->warning( "$msg\n" );
+                       $this->perfLogger->warning( "$msg:\n" . wfBacktrace( true ) );
                        $ok = false;
                } else {
-                       wfDebugLog( 'replication', __METHOD__ . ": Done\n" );
+                       $this->replLogger->info( __METHOD__ . ": Done\n" );
                        $ok = true;
                        // Remember that the DB reached this point
                        $this->srvCache->set( $key, $this->mWaitForPos, BagOStuff::TTL_DAY );
@@ -515,24 +494,13 @@ class LoadBalancer {
                return $ok;
        }
 
-       /**
-        * Get a connection by index
-        * This is the main entry point for this class.
-        *
-        * @param int $i Server index
-        * @param array|string|bool $groups Query group(s), or false for the generic reader
-        * @param string|bool $wiki Wiki ID, or false for the current wiki
-        *
-        * @throws MWException
-        * @return DatabaseBase
-        */
        public function getConnection( $i, $groups = [], $wiki = false ) {
                if ( $i === null || $i === false ) {
-                       throw new MWException( 'Attempt to call ' . __METHOD__ .
+                       throw new InvalidArgumentException( 'Attempt to call ' . __METHOD__ .
                                ' with invalid server index' );
                }
 
-               if ( $wiki === wfWikiID() ) {
+               if ( $wiki === $this->localDomain ) {
                        $wiki = false;
                }
 
@@ -581,8 +549,7 @@ class LoadBalancer {
                if ( $this->connsOpened > $oldConnsOpened ) {
                        $host = $conn->getServer();
                        $dbname = $conn->getDBname();
-                       $trxProf = Profiler::instance()->getTransactionProfiler();
-                       $trxProf->recordConnection( $host, $dbname, $masterOnly );
+                       $this->trxProfiler->recordConnection( $host, $dbname, $masterOnly );
                }
 
                if ( $masterOnly ) {
@@ -593,14 +560,6 @@ class LoadBalancer {
                return $conn;
        }
 
-       /**
-        * Mark a foreign connection as being available for reuse under a different
-        * DB name or prefix. This mechanism is reference-counted, and must be called
-        * the same number of times as getConnection() to work.
-        *
-        * @param DatabaseBase $conn
-        * @throws MWException
-        */
        public function reuseConnection( $conn ) {
                $serverIndex = $conn->getLBInfo( 'serverIndex' );
                $refCount = $conn->getLBInfo( 'foreignPoolRefCount' );
@@ -626,7 +585,7 @@ class LoadBalancer {
                        $wiki = $dbName;
                }
                if ( $this->mConns['foreignUsed'][$serverIndex][$wiki] !== $conn ) {
-                       throw new MWException( __METHOD__ . ": connection not found, has " .
+                       throw new InvalidArgumentException( __METHOD__ . ": connection not found, has " .
                                "the connection been freed already?" );
                }
                $conn->setLBInfo( 'foreignPoolRefCount', --$refCount );
@@ -650,6 +609,7 @@ class LoadBalancer {
         * @param array|string|bool $groups Query group(s), or false for the generic reader
         * @param string|bool $wiki Wiki ID, or false for the current wiki
         * @return DBConnRef
+        * @since 1.22
         */
        public function getConnectionRef( $db, $groups = [], $wiki = false ) {
                return new DBConnRef( $this, $this->getConnection( $db, $groups, $wiki ) );
@@ -666,25 +626,14 @@ class LoadBalancer {
         * @param array|string|bool $groups Query group(s), or false for the generic reader
         * @param string|bool $wiki Wiki ID, or false for the current wiki
         * @return DBConnRef
+        * @since 1.22
         */
        public function getLazyConnectionRef( $db, $groups = [], $wiki = false ) {
+               $wiki = ( $wiki !== false ) ? $wiki : $this->localDomain;
+
                return new DBConnRef( $this, [ $db, $groups, $wiki ] );
        }
 
-       /**
-        * Open a connection to the server given by the specified index
-        * Index must be an actual index into the array.
-        * If the server is already open, returns it.
-        *
-        * On error, returns false, and the connection which caused the
-        * error will be available via $this->mErrorConnection.
-        *
-        * @note If disable() was called on this LoadBalancer, this method will throw a DBAccessError.
-        *
-        * @param int $i Server index
-        * @param string|bool $wiki Wiki ID, or false for the current wiki
-        * @return DatabaseBase|bool Returns false on errors
-        */
        public function openConnection( $i, $wiki = false ) {
                if ( $wiki !== false ) {
                        $conn = $this->openForeignConnection( $i, $wiki );
@@ -696,10 +645,10 @@ class LoadBalancer {
                        $conn = $this->reallyOpenConnection( $server, false );
                        $serverName = $this->getServerName( $i );
                        if ( $conn->isOpen() ) {
-                               wfDebugLog( 'connect', "Connected to database $i at $serverName\n" );
+                               $this->connLogger->debug( "Connected to database $i at '$serverName'." );
                                $this->mConns['local'][$i][0] = $conn;
                        } else {
-                               wfDebugLog( 'connect', "Failed to connect to database $i at $serverName\n" );
+                               $this->connLogger->warning( "Failed to connect to database $i at '$serverName'." );
                                $this->mErrorConnection = $conn;
                                $conn = false;
                        }
@@ -738,7 +687,8 @@ class LoadBalancer {
         * @return DatabaseBase
         */
        private function openForeignConnection( $i, $wiki ) {
-               list( $dbName, $prefix ) = wfSplitWikiID( $wiki );
+               list( $dbName, $prefix ) = explode( '-', $wiki, 2 ) + [ '', '' ];
+
                if ( isset( $this->mConns['foreignUsed'][$i][$wiki] ) ) {
                        // Reuse an already-used connection
                        $conn = $this->mConns['foreignUsed'][$i][$wiki];
@@ -825,7 +775,7 @@ class LoadBalancer {
                }
 
                if ( !is_array( $server ) ) {
-                       throw new MWException( 'You must update your load-balancing configuration. ' .
+                       throw new InvalidArgumentException( 'You must update your load-balancing configuration. ' .
                                'See DefaultSettings.php entry for $wgDBservers.' );
                }
 
@@ -839,12 +789,16 @@ class LoadBalancer {
 
                // Log when many connection are made on requests
                if ( ++$this->connsOpened >= self::CONN_HELD_WARN_THRESHOLD ) {
-                       wfDebugLog( 'DBPerformance', __METHOD__ . ": " .
+                       $this->perfLogger->warning( __METHOD__ . ": " .
                                "{$this->connsOpened}+ connections made (master=$masterName)\n" .
                                wfBacktrace( true ) );
                }
 
-               # Create object
+               // Set loggers
+               $server['connLogger'] = $this->connLogger;
+               $server['queryLogger'] = $this->queryLogger;
+
+               // Create a live connection object
                try {
                        $db = DatabaseBase::factory( $server['type'], $server );
                } catch ( DBConnectionError $e ) {
@@ -905,49 +859,22 @@ class LoadBalancer {
                return false; /* not reached */
        }
 
-       /**
-        * @return int
-        * @since 1.26
-        */
        public function getWriterIndex() {
                return 0;
        }
 
-       /**
-        * Returns true if the specified index is a valid server index
-        *
-        * @param string $i
-        * @return bool
-        */
        public function haveIndex( $i ) {
                return array_key_exists( $i, $this->mServers );
        }
 
-       /**
-        * Returns true if the specified index is valid and has non-zero load
-        *
-        * @param string $i
-        * @return bool
-        */
        public function isNonZeroLoad( $i ) {
                return array_key_exists( $i, $this->mServers ) && $this->mLoads[$i] != 0;
        }
 
-       /**
-        * Get the number of defined servers (not the number of open connections)
-        *
-        * @return int
-        */
        public function getServerCount() {
                return count( $this->mServers );
        }
 
-       /**
-        * Get the host name or IP address of the server with the specified index
-        * Prefer a readable name if available.
-        * @param string $i
-        * @return string
-        */
        public function getServerName( $i ) {
                if ( isset( $this->mServers[$i]['hostName'] ) ) {
                        $name = $this->mServers[$i]['hostName'];
@@ -960,11 +887,6 @@ class LoadBalancer {
                return ( $name != '' ) ? $name : 'localhost';
        }
 
-       /**
-        * Return the server info structure for a given index, or false if the index is invalid.
-        * @param int $i
-        * @return array|bool
-        */
        public function getServerInfo( $i ) {
                if ( isset( $this->mServers[$i] ) ) {
                        return $this->mServers[$i];
@@ -973,20 +895,10 @@ class LoadBalancer {
                }
        }
 
-       /**
-        * Sets the server info structure for the given index. Entry at index $i
-        * is created if it doesn't exist
-        * @param int $i
-        * @param array $serverInfo
-        */
        public function setServerInfo( $i, array $serverInfo ) {
                $this->mServers[$i] = $serverInfo;
        }
 
-       /**
-        * Get the current master position for chronology control purposes
-        * @return DBMasterPos|bool Returns false if not applicable
-        */
        public function getMasterPos() {
                # If this entire request was served from a replica DB without opening a connection to the
                # master (however unlikely that may be), then we can fetch the position from the replica DB.
@@ -1017,9 +929,6 @@ class LoadBalancer {
                $this->disabled = true;
        }
 
-       /**
-        * Close all open connections
-        */
        public function closeAll() {
                $this->forEachOpenConnection( function ( DatabaseBase $conn ) {
                        $conn->close();
@@ -1033,15 +942,7 @@ class LoadBalancer {
                $this->connsOpened = 0;
        }
 
-       /**
-        * Close a connection
-        *
-        * Using this function makes sure the LoadBalancer knows the connection is closed.
-        * If you use $conn->close() directly, the load balancer won't update its state.
-        *
-        * @param DatabaseBase $conn
-        */
-       public function closeConnection( DatabaseBase $conn ) {
+       public function closeConnection( IDatabase $conn ) {
                $serverIndex = $conn->getLBInfo( 'serverIndex' ); // second index level of mConns
                foreach ( $this->mConns as $type => $connsByServer ) {
                        if ( !isset( $connsByServer[$serverIndex] ) ) {
@@ -1060,11 +961,6 @@ class LoadBalancer {
                $conn->close();
        }
 
-       /**
-        * Commit transactions on all open connections
-        * @param string $fname Caller name
-        * @throws DBExpectedError
-        */
        public function commitAll( $fname = __METHOD__ ) {
                $failures = [];
 
@@ -1075,7 +971,7 @@ class LoadBalancer {
                                try {
                                        $conn->commit( $fname, $conn::FLUSHING_ALL_PEERS );
                                } catch ( DBError $e ) {
-                                       MWExceptionHandler::logException( $e );
+                                       call_user_func( $this->errorLogger, $e );
                                        $failures[] = "{$conn->getServer()}: {$e->getMessage()}";
                                }
                                if ( $restore && $conn->getLBInfo( 'master' ) ) {
@@ -1175,7 +1071,7 @@ class LoadBalancer {
                                try {
                                        $conn->flushSnapshot( $fname );
                                } catch ( DBError $e ) {
-                                       MWExceptionHandler::logException( $e );
+                                       call_user_func( $this->errorLogger, $e );
                                        $failures[] = "{$conn->getServer()}: {$e->getMessage()}";
                                }
                                $conn->setTrxEndCallbackSuppression( false );
@@ -1191,11 +1087,6 @@ class LoadBalancer {
                }
        }
 
-       /**
-        * Issue COMMIT on all master connections where writes where done
-        * @param string $fname Caller name
-        * @throws DBExpectedError
-        */
        public function commitMasterChanges( $fname = __METHOD__ ) {
                $failures = [];
 
@@ -1210,7 +1101,7 @@ class LoadBalancer {
                                                $conn->flushSnapshot( $fname );
                                        }
                                } catch ( DBError $e ) {
-                                       MWExceptionHandler::logException( $e );
+                                       call_user_func( $this->errorLogger, $e );
                                        $failures[] = "{$conn->getServer()}: {$e->getMessage()}";
                                }
                                if ( $restore ) {
@@ -1299,9 +1190,9 @@ class LoadBalancer {
        }
 
        /**
-        * @param DatabaseBase $conn
+        * @param IDatabase $conn
         */
-       private function applyTransactionRoundFlags( DatabaseBase $conn ) {
+       private function applyTransactionRoundFlags( IDatabase $conn ) {
                if ( $conn->getFlag( DBO_DEFAULT ) ) {
                        // 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.
@@ -1313,9 +1204,9 @@ class LoadBalancer {
        }
 
        /**
-        * @param DatabaseBase $conn
+        * @param IDatabase $conn
         */
-       private function undoTransactionRoundFlags( DatabaseBase $conn ) {
+       private function undoTransactionRoundFlags( IDatabase $conn ) {
                if ( $conn->getFlag( DBO_DEFAULT ) ) {
                        $conn->restoreFlags( $conn::RESTORE_PRIOR );
                }
@@ -1399,11 +1290,6 @@ class LoadBalancer {
                return $fnames;
        }
 
-       /**
-        * @note This method will trigger a DB connection if not yet done
-        * @param string|bool $wiki Wiki ID, or false for the current wiki
-        * @return bool Whether the generic connection for reads is highly "lagged"
-        */
        public function getLaggedReplicaMode( $wiki = false ) {
                // No-op if there is only one DB (also avoids recursion)
                if ( !$this->laggedReplicaMode && $this->getServerCount() > 1 ) {
@@ -1451,11 +1337,11 @@ class LoadBalancer {
        /**
         * @note This method may trigger a DB connection if not yet done
         * @param string|bool $wiki Wiki ID, or false for the current wiki
-        * @param DatabaseBase|null DB master connection; used to avoid loops [optional]
+        * @param IDatabase|null DB master connection; used to avoid loops [optional]
         * @return string|bool Reason the master is read-only or false if it is not
         * @since 1.27
         */
-       public function getReadOnlyReason( $wiki = false, DatabaseBase $conn = null ) {
+       public function getReadOnlyReason( $wiki = false, IDatabase $conn = null ) {
                if ( $this->readOnlyReason !== false ) {
                        return $this->readOnlyReason;
                } elseif ( $this->getLaggedReplicaMode( $wiki ) ) {
@@ -1475,10 +1361,10 @@ class LoadBalancer {
 
        /**
         * @param string $wiki Wiki ID, or false for the current wiki
-        * @param DatabaseBase|null DB master connectionl used to avoid loops [optional]
+        * @param IDatabase|null DB master connectionl used to avoid loops [optional]
         * @return bool
         */
-       private function masterRunningReadOnly( $wiki, DatabaseBase $conn = null ) {
+       private function masterRunningReadOnly( $wiki, IDatabase $conn = null ) {
                $cache = $this->wanCache;
                $masterServer = $this->getServerName( $this->getWriterIndex() );
 
@@ -1500,11 +1386,6 @@ class LoadBalancer {
                );
        }
 
-       /**
-        * Disables/enables lag checks
-        * @param null|bool $mode
-        * @return bool
-        */
        public function allowLagged( $mode = null ) {
                if ( $mode === null ) {
                        return $this->mAllowLagged;
@@ -1514,9 +1395,6 @@ class LoadBalancer {
                return $this->mAllowLagged;
        }
 
-       /**
-        * @return bool
-        */
        public function pingAll() {
                $success = true;
                $this->forEachOpenConnection( function ( DatabaseBase $conn ) use ( &$success ) {
@@ -1528,11 +1406,6 @@ class LoadBalancer {
                return $success;
        }
 
-       /**
-        * Call a function with each open connection object
-        * @param callable $callback
-        * @param array $params
-        */
        public function forEachOpenConnection( $callback, array $params = [] ) {
                foreach ( $this->mConns as $connsByServer ) {
                        foreach ( $connsByServer as $serverConns ) {
@@ -1583,16 +1456,6 @@ class LoadBalancer {
                }
        }
 
-       /**
-        * Get the hostname and lag time of the most-lagged replica DB
-        *
-        * This is useful for maintenance scripts that need to throttle their updates.
-        * May attempt to open connections to replica DBs on the default DB. If there is
-        * no lag, the maximum lag will be reported as -1.
-        *
-        * @param bool|string $wiki Wiki ID, or false for the default database
-        * @return array ( host, max lag, index of max lagged host )
-        */
        public function getMaxLag( $wiki = false ) {
                $maxLag = -1;
                $host = '';
@@ -1614,16 +1477,6 @@ class LoadBalancer {
                return [ $host, $maxLag, $maxIndex ];
        }
 
-       /**
-        * Get an estimate of replication lag (in seconds) for each server
-        *
-        * Results are cached for a short time in memcached/process cache
-        *
-        * Values may be "false" if replication is too broken to estimate
-        *
-        * @param string|bool $wiki
-        * @return int[] Map of (server index => float|int|bool)
-        */
        public function getLagTimes( $wiki = false ) {
                if ( $this->getServerCount() <= 1 ) {
                        return [ 0 => 0 ]; // no replication = no lag
@@ -1633,20 +1486,6 @@ class LoadBalancer {
                return $this->getLoadMonitor()->getLagTimes( array_keys( $this->mServers ), $wiki );
        }
 
-       /**
-        * Get the lag in seconds for a given connection, or zero if this load
-        * balancer does not have replication enabled.
-        *
-        * This should be used in preference to Database::getLag() in cases where
-        * replication may not be in use, since there is no way to determine if
-        * replication is in use at the connection level without running
-        * potentially restricted queries such as SHOW SLAVE STATUS. Using this
-        * function instead of Database::getLag() avoids a fatal error in this
-        * case on many installations.
-        *
-        * @param IDatabase $conn
-        * @return int|bool Returns false on error
-        */
        public function safeGetLag( IDatabase $conn ) {
                if ( $this->getServerCount() == 1 ) {
                        return 0;
@@ -1662,11 +1501,11 @@ class LoadBalancer {
         *
         * @param IDatabase $conn Replica DB
         * @param DBMasterPos|bool $pos Master position; default: current position
-        * @param integer|null $timeout Timeout in seconds [optional]
+        * @param integer $timeout Timeout in seconds
         * @return bool Success
         * @since 1.27
         */
-       public function safeWaitForMasterPos( IDatabase $conn, $pos = false, $timeout = null ) {
+       public function safeWaitForMasterPos( IDatabase $conn, $pos = false, $timeout = 10 ) {
                if ( $this->getServerCount() == 1 || !$conn->getLBInfo( 'replica' ) ) {
                        return true; // server is not a replica DB
                }
@@ -1676,15 +1515,14 @@ class LoadBalancer {
                        return false; // something is misconfigured
                }
 
-               $timeout = $timeout ?: $this->mWaitTimeout;
                $result = $conn->masterPosWait( $pos, $timeout );
                if ( $result == -1 || is_null( $result ) ) {
                        $msg = __METHOD__ . ": Timed out waiting on {$conn->getServer()} pos {$pos}";
-                       wfDebugLog( 'replication', "$msg\n" );
-                       wfDebugLog( 'DBPerformance', "$msg:\n" . wfBacktrace( true ) );
+                       $this->replLogger->warning( "$msg\n" );
+                       $this->perfLogger->warning( "$msg:\n" . wfBacktrace( true ) );
                        $ok = false;
                } else {
-                       wfDebugLog( 'replication', __METHOD__ . ": Done\n" );
+                       $this->replLogger->info( __METHOD__ . ": Done\n" );
                        $ok = true;
                }
 
@@ -1695,6 +1533,7 @@ class LoadBalancer {
         * Clear the cache for slag lag delay times
         *
         * This is only used for testing
+        * @since 1.26
         */
        public function clearLagTimeCache() {
                $this->getLoadMonitor()->clearCaches();
@@ -1720,4 +1559,16 @@ class LoadBalancer {
                        }
                );
        }
+
+       /**
+        * Set a new table prefix for the existing local wiki ID for testing
+        *
+        * @param string $prefix
+        * @since 1.28
+        */
+       public function setDomainPrefix( $prefix ) {
+               list( $dbName, ) = explode( '-', $this->localDomain, 2 );
+
+               $this->localDomain = "{$dbName}-{$prefix}";
+       }
 }
diff --git a/includes/db/loadbalancer/LoadMonitor.php b/includes/db/loadbalancer/LoadMonitor.php
deleted file mode 100644 (file)
index e68cf1a..0000000
+++ /dev/null
@@ -1,78 +0,0 @@
-<?php
-/**
- * Database load monitoring.
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- * @ingroup Database
- */
-
-/**
- * An interface for database load monitoring
- *
- * @ingroup Database
- */
-interface LoadMonitor {
-       /**
-        * Construct a new LoadMonitor with a given LoadBalancer parent
-        *
-        * @param LoadBalancer $parent
-        */
-       public function __construct( $parent );
-
-       /**
-        * Perform pre-connection load ratio adjustment.
-        * @param array &$loads
-        * @param string|bool $group The selected query group. Default: false
-        * @param string|bool $wiki Default: false
-        */
-       public function scaleLoads( &$loads, $group = false, $wiki = false );
-
-       /**
-        * Get an estimate of replication lag (in seconds) for each server
-        *
-        * Values may be "false" if replication is too broken to estimate
-        *
-        * @param array $serverIndexes
-        * @param string $wiki
-        *
-        * @return array Map of (server index => float|int|bool)
-        */
-       public function getLagTimes( $serverIndexes, $wiki );
-
-       /**
-        * Clear any process and persistent cache of lag times
-        * @since 1.27
-        */
-       public function clearCaches();
-}
-
-class LoadMonitorNull implements LoadMonitor {
-       public function __construct( $parent ) {
-       }
-
-       public function scaleLoads( &$loads, $group = false, $wiki = false ) {
-       }
-
-       public function getLagTimes( $serverIndexes, $wiki ) {
-               return array_fill_keys( $serverIndexes, 0 );
-       }
-
-       public function clearCaches() {
-
-       }
-}
diff --git a/includes/db/loadbalancer/LoadMonitorMySQL.php b/includes/db/loadbalancer/LoadMonitorMySQL.php
deleted file mode 100644 (file)
index 444c4b4..0000000
+++ /dev/null
@@ -1,148 +0,0 @@
-<?php
-/**
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- * @ingroup Database
- */
-
-/**
- * Basic MySQL load monitor with no external dependencies
- * Uses memcached to cache the replication lag for a short time
- *
- * @ingroup Database
- */
-class LoadMonitorMySQL implements LoadMonitor {
-       /** @var LoadBalancer */
-       public $parent;
-       /** @var BagOStuff */
-       protected $srvCache;
-       /** @var BagOStuff */
-       protected $mainCache;
-
-       public function __construct( $parent ) {
-               $this->parent = $parent;
-
-               $this->srvCache = ObjectCache::getLocalServerInstance( 'hash' );
-               $this->mainCache = ObjectCache::getLocalClusterInstance();
-       }
-
-       public function scaleLoads( &$loads, $group = false, $wiki = false ) {
-       }
-
-       public function getLagTimes( $serverIndexes, $wiki ) {
-               if ( count( $serverIndexes ) == 1 && reset( $serverIndexes ) == 0 ) {
-                       # Single server only, just return zero without caching
-                       return [ 0 => 0 ];
-               }
-
-               $key = $this->getLagTimeCacheKey();
-               # Randomize TTLs to reduce stampedes (4.0 - 5.0 sec)
-               $ttl = mt_rand( 4e6, 5e6 ) / 1e6;
-               # Keep keys around longer as fallbacks
-               $staleTTL = 60;
-
-               # (a) Check the local APC cache
-               $value = $this->srvCache->get( $key );
-               if ( $value && $value['timestamp'] > ( microtime( true ) - $ttl ) ) {
-                       wfDebugLog( 'replication', __METHOD__ . ": got lag times ($key) from local cache" );
-                       return $value['lagTimes']; // cache hit
-               }
-               $staleValue = $value ?: false;
-
-               # (b) Check the shared cache and backfill APC
-               $value = $this->mainCache->get( $key );
-               if ( $value && $value['timestamp'] > ( microtime( true ) - $ttl ) ) {
-                       $this->srvCache->set( $key, $value, $staleTTL );
-                       wfDebugLog( 'replication', __METHOD__ . ": got lag times ($key) from main cache" );
-
-                       return $value['lagTimes']; // cache hit
-               }
-               $staleValue = $value ?: $staleValue;
-
-               # (c) Cache key missing or expired; regenerate and backfill
-               if ( $this->mainCache->lock( $key, 0, 10 ) ) {
-                       # Let this process alone update the cache value
-                       $cache = $this->mainCache;
-                       /** @noinspection PhpUnusedLocalVariableInspection */
-                       $unlocker = new ScopedCallback( function () use ( $cache, $key ) {
-                               $cache->unlock( $key );
-                       } );
-               } elseif ( $staleValue ) {
-                       # Could not acquire lock but an old cache exists, so use it
-                       return $staleValue['lagTimes'];
-               }
-
-               $lagTimes = [];
-               foreach ( $serverIndexes as $i ) {
-                       if ( $i == $this->parent->getWriterIndex() ) {
-                               $lagTimes[$i] = 0; // master always has no lag
-                               continue;
-                       }
-
-                       $conn = $this->parent->getAnyOpenConnection( $i );
-                       if ( $conn ) {
-                               $close = false; // already open
-                       } else {
-                               $conn = $this->parent->openConnection( $i, $wiki );
-                               $close = true; // new connection
-                       }
-
-                       if ( !$conn ) {
-                               $lagTimes[$i] = false;
-                               $host = $this->parent->getServerName( $i );
-                               wfDebugLog( 'replication', __METHOD__ . ": host $host (#$i) is unreachable" );
-                               continue;
-                       }
-
-                       $lagTimes[$i] = $conn->getLag();
-                       if ( $lagTimes[$i] === false ) {
-                               $host = $this->parent->getServerName( $i );
-                               wfDebugLog( 'replication', __METHOD__ . ": host $host (#$i) is not replicating?" );
-                       }
-
-                       if ( $close ) {
-                               # Close the connection to avoid sleeper connections piling up.
-                               # Note that the caller will pick one of these DBs and reconnect,
-                               # which is slightly inefficient, but this only matters for the lag
-                               # time cache miss cache, which is far less common that cache hits.
-                               $this->parent->closeConnection( $conn );
-                       }
-               }
-
-               # Add a timestamp key so we know when it was cached
-               $value = [ 'lagTimes' => $lagTimes, 'timestamp' => microtime( true ) ];
-               $this->mainCache->set( $key, $value, $staleTTL );
-               $this->srvCache->set( $key, $value, $staleTTL );
-               wfDebugLog( 'replication', __METHOD__ . ": re-calculated lag times ($key)" );
-
-               return $value['lagTimes'];
-       }
-
-       public function clearCaches() {
-               $key = $this->getLagTimeCacheKey();
-               $this->srvCache->delete( $key );
-               $this->mainCache->delete( $key );
-       }
-
-       private function getLagTimeCacheKey() {
-               $writerIndex = $this->parent->getWriterIndex();
-               // Lag is per-server, not per-DB, so key on the master DB name
-               return $this->srvCache->makeGlobalKey(
-                       'lag-times', $this->parent->getServerName( $writerIndex )
-               );
-       }
-}
index 022abd9..ed3aa9a 100644 (file)
@@ -280,7 +280,6 @@ class JobRunner implements LoggerAwareInterface {
                        MWExceptionHandler::rollbackMasterChangesAndLog( $e );
                        $status = false;
                        $error = get_class( $e ) . ': ' . $e->getMessage();
-                       MWExceptionHandler::logException( $e );
                }
                // Always attempt to call teardown() even if Job throws exception.
                try {
index cd37263..a52ff06 100644 (file)
@@ -60,7 +60,7 @@ class CategoryMembershipChangeJob extends Job {
 
                // Use a named lock so that jobs for this page see each others' changes
                $lockKey = "CategoryMembershipUpdates:{$page->getId()}";
-               $scopedLock = $dbw->getScopedLockAndFlush( $lockKey, __METHOD__, 10 );
+               $scopedLock = $dbw->getScopedLockAndFlush( $lockKey, __METHOD__, 3 );
                if ( !$scopedLock ) {
                        $this->setLastError( "Could not acquire lock '$lockKey'" );
                        return false;
diff --git a/includes/libs/objectcache/MemcachedPeclBagOStuff.php b/includes/libs/objectcache/MemcachedPeclBagOStuff.php
new file mode 100644 (file)
index 0000000..5983c1b
--- /dev/null
@@ -0,0 +1,256 @@
+<?php
+/**
+ * Object caching using memcached.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup Cache
+ */
+
+/**
+ * A wrapper class for the PECL memcached client
+ *
+ * @ingroup Cache
+ */
+class MemcachedPeclBagOStuff extends MemcachedBagOStuff {
+
+       /**
+        * Constructor
+        *
+        * Available parameters are:
+        *   - servers:             The list of IP:port combinations holding the memcached servers.
+        *   - persistent:          Whether to use a persistent connection
+        *   - compress_threshold:  The minimum size an object must be before it is compressed
+        *   - timeout:             The read timeout in microseconds
+        *   - connect_timeout:     The connect timeout in seconds
+        *   - retry_timeout:       Time in seconds to wait before retrying a failed connect attempt
+        *   - server_failure_limit:  Limit for server connect failures before it is removed
+        *   - serializer:          May be either "php" or "igbinary". Igbinary produces more compact
+        *                          values, but serialization is much slower unless the php.ini option
+        *                          igbinary.compact_strings is off.
+        *   - use_binary_protocol  Whether to enable the binary protocol (default is ASCII) (boolean)
+        * @param array $params
+        * @throws InvalidArgumentException
+        */
+       function __construct( $params ) {
+               parent::__construct( $params );
+               $params = $this->applyDefaultParams( $params );
+
+               if ( $params['persistent'] ) {
+                       // The pool ID must be unique to the server/option combination.
+                       // The Memcached object is essentially shared for each pool ID.
+                       // We can only reuse a pool ID if we keep the config consistent.
+                       $this->client = new Memcached( md5( serialize( $params ) ) );
+                       if ( count( $this->client->getServerList() ) ) {
+                               $this->logger->debug( __METHOD__ . ": persistent Memcached object already loaded." );
+                               return; // already initialized; don't add duplicate servers
+                       }
+               } else {
+                       $this->client = new Memcached;
+               }
+
+               if ( $params['use_binary_protocol'] ) {
+                       $this->client->setOption( Memcached::OPT_BINARY_PROTOCOL, true );
+               }
+
+               if ( isset( $params['retry_timeout'] ) ) {
+                       $this->client->setOption( Memcached::OPT_RETRY_TIMEOUT, $params['retry_timeout'] );
+               }
+
+               if ( isset( $params['server_failure_limit'] ) ) {
+                       $this->client->setOption( Memcached::OPT_SERVER_FAILURE_LIMIT, $params['server_failure_limit'] );
+               }
+
+               // The compression threshold is an undocumented php.ini option for some
+               // reason. There's probably not much harm in setting it globally, for
+               // compatibility with the settings for the PHP client.
+               ini_set( 'memcached.compression_threshold', $params['compress_threshold'] );
+
+               // Set timeouts
+               $this->client->setOption( Memcached::OPT_CONNECT_TIMEOUT, $params['connect_timeout'] * 1000 );
+               $this->client->setOption( Memcached::OPT_SEND_TIMEOUT, $params['timeout'] );
+               $this->client->setOption( Memcached::OPT_RECV_TIMEOUT, $params['timeout'] );
+               $this->client->setOption( Memcached::OPT_POLL_TIMEOUT, $params['timeout'] / 1000 );
+
+               // Set libketama mode since it's recommended by the documentation and
+               // is as good as any. There's no way to configure libmemcached to use
+               // hashes identical to the ones currently in use by the PHP client, and
+               // even implementing one of the libmemcached hashes in pure PHP for
+               // forwards compatibility would require MemcachedClient::get_sock() to be
+               // rewritten.
+               $this->client->setOption( Memcached::OPT_LIBKETAMA_COMPATIBLE, true );
+
+               // Set the serializer
+               switch ( $params['serializer'] ) {
+                       case 'php':
+                               $this->client->setOption( Memcached::OPT_SERIALIZER, Memcached::SERIALIZER_PHP );
+                               break;
+                       case 'igbinary':
+                               if ( !Memcached::HAVE_IGBINARY ) {
+                                       throw new InvalidArgumentException(
+                                               __CLASS__ . ': the igbinary extension is not available ' .
+                                               'but igbinary serialization was requested.'
+                                       );
+                               }
+                               $this->client->setOption( Memcached::OPT_SERIALIZER, Memcached::SERIALIZER_IGBINARY );
+                               break;
+                       default:
+                               throw new InvalidArgumentException(
+                                       __CLASS__ . ': invalid value for serializer parameter'
+                               );
+               }
+               $servers = [];
+               foreach ( $params['servers'] as $host ) {
+                       if ( preg_match( '/^\[(.+)\]:(\d+)$/', $host, $m ) ) {
+                               $servers[] = [ $m[1], (int)$m[2] ]; // (ip, port)
+                       } elseif ( preg_match( '/^([^:]+):(\d+)$/', $host, $m ) ) {
+                               $servers[] = [ $m[1], (int)$m[2] ]; // (ip or path, port)
+                       } else {
+                               $servers[] = [ $host, false ]; // (ip or path, port)
+                       }
+               }
+               $this->client->addServers( $servers );
+       }
+
+       protected function applyDefaultParams( $params ) {
+               $params = parent::applyDefaultParams( $params );
+
+               if ( !isset( $params['use_binary_protocol'] ) ) {
+                       $params['use_binary_protocol'] = false;
+               }
+
+               if ( !isset( $params['serializer'] ) ) {
+                       $params['serializer'] = 'php';
+               }
+
+               return $params;
+       }
+
+       protected function getWithToken( $key, &$casToken, $flags = 0 ) {
+               $this->debugLog( "get($key)" );
+               $result = $this->client->get( $this->validateKeyEncoding( $key ), null, $casToken );
+               $result = $this->checkResult( $key, $result );
+               return $result;
+       }
+
+       public function set( $key, $value, $exptime = 0, $flags = 0 ) {
+               $this->debugLog( "set($key)" );
+               return $this->checkResult( $key, parent::set( $key, $value, $exptime ) );
+       }
+
+       protected function cas( $casToken, $key, $value, $exptime = 0 ) {
+               $this->debugLog( "cas($key)" );
+               return $this->checkResult( $key, parent::cas( $casToken, $key, $value, $exptime ) );
+       }
+
+       public function delete( $key ) {
+               $this->debugLog( "delete($key)" );
+               $result = parent::delete( $key );
+               if ( $result === false && $this->client->getResultCode() === Memcached::RES_NOTFOUND ) {
+                       // "Not found" is counted as success in our interface
+                       return true;
+               } else {
+                       return $this->checkResult( $key, $result );
+               }
+       }
+
+       public function add( $key, $value, $exptime = 0 ) {
+               $this->debugLog( "add($key)" );
+               return $this->checkResult( $key, parent::add( $key, $value, $exptime ) );
+       }
+
+       public function incr( $key, $value = 1 ) {
+               $this->debugLog( "incr($key)" );
+               $result = $this->client->increment( $key, $value );
+               return $this->checkResult( $key, $result );
+       }
+
+       public function decr( $key, $value = 1 ) {
+               $this->debugLog( "decr($key)" );
+               $result = $this->client->decrement( $key, $value );
+               return $this->checkResult( $key, $result );
+       }
+
+       /**
+        * Check the return value from a client method call and take any necessary
+        * action. Returns the value that the wrapper function should return. At
+        * present, the return value is always the same as the return value from
+        * the client, but some day we might find a case where it should be
+        * different.
+        *
+        * @param string $key The key used by the caller, or false if there wasn't one.
+        * @param mixed $result The return value
+        * @return mixed
+        */
+       protected function checkResult( $key, $result ) {
+               if ( $result !== false ) {
+                       return $result;
+               }
+               switch ( $this->client->getResultCode() ) {
+                       case Memcached::RES_SUCCESS:
+                               break;
+                       case Memcached::RES_DATA_EXISTS:
+                       case Memcached::RES_NOTSTORED:
+                       case Memcached::RES_NOTFOUND:
+                               $this->debugLog( "result: " . $this->client->getResultMessage() );
+                               break;
+                       default:
+                               $msg = $this->client->getResultMessage();
+                               $logCtx = [];
+                               if ( $key !== false ) {
+                                       $server = $this->client->getServerByKey( $key );
+                                       $logCtx['memcached-server'] = "{$server['host']}:{$server['port']}";
+                                       $logCtx['memcached-key'] = $key;
+                                       $msg = "Memcached error for key \"{memcached-key}\" on server \"{memcached-server}\": $msg";
+                               } else {
+                                       $msg = "Memcached error: $msg";
+                               }
+                               $this->logger->error( $msg, $logCtx );
+                               $this->setLastError( BagOStuff::ERR_UNEXPECTED );
+               }
+               return $result;
+       }
+
+       public function getMulti( array $keys, $flags = 0 ) {
+               $this->debugLog( 'getMulti(' . implode( ', ', $keys ) . ')' );
+               foreach ( $keys as $key ) {
+                       $this->validateKeyEncoding( $key );
+               }
+               $result = $this->client->getMulti( $keys ) ?: [];
+               return $this->checkResult( false, $result );
+       }
+
+       /**
+        * @param array $data
+        * @param int $exptime
+        * @return bool
+        */
+       public function setMulti( array $data, $exptime = 0 ) {
+               $this->debugLog( 'setMulti(' . implode( ', ', array_keys( $data ) ) . ')' );
+               foreach ( array_keys( $data ) as $key ) {
+                       $this->validateKeyEncoding( $key );
+               }
+               $result = $this->client->setMulti( $data, $this->fixExpiry( $exptime ) );
+               return $this->checkResult( false, $result );
+       }
+
+       public function changeTTL( $key, $expiry = 0 ) {
+               $this->debugLog( "touch($key)" );
+               $result = $this->client->touch( $key, $expiry );
+               return $this->checkResult( $key, $result );
+       }
+}
diff --git a/includes/libs/rdbms/loadmonitor/LoadMonitor.php b/includes/libs/rdbms/loadmonitor/LoadMonitor.php
new file mode 100644 (file)
index 0000000..46af068
--- /dev/null
@@ -0,0 +1,65 @@
+<?php
+/**
+ * Database load monitoring.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup Database
+ */
+use Psr\Log\LoggerAwareInterface;
+
+/**
+ * An interface for database load monitoring
+ *
+ * @ingroup Database
+ */
+interface LoadMonitor extends LoggerAwareInterface {
+       /**
+        * Construct a new LoadMonitor with a given LoadBalancer parent
+        *
+        * @param BagOStuff $sCache Server local memory cache
+        * @param BagOStuff $cCache Server local memory cache
+        * @param ILoadBalancer $parent LoadBalancer this instance serves
+        */
+       public function __construct( ILoadBalancer $parent, BagOStuff $sCache, BagOStuff $cCache );
+
+       /**
+        * Perform pre-connection load ratio adjustment.
+        * @param int[] &$loads
+        * @param string|bool $group The selected query group. Default: false
+        * @param string|bool $domain Default: false
+        */
+       public function scaleLoads( &$loads, $group = false, $domain = false );
+
+       /**
+        * Get an estimate of replication lag (in seconds) for each server
+        *
+        * Values may be "false" if replication is too broken to estimate
+        *
+        * @param integer[] $serverIndexes
+        * @param string $domain
+        *
+        * @return array Map of (server index => float|int|bool)
+        */
+       public function getLagTimes( $serverIndexes, $domain );
+
+       /**
+        * Clear any process and persistent cache of lag times
+        * @since 1.27
+        */
+       public function clearCaches();
+}
diff --git a/includes/libs/rdbms/loadmonitor/LoadMonitorMySQL.php b/includes/libs/rdbms/loadmonitor/LoadMonitorMySQL.php
new file mode 100644 (file)
index 0000000..83f4462
--- /dev/null
@@ -0,0 +1,157 @@
+<?php
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup Database
+ */
+
+use Psr\Log\LoggerInterface;
+
+/**
+ * Basic MySQL load monitor with no external dependencies
+ * Uses memcached to cache the replication lag for a short time
+ *
+ * @ingroup Database
+ */
+class LoadMonitorMySQL implements LoadMonitor {
+       /** @var ILoadBalancer */
+       protected $parent;
+       /** @var BagOStuff */
+       protected $srvCache;
+       /** @var BagOStuff */
+       protected $mainCache;
+       /** @var LoggerInterface */
+       protected $replLogger;
+
+       public function __construct( ILoadBalancer $parent, BagOStuff $sCache, BagOStuff $cCache ) {
+               $this->parent = $parent;
+               $this->srvCache = $sCache;
+               $this->mainCache = $cCache;
+               $this->replLogger = new \Psr\Log\NullLogger();
+       }
+
+       public function setLogger( LoggerInterface $logger ) {
+               $this->replLogger = $logger;
+       }
+
+       public function scaleLoads( &$loads, $group = false, $wiki = false ) {
+       }
+
+       public function getLagTimes( $serverIndexes, $wiki ) {
+               if ( count( $serverIndexes ) == 1 && reset( $serverIndexes ) == 0 ) {
+                       # Single server only, just return zero without caching
+                       return [ 0 => 0 ];
+               }
+
+               $key = $this->getLagTimeCacheKey();
+               # Randomize TTLs to reduce stampedes (4.0 - 5.0 sec)
+               $ttl = mt_rand( 4e6, 5e6 ) / 1e6;
+               # Keep keys around longer as fallbacks
+               $staleTTL = 60;
+
+               # (a) Check the local APC cache
+               $value = $this->srvCache->get( $key );
+               if ( $value && $value['timestamp'] > ( microtime( true ) - $ttl ) ) {
+                       $this->replLogger->debug( __METHOD__ . ": got lag times ($key) from local cache" );
+                       return $value['lagTimes']; // cache hit
+               }
+               $staleValue = $value ?: false;
+
+               # (b) Check the shared cache and backfill APC
+               $value = $this->mainCache->get( $key );
+               if ( $value && $value['timestamp'] > ( microtime( true ) - $ttl ) ) {
+                       $this->srvCache->set( $key, $value, $staleTTL );
+                       $this->replLogger->debug( __METHOD__ . ": got lag times ($key) from main cache" );
+
+                       return $value['lagTimes']; // cache hit
+               }
+               $staleValue = $value ?: $staleValue;
+
+               # (c) Cache key missing or expired; regenerate and backfill
+               if ( $this->mainCache->lock( $key, 0, 10 ) ) {
+                       # Let this process alone update the cache value
+                       $cache = $this->mainCache;
+                       /** @noinspection PhpUnusedLocalVariableInspection */
+                       $unlocker = new ScopedCallback( function () use ( $cache, $key ) {
+                               $cache->unlock( $key );
+                       } );
+               } elseif ( $staleValue ) {
+                       # Could not acquire lock but an old cache exists, so use it
+                       return $staleValue['lagTimes'];
+               }
+
+               $lagTimes = [];
+               foreach ( $serverIndexes as $i ) {
+                       if ( $i == $this->parent->getWriterIndex() ) {
+                               $lagTimes[$i] = 0; // master always has no lag
+                               continue;
+                       }
+
+                       $conn = $this->parent->getAnyOpenConnection( $i );
+                       if ( $conn ) {
+                               $close = false; // already open
+                       } else {
+                               $conn = $this->parent->openConnection( $i, $wiki );
+                               $close = true; // new connection
+                       }
+
+                       if ( !$conn ) {
+                               $lagTimes[$i] = false;
+                               $host = $this->parent->getServerName( $i );
+                               $this->replLogger->error( __METHOD__ . ": host $host (#$i) is unreachable" );
+                               continue;
+                       }
+
+                       $lagTimes[$i] = $conn->getLag();
+                       if ( $lagTimes[$i] === false ) {
+                               $host = $this->parent->getServerName( $i );
+                               $this->replLogger->error( __METHOD__ . ": host $host (#$i) is not replicating?" );
+                       }
+
+                       if ( $close ) {
+                               # Close the connection to avoid sleeper connections piling up.
+                               # Note that the caller will pick one of these DBs and reconnect,
+                               # which is slightly inefficient, but this only matters for the lag
+                               # time cache miss cache, which is far less common that cache hits.
+                               $this->parent->closeConnection( $conn );
+                       }
+               }
+
+               # Add a timestamp key so we know when it was cached
+               $value = [ 'lagTimes' => $lagTimes, 'timestamp' => microtime( true ) ];
+               $this->mainCache->set( $key, $value, $staleTTL );
+               $this->srvCache->set( $key, $value, $staleTTL );
+               $this->replLogger->info( __METHOD__ . ": re-calculated lag times ($key)" );
+
+               return $value['lagTimes'];
+       }
+
+       public function clearCaches() {
+               $key = $this->getLagTimeCacheKey();
+               $this->srvCache->delete( $key );
+               $this->mainCache->delete( $key );
+       }
+
+       private function getLagTimeCacheKey() {
+               $writerIndex = $this->parent->getWriterIndex();
+               // Lag is per-server, not per-DB, so key on the master DB name
+               return $this->srvCache->makeGlobalKey(
+                       'lag-times',
+                       $this->parent->getServerName( $writerIndex )
+               );
+       }
+}
diff --git a/includes/libs/rdbms/loadmonitor/LoadMonitorNull.php b/includes/libs/rdbms/loadmonitor/LoadMonitorNull.php
new file mode 100644 (file)
index 0000000..df95b0a
--- /dev/null
@@ -0,0 +1,40 @@
+<?php
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup Database
+ */
+use Psr\Log\LoggerInterface;
+
+class LoadMonitorNull implements LoadMonitor {
+       public function __construct( ILoadBalancer $parent, BagOStuff $sCache, BagOStuff $cCache ) {
+       }
+
+       public function setLogger( LoggerInterface $logger ) {
+       }
+
+       public function scaleLoads( &$loads, $group = false, $wiki = false ) {
+       }
+
+       public function getLagTimes( $serverIndexes, $wiki ) {
+               return array_fill_keys( $serverIndexes, 0 );
+       }
+
+       public function clearCaches() {
+
+       }
+}
diff --git a/includes/objectcache/MemcachedPeclBagOStuff.php b/includes/objectcache/MemcachedPeclBagOStuff.php
deleted file mode 100644 (file)
index aefda79..0000000
+++ /dev/null
@@ -1,250 +0,0 @@
-<?php
-/**
- * Object caching using memcached.
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- * @ingroup Cache
- */
-
-/**
- * A wrapper class for the PECL memcached client
- *
- * @ingroup Cache
- */
-class MemcachedPeclBagOStuff extends MemcachedBagOStuff {
-
-       /**
-        * Constructor
-        *
-        * Available parameters are:
-        *   - servers:             The list of IP:port combinations holding the memcached servers.
-        *   - persistent:          Whether to use a persistent connection
-        *   - compress_threshold:  The minimum size an object must be before it is compressed
-        *   - timeout:             The read timeout in microseconds
-        *   - connect_timeout:     The connect timeout in seconds
-        *   - retry_timeout:       Time in seconds to wait before retrying a failed connect attempt
-        *   - server_failure_limit:  Limit for server connect failures before it is removed
-        *   - serializer:          May be either "php" or "igbinary". Igbinary produces more compact
-        *                          values, but serialization is much slower unless the php.ini option
-        *                          igbinary.compact_strings is off.
-        *   - use_binary_protocol  Whether to enable the binary protocol (default is ASCII) (boolean)
-        * @param array $params
-        * @throws InvalidArgumentException
-        */
-       function __construct( $params ) {
-               parent::__construct( $params );
-               $params = $this->applyDefaultParams( $params );
-
-               if ( $params['persistent'] ) {
-                       // The pool ID must be unique to the server/option combination.
-                       // The Memcached object is essentially shared for each pool ID.
-                       // We can only reuse a pool ID if we keep the config consistent.
-                       $this->client = new Memcached( md5( serialize( $params ) ) );
-                       if ( count( $this->client->getServerList() ) ) {
-                               $this->logger->debug( __METHOD__ . ": persistent Memcached object already loaded." );
-                               return; // already initialized; don't add duplicate servers
-                       }
-               } else {
-                       $this->client = new Memcached;
-               }
-
-               if ( $params['use_binary_protocol'] ) {
-                       $this->client->setOption( Memcached::OPT_BINARY_PROTOCOL, true );
-               }
-
-               if ( isset( $params['retry_timeout'] ) ) {
-                       $this->client->setOption( Memcached::OPT_RETRY_TIMEOUT, $params['retry_timeout'] );
-               }
-
-               if ( isset( $params['server_failure_limit'] ) ) {
-                       $this->client->setOption( Memcached::OPT_SERVER_FAILURE_LIMIT, $params['server_failure_limit'] );
-               }
-
-               // The compression threshold is an undocumented php.ini option for some
-               // reason. There's probably not much harm in setting it globally, for
-               // compatibility with the settings for the PHP client.
-               ini_set( 'memcached.compression_threshold', $params['compress_threshold'] );
-
-               // Set timeouts
-               $this->client->setOption( Memcached::OPT_CONNECT_TIMEOUT, $params['connect_timeout'] * 1000 );
-               $this->client->setOption( Memcached::OPT_SEND_TIMEOUT, $params['timeout'] );
-               $this->client->setOption( Memcached::OPT_RECV_TIMEOUT, $params['timeout'] );
-               $this->client->setOption( Memcached::OPT_POLL_TIMEOUT, $params['timeout'] / 1000 );
-
-               // Set libketama mode since it's recommended by the documentation and
-               // is as good as any. There's no way to configure libmemcached to use
-               // hashes identical to the ones currently in use by the PHP client, and
-               // even implementing one of the libmemcached hashes in pure PHP for
-               // forwards compatibility would require MemcachedClient::get_sock() to be
-               // rewritten.
-               $this->client->setOption( Memcached::OPT_LIBKETAMA_COMPATIBLE, true );
-
-               // Set the serializer
-               switch ( $params['serializer'] ) {
-                       case 'php':
-                               $this->client->setOption( Memcached::OPT_SERIALIZER, Memcached::SERIALIZER_PHP );
-                               break;
-                       case 'igbinary':
-                               if ( !Memcached::HAVE_IGBINARY ) {
-                                       throw new InvalidArgumentException(
-                                               __CLASS__ . ': the igbinary extension is not available ' .
-                                               'but igbinary serialization was requested.'
-                                       );
-                               }
-                               $this->client->setOption( Memcached::OPT_SERIALIZER, Memcached::SERIALIZER_IGBINARY );
-                               break;
-                       default:
-                               throw new InvalidArgumentException(
-                                       __CLASS__ . ': invalid value for serializer parameter'
-                               );
-               }
-               $servers = [];
-               foreach ( $params['servers'] as $host ) {
-                       $servers[] = IP::splitHostAndPort( $host ); // (ip, port)
-               }
-               $this->client->addServers( $servers );
-       }
-
-       protected function applyDefaultParams( $params ) {
-               $params = parent::applyDefaultParams( $params );
-
-               if ( !isset( $params['use_binary_protocol'] ) ) {
-                       $params['use_binary_protocol'] = false;
-               }
-
-               if ( !isset( $params['serializer'] ) ) {
-                       $params['serializer'] = 'php';
-               }
-
-               return $params;
-       }
-
-       protected function getWithToken( $key, &$casToken, $flags = 0 ) {
-               $this->debugLog( "get($key)" );
-               $result = $this->client->get( $this->validateKeyEncoding( $key ), null, $casToken );
-               $result = $this->checkResult( $key, $result );
-               return $result;
-       }
-
-       public function set( $key, $value, $exptime = 0, $flags = 0 ) {
-               $this->debugLog( "set($key)" );
-               return $this->checkResult( $key, parent::set( $key, $value, $exptime ) );
-       }
-
-       protected function cas( $casToken, $key, $value, $exptime = 0 ) {
-               $this->debugLog( "cas($key)" );
-               return $this->checkResult( $key, parent::cas( $casToken, $key, $value, $exptime ) );
-       }
-
-       public function delete( $key ) {
-               $this->debugLog( "delete($key)" );
-               $result = parent::delete( $key );
-               if ( $result === false && $this->client->getResultCode() === Memcached::RES_NOTFOUND ) {
-                       // "Not found" is counted as success in our interface
-                       return true;
-               } else {
-                       return $this->checkResult( $key, $result );
-               }
-       }
-
-       public function add( $key, $value, $exptime = 0 ) {
-               $this->debugLog( "add($key)" );
-               return $this->checkResult( $key, parent::add( $key, $value, $exptime ) );
-       }
-
-       public function incr( $key, $value = 1 ) {
-               $this->debugLog( "incr($key)" );
-               $result = $this->client->increment( $key, $value );
-               return $this->checkResult( $key, $result );
-       }
-
-       public function decr( $key, $value = 1 ) {
-               $this->debugLog( "decr($key)" );
-               $result = $this->client->decrement( $key, $value );
-               return $this->checkResult( $key, $result );
-       }
-
-       /**
-        * Check the return value from a client method call and take any necessary
-        * action. Returns the value that the wrapper function should return. At
-        * present, the return value is always the same as the return value from
-        * the client, but some day we might find a case where it should be
-        * different.
-        *
-        * @param string $key The key used by the caller, or false if there wasn't one.
-        * @param mixed $result The return value
-        * @return mixed
-        */
-       protected function checkResult( $key, $result ) {
-               if ( $result !== false ) {
-                       return $result;
-               }
-               switch ( $this->client->getResultCode() ) {
-                       case Memcached::RES_SUCCESS:
-                               break;
-                       case Memcached::RES_DATA_EXISTS:
-                       case Memcached::RES_NOTSTORED:
-                       case Memcached::RES_NOTFOUND:
-                               $this->debugLog( "result: " . $this->client->getResultMessage() );
-                               break;
-                       default:
-                               $msg = $this->client->getResultMessage();
-                               $logCtx = [];
-                               if ( $key !== false ) {
-                                       $server = $this->client->getServerByKey( $key );
-                                       $logCtx['memcached-server'] = "{$server['host']}:{$server['port']}";
-                                       $logCtx['memcached-key'] = $key;
-                                       $msg = "Memcached error for key \"{memcached-key}\" on server \"{memcached-server}\": $msg";
-                               } else {
-                                       $msg = "Memcached error: $msg";
-                               }
-                               $this->logger->error( $msg, $logCtx );
-                               $this->setLastError( BagOStuff::ERR_UNEXPECTED );
-               }
-               return $result;
-       }
-
-       public function getMulti( array $keys, $flags = 0 ) {
-               $this->debugLog( 'getMulti(' . implode( ', ', $keys ) . ')' );
-               foreach ( $keys as $key ) {
-                       $this->validateKeyEncoding( $key );
-               }
-               $result = $this->client->getMulti( $keys ) ?: [];
-               return $this->checkResult( false, $result );
-       }
-
-       /**
-        * @param array $data
-        * @param int $exptime
-        * @return bool
-        */
-       public function setMulti( array $data, $exptime = 0 ) {
-               $this->debugLog( 'setMulti(' . implode( ', ', array_keys( $data ) ) . ')' );
-               foreach ( array_keys( $data ) as $key ) {
-                       $this->validateKeyEncoding( $key );
-               }
-               $result = $this->client->setMulti( $data, $this->fixExpiry( $exptime ) );
-               return $this->checkResult( false, $result );
-       }
-
-       public function changeTTL( $key, $expiry = 0 ) {
-               $this->debugLog( "touch($key)" );
-               $result = $this->client->touch( $key, $expiry );
-               return $this->checkResult( $key, $result );
-       }
-}
index af6d039..4895b4f 100644 (file)
@@ -64,12 +64,12 @@ abstract class ReverseChronologicalPager extends IndexPager {
        /**
         * Set and return the mOffset timestamp such that we can get all revisions with
         * a timestamp up to the specified parameters.
-        * @param int $year [optional] Year up to which we want revisions. Default is current year.
-        * @param int $month [optional] Month up to which we want revisions. Default is end of year.
+        * @param int $year Year up to which we want revisions
+        * @param int $month Month up to which we want revisions
         * @param int $day [optional] Day up to which we want revisions. Default is end of month.
-        * @return string Timestamp
+        * @return string|null Timestamp or null if year and month are false/invalid
         */
-       function getDateCond( $year = -1, $month = -1, $day = -1 ) {
+       function getDateCond( $year, $month, $day = -1 ) {
                $year = intval( $year );
                $month = intval( $month );
                $day = intval( $day );
@@ -78,6 +78,11 @@ abstract class ReverseChronologicalPager extends IndexPager {
                $this->mYear = $year > 0 ? $year : false;
                $this->mMonth = ( $month > 0 && $month < 13 ) ? $month : false;
 
+               // If year and month are false, don't update the mOffset
+               if ( !$this->mYear && !$this->mMonth ) {
+                       return;
+               }
+
                // Given an optional year, month, and day, we need to generate a timestamp
                // to use as "WHERE rev_timestamp <= result"
                // Examples: year = 2006      equals < 20070101 (+000000)
index b37c475..dd7f0ed 100644 (file)
@@ -221,6 +221,22 @@ class SpecialChangeContentModel extends FormSpecialPage {
                # Truncate for whole multibyte characters.
                $reason = $wgContLang->truncate( $reason, 255 );
 
+               // Run edit filters
+               $derivativeContext = new DerivativeContext( $this->getContext() );
+               $derivativeContext->setTitle( $this->title );
+               $derivativeContext->setWikiPage( $page );
+               $status = new Status();
+               if ( !Hooks::run( 'EditFilterMergedContent',
+                               [ $derivativeContext, $newContent, $status, $reason,
+                               $user, false ] )
+               ) {
+                       if ( $status->isGood() ) {
+                               // TODO: extensions should really specify an error message
+                               $status->fatal( 'hookaborted' );
+                       }
+                       return $status;
+               }
+
                $status = $page->doEditContent(
                        $newContent,
                        $reason,
index 0bbe12e..eae57f4 100644 (file)
@@ -411,11 +411,13 @@ class BotPassword implements IDBAccessObject {
         */
        public static function canonicalizeLoginData( $username, $password ) {
                $sep = BotPassword::getSeparator();
-               if ( strpos( $username, $sep ) !== false ) {
-                       // the separator is not valid in usernames so this must be a bot login
-                       return [ $username, $password, false ];
+               // the strlen check helps minimize the password information obtainable from timing
+               if ( strlen( $password ) >= 32 && strpos( $username, $sep ) !== false ) {
+                       // the separator is not valid in new usernames but might appear in legacy ones
+                       if ( preg_match( '/^[0-9a-w]{32,}$/', $password ) ) {
+                               return [ $username, $password, true ];
+                       }
                } elseif ( strlen( $password ) > 32 && strpos( $password, $sep ) !== false ) {
-                       // the strlen check helps minimize the password information obtainable from timing
                        $segments = explode( $sep, $password );
                        $password = array_pop( $segments );
                        $appId = implode( $sep, $segments );
index 24c8c11..106be1f 100644 (file)
@@ -29,6 +29,9 @@ require_once __DIR__ . '/Maintenance.php';
  * @ingroup Maintenance
  */
 class RefreshLinks extends Maintenance {
+       /** @var int|bool */
+       protected $namespace = false;
+
        public function __construct() {
                parent::__construct();
                $this->addDescription( 'Refresh link tables' );
@@ -39,6 +42,7 @@ class RefreshLinks extends Maintenance {
                $this->addOption( 'e', 'Last page id to refresh', false, true );
                $this->addOption( 'dfn-chunk-size', 'Maximum number of existent IDs to check per ' .
                        'query, default 100000', false, true );
+               $this->addOption( 'namespace', 'Only fix pages in this namespace', false, true );
                $this->addArg( 'start', 'Page_id to start from, default 1', false );
                $this->setBatchSize( 100 );
        }
@@ -51,6 +55,12 @@ class RefreshLinks extends Maintenance {
                $start = (int)$this->getArg( 0 ) ?: null;
                $end = (int)$this->getOption( 'e' ) ?: null;
                $dfnChunkSize = (int)$this->getOption( 'dfn-chunk-size', 100000 );
+               $ns = $this->getOption( 'namespace' );
+               if ( $ns === null ) {
+                       $this->namespace = false;
+               } else {
+                       $this->namespace = (int)$ns;
+               }
                if ( !$this->hasOption( 'dfn-only' ) ) {
                        $new = $this->getOption( 'new-only', false );
                        $redir = $this->getOption( 'redirects-only', false );
@@ -62,6 +72,12 @@ class RefreshLinks extends Maintenance {
                }
        }
 
+       private function namespaceCond() {
+               return $this->namespace !== false
+                       ? [ 'page_namespace' => $this->namespace ]
+                       : [];
+       }
+
        /**
         * Do the actual link refreshing.
         * @param int|null $start Page_id to start from
@@ -92,7 +108,7 @@ class RefreshLinks extends Maintenance {
                                "page_is_redirect=1",
                                "rd_from IS NULL",
                                self::intervalCond( $dbr, 'page_id', $start, $end ),
-                       ];
+                       ] + $this->namespaceCond();
 
                        $res = $dbr->select(
                                [ 'page', 'redirect' ],
@@ -121,7 +137,7 @@ class RefreshLinks extends Maintenance {
                                [
                                        'page_is_new' => 1,
                                        self::intervalCond( $dbr, 'page_id', $start, $end ),
-                               ],
+                               ] + $this->namespaceCond(),
                                __METHOD__
                        );
                        $num = $res->numRows();
@@ -136,7 +152,7 @@ class RefreshLinks extends Maintenance {
                                if ( $redirectsOnly ) {
                                        $this->fixRedirect( $row->page_id );
                                } else {
-                                       self::fixLinksFromArticle( $row->page_id );
+                                       self::fixLinksFromArticle( $row->page_id, $this->namespace );
                                }
                        }
                } else {
@@ -167,7 +183,7 @@ class RefreshLinks extends Maintenance {
                                                $this->output( "$id\n" );
                                                wfWaitForSlaves();
                                        }
-                                       self::fixLinksFromArticle( $id );
+                                       self::fixLinksFromArticle( $id, $this->namespace );
                                }
                        }
                }
@@ -195,6 +211,10 @@ class RefreshLinks extends Maintenance {
                        $dbw->delete( 'redirect', [ 'rd_from' => $id ],
                                __METHOD__ );
 
+                       return;
+               } elseif ( $this->namespace !== false
+                       && !$page->getTitle()->inNamespace( $this->namespace )
+               ) {
                        return;
                }
 
@@ -222,14 +242,18 @@ class RefreshLinks extends Maintenance {
        /**
         * Run LinksUpdate for all links on a given page_id
         * @param int $id The page_id
+        * @param int|bool $ns Only fix links if it is in this namespace
         */
-       public static function fixLinksFromArticle( $id ) {
+       public static function fixLinksFromArticle( $id, $ns = false ) {
                $page = WikiPage::newFromID( $id );
 
                LinkCache::singleton()->clear();
 
                if ( $page === null ) {
                        return;
+               } elseif ( $ns !== false
+                       && !$page->getTitle()->inNamespace( $ns ) ) {
+                       return;
                }
 
                $content = $page->getContent( Revision::RAW );
@@ -265,7 +289,8 @@ class RefreshLinks extends Maintenance {
                        $nextStart = $dbr->selectField(
                                'page',
                                'page_id',
-                               self::intervalCond( $dbr, 'page_id', $start, $end ),
+                               [ self::intervalCond( $dbr, 'page_id', $start, $end ) ]
+                               + $this->namespaceCond(),
                                __METHOD__,
                                [ 'ORDER BY' => 'page_id', 'OFFSET' => $chunkSize ]
                        );
index 6659ff8..4ef778d 100644 (file)
@@ -1517,7 +1517,10 @@ class ParserTestRunner {
                        throw new MWException( "duplicate article '$name' at $file:$line\n" );
                }
 
-               $page->doEditContent( ContentHandler::makeContent( $text, $title ), '', EDIT_NEW );
+               $status = $page->doEditContent( ContentHandler::makeContent( $text, $title ), '', EDIT_NEW );
+               if ( !$status->isOK() ) {
+                       throw new MWException( $status->getWikiText( false, false, 'en' ) );
+               }
 
                // The RepoGroup cache is invalidated by the creation of file redirects
                if ( $title->getNamespace() === NS_IMAGE ) {
index 487ab84..97681eb 100644 (file)
@@ -231,10 +231,11 @@ class ApiLoginTest extends ApiTestCase {
                $centralId = CentralIdLookup::factory()->centralIdFromLocalUser( $user->getUser() );
                $this->assertNotEquals( 0, $centralId, 'sanity check' );
 
+               $password = 'ngfhmjm64hv0854493hsj5nncjud2clk';
                $passwordFactory = new PasswordFactory();
                $passwordFactory->init( RequestContext::getMain()->getConfig() );
                // A is unsalted MD5 (thus fast) ... we don't care about security here, this is test only
-               $passwordHash = $passwordFactory->newFromPlaintext( 'foobaz' );
+               $passwordHash = $passwordFactory->newFromPlaintext( $password );
 
                $dbw = wfGetDB( DB_MASTER );
                $dbw->insert(
@@ -255,7 +256,7 @@ class ApiLoginTest extends ApiTestCase {
                $ret = $this->doApiRequest( [
                        'action' => 'login',
                        'lgname' => $lgName,
-                       'lgpassword' => 'foobaz',
+                       'lgpassword' => $password,
                ] );
 
                $result = $ret[0];
@@ -270,7 +271,7 @@ class ApiLoginTest extends ApiTestCase {
                        'action' => 'login',
                        'lgtoken' => $token,
                        'lgname' => $lgName,
-                       'lgpassword' => 'foobaz',
+                       'lgpassword' => $password,
                ], $ret[2] );
 
                $result = $ret[0];
index 824b5c4..fc5d660 100644 (file)
@@ -19,14 +19,9 @@ class ReverseChronologicalPagerTest extends MediaWikiLangTestCase {
 
                $currYear = $timestamp->format( 'Y' );
                $currMonth = $timestamp->format( 'n' );
-               $currYearTimestamp = $db->timestamp( $currYear + 1 . '0101000000' );
 
                // Test that getDateCond sets and returns mOffset
-               $this->assertEquals( $pager->getDateCond( 2006 ), $pager->mOffset );
-
-               // Test year
-               $pager->getDateCond( 2006 );
-               $this->assertEquals( $pager->mOffset, $db->timestamp( '20070101000000' ) );
+               $this->assertEquals( $pager->getDateCond( 2006, 6 ), $pager->mOffset );
 
                // Test year and month
                $pager->getDateCond( 2006, 6 );
@@ -44,22 +39,14 @@ class ReverseChronologicalPagerTest extends MediaWikiLangTestCase {
                $pager->getDateCond( 2006, 6, 30 );
                $this->assertEquals( $pager->mOffset, $db->timestamp( '20060701000000' ) );
 
-               // Test invalid year (should use current year)
-               $pager->getDateCond( -1337 );
-               $this->assertEquals( $pager->mOffset, $currYearTimestamp );
-
-               // Test invalid month
+               // Test invalid month (should use end of year)
                $pager->getDateCond( 2006, -1 );
                $this->assertEquals( $pager->mOffset, $db->timestamp( '20070101000000' ) );
 
-               // Test invalid day
+               // Test invalid day (should use end of month)
                $pager->getDateCond( 2006, 6, 1337 );
                $this->assertEquals( $pager->mOffset, $db->timestamp( '20060701000000' ) );
 
-               // Test no year or month (should use end of current year)
-               $pager->getDateCond();
-               $this->assertEquals( $pager->mOffset, $currYearTimestamp );
-
                // Test last day of year
                $pager->getDateCond( 2006, 12, 31 );
                $this->assertEquals( $pager->mOffset, $db->timestamp( '20070101000000' ) );
index d637704..cb27fde 100644 (file)
@@ -244,8 +244,9 @@ class BotPasswordTest extends MediaWikiTestCase {
                return [
                        [ 'user', 'pass', false ],
                        [ 'user', 'abc@def', false ],
+                       [ 'legacy@user', 'pass', false ],
                        [ 'user@bot', '12345678901234567890123456789012',
-                               [ 'user@bot', '12345678901234567890123456789012', false ] ],
+                               [ 'user@bot', '12345678901234567890123456789012', true ] ],
                        [ 'user', 'bot@12345678901234567890123456789012',
                                [ 'user@bot', '12345678901234567890123456789012', true ] ],
                        [ 'user', 'bot@12345678901234567890123456789012345',