Move DBConnRef and ChronologyProtector to /libs/rdbms
authorAaron Schulz <aschulz@wikimedia.org>
Wed, 14 Sep 2016 09:21:18 +0000 (02:21 -0700)
committerAaron Schulz <aschulz@wikimedia.org>
Thu, 15 Sep 2016 00:29:54 +0000 (17:29 -0700)
Change-Id: If2c4b314a5c39311328843f534d91bf90823e179

autoload.php
includes/db/ChronologyProtector.php [deleted file]
includes/db/DBConnRef.php [deleted file]
includes/libs/rdbms/chronologyprotector/ChronologyProtector.php [new file with mode: 0644]
includes/libs/rdbms/database/RBConnRef.php [new file with mode: 0644]

index 66736b3..557df83 100644 (file)
@@ -241,7 +241,7 @@ $wgAutoloadLocalClasses = [
        'CheckStorage' => __DIR__ . '/maintenance/storage/checkStorage.php',
        'CheckSyntax' => __DIR__ . '/maintenance/checkSyntax.php',
        'CheckUsernames' => __DIR__ . '/maintenance/checkUsernames.php',
-       'ChronologyProtector' => __DIR__ . '/includes/db/ChronologyProtector.php',
+       'ChronologyProtector' => __DIR__ . '/includes/libs/rdbms/chronologyprotector/ChronologyProtector.php',
        'ClassCollector' => __DIR__ . '/includes/utils/AutoloadGenerator.php',
        'CleanupAncientTables' => __DIR__ . '/maintenance/cleanupAncientTables.php',
        'CleanupBlocks' => __DIR__ . '/maintenance/cleanupBlocks.php',
@@ -299,7 +299,7 @@ $wgAutoloadLocalClasses = [
        'DBAccessBase' => __DIR__ . '/includes/dao/DBAccessBase.php',
        'DBAccessError' => __DIR__ . '/includes/libs/rdbms/exception/DBError.php',
        'DBAccessObjectUtils' => __DIR__ . '/includes/dao/DBAccessObjectUtils.php',
-       'DBConnRef' => __DIR__ . '/includes/db/DBConnRef.php',
+       'DBConnRef' => __DIR__ . '/includes/libs/rdbms/database/RBConnRef.php',
        'DBConnectionError' => __DIR__ . '/includes/libs/rdbms/exception/DBError.php',
        'DBError' => __DIR__ . '/includes/libs/rdbms/exception/DBError.php',
        'DBExpectedError' => __DIR__ . '/includes/libs/rdbms/exception/DBError.php',
diff --git a/includes/db/ChronologyProtector.php b/includes/db/ChronologyProtector.php
deleted file mode 100644 (file)
index 09b820b..0000000
+++ /dev/null
@@ -1,326 +0,0 @@
-<?php
-/**
- * Generator of database load balancing objects.
- *
- * 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;
-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 implements LoggerAwareInterface{
-       /** @var BagOStuff */
-       protected $store;
-       /** @var LoggerInterface */
-       protected $logger;
-
-       /** @var string Storage key name */
-       protected $key;
-       /** @var string Hash of client parameters */
-       protected $clientId;
-       /** @var float|null Minimum UNIX timestamp of 1+ expected startup positions */
-       protected $waitForPosTime;
-       /** @var int Max seconds to wait on positions to appear */
-       protected $waitForPosTimeout = self::POS_WAIT_TIMEOUT;
-       /** @var bool Whether to no-op all method calls */
-       protected $enabled = true;
-       /** @var bool Whether to check and wait on positions */
-       protected $wait = true;
-
-       /** @var bool Whether the client data was loaded */
-       protected $initialized = false;
-       /** @var DBMasterPos[] Map of (DB master name => position) */
-       protected $startupPositions = [];
-       /** @var DBMasterPos[] Map of (DB master name => position) */
-       protected $shutdownPositions = [];
-       /** @var float[] Map of (DB master name => 1) */
-       protected $shutdownTouchDBs = [];
-
-       /** @var integer Seconds to store positions */
-       const POSITION_TTL = 60;
-       /** @var integer Max time to wait for positions to appear */
-       const POS_WAIT_TIMEOUT = 5;
-
-       /**
-        * @param BagOStuff $store
-        * @param array $client Map of (ip: <IP>, agent: <user-agent>)
-        * @param float $posTime UNIX timestamp
-        * @since 1.27
-        */
-       public function __construct( BagOStuff $store, array $client, $posTime = null ) {
-               $this->store = $store;
-               $this->clientId = md5( $client['ip'] . "\n" . $client['agent'] );
-               $this->key = $store->makeGlobalKey( __CLASS__, $this->clientId );
-               $this->waitForPosTime = $posTime;
-               $this->logger = new \Psr\Log\NullLogger();
-       }
-
-       public function setLogger( LoggerInterface $logger ) {
-               $this->logger = $logger;
-       }
-
-       /**
-        * @param bool $enabled Whether to no-op all method calls
-        * @since 1.27
-        */
-       public function setEnabled( $enabled ) {
-               $this->enabled = $enabled;
-       }
-
-       /**
-        * @param bool $enabled Whether to check and wait on positions
-        * @since 1.27
-        */
-       public function setWaitEnabled( $enabled ) {
-               $this->wait = $enabled;
-       }
-
-       /**
-        * Initialise a LoadBalancer to give it appropriate chronology protection.
-        *
-        * If the stash has a previous master position recorded, this will try to
-        * make sure that the next query to a replica DB of that master will see changes up
-        * to that position by delaying execution. The delay may timeout and allow stale
-        * data if no non-lagged replica DBs are available.
-        *
-        * @param LoadBalancer $lb
-        * @return void
-        */
-       public function initLB( LoadBalancer $lb ) {
-               if ( !$this->enabled || $lb->getServerCount() <= 1 ) {
-                       return; // non-replicated setup or disabled
-               }
-
-               $this->initPositions();
-
-               $masterName = $lb->getServerName( $lb->getWriterIndex() );
-               if ( !empty( $this->startupPositions[$masterName] ) ) {
-                       $pos = $this->startupPositions[$masterName];
-                       $this->logger->info( __METHOD__ . ": LB for '$masterName' set to pos $pos\n" );
-                       $lb->waitFor( $pos );
-               }
-       }
-
-       /**
-        * Notify the ChronologyProtector that the LoadBalancer is about to shut
-        * down. Saves replication positions.
-        *
-        * @param LoadBalancer $lb
-        * @return void
-        */
-       public function shutdownLB( LoadBalancer $lb ) {
-               if ( !$this->enabled ) {
-                       return; // not enabled
-               } elseif ( !$lb->hasOrMadeRecentMasterChanges( INF ) ) {
-                       // Only save the position if writes have been done on the connection
-                       return;
-               }
-
-               $masterName = $lb->getServerName( $lb->getWriterIndex() );
-               if ( $lb->getServerCount() > 1 ) {
-                       $pos = $lb->getMasterPos();
-                       $this->logger->info( __METHOD__ . ": LB for '$masterName' has pos $pos\n" );
-                       $this->shutdownPositions[$masterName] = $pos;
-               } else {
-                       $this->logger->info( __METHOD__ . ": DB '$masterName' touched\n" );
-               }
-               $this->shutdownTouchDBs[$masterName] = 1;
-       }
-
-       /**
-        * Notify the ChronologyProtector that the LBFactory is done calling shutdownLB() for now.
-        * May commit chronology data to persistent storage.
-        *
-        * @param callable|null $workCallback Work to do instead of waiting on syncing positions
-        * @param string $mode One of (sync, async); whether to wait on remote datacenters
-        * @return DBMasterPos[] Empty on success; returns the (db name => position) map on failure
-        */
-       public function shutdown( callable $workCallback = null, $mode = 'sync' ) {
-               if ( !$this->enabled ) {
-                       return [];
-               }
-
-               $store = $this->store;
-               // Some callers might want to know if a user recently touched a DB.
-               // These writes do not need to block on all datacenters receiving them.
-               foreach ( $this->shutdownTouchDBs as $dbName => $unused ) {
-                       $store->set(
-                               $this->getTouchedKey( $this->store, $dbName ),
-                               microtime( true ),
-                               $store::TTL_DAY
-                       );
-               }
-
-               if ( !count( $this->shutdownPositions ) ) {
-                       return []; // nothing to save
-               }
-
-               $this->logger->info( __METHOD__ . ": saving master pos for " .
-                       implode( ', ', array_keys( $this->shutdownPositions ) ) . "\n"
-               );
-
-               // CP-protected writes should overwhemingly go to the master datacenter, so get DC-local
-               // lock to merge the values. Use a DC-local get() and a synchronous all-DC set(). This
-               // makes it possible for the BagOStuff class to write in parallel to all DCs with one RTT.
-               if ( $store->lock( $this->key, 3 ) ) {
-                       if ( $workCallback ) {
-                               // Let the store run the work before blocking on a replication sync barrier. By the
-                               // time it's done with the work, the barrier should be fast if replication caught up.
-                               $store->addBusyCallback( $workCallback );
-                       }
-                       $ok = $store->set(
-                               $this->key,
-                               self::mergePositions( $store->get( $this->key ), $this->shutdownPositions ),
-                               self::POSITION_TTL,
-                               ( $mode === 'sync' ) ? $store::WRITE_SYNC : 0
-                       );
-                       $store->unlock( $this->key );
-               } else {
-                       $ok = false;
-               }
-
-               if ( !$ok ) {
-                       $bouncedPositions = $this->shutdownPositions;
-                       // Raced out too many times or stash is down
-                       $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
-                       $this->logger->info( __METHOD__ . ": store may not support synchronous writes." );
-                       $bouncedPositions = $this->shutdownPositions;
-               } else {
-                       $bouncedPositions = [];
-               }
-
-               return $bouncedPositions;
-       }
-
-       /**
-        * @param string $dbName DB master name (e.g. "db1052")
-        * @return float|bool UNIX timestamp when client last touched the DB; false if not on record
-        * @since 1.28
-        */
-       public function getTouched( $dbName ) {
-               return $this->store->get( $this->getTouchedKey( $this->store, $dbName ) );
-       }
-
-       /**
-        * @param BagOStuff $store
-        * @param string $dbName
-        * @return string
-        */
-       private function getTouchedKey( BagOStuff $store, $dbName ) {
-               return $store->makeGlobalKey( __CLASS__, 'mtime', $this->clientId, $dbName );
-       }
-
-       /**
-        * Load in previous master positions for the client
-        */
-       protected function initPositions() {
-               if ( $this->initialized ) {
-                       return;
-               }
-
-               $this->initialized = true;
-               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 > 0.0 ) {
-                               $data = null;
-                               $loop = new WaitConditionLoop(
-                                       function () use ( &$data ) {
-                                               $data = $this->store->get( $this->key );
-
-                                               return ( self::minPosTime( $data ) >= $this->waitForPosTime )
-                                                       ? WaitConditionLoop::CONDITION_REACHED
-                                                       : WaitConditionLoop::CONDITION_CONTINUE;
-                                       },
-                                       $this->waitForPosTimeout
-                               );
-                               $result = $loop->invoke();
-                               $waitedMs = $loop->getLastWaitTime() * 1e3;
-
-                               if ( $result == $loop::CONDITION_REACHED ) {
-                                       $msg = "expected and found pos time {$this->waitForPosTime} ({$waitedMs}ms)";
-                                       $this->logger->debug( $msg );
-                               } else {
-                                       $msg = "expected but missed pos time {$this->waitForPosTime} ({$waitedMs}ms)";
-                                       $this->logger->info( $msg );
-                               }
-                       } else {
-                               $data = $this->store->get( $this->key );
-                       }
-
-                       $this->startupPositions = $data ? $data['positions'] : [];
-                       $this->logger->info( __METHOD__ . ": key is {$this->key} (read)\n" );
-               } else {
-                       $this->startupPositions = [];
-                       $this->logger->info( __METHOD__ . ": key is {$this->key} (unread)\n" );
-               }
-       }
-
-       /**
-        * @param array|bool $data
-        * @return float|null
-        */
-       private static function minPosTime( $data ) {
-               if ( !isset( $data['positions'] ) ) {
-                       return null;
-               }
-
-               $min = null;
-               foreach ( $data['positions'] as $pos ) {
-                       /** @var DBMasterPos $pos */
-                       $min = $min ? min( $pos->asOfTime(), $min ) : $pos->asOfTime();
-               }
-
-               return $min;
-       }
-
-       /**
-        * @param array|bool $curValue
-        * @param DBMasterPos[] $shutdownPositions
-        * @return array
-        */
-       private static function mergePositions( $curValue, array $shutdownPositions ) {
-               /** @var $curPositions DBMasterPos[] */
-               if ( $curValue === false ) {
-                       $curPositions = $shutdownPositions;
-               } else {
-                       $curPositions = $curValue['positions'];
-                       // Use the newest positions for each DB master
-                       foreach ( $shutdownPositions as $db => $pos ) {
-                               if ( !isset( $curPositions[$db] )
-                                       || $pos->asOfTime() > $curPositions[$db]->asOfTime()
-                               ) {
-                                       $curPositions[$db] = $pos;
-                               }
-                       }
-               }
-
-               return [ 'positions' => $curPositions ];
-       }
-}
diff --git a/includes/db/DBConnRef.php b/includes/db/DBConnRef.php
deleted file mode 100644 (file)
index 8604295..0000000
+++ /dev/null
@@ -1,581 +0,0 @@
-<?php
-/**
- * Helper class to handle automatically marking connections as reusable (via RAII pattern)
- * as well handling deferring the actual network connection until the handle is used
- *
- * @note: proxy methods are defined explicity to avoid interface errors
- * @ingroup Database
- * @since 1.22
- */
-class DBConnRef implements IDatabase {
-       /** @var LoadBalancer */
-       private $lb;
-
-       /** @var DatabaseBase|null */
-       private $conn;
-
-       /** @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)
-        */
-       public function __construct( LoadBalancer $lb, $conn ) {
-               $this->lb = $lb;
-               if ( $conn instanceof DatabaseBase ) {
-                       $this->conn = $conn;
-               } elseif ( count( $conn ) >= 3 && $conn[self::FLD_WIKI] !== false ) {
-                       $this->params = $conn;
-               } else {
-                       throw new InvalidArgumentException( "Missing lazy connection arguments." );
-               }
-       }
-
-       function __call( $name, array $arguments ) {
-               if ( $this->conn === null ) {
-                       list( $db, $groups, $wiki ) = $this->params;
-                       $this->conn = $this->lb->getConnection( $db, $groups, $wiki );
-               }
-
-               return call_user_func_array( [ $this->conn, $name ], $arguments );
-       }
-
-       public function getServerInfo() {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function bufferResults( $buffer = null ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function trxLevel() {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function trxTimestamp() {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function explicitTrxActive() {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function tablePrefix( $prefix = null ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function dbSchema( $schema = null ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function getLBInfo( $name = null ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function setLBInfo( $name, $value = null ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function implicitGroupby() {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function implicitOrderby() {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function lastQuery() {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function doneWrites() {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function lastDoneWrites() {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function writesPending() {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function writesOrCallbacksPending() {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function pendingWriteQueryDuration( $type = self::ESTIMATE_TOTAL ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function pendingWriteCallers() {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function isOpen() {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function setFlag( $flag, $remember = self::REMEMBER_NOTHING ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function clearFlag( $flag, $remember = self::REMEMBER_NOTHING ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function restoreFlags( $state = self::RESTORE_PRIOR ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function getFlag( $flag ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function getProperty( $name ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function getWikiID() {
-               if ( $this->conn === null ) {
-                       // Avoid triggering a connection
-                       return $this->params[self::FLD_WIKI];
-               }
-
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function getType() {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function open( $server, $user, $password, $dbName ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function fetchObject( $res ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function fetchRow( $res ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function numRows( $res ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function numFields( $res ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function fieldName( $res, $n ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function insertId() {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function dataSeek( $res, $row ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function lastErrno() {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function lastError() {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function fieldInfo( $table, $field ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function affectedRows() {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function getSoftwareLink() {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function getServerVersion() {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function close() {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function reportConnectionError( $error = 'Unknown error' ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function query( $sql, $fname = __METHOD__, $tempIgnore = false ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function reportQueryError( $error, $errno, $sql, $fname, $tempIgnore = false ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function freeResult( $res ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function selectField(
-               $table, $var, $cond = '', $fname = __METHOD__, $options = []
-       ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function selectFieldValues(
-               $table, $var, $cond = '', $fname = __METHOD__, $options = []
-       ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function select(
-               $table, $vars, $conds = '', $fname = __METHOD__,
-               $options = [], $join_conds = []
-       ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function selectSQLText(
-               $table, $vars, $conds = '', $fname = __METHOD__,
-               $options = [], $join_conds = []
-       ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function selectRow(
-               $table, $vars, $conds, $fname = __METHOD__,
-               $options = [], $join_conds = []
-       ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function estimateRowCount(
-               $table, $vars = '*', $conds = '', $fname = __METHOD__, $options = []
-       ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function selectRowCount(
-               $tables, $vars = '*', $conds = '', $fname = __METHOD__, $options = [], $join_conds = []
-       ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function fieldExists( $table, $field, $fname = __METHOD__ ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function indexExists( $table, $index, $fname = __METHOD__ ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function tableExists( $table, $fname = __METHOD__ ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function indexUnique( $table, $index ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function insert( $table, $a, $fname = __METHOD__, $options = [] ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function update( $table, $values, $conds, $fname = __METHOD__, $options = [] ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function makeList( $a, $mode = LIST_COMMA ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function makeWhereFrom2d( $data, $baseKey, $subKey ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function bitNot( $field ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function bitAnd( $fieldLeft, $fieldRight ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function bitOr( $fieldLeft, $fieldRight ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function buildConcat( $stringList ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function buildGroupConcatField(
-               $delim, $table, $field, $conds = '', $join_conds = []
-       ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function selectDB( $db ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function getDBname() {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function getServer() {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function addQuotes( $s ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function buildLike() {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function anyChar() {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function anyString() {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function nextSequenceValue( $seqName ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function replace( $table, $uniqueIndexes, $rows, $fname = __METHOD__ ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function upsert(
-               $table, array $rows, array $uniqueIndexes, array $set, $fname = __METHOD__
-       ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function deleteJoin(
-               $delTable, $joinTable, $delVar, $joinVar, $conds, $fname = __METHOD__
-       ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function delete( $table, $conds, $fname = __METHOD__ ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function insertSelect(
-               $destTable, $srcTable, $varMap, $conds,
-               $fname = __METHOD__, $insertOptions = [], $selectOptions = []
-       ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function unionSupportsOrderAndLimit() {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function unionQueries( $sqls, $all ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function conditional( $cond, $trueVal, $falseVal ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function strreplace( $orig, $old, $new ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function getServerUptime() {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function wasDeadlock() {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function wasLockTimeout() {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function wasErrorReissuable() {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function wasReadOnlyError() {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function masterPosWait( DBMasterPos $pos, $timeout ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function getSlavePos() {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function getMasterPos() {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function serverIsReadOnly() {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function onTransactionResolution( callable $callback ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function onTransactionIdle( callable $callback ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function onTransactionPreCommitOrIdle( callable $callback ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function setTransactionListener( $name, callable $callback = null ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function startAtomic( $fname = __METHOD__ ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function endAtomic( $fname = __METHOD__ ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function doAtomicSection( $fname, callable $callback ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function begin( $fname = __METHOD__, $mode = IDatabase::TRANSACTION_EXPLICIT ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function commit( $fname = __METHOD__, $flush = '' ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function rollback( $fname = __METHOD__, $flush = '' ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function flushSnapshot( $fname = __METHOD__ ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function listTables( $prefix = null, $fname = __METHOD__ ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function timestamp( $ts = 0 ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function timestampOrNull( $ts = null ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function ping( &$rtt = null ) {
-               return func_num_args()
-                       ? $this->__call( __FUNCTION__, [ &$rtt ] )
-                       : $this->__call( __FUNCTION__, [] ); // method cares about null vs missing
-       }
-
-       public function getLag() {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function getSessionLagStatus() {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function maxListLen() {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function encodeBlob( $b ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function decodeBlob( $b ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function setSessionOptions( array $options ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function setSchemaVars( $vars ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function lockIsFree( $lockName, $method ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function lock( $lockName, $method, $timeout = 5 ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function unlock( $lockName, $method ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function getScopedLockAndFlush( $lockKey, $fname, $timeout ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function namedLocksEnqueue() {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function getInfinity() {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function encodeExpiry( $expiry ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function decodeExpiry( $expiry, $format = TS_MW ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function setBigSelects( $value = true ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function isReadOnly() {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       /**
-        * Clean up the connection when out of scope
-        */
-       function __destruct() {
-               if ( $this->conn !== null ) {
-                       $this->lb->reuseConnection( $this->conn );
-               }
-       }
-}
diff --git a/includes/libs/rdbms/chronologyprotector/ChronologyProtector.php b/includes/libs/rdbms/chronologyprotector/ChronologyProtector.php
new file mode 100644 (file)
index 0000000..09b820b
--- /dev/null
@@ -0,0 +1,326 @@
+<?php
+/**
+ * Generator of database load balancing objects.
+ *
+ * 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;
+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 implements LoggerAwareInterface{
+       /** @var BagOStuff */
+       protected $store;
+       /** @var LoggerInterface */
+       protected $logger;
+
+       /** @var string Storage key name */
+       protected $key;
+       /** @var string Hash of client parameters */
+       protected $clientId;
+       /** @var float|null Minimum UNIX timestamp of 1+ expected startup positions */
+       protected $waitForPosTime;
+       /** @var int Max seconds to wait on positions to appear */
+       protected $waitForPosTimeout = self::POS_WAIT_TIMEOUT;
+       /** @var bool Whether to no-op all method calls */
+       protected $enabled = true;
+       /** @var bool Whether to check and wait on positions */
+       protected $wait = true;
+
+       /** @var bool Whether the client data was loaded */
+       protected $initialized = false;
+       /** @var DBMasterPos[] Map of (DB master name => position) */
+       protected $startupPositions = [];
+       /** @var DBMasterPos[] Map of (DB master name => position) */
+       protected $shutdownPositions = [];
+       /** @var float[] Map of (DB master name => 1) */
+       protected $shutdownTouchDBs = [];
+
+       /** @var integer Seconds to store positions */
+       const POSITION_TTL = 60;
+       /** @var integer Max time to wait for positions to appear */
+       const POS_WAIT_TIMEOUT = 5;
+
+       /**
+        * @param BagOStuff $store
+        * @param array $client Map of (ip: <IP>, agent: <user-agent>)
+        * @param float $posTime UNIX timestamp
+        * @since 1.27
+        */
+       public function __construct( BagOStuff $store, array $client, $posTime = null ) {
+               $this->store = $store;
+               $this->clientId = md5( $client['ip'] . "\n" . $client['agent'] );
+               $this->key = $store->makeGlobalKey( __CLASS__, $this->clientId );
+               $this->waitForPosTime = $posTime;
+               $this->logger = new \Psr\Log\NullLogger();
+       }
+
+       public function setLogger( LoggerInterface $logger ) {
+               $this->logger = $logger;
+       }
+
+       /**
+        * @param bool $enabled Whether to no-op all method calls
+        * @since 1.27
+        */
+       public function setEnabled( $enabled ) {
+               $this->enabled = $enabled;
+       }
+
+       /**
+        * @param bool $enabled Whether to check and wait on positions
+        * @since 1.27
+        */
+       public function setWaitEnabled( $enabled ) {
+               $this->wait = $enabled;
+       }
+
+       /**
+        * Initialise a LoadBalancer to give it appropriate chronology protection.
+        *
+        * If the stash has a previous master position recorded, this will try to
+        * make sure that the next query to a replica DB of that master will see changes up
+        * to that position by delaying execution. The delay may timeout and allow stale
+        * data if no non-lagged replica DBs are available.
+        *
+        * @param LoadBalancer $lb
+        * @return void
+        */
+       public function initLB( LoadBalancer $lb ) {
+               if ( !$this->enabled || $lb->getServerCount() <= 1 ) {
+                       return; // non-replicated setup or disabled
+               }
+
+               $this->initPositions();
+
+               $masterName = $lb->getServerName( $lb->getWriterIndex() );
+               if ( !empty( $this->startupPositions[$masterName] ) ) {
+                       $pos = $this->startupPositions[$masterName];
+                       $this->logger->info( __METHOD__ . ": LB for '$masterName' set to pos $pos\n" );
+                       $lb->waitFor( $pos );
+               }
+       }
+
+       /**
+        * Notify the ChronologyProtector that the LoadBalancer is about to shut
+        * down. Saves replication positions.
+        *
+        * @param LoadBalancer $lb
+        * @return void
+        */
+       public function shutdownLB( LoadBalancer $lb ) {
+               if ( !$this->enabled ) {
+                       return; // not enabled
+               } elseif ( !$lb->hasOrMadeRecentMasterChanges( INF ) ) {
+                       // Only save the position if writes have been done on the connection
+                       return;
+               }
+
+               $masterName = $lb->getServerName( $lb->getWriterIndex() );
+               if ( $lb->getServerCount() > 1 ) {
+                       $pos = $lb->getMasterPos();
+                       $this->logger->info( __METHOD__ . ": LB for '$masterName' has pos $pos\n" );
+                       $this->shutdownPositions[$masterName] = $pos;
+               } else {
+                       $this->logger->info( __METHOD__ . ": DB '$masterName' touched\n" );
+               }
+               $this->shutdownTouchDBs[$masterName] = 1;
+       }
+
+       /**
+        * Notify the ChronologyProtector that the LBFactory is done calling shutdownLB() for now.
+        * May commit chronology data to persistent storage.
+        *
+        * @param callable|null $workCallback Work to do instead of waiting on syncing positions
+        * @param string $mode One of (sync, async); whether to wait on remote datacenters
+        * @return DBMasterPos[] Empty on success; returns the (db name => position) map on failure
+        */
+       public function shutdown( callable $workCallback = null, $mode = 'sync' ) {
+               if ( !$this->enabled ) {
+                       return [];
+               }
+
+               $store = $this->store;
+               // Some callers might want to know if a user recently touched a DB.
+               // These writes do not need to block on all datacenters receiving them.
+               foreach ( $this->shutdownTouchDBs as $dbName => $unused ) {
+                       $store->set(
+                               $this->getTouchedKey( $this->store, $dbName ),
+                               microtime( true ),
+                               $store::TTL_DAY
+                       );
+               }
+
+               if ( !count( $this->shutdownPositions ) ) {
+                       return []; // nothing to save
+               }
+
+               $this->logger->info( __METHOD__ . ": saving master pos for " .
+                       implode( ', ', array_keys( $this->shutdownPositions ) ) . "\n"
+               );
+
+               // CP-protected writes should overwhemingly go to the master datacenter, so get DC-local
+               // lock to merge the values. Use a DC-local get() and a synchronous all-DC set(). This
+               // makes it possible for the BagOStuff class to write in parallel to all DCs with one RTT.
+               if ( $store->lock( $this->key, 3 ) ) {
+                       if ( $workCallback ) {
+                               // Let the store run the work before blocking on a replication sync barrier. By the
+                               // time it's done with the work, the barrier should be fast if replication caught up.
+                               $store->addBusyCallback( $workCallback );
+                       }
+                       $ok = $store->set(
+                               $this->key,
+                               self::mergePositions( $store->get( $this->key ), $this->shutdownPositions ),
+                               self::POSITION_TTL,
+                               ( $mode === 'sync' ) ? $store::WRITE_SYNC : 0
+                       );
+                       $store->unlock( $this->key );
+               } else {
+                       $ok = false;
+               }
+
+               if ( !$ok ) {
+                       $bouncedPositions = $this->shutdownPositions;
+                       // Raced out too many times or stash is down
+                       $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
+                       $this->logger->info( __METHOD__ . ": store may not support synchronous writes." );
+                       $bouncedPositions = $this->shutdownPositions;
+               } else {
+                       $bouncedPositions = [];
+               }
+
+               return $bouncedPositions;
+       }
+
+       /**
+        * @param string $dbName DB master name (e.g. "db1052")
+        * @return float|bool UNIX timestamp when client last touched the DB; false if not on record
+        * @since 1.28
+        */
+       public function getTouched( $dbName ) {
+               return $this->store->get( $this->getTouchedKey( $this->store, $dbName ) );
+       }
+
+       /**
+        * @param BagOStuff $store
+        * @param string $dbName
+        * @return string
+        */
+       private function getTouchedKey( BagOStuff $store, $dbName ) {
+               return $store->makeGlobalKey( __CLASS__, 'mtime', $this->clientId, $dbName );
+       }
+
+       /**
+        * Load in previous master positions for the client
+        */
+       protected function initPositions() {
+               if ( $this->initialized ) {
+                       return;
+               }
+
+               $this->initialized = true;
+               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 > 0.0 ) {
+                               $data = null;
+                               $loop = new WaitConditionLoop(
+                                       function () use ( &$data ) {
+                                               $data = $this->store->get( $this->key );
+
+                                               return ( self::minPosTime( $data ) >= $this->waitForPosTime )
+                                                       ? WaitConditionLoop::CONDITION_REACHED
+                                                       : WaitConditionLoop::CONDITION_CONTINUE;
+                                       },
+                                       $this->waitForPosTimeout
+                               );
+                               $result = $loop->invoke();
+                               $waitedMs = $loop->getLastWaitTime() * 1e3;
+
+                               if ( $result == $loop::CONDITION_REACHED ) {
+                                       $msg = "expected and found pos time {$this->waitForPosTime} ({$waitedMs}ms)";
+                                       $this->logger->debug( $msg );
+                               } else {
+                                       $msg = "expected but missed pos time {$this->waitForPosTime} ({$waitedMs}ms)";
+                                       $this->logger->info( $msg );
+                               }
+                       } else {
+                               $data = $this->store->get( $this->key );
+                       }
+
+                       $this->startupPositions = $data ? $data['positions'] : [];
+                       $this->logger->info( __METHOD__ . ": key is {$this->key} (read)\n" );
+               } else {
+                       $this->startupPositions = [];
+                       $this->logger->info( __METHOD__ . ": key is {$this->key} (unread)\n" );
+               }
+       }
+
+       /**
+        * @param array|bool $data
+        * @return float|null
+        */
+       private static function minPosTime( $data ) {
+               if ( !isset( $data['positions'] ) ) {
+                       return null;
+               }
+
+               $min = null;
+               foreach ( $data['positions'] as $pos ) {
+                       /** @var DBMasterPos $pos */
+                       $min = $min ? min( $pos->asOfTime(), $min ) : $pos->asOfTime();
+               }
+
+               return $min;
+       }
+
+       /**
+        * @param array|bool $curValue
+        * @param DBMasterPos[] $shutdownPositions
+        * @return array
+        */
+       private static function mergePositions( $curValue, array $shutdownPositions ) {
+               /** @var $curPositions DBMasterPos[] */
+               if ( $curValue === false ) {
+                       $curPositions = $shutdownPositions;
+               } else {
+                       $curPositions = $curValue['positions'];
+                       // Use the newest positions for each DB master
+                       foreach ( $shutdownPositions as $db => $pos ) {
+                               if ( !isset( $curPositions[$db] )
+                                       || $pos->asOfTime() > $curPositions[$db]->asOfTime()
+                               ) {
+                                       $curPositions[$db] = $pos;
+                               }
+                       }
+               }
+
+               return [ 'positions' => $curPositions ];
+       }
+}
diff --git a/includes/libs/rdbms/database/RBConnRef.php b/includes/libs/rdbms/database/RBConnRef.php
new file mode 100644 (file)
index 0000000..e606340
--- /dev/null
@@ -0,0 +1,581 @@
+<?php
+/**
+ * Helper class to handle automatically marking connections as reusable (via RAII pattern)
+ * as well handling deferring the actual network connection until the handle is used
+ *
+ * @note: proxy methods are defined explicity to avoid interface errors
+ * @ingroup Database
+ * @since 1.22
+ */
+class DBConnRef implements IDatabase {
+       /** @var ILoadBalancer */
+       private $lb;
+
+       /** @var IDatabase|null Live connection handle */
+       private $conn;
+
+       /** @var array|null */
+       private $params;
+
+       const FLD_INDEX = 0;
+       const FLD_GROUP = 1;
+       const FLD_WIKI = 2;
+
+       /**
+        * @param ILoadBalancer $lb
+        * @param IDatabase|array $conn Connection or (server index, group, wiki ID)
+        */
+       public function __construct( ILoadBalancer $lb, $conn ) {
+               $this->lb = $lb;
+               if ( $conn instanceof IDatabase ) {
+                       $this->conn = $conn; // live handle
+               } elseif ( count( $conn ) >= 3 && $conn[self::FLD_WIKI] !== false ) {
+                       $this->params = $conn;
+               } else {
+                       throw new InvalidArgumentException( "Missing lazy connection arguments." );
+               }
+       }
+
+       function __call( $name, array $arguments ) {
+               if ( $this->conn === null ) {
+                       list( $db, $groups, $wiki ) = $this->params;
+                       $this->conn = $this->lb->getConnection( $db, $groups, $wiki );
+               }
+
+               return call_user_func_array( [ $this->conn, $name ], $arguments );
+       }
+
+       public function getServerInfo() {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function bufferResults( $buffer = null ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function trxLevel() {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function trxTimestamp() {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function explicitTrxActive() {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function tablePrefix( $prefix = null ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function dbSchema( $schema = null ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function getLBInfo( $name = null ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function setLBInfo( $name, $value = null ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function implicitGroupby() {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function implicitOrderby() {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function lastQuery() {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function doneWrites() {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function lastDoneWrites() {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function writesPending() {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function writesOrCallbacksPending() {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function pendingWriteQueryDuration( $type = self::ESTIMATE_TOTAL ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function pendingWriteCallers() {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function isOpen() {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function setFlag( $flag, $remember = self::REMEMBER_NOTHING ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function clearFlag( $flag, $remember = self::REMEMBER_NOTHING ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function restoreFlags( $state = self::RESTORE_PRIOR ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function getFlag( $flag ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function getProperty( $name ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function getWikiID() {
+               if ( $this->conn === null ) {
+                       // Avoid triggering a connection
+                       return $this->params[self::FLD_WIKI];
+               }
+
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function getType() {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function open( $server, $user, $password, $dbName ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function fetchObject( $res ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function fetchRow( $res ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function numRows( $res ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function numFields( $res ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function fieldName( $res, $n ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function insertId() {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function dataSeek( $res, $row ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function lastErrno() {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function lastError() {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function fieldInfo( $table, $field ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function affectedRows() {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function getSoftwareLink() {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function getServerVersion() {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function close() {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function reportConnectionError( $error = 'Unknown error' ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function query( $sql, $fname = __METHOD__, $tempIgnore = false ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function reportQueryError( $error, $errno, $sql, $fname, $tempIgnore = false ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function freeResult( $res ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function selectField(
+               $table, $var, $cond = '', $fname = __METHOD__, $options = []
+       ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function selectFieldValues(
+               $table, $var, $cond = '', $fname = __METHOD__, $options = []
+       ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function select(
+               $table, $vars, $conds = '', $fname = __METHOD__,
+               $options = [], $join_conds = []
+       ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function selectSQLText(
+               $table, $vars, $conds = '', $fname = __METHOD__,
+               $options = [], $join_conds = []
+       ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function selectRow(
+               $table, $vars, $conds, $fname = __METHOD__,
+               $options = [], $join_conds = []
+       ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function estimateRowCount(
+               $table, $vars = '*', $conds = '', $fname = __METHOD__, $options = []
+       ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function selectRowCount(
+               $tables, $vars = '*', $conds = '', $fname = __METHOD__, $options = [], $join_conds = []
+       ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function fieldExists( $table, $field, $fname = __METHOD__ ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function indexExists( $table, $index, $fname = __METHOD__ ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function tableExists( $table, $fname = __METHOD__ ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function indexUnique( $table, $index ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function insert( $table, $a, $fname = __METHOD__, $options = [] ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function update( $table, $values, $conds, $fname = __METHOD__, $options = [] ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function makeList( $a, $mode = LIST_COMMA ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function makeWhereFrom2d( $data, $baseKey, $subKey ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function bitNot( $field ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function bitAnd( $fieldLeft, $fieldRight ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function bitOr( $fieldLeft, $fieldRight ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function buildConcat( $stringList ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function buildGroupConcatField(
+               $delim, $table, $field, $conds = '', $join_conds = []
+       ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function selectDB( $db ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function getDBname() {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function getServer() {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function addQuotes( $s ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function buildLike() {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function anyChar() {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function anyString() {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function nextSequenceValue( $seqName ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function replace( $table, $uniqueIndexes, $rows, $fname = __METHOD__ ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function upsert(
+               $table, array $rows, array $uniqueIndexes, array $set, $fname = __METHOD__
+       ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function deleteJoin(
+               $delTable, $joinTable, $delVar, $joinVar, $conds, $fname = __METHOD__
+       ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function delete( $table, $conds, $fname = __METHOD__ ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function insertSelect(
+               $destTable, $srcTable, $varMap, $conds,
+               $fname = __METHOD__, $insertOptions = [], $selectOptions = []
+       ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function unionSupportsOrderAndLimit() {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function unionQueries( $sqls, $all ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function conditional( $cond, $trueVal, $falseVal ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function strreplace( $orig, $old, $new ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function getServerUptime() {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function wasDeadlock() {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function wasLockTimeout() {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function wasErrorReissuable() {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function wasReadOnlyError() {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function masterPosWait( DBMasterPos $pos, $timeout ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function getSlavePos() {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function getMasterPos() {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function serverIsReadOnly() {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function onTransactionResolution( callable $callback ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function onTransactionIdle( callable $callback ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function onTransactionPreCommitOrIdle( callable $callback ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function setTransactionListener( $name, callable $callback = null ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function startAtomic( $fname = __METHOD__ ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function endAtomic( $fname = __METHOD__ ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function doAtomicSection( $fname, callable $callback ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function begin( $fname = __METHOD__, $mode = IDatabase::TRANSACTION_EXPLICIT ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function commit( $fname = __METHOD__, $flush = '' ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function rollback( $fname = __METHOD__, $flush = '' ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function flushSnapshot( $fname = __METHOD__ ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function listTables( $prefix = null, $fname = __METHOD__ ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function timestamp( $ts = 0 ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function timestampOrNull( $ts = null ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function ping( &$rtt = null ) {
+               return func_num_args()
+                       ? $this->__call( __FUNCTION__, [ &$rtt ] )
+                       : $this->__call( __FUNCTION__, [] ); // method cares about null vs missing
+       }
+
+       public function getLag() {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function getSessionLagStatus() {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function maxListLen() {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function encodeBlob( $b ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function decodeBlob( $b ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function setSessionOptions( array $options ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function setSchemaVars( $vars ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function lockIsFree( $lockName, $method ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function lock( $lockName, $method, $timeout = 5 ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function unlock( $lockName, $method ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function getScopedLockAndFlush( $lockKey, $fname, $timeout ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function namedLocksEnqueue() {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function getInfinity() {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function encodeExpiry( $expiry ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function decodeExpiry( $expiry, $format = TS_MW ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function setBigSelects( $value = true ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function isReadOnly() {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       /**
+        * Clean up the connection when out of scope
+        */
+       function __destruct() {
+               if ( $this->conn !== null ) {
+                       $this->lb->reuseConnection( $this->conn );
+               }
+       }
+}