Aaron Schulz has uploaded a new change for review.

  https://gerrit.wikimedia.org/r/310536

Change subject: Rename LBFactory => LBFactoryMW and make LBFactory in /libs
......................................................................

Rename LBFactory => LBFactoryMW and make LBFactory in /libs

The former extends the later with MW-specific logic.

Also removed a wf* method call from ChronologyProtector.

Change-Id: I325f59b7467ab9c2137731d1ce69816f5a020f03
---
M autoload.php
M includes/ServiceWiring.php
M includes/db/ChronologyProtector.php
M includes/db/loadbalancer/LBFactory.php
M includes/db/loadbalancer/LBFactoryFake.php
M includes/db/loadbalancer/LBFactoryMulti.php
M includes/db/loadbalancer/LBFactorySimple.php
M includes/db/loadbalancer/LBFactorySingle.php
A includes/libs/rdbms/lbfactory/LBFactory.php
9 files changed, 645 insertions(+), 549 deletions(-)


  git pull ssh://gerrit.wikimedia.org:29418/mediawiki/core 
refs/changes/36/310536/1

diff --git a/autoload.php b/autoload.php
index 4b5d9e8..d2c3f27 100644
--- a/autoload.php
+++ b/autoload.php
@@ -654,6 +654,7 @@
        'KuConverter' => __DIR__ . '/languages/classes/LanguageKu.php',
        'LBFactory' => __DIR__ . '/includes/db/loadbalancer/LBFactory.php',
        'LBFactoryFake' => __DIR__ . 
'/includes/db/loadbalancer/LBFactoryFake.php',
+       'LBFactoryMW' => __DIR__ . 
'/includes/libs/rdbms/lbfactory/LBFactory.php',
        'LBFactoryMulti' => __DIR__ . 
'/includes/db/loadbalancer/LBFactoryMulti.php',
        'LBFactorySimple' => __DIR__ . 
'/includes/db/loadbalancer/LBFactorySimple.php',
        'LBFactorySingle' => __DIR__ . 
'/includes/db/loadbalancer/LBFactorySingle.php',
diff --git a/includes/ServiceWiring.php b/includes/ServiceWiring.php
index 8734bd6..4ab412e 100644
--- a/includes/ServiceWiring.php
+++ b/includes/ServiceWiring.php
@@ -45,7 +45,7 @@
        'DBLoadBalancerFactory' => function( MediaWikiServices $services ) {
                $config = $services->getMainConfig()->get( 'LBFactoryConf' );
 
-               $class = LBFactory::getLBFactoryClass( $config );
+               $class = LBFactoryMW::getLBFactoryClass( $config );
                if ( !isset( $config['readOnlyReason'] ) ) {
                        // TODO: replace the global 
wfConfiguredReadOnlyReason() with a service.
                        $config['readOnlyReason'] = 
wfConfiguredReadOnlyReason();
diff --git a/includes/db/ChronologyProtector.php 
b/includes/db/ChronologyProtector.php
index 4d03bc6..f37006c 100644
--- a/includes/db/ChronologyProtector.php
+++ b/includes/db/ChronologyProtector.php
@@ -96,17 +96,17 @@
        }
 
        /**
-        * Initialise a LoadBalancer to give it appropriate chronology 
protection.
+        * Initialise a ILoadBalancer 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
+        * @param ILoadBalancer $lb
         * @return void
         */
-       public function initLB( LoadBalancer $lb ) {
+       public function initLB( ILoadBalancer $lb ) {
                if ( !$this->enabled || $lb->getServerCount() <= 1 ) {
                        return; // non-replicated setup or disabled
                }
@@ -122,13 +122,13 @@
        }
 
        /**
-        * Notify the ChronologyProtector that the LoadBalancer is about to shut
+        * Notify the ChronologyProtector that the ILoadBalancer is about to 
shut
         * down. Saves replication positions.
         *
-        * @param LoadBalancer $lb
+        * @param ILoadBalancer $lb
         * @return void
         */
-       public function shutdownLB( LoadBalancer $lb ) {
+       public function shutdownLB( ILoadBalancer $lb ) {
                if ( !$this->enabled ) {
                        return; // not enabled
                } elseif ( !$lb->hasOrMadeRecentMasterChanges( INF ) ) {
@@ -265,10 +265,11 @@
 
                                if ( $result == $loop::CONDITION_REACHED ) {
                                        $msg = "expected and found pos time 
{$this->waitForPosTime} ({$waitedMs}ms)";
+                                       $this->logger->info( $msg );
                                } else {
                                        $msg = "expected but missed pos time 
{$this->waitForPosTime} ({$waitedMs}ms)";
+                                       $this->logger->warning( $msg );
                                }
-                               wfDebugLog( 'replication', $msg );
                        } else {
                                $data = $this->store->get( $this->key );
                        }
diff --git a/includes/db/loadbalancer/LBFactory.php 
b/includes/db/loadbalancer/LBFactory.php
index 5115fbe..04c1764 100644
--- a/includes/db/loadbalancer/LBFactory.php
+++ b/includes/db/loadbalancer/LBFactory.php
@@ -21,7 +21,6 @@
  * @ingroup Database
  */
 
-use Psr\Log\LoggerInterface;
 use MediaWiki\MediaWikiServices;
 use MediaWiki\Services\DestructibleService;
 use MediaWiki\Logger\LoggerFactory;
@@ -30,35 +29,8 @@
  * An interface for generating database load balancers
  * @ingroup Database
  */
-abstract class LBFactory implements DestructibleService {
-       /** @var ChronologyProtector */
-       protected $chronProt;
-       /** @var TransactionProfiler */
-       protected $trxProfiler;
-       /** @var LoggerInterface */
-       protected $trxLogger;
-       /** @var LoggerInterface */
-       protected $replLogger;
-       /** @var BagOStuff */
-       protected $srvCache;
-       /** @var BagOStuff */
-       protected $memCache;
-       /** @var WANObjectCache */
-       protected $wanCache;
-
-       /** @var mixed */
-       protected $ticket;
-       /** @var string|bool String if a requested DBO_TRX transaction round is 
active */
-       protected $trxRoundId = false;
-       /** @var string|bool Reason all LBs are read-only or false if not */
-       protected $readOnlyReason = false;
-       /** @var callable[] */
-       protected $replicationWaitCallbacks = [];
-
-       const SHUTDOWN_NO_CHRONPROT = 0; // don't save DB positions at all
-       const SHUTDOWN_CHRONPROT_ASYNC = 1; // save DB positions, but don't 
wait on remote DCs
-       const SHUTDOWN_CHRONPROT_SYNC = 2; // save DB positions, waiting on all 
DCs
-
+abstract class LBFactoryMW extends LBFactory implements DestructibleService {
+       /** @noinspection PhpMissingParentConstructorInspection */
        /**
         * Construct a factory based on a configuration array (typically from 
$wgLBFactoryConf)
         * @param array $conf
@@ -95,18 +67,10 @@
        }
 
        /**
-        * Disables all load balancers. All connections are closed, and any 
attempt to
-        * open a new connection will result in a DBAccessError.
-        * @see LoadBalancer::disable()
-        */
-       public function destroy() {
-               $this->shutdown( self::SHUTDOWN_NO_CHRONPROT );
-               $this->forEachLBCallMethod( 'disable' );
-       }
-
-       /**
         * Disables all access to the load balancer, will cause all database 
access
         * to throw a DBAccessError
+        *
+        * @deprecated since 1.28, Use 
MediaWikiServices::disableStorageBackend()
         */
        public static function disableBackend() {
                MediaWikiServices::disableStorageBackend();
@@ -164,238 +128,6 @@
        }
 
        /**
-        * Create a new load balancer object. The resulting object will be 
untracked,
-        * not chronology-protected, and the caller is responsible for cleaning 
it up.
-        *
-        * @param bool|string $wiki Wiki ID, or false for the current wiki
-        * @return LoadBalancer
-        */
-       abstract public function newMainLB( $wiki = false );
-
-       /**
-        * Get a cached (tracked) load balancer object.
-        *
-        * @param bool|string $wiki Wiki ID, or false for the current wiki
-        * @return LoadBalancer
-        */
-       abstract public function getMainLB( $wiki = false );
-
-       /**
-        * Create a new load balancer for external storage. The resulting 
object will be
-        * untracked, not chronology-protected, and the caller is responsible 
for
-        * cleaning it up.
-        *
-        * @param string $cluster External storage cluster, or false for core
-        * @param bool|string $wiki Wiki ID, or false for the current wiki
-        * @return LoadBalancer
-        */
-       abstract protected function newExternalLB( $cluster, $wiki = false );
-
-       /**
-        * Get a cached (tracked) load balancer for external storage
-        *
-        * @param string $cluster External storage cluster, or false for core
-        * @param bool|string $wiki Wiki ID, or false for the current wiki
-        * @return LoadBalancer
-        */
-       abstract public function getExternalLB( $cluster, $wiki = false );
-
-       /**
-        * Execute a function for each tracked load balancer
-        * The callback is called with the load balancer as the first parameter,
-        * and $params passed as the subsequent parameters.
-        *
-        * @param callable $callback
-        * @param array $params
-        */
-       abstract public function forEachLB( $callback, array $params = [] );
-
-       /**
-        * Prepare all tracked load balancers for shutdown
-        * @param integer $mode One of the class SHUTDOWN_* constants
-        * @param callable|null $workCallback Work to mask ChronologyProtector 
writes
-        */
-       public function shutdown(
-               $mode = self::SHUTDOWN_CHRONPROT_SYNC, callable $workCallback = 
null
-       ) {
-               if ( $mode === self::SHUTDOWN_CHRONPROT_SYNC ) {
-                       $this->shutdownChronologyProtector( $this->chronProt, 
$workCallback, 'sync' );
-               } elseif ( $mode === self::SHUTDOWN_CHRONPROT_ASYNC ) {
-                       $this->shutdownChronologyProtector( $this->chronProt, 
null, 'async' );
-               }
-
-               $this->commitMasterChanges( __METHOD__ ); // sanity
-       }
-
-       /**
-        * Call a method of each tracked load balancer
-        *
-        * @param string $methodName
-        * @param array $args
-        */
-       private function forEachLBCallMethod( $methodName, array $args = [] ) {
-               $this->forEachLB(
-                       function ( LoadBalancer $loadBalancer, $methodName, 
array $args ) {
-                               call_user_func_array( [ $loadBalancer, 
$methodName ], $args );
-                       },
-                       [ $methodName, $args ]
-               );
-       }
-
-       /**
-        * Commit all replica DB transactions so as to flush any 
REPEATABLE-READ or SSI snapshot
-        *
-        * @param string $fname Caller name
-        * @since 1.28
-        */
-       public function flushReplicaSnapshots( $fname = __METHOD__ ) {
-               $this->forEachLBCallMethod( 'flushReplicaSnapshots', [ $fname ] 
);
-       }
-
-       /**
-        * Commit on all connections. Done for two reasons:
-        * 1. To commit changes to the masters.
-        * 2. To release the snapshot on all connections, master and replica DB.
-        * @param string $fname Caller name
-        * @param array $options Options map:
-        *   - maxWriteDuration: abort if more than this much time was spent in 
write queries
-        */
-       public function commitAll( $fname = __METHOD__, array $options = [] ) {
-               $this->commitMasterChanges( $fname, $options );
-               $this->forEachLBCallMethod( 'commitAll', [ $fname ] );
-       }
-
-       /**
-        * 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 DBTransactionError
-        * @since 1.28
-        */
-       public function beginMasterChanges( $fname = __METHOD__ ) {
-               if ( $this->trxRoundId !== false ) {
-                       throw new DBTransactionError(
-                               null,
-                               "$fname: transaction round 
'{$this->trxRoundId}' already started."
-                       );
-               }
-               $this->trxRoundId = $fname;
-               // Set DBO_TRX flags on all appropriate DBs
-               $this->forEachLBCallMethod( 'beginMasterChanges', [ $fname ] );
-       }
-
-       /**
-        * Commit changes on all master connections
-        * @param string $fname Caller name
-        * @param array $options Options map:
-        *   - maxWriteDuration: abort if more than this much time was spent in 
write queries
-        * @throws Exception
-        */
-       public function commitMasterChanges( $fname = __METHOD__, array 
$options = [] ) {
-               if ( $this->trxRoundId !== false && $this->trxRoundId !== 
$fname ) {
-                       throw new DBTransactionError(
-                               null,
-                               "$fname: transaction round 
'{$this->trxRoundId}' still running."
-                       );
-               }
-               // Run pre-commit callbacks and suppress post-commit callbacks, 
aborting on failure
-               $this->forEachLBCallMethod( 'finalizeMasterChanges' );
-               $this->trxRoundId = false;
-               // Perform pre-commit checks, aborting on failure
-               $this->forEachLBCallMethod( 'approveMasterChanges', [ $options 
] );
-               // Log the DBs and methods involved in multi-DB transactions
-               $this->logIfMultiDbTransaction();
-               // Actually perform the commit on all master DB connections and 
revert DBO_TRX
-               $this->forEachLBCallMethod( 'commitMasterChanges', [ $fname ] );
-               // Run all post-commit callbacks
-               /** @var Exception $e */
-               $e = null; // first callback exception
-               $this->forEachLB( function ( LoadBalancer $lb ) use ( &$e ) {
-                       $ex = $lb->runMasterPostTrxCallbacks( 
IDatabase::TRIGGER_COMMIT );
-                       $e = $e ?: $ex;
-               } );
-               // Commit any dangling DBO_TRX transactions from callbacks on 
one DB to another DB
-               $this->forEachLBCallMethod( 'commitMasterChanges', [ $fname ] );
-               // Throw any last post-commit callback error
-               if ( $e instanceof Exception ) {
-                       throw $e;
-               }
-       }
-
-       /**
-        * Rollback changes on all master connections
-        * @param string $fname Caller name
-        * @since 1.23
-        */
-       public function rollbackMasterChanges( $fname = __METHOD__ ) {
-               $this->trxRoundId = false;
-               $this->forEachLBCallMethod( 'suppressTransactionEndCallbacks' );
-               $this->forEachLBCallMethod( 'rollbackMasterChanges', [ $fname ] 
);
-               // Run all post-rollback callbacks
-               $this->forEachLB( function ( LoadBalancer $lb ) {
-                       $lb->runMasterPostTrxCallbacks( 
IDatabase::TRIGGER_ROLLBACK );
-               } );
-       }
-
-       /**
-        * Log query info if multi DB transactions are going to be committed now
-        */
-       private function logIfMultiDbTransaction() {
-               $callersByDB = [];
-               $this->forEachLB( function ( LoadBalancer $lb ) use ( 
&$callersByDB ) {
-                       $masterName = $lb->getServerName( $lb->getWriterIndex() 
);
-                       $callers = $lb->pendingMasterChangeCallers();
-                       if ( $callers ) {
-                               $callersByDB[$masterName] = $callers;
-                       }
-               } );
-
-               if ( count( $callersByDB ) >= 2 ) {
-                       $dbs = implode( ', ', array_keys( $callersByDB ) );
-                       $msg = "Multi-DB transaction [{$dbs}]:\n";
-                       foreach ( $callersByDB as $db => $callers ) {
-                               $msg .= "$db: " . implode( '; ', $callers ) . 
"\n";
-                       }
-                       $this->trxLogger->info( $msg );
-               }
-       }
-
-       /**
-        * Determine if any master connection has pending changes
-        * @return bool
-        * @since 1.23
-        */
-       public function hasMasterChanges() {
-               $ret = false;
-               $this->forEachLB( function ( LoadBalancer $lb ) use ( &$ret ) {
-                       $ret = $ret || $lb->hasMasterChanges();
-               } );
-
-               return $ret;
-       }
-
-       /**
-        * Detemine if any lagged replica DB connection was used
-        * @return bool
-        * @since 1.28
-        */
-       public function laggedReplicaUsed() {
-               $ret = false;
-               $this->forEachLB( function ( LoadBalancer $lb ) use ( &$ret ) {
-                       $ret = $ret || $lb->laggedReplicaUsed();
-               } );
-
-               return $ret;
-       }
-
-       /**
         * @return bool
         * @since 1.27
         * @deprecated Since 1.28; use laggedReplicaUsed()
@@ -404,202 +136,6 @@
                return $this->laggedReplicaUsed();
        }
 
-       /**
-        * Determine if any master connection has pending/written changes from 
this request
-        * @param float $age How many seconds ago is "recent" [defaults to LB 
lag wait timeout]
-        * @return bool
-        * @since 1.27
-        */
-       public function hasOrMadeRecentMasterChanges( $age = null ) {
-               $ret = false;
-               $this->forEachLB( function ( LoadBalancer $lb ) use ( $age, 
&$ret ) {
-                       $ret = $ret || $lb->hasOrMadeRecentMasterChanges( $age 
);
-               } );
-               return $ret;
-       }
-
-       /**
-        * Waits for the replica DBs to catch up to the current master position
-        *
-        * Use this when updating very large numbers of rows, as in maintenance 
scripts,
-        * to avoid causing too much lag. Of course, this is a no-op if there 
are no replica DBs.
-        *
-        * By default this waits on all DB clusters actually used in this 
request.
-        * This makes sense when lag being waiting on is caused by the code 
that does this check.
-        * In that case, setting "ifWritesSince" can avoid the overhead of 
waiting for clusters
-        * that were not changed since the last wait check. To forcefully wait 
on a specific cluster
-        * for a given wiki, use the 'wiki' parameter. To forcefully wait on an 
"external" cluster,
-        * use the "cluster" parameter.
-        *
-        * Never call this function after a large DB write that is *still* in a 
transaction.
-        * It only makes sense to call this after the possible lag inducing 
changes were committed.
-        *
-        * @param array $opts Optional fields that include:
-        *   - wiki : wait on the load balancer DBs that handles the given wiki
-        *   - cluster : wait on the given external load balancer DBs
-        *   - timeout : Max wait time. Default: ~60 seconds
-        *   - ifWritesSince: Only wait if writes were done since this UNIX 
timestamp
-        * @throws DBReplicationWaitError If a timeout or error occured waiting 
on a DB cluster
-        * @since 1.27
-        */
-       public function waitForReplication( array $opts = [] ) {
-               $opts += [
-                       'wiki' => false,
-                       'cluster' => false,
-                       'timeout' => 60,
-                       'ifWritesSince' => null
-               ];
-
-               // Figure out which clusters need to be checked
-               /** @var LoadBalancer[] $lbs */
-               $lbs = [];
-               if ( $opts['cluster'] !== false ) {
-                       $lbs[] = $this->getExternalLB( $opts['cluster'] );
-               } elseif ( $opts['wiki'] !== false ) {
-                       $lbs[] = $this->getMainLB( $opts['wiki'] );
-               } else {
-                       $this->forEachLB( function ( LoadBalancer $lb ) use ( 
&$lbs ) {
-                               $lbs[] = $lb;
-                       } );
-                       if ( !$lbs ) {
-                               return; // nothing actually used
-                       }
-               }
-
-               // Get all the master positions of applicable DBs right now.
-               // This can be faster since waiting on one cluster reduces the
-               // time needed to wait on the next clusters.
-               $masterPositions = array_fill( 0, count( $lbs ), false );
-               foreach ( $lbs as $i => $lb ) {
-                       if ( $lb->getServerCount() <= 1 ) {
-                               // Bug 27975 - Don't try to wait for replica 
DBs if there are none
-                               // Prevents permission error when getting 
master position
-                               continue;
-                       } elseif ( $opts['ifWritesSince']
-                               && $lb->lastMasterChangeTimestamp() < 
$opts['ifWritesSince']
-                       ) {
-                               continue; // no writes since the last wait
-                       }
-                       $masterPositions[$i] = $lb->getMasterPos();
-               }
-
-               // Run any listener callbacks *after* getting the DB positions. 
The more
-               // time spent in the callbacks, the less time is spent in 
waitForAll().
-               foreach ( $this->replicationWaitCallbacks as $callback ) {
-                       $callback();
-               }
-
-               $failed = [];
-               foreach ( $lbs as $i => $lb ) {
-                       if ( $masterPositions[$i] ) {
-                               // The DBMS may not support getMasterPos() or 
the whole
-                               // load balancer might be fake (e.g. 
$wgAllDBsAreLocalhost).
-                               if ( !$lb->waitForAll( $masterPositions[$i], 
$opts['timeout'] ) ) {
-                                       $failed[] = $lb->getServerName( 
$lb->getWriterIndex() );
-                               }
-                       }
-               }
-
-               if ( $failed ) {
-                       throw new DBReplicationWaitError(
-                               "Could not wait for replica DBs to catch up to 
" .
-                               implode( ', ', $failed )
-                       );
-               }
-       }
-
-       /**
-        * Add a callback to be run in every call to waitForReplication() 
before waiting
-        *
-        * Callbacks must clear any transactions that they start
-        *
-        * @param string $name Callback name
-        * @param callable|null $callback Use null to unset a callback
-        * @since 1.28
-        */
-       public function setWaitForReplicationListener( $name, callable 
$callback = null ) {
-               if ( $callback ) {
-                       $this->replicationWaitCallbacks[$name] = $callback;
-               } else {
-                       unset( $this->replicationWaitCallbacks[$name] );
-               }
-       }
-
-       /**
-        * Get a token asserting that no transaction writes are active
-        *
-        * @param string $fname Caller name (e.g. __METHOD__)
-        * @return mixed A value to pass to commitAndWaitForReplication()
-        * @since 1.28
-        */
-       public function getEmptyTransactionTicket( $fname ) {
-               if ( $this->hasMasterChanges() ) {
-                       $this->trxLogger->error( __METHOD__ . ": $fname does 
not have outer scope." );
-                       return null;
-               }
-
-               return $this->ticket;
-       }
-
-       /**
-        * Convenience method for safely running 
commitMasterChanges()/waitForReplication()
-        *
-        * This will commit and wait unless $ticket indicates it is unsafe to 
do so
-        *
-        * @param string $fname Caller name (e.g. __METHOD__)
-        * @param mixed $ticket Result of getEmptyTransactionTicket()
-        * @param array $opts Options to waitForReplication()
-        * @throws DBReplicationWaitError
-        * @since 1.28
-        */
-       public function commitAndWaitForReplication( $fname, $ticket, array 
$opts = [] ) {
-               if ( $ticket !== $this->ticket ) {
-                       $logger = LoggerFactory::getInstance( 'DBPerformance' );
-                       $logger->error( __METHOD__ . ": cannot commit; $fname 
does not have outer scope." );
-                       return;
-               }
-
-               // The transaction owner and any caller with the empty 
transaction ticket can commit
-               // so that getEmptyTransactionTicket() callers don't risk 
seeing DBTransactionError.
-               if ( $this->trxRoundId !== false && $fname !== 
$this->trxRoundId ) {
-                       $this->trxLogger->info( "$fname: committing on behalf 
of {$this->trxRoundId}." );
-                       $fnameEffective = $this->trxRoundId;
-               } else {
-                       $fnameEffective = $fname;
-               }
-
-               $this->commitMasterChanges( $fnameEffective );
-               $this->waitForReplication( $opts );
-               // If a nested caller committed on behalf of $fname, start 
another empty $fname
-               // transaction, leaving the caller with the same empty 
transaction state as before.
-               if ( $fnameEffective !== $fname ) {
-                       $this->beginMasterChanges( $fnameEffective );
-               }
-       }
-
-       /**
-        * @param string $dbName DB master name (e.g. "db1052")
-        * @return float|bool UNIX timestamp when client last touched the DB or 
false if not recent
-        * @since 1.28
-        */
-       public function getChronologyProtectorTouched( $dbName ) {
-               return $this->chronProt->getTouched( $dbName );
-       }
-
-       /**
-        * Disable the ChronologyProtector for all load balancers
-        *
-        * This can be called at the start of special API entry points
-        *
-        * @since 1.27
-        */
-       public function disableChronologyProtection() {
-               $this->chronProt->setEnabled( false );
-       }
-
-       /**
-        * @return ChronologyProtector
-        */
        protected function newChronologyProtector() {
                $request = RequestContext::getMain()->getRequest();
                $chronProt = new ChronologyProtector(
@@ -619,67 +155,6 @@
                }
 
                return $chronProt;
-       }
-
-       /**
-        * Get and record all of the staged DB positions into persistent memory 
storage
-        *
-        * @param ChronologyProtector $cp
-        * @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
-        */
-       protected function shutdownChronologyProtector(
-               ChronologyProtector $cp, $workCallback, $mode
-       ) {
-               // Record all the master positions needed
-               $this->forEachLB( function ( LoadBalancer $lb ) use ( $cp ) {
-                       $cp->shutdownLB( $lb );
-               } );
-               // Write them to the persistent stash. Try to do something 
useful by running $work
-               // while ChronologyProtector waits for the stash write to 
replicate to all DCs.
-               $unsavedPositions = $cp->shutdown( $workCallback, $mode );
-               if ( $unsavedPositions && $workCallback ) {
-                       // Invoke callback in case it did not cache the result 
yet
-                       $workCallback(); // work now to block for less time in 
waitForAll()
-               }
-               // If the positions failed to write to the stash, at least wait 
on local datacenter
-               // replica DBs to catch up before responding. Even if there are 
several DCs, this increases
-               // the chance that the user will see their own changes 
immediately afterwards. As long
-               // as the sticky DC cookie applies (same domain), this is not 
even an issue.
-               $this->forEachLB( function ( LoadBalancer $lb ) use ( 
$unsavedPositions ) {
-                       $masterName = $lb->getServerName( $lb->getWriterIndex() 
);
-                       if ( isset( $unsavedPositions[$masterName] ) ) {
-                               $lb->waitForAll( $unsavedPositions[$masterName] 
);
-                       }
-               } );
-       }
-
-       /**
-        * 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
-        */
-       protected function initLoadBalancer( LoadBalancer $lb ) {
-               if ( $this->trxRoundId !== false ) {
-                       $lb->beginMasterChanges( $this->trxRoundId ); // set 
DBO_TRX
-               }
        }
 
        /**
@@ -703,13 +178,5 @@
                }
 
                return wfAppendQuery( $url, [ 'cpPosTime' => $time ] );
-       }
-
-       /**
-        * Close all open database connections on all open load balancers.
-        * @since 1.28
-        */
-       public function closeAll() {
-               $this->forEachLBCallMethod( 'closeAll', [] );
        }
 }
diff --git a/includes/db/loadbalancer/LBFactoryFake.php 
b/includes/db/loadbalancer/LBFactoryFake.php
index 5cd1d4b..b97c19b 100644
--- a/includes/db/loadbalancer/LBFactoryFake.php
+++ b/includes/db/loadbalancer/LBFactoryFake.php
@@ -27,7 +27,7 @@
  * Call LBFactory::disableBackend() to start using this, and
  * LBFactory::enableBackend() to return to normal behavior
  */
-class LBFactoryFake extends LBFactory {
+class LBFactoryFake extends LBFactoryMW {
        public function newMainLB( $wiki = false ) {
                throw new DBAccessError;
        }
diff --git a/includes/db/loadbalancer/LBFactoryMulti.php 
b/includes/db/loadbalancer/LBFactoryMulti.php
index dd7737b..fc4a73d 100644
--- a/includes/db/loadbalancer/LBFactoryMulti.php
+++ b/includes/db/loadbalancer/LBFactoryMulti.php
@@ -83,7 +83,7 @@
  *
  * @ingroup Database
  */
-class LBFactoryMulti extends LBFactory {
+class LBFactoryMulti extends LBFactoryMW {
        /** @var array A map of database names to section names */
        private $sectionsByDB;
 
diff --git a/includes/db/loadbalancer/LBFactorySimple.php 
b/includes/db/loadbalancer/LBFactorySimple.php
index d8590b7..9f6d847 100644
--- a/includes/db/loadbalancer/LBFactorySimple.php
+++ b/includes/db/loadbalancer/LBFactorySimple.php
@@ -24,7 +24,7 @@
 /**
  * A simple single-master LBFactory that gets its configuration from the b/c 
globals
  */
-class LBFactorySimple extends LBFactory {
+class LBFactorySimple extends LBFactoryMW {
        /** @var LoadBalancer */
        private $mainLB;
        /** @var LoadBalancer[] */
diff --git a/includes/db/loadbalancer/LBFactorySingle.php 
b/includes/db/loadbalancer/LBFactorySingle.php
index de82a1f..77e41e3 100644
--- a/includes/db/loadbalancer/LBFactorySingle.php
+++ b/includes/db/loadbalancer/LBFactorySingle.php
@@ -24,7 +24,7 @@
 /**
  * An LBFactory class that always returns a single database object.
  */
-class LBFactorySingle extends LBFactory {
+class LBFactorySingle extends LBFactoryMW {
        /** @var LoadBalancerSingle */
        private $lb;
 
diff --git a/includes/libs/rdbms/lbfactory/LBFactory.php 
b/includes/libs/rdbms/lbfactory/LBFactory.php
new file mode 100644
index 0000000..c98670d
--- /dev/null
+++ b/includes/libs/rdbms/lbfactory/LBFactory.php
@@ -0,0 +1,627 @@
+<?php
+/**
+ * Generator and manager 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\LoggerInterface;
+
+/**
+ * An interface for generating database load balancers
+ * @ingroup Database
+ */
+abstract class LBFactory {
+       /** @var ChronologyProtector */
+       protected $chronProt;
+       /** @var TransactionProfiler */
+       protected $trxProfiler;
+       /** @var LoggerInterface */
+       protected $trxLogger;
+       /** @var LoggerInterface */
+       protected $replLogger;
+       /** @var LoggerInterface */
+       protected $connLogger;
+       /** @var LoggerInterface */
+       protected $queryLogger;
+       /** @var LoggerInterface */
+       protected $perfLogger;
+       /** @var callable Error logger */
+       protected $errorLogger;
+       /** @var BagOStuff */
+       protected $srvCache;
+       /** @var BagOStuff */
+       protected $memCache;
+       /** @var WANObjectCache */
+       protected $wanCache;
+
+       /** @var string Local domain */
+       protected $domain;
+       /** @var mixed */
+       protected $ticket;
+       /** @var string|bool String if a requested DBO_TRX transaction round is 
active */
+       protected $trxRoundId = false;
+       /** @var string|bool Reason all LBs are read-only or false if not */
+       protected $readOnlyReason = false;
+       /** @var callable[] */
+       protected $replicationWaitCallbacks = [];
+
+       const SHUTDOWN_NO_CHRONPROT = 0; // don't save DB positions at all
+       const SHUTDOWN_CHRONPROT_ASYNC = 1; // save DB positions, but don't 
wait on remote DCs
+       const SHUTDOWN_CHRONPROT_SYNC = 2; // save DB positions, waiting on all 
DCs
+
+       private static $loggerFields =
+               [ 'trxLogger', 'replLogger', 'connLogger', 'queryLogger', 
'perfLogger' ];
+
+       /**
+        * @TODO: document base params here
+        * @param array $conf
+        */
+       public function __construct( array $conf ) {
+               $this->domain = isset( $conf['domain'] ) ? $conf['domain'] : '';
+               if ( isset( $conf['readOnlyReason'] ) && is_string( 
$conf['readOnlyReason'] ) ) {
+                       $this->readOnlyReason = $conf['readOnlyReason'];
+               }
+
+               $this->srvCache = isset( $conf['srvCache'] ) ? 
$conf['srvCache'] : new EmptyBagOStuff();
+               $this->memCache = isset( $conf['memCache'] ) ? 
$conf['memCache'] : new EmptyBagOStuff();
+               $this->wanCache = isset( $conf['wanCache'] )
+                       ? $conf['wanCache']
+                       : WANObjectCache::newEmpty();
+
+               foreach ( self::$loggerFields as $key ) {
+                       $this->$key = isset( $conf[$key] ) ? $conf[$key] : new 
\Psr\Log\NullLogger();
+               }
+               $this->errorLogger = isset( $conf['errorLogger'] )
+                       ? $conf['errorLogger']
+                       : function ( Exception $e ) {
+                               trigger_error( E_WARNING, $e->getMessage() );
+                       };
+
+               $this->chronProt = isset( $conf['chronProt'] )
+                       ? $conf['chronProt']
+                       : $this->newChronologyProtector();
+               $this->trxProfiler = isset( $conf['trxProfiler'] )
+                       ? $conf['trxProfiler']
+                       : new TransactionProfiler();
+
+               $this->ticket = mt_rand();
+       }
+
+       /**
+        * Disables all load balancers. All connections are closed, and any 
attempt to
+        * open a new connection will result in a DBAccessError.
+        * @see LoadBalancer::disable()
+        */
+       public function destroy() {
+               $this->shutdown( self::SHUTDOWN_NO_CHRONPROT );
+               $this->forEachLBCallMethod( 'disable' );
+       }
+
+       /**
+        * Create a new load balancer object. The resulting object will be 
untracked,
+        * not chronology-protected, and the caller is responsible for cleaning 
it up.
+        *
+        * @param bool|string $domain Wiki ID, or false for the current wiki
+        * @return ILoadBalancer
+        */
+       abstract public function newMainLB( $domain = false );
+
+       /**
+        * Get a cached (tracked) load balancer object.
+        *
+        * @param bool|string $domain Wiki ID, or false for the current wiki
+        * @return ILoadBalancer
+        */
+       abstract public function getMainLB( $domain = false );
+
+       /**
+        * Create a new load balancer for external storage. The resulting 
object will be
+        * untracked, not chronology-protected, and the caller is responsible 
for
+        * cleaning it up.
+        *
+        * @param string $cluster External storage cluster, or false for core
+        * @param bool|string $domain Wiki ID, or false for the current wiki
+        * @return ILoadBalancer
+        */
+       abstract protected function newExternalLB( $cluster, $domain = false );
+
+       /**
+        * Get a cached (tracked) load balancer for external storage
+        *
+        * @param string $cluster External storage cluster, or false for core
+        * @param bool|string $domain Wiki ID, or false for the current wiki
+        * @return ILoadBalancer
+        */
+       abstract public function getExternalLB( $cluster, $domain = false );
+
+       /**
+        * Execute a function for each tracked load balancer
+        * The callback is called with the load balancer as the first parameter,
+        * and $params passed as the subsequent parameters.
+        *
+        * @param callable $callback
+        * @param array $params
+        */
+       abstract public function forEachLB( $callback, array $params = [] );
+
+       /**
+        * Prepare all tracked load balancers for shutdown
+        * @param integer $mode One of the class SHUTDOWN_* constants
+        * @param callable|null $workCallback Work to mask ChronologyProtector 
writes
+        */
+       public function shutdown(
+               $mode = self::SHUTDOWN_CHRONPROT_SYNC, callable $workCallback = 
null
+       ) {
+               if ( $mode === self::SHUTDOWN_CHRONPROT_SYNC ) {
+                       $this->shutdownChronologyProtector( $this->chronProt, 
$workCallback, 'sync' );
+               } elseif ( $mode === self::SHUTDOWN_CHRONPROT_ASYNC ) {
+                       $this->shutdownChronologyProtector( $this->chronProt, 
null, 'async' );
+               }
+
+               $this->commitMasterChanges( __METHOD__ ); // sanity
+       }
+
+       /**
+        * Call a method of each tracked load balancer
+        *
+        * @param string $methodName
+        * @param array $args
+        */
+       protected function forEachLBCallMethod( $methodName, array $args = [] ) 
{
+               $this->forEachLB(
+                       function ( ILoadBalancer $loadBalancer, $methodName, 
array $args ) {
+                               call_user_func_array( [ $loadBalancer, 
$methodName ], $args );
+                       },
+                       [ $methodName, $args ]
+               );
+       }
+
+       /**
+        * Commit all replica DB transactions so as to flush any 
REPEATABLE-READ or SSI snapshot
+        *
+        * @param string $fname Caller name
+        * @since 1.28
+        */
+       public function flushReplicaSnapshots( $fname = __METHOD__ ) {
+               $this->forEachLBCallMethod( 'flushReplicaSnapshots', [ $fname ] 
);
+       }
+
+       /**
+        * Commit on all connections. Done for two reasons:
+        * 1. To commit changes to the masters.
+        * 2. To release the snapshot on all connections, master and replica DB.
+        * @param string $fname Caller name
+        * @param array $options Options map:
+        *   - maxWriteDuration: abort if more than this much time was spent in 
write queries
+        */
+       public function commitAll( $fname = __METHOD__, array $options = [] ) {
+               $this->commitMasterChanges( $fname, $options );
+               $this->forEachLBCallMethod( 'commitAll', [ $fname ] );
+       }
+
+       /**
+        * 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 DBTransactionError
+        * @since 1.28
+        */
+       public function beginMasterChanges( $fname = __METHOD__ ) {
+               if ( $this->trxRoundId !== false ) {
+                       throw new DBTransactionError(
+                               null,
+                               "$fname: transaction round 
'{$this->trxRoundId}' already started."
+                       );
+               }
+               $this->trxRoundId = $fname;
+               // Set DBO_TRX flags on all appropriate DBs
+               $this->forEachLBCallMethod( 'beginMasterChanges', [ $fname ] );
+       }
+
+       /**
+        * Commit changes on all master connections
+        * @param string $fname Caller name
+        * @param array $options Options map:
+        *   - maxWriteDuration: abort if more than this much time was spent in 
write queries
+        * @throws Exception
+        */
+       public function commitMasterChanges( $fname = __METHOD__, array 
$options = [] ) {
+               if ( $this->trxRoundId !== false && $this->trxRoundId !== 
$fname ) {
+                       throw new DBTransactionError(
+                               null,
+                               "$fname: transaction round 
'{$this->trxRoundId}' still running."
+                       );
+               }
+               // Run pre-commit callbacks and suppress post-commit callbacks, 
aborting on failure
+               $this->forEachLBCallMethod( 'finalizeMasterChanges' );
+               $this->trxRoundId = false;
+               // Perform pre-commit checks, aborting on failure
+               $this->forEachLBCallMethod( 'approveMasterChanges', [ $options 
] );
+               // Log the DBs and methods involved in multi-DB transactions
+               $this->logIfMultiDbTransaction();
+               // Actually perform the commit on all master DB connections and 
revert DBO_TRX
+               $this->forEachLBCallMethod( 'commitMasterChanges', [ $fname ] );
+               // Run all post-commit callbacks
+               /** @var Exception $e */
+               $e = null; // first callback exception
+               $this->forEachLB( function ( ILoadBalancer $lb ) use ( &$e ) {
+                       $ex = $lb->runMasterPostTrxCallbacks( 
IDatabase::TRIGGER_COMMIT );
+                       $e = $e ?: $ex;
+               } );
+               // Commit any dangling DBO_TRX transactions from callbacks on 
one DB to another DB
+               $this->forEachLBCallMethod( 'commitMasterChanges', [ $fname ] );
+               // Throw any last post-commit callback error
+               if ( $e instanceof Exception ) {
+                       throw $e;
+               }
+       }
+
+       /**
+        * Rollback changes on all master connections
+        * @param string $fname Caller name
+        * @since 1.23
+        */
+       public function rollbackMasterChanges( $fname = __METHOD__ ) {
+               $this->trxRoundId = false;
+               $this->forEachLBCallMethod( 'suppressTransactionEndCallbacks' );
+               $this->forEachLBCallMethod( 'rollbackMasterChanges', [ $fname ] 
);
+               // Run all post-rollback callbacks
+               $this->forEachLB( function ( ILoadBalancer $lb ) {
+                       $lb->runMasterPostTrxCallbacks( 
IDatabase::TRIGGER_ROLLBACK );
+               } );
+       }
+
+       /**
+        * Log query info if multi DB transactions are going to be committed now
+        */
+       private function logIfMultiDbTransaction() {
+               $callersByDB = [];
+               $this->forEachLB( function ( ILoadBalancer $lb ) use ( 
&$callersByDB ) {
+                       $masterName = $lb->getServerName( $lb->getWriterIndex() 
);
+                       $callers = $lb->pendingMasterChangeCallers();
+                       if ( $callers ) {
+                               $callersByDB[$masterName] = $callers;
+                       }
+               } );
+
+               if ( count( $callersByDB ) >= 2 ) {
+                       $dbs = implode( ', ', array_keys( $callersByDB ) );
+                       $msg = "Multi-DB transaction [{$dbs}]:\n";
+                       foreach ( $callersByDB as $db => $callers ) {
+                               $msg .= "$db: " . implode( '; ', $callers ) . 
"\n";
+                       }
+                       $this->trxLogger->info( $msg );
+               }
+       }
+
+       /**
+        * Determine if any master connection has pending changes
+        * @return bool
+        * @since 1.23
+        */
+       public function hasMasterChanges() {
+               $ret = false;
+               $this->forEachLB( function ( ILoadBalancer $lb ) use ( &$ret ) {
+                       $ret = $ret || $lb->hasMasterChanges();
+               } );
+
+               return $ret;
+       }
+
+       /**
+        * Detemine if any lagged replica DB connection was used
+        * @return bool
+        * @since 1.28
+        */
+       public function laggedReplicaUsed() {
+               $ret = false;
+               $this->forEachLB( function ( ILoadBalancer $lb ) use ( &$ret ) {
+                       $ret = $ret || $lb->laggedReplicaUsed();
+               } );
+
+               return $ret;
+       }
+
+       /**
+        * Determine if any master connection has pending/written changes from 
this request
+        * @param float $age How many seconds ago is "recent" [defaults to LB 
lag wait timeout]
+        * @return bool
+        * @since 1.27
+        */
+       public function hasOrMadeRecentMasterChanges( $age = null ) {
+               $ret = false;
+               $this->forEachLB( function ( ILoadBalancer $lb ) use ( $age, 
&$ret ) {
+                       $ret = $ret || $lb->hasOrMadeRecentMasterChanges( $age 
);
+               } );
+               return $ret;
+       }
+
+       /**
+        * Waits for the replica DBs to catch up to the current master position
+        *
+        * Use this when updating very large numbers of rows, as in maintenance 
scripts,
+        * to avoid causing too much lag. Of course, this is a no-op if there 
are no replica DBs.
+        *
+        * By default this waits on all DB clusters actually used in this 
request.
+        * This makes sense when lag being waiting on is caused by the code 
that does this check.
+        * In that case, setting "ifWritesSince" can avoid the overhead of 
waiting for clusters
+        * that were not changed since the last wait check. To forcefully wait 
on a specific cluster
+        * for a given wiki, use the 'wiki' parameter. To forcefully wait on an 
"external" cluster,
+        * use the "cluster" parameter.
+        *
+        * Never call this function after a large DB write that is *still* in a 
transaction.
+        * It only makes sense to call this after the possible lag inducing 
changes were committed.
+        *
+        * @param array $opts Optional fields that include:
+        *   - wiki : wait on the load balancer DBs that handles the given wiki
+        *   - cluster : wait on the given external load balancer DBs
+        *   - timeout : Max wait time. Default: ~60 seconds
+        *   - ifWritesSince: Only wait if writes were done since this UNIX 
timestamp
+        * @throws DBReplicationWaitError If a timeout or error occured waiting 
on a DB cluster
+        * @since 1.27
+        */
+       public function waitForReplication( array $opts = [] ) {
+               $opts += [
+                       'wiki' => false,
+                       'cluster' => false,
+                       'timeout' => 60,
+                       'ifWritesSince' => null
+               ];
+
+               // Figure out which clusters need to be checked
+               /** @var ILoadBalancer[] $lbs */
+               $lbs = [];
+               if ( $opts['cluster'] !== false ) {
+                       $lbs[] = $this->getExternalLB( $opts['cluster'] );
+               } elseif ( $opts['wiki'] !== false ) {
+                       $lbs[] = $this->getMainLB( $opts['wiki'] );
+               } else {
+                       $this->forEachLB( function ( ILoadBalancer $lb ) use ( 
&$lbs ) {
+                               $lbs[] = $lb;
+                       } );
+                       if ( !$lbs ) {
+                               return; // nothing actually used
+                       }
+               }
+
+               // Get all the master positions of applicable DBs right now.
+               // This can be faster since waiting on one cluster reduces the
+               // time needed to wait on the next clusters.
+               $masterPositions = array_fill( 0, count( $lbs ), false );
+               foreach ( $lbs as $i => $lb ) {
+                       if ( $lb->getServerCount() <= 1 ) {
+                               // Bug 27975 - Don't try to wait for replica 
DBs if there are none
+                               // Prevents permission error when getting 
master position
+                               continue;
+                       } elseif ( $opts['ifWritesSince']
+                               && $lb->lastMasterChangeTimestamp() < 
$opts['ifWritesSince']
+                       ) {
+                               continue; // no writes since the last wait
+                       }
+                       $masterPositions[$i] = $lb->getMasterPos();
+               }
+
+               // Run any listener callbacks *after* getting the DB positions. 
The more
+               // time spent in the callbacks, the less time is spent in 
waitForAll().
+               foreach ( $this->replicationWaitCallbacks as $callback ) {
+                       $callback();
+               }
+
+               $failed = [];
+               foreach ( $lbs as $i => $lb ) {
+                       if ( $masterPositions[$i] ) {
+                               // The DBMS may not support getMasterPos() or 
the whole
+                               // load balancer might be fake (e.g. 
$wgAllDBsAreLocalhost).
+                               if ( !$lb->waitForAll( $masterPositions[$i], 
$opts['timeout'] ) ) {
+                                       $failed[] = $lb->getServerName( 
$lb->getWriterIndex() );
+                               }
+                       }
+               }
+
+               if ( $failed ) {
+                       throw new DBReplicationWaitError(
+                               "Could not wait for replica DBs to catch up to 
" .
+                               implode( ', ', $failed )
+                       );
+               }
+       }
+
+       /**
+        * Add a callback to be run in every call to waitForReplication() 
before waiting
+        *
+        * Callbacks must clear any transactions that they start
+        *
+        * @param string $name Callback name
+        * @param callable|null $callback Use null to unset a callback
+        * @since 1.28
+        */
+       public function setWaitForReplicationListener( $name, callable 
$callback = null ) {
+               if ( $callback ) {
+                       $this->replicationWaitCallbacks[$name] = $callback;
+               } else {
+                       unset( $this->replicationWaitCallbacks[$name] );
+               }
+       }
+
+       /**
+        * Get a token asserting that no transaction writes are active
+        *
+        * @param string $fname Caller name (e.g. __METHOD__)
+        * @return mixed A value to pass to commitAndWaitForReplication()
+        * @since 1.28
+        */
+       public function getEmptyTransactionTicket( $fname ) {
+               if ( $this->hasMasterChanges() ) {
+                       $this->trxLogger->error( __METHOD__ . ": $fname does 
not have outer scope." );
+                       return null;
+               }
+
+               return $this->ticket;
+       }
+
+       /**
+        * Convenience method for safely running 
commitMasterChanges()/waitForReplication()
+        *
+        * This will commit and wait unless $ticket indicates it is unsafe to 
do so
+        *
+        * @param string $fname Caller name (e.g. __METHOD__)
+        * @param mixed $ticket Result of getEmptyTransactionTicket()
+        * @param array $opts Options to waitForReplication()
+        * @throws DBReplicationWaitError
+        * @since 1.28
+        */
+       public function commitAndWaitForReplication( $fname, $ticket, array 
$opts = [] ) {
+               if ( $ticket !== $this->ticket ) {
+                       $this->perfLogger->error( __METHOD__ . ": $fname does 
not have outer scope." );
+                       return;
+               }
+
+               // The transaction owner and any caller with the empty 
transaction ticket can commit
+               // so that getEmptyTransactionTicket() callers don't risk 
seeing DBTransactionError.
+               if ( $this->trxRoundId !== false && $fname !== 
$this->trxRoundId ) {
+                       $this->trxLogger->info( "$fname: committing on behalf 
of {$this->trxRoundId}." );
+                       $fnameEffective = $this->trxRoundId;
+               } else {
+                       $fnameEffective = $fname;
+               }
+
+               $this->commitMasterChanges( $fnameEffective );
+               $this->waitForReplication( $opts );
+               // If a nested caller committed on behalf of $fname, start 
another empty $fname
+               // transaction, leaving the caller with the same empty 
transaction state as before.
+               if ( $fnameEffective !== $fname ) {
+                       $this->beginMasterChanges( $fnameEffective );
+               }
+       }
+
+       /**
+        * @param string $dbName DB master name (e.g. "db1052")
+        * @return float|bool UNIX timestamp when client last touched the DB or 
false if not recent
+        * @since 1.28
+        */
+       public function getChronologyProtectorTouched( $dbName ) {
+               return $this->chronProt->getTouched( $dbName );
+       }
+
+       /**
+        * Disable the ChronologyProtector for all load balancers
+        *
+        * This can be called at the start of special API entry points
+        *
+        * @since 1.27
+        */
+       public function disableChronologyProtection() {
+               $this->chronProt->setEnabled( false );
+       }
+
+       /**
+        * @return ChronologyProtector
+        */
+       protected function newChronologyProtector() {
+               $chronProt = new ChronologyProtector(
+                       $this->memCache,
+                       [
+                               'ip' => isset( $_SERVER[ 'REMOTE_ADDR' ] ) ? 
$_SERVER[ 'REMOTE_ADDR' ] : '',
+                               'agent' => isset( $_SERVER['HTTP_USER_AGENT'] ) 
? $_SERVER['HTTP_USER_AGENT'] : ''
+                       ],
+                       isset( $_GET['cpPosTime'] ) ? $_GET['cpPosTime'] : null
+               );
+               if ( PHP_SAPI === 'cli' ) {
+                       $chronProt->setEnabled( false );
+               }
+
+               return $chronProt;
+       }
+
+       /**
+        * Get and record all of the staged DB positions into persistent memory 
storage
+        *
+        * @param ChronologyProtector $cp
+        * @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
+        */
+       protected function shutdownChronologyProtector(
+               ChronologyProtector $cp, $workCallback, $mode
+       ) {
+               // Record all the master positions needed
+               $this->forEachLB( function ( ILoadBalancer $lb ) use ( $cp ) {
+                       $cp->shutdownLB( $lb );
+               } );
+               // Write them to the persistent stash. Try to do something 
useful by running $work
+               // while ChronologyProtector waits for the stash write to 
replicate to all DCs.
+               $unsavedPositions = $cp->shutdown( $workCallback, $mode );
+               if ( $unsavedPositions && $workCallback ) {
+                       // Invoke callback in case it did not cache the result 
yet
+                       $workCallback(); // work now to block for less time in 
waitForAll()
+               }
+               // If the positions failed to write to the stash, at least wait 
on local datacenter
+               // replica DBs to catch up before responding. Even if there are 
several DCs, this increases
+               // the chance that the user will see their own changes 
immediately afterwards. As long
+               // as the sticky DC cookie applies (same domain), this is not 
even an issue.
+               $this->forEachLB( function ( ILoadBalancer $lb ) use ( 
$unsavedPositions ) {
+                       $masterName = $lb->getServerName( $lb->getWriterIndex() 
);
+                       if ( isset( $unsavedPositions[$masterName] ) ) {
+                               $lb->waitForAll( $unsavedPositions[$masterName] 
);
+                       }
+               } );
+       }
+
+       /**
+        * Base parameters to LoadBalancer::__construct()
+        * @return array
+        */
+       final protected function baseLoadBalancerParams() {
+               return [
+                       'localDomain' => $this->domain,
+                       'readOnlyReason' => $this->readOnlyReason,
+                       'srvCache' => $this->srvCache,
+                       'wanCache' => $this->wanCache,
+                       'trxProfiler' => $this->trxProfiler,
+                       'queryLogger' => $this->queryLogger,
+                       'connLogger' => $this->connLogger,
+                       'replLogger' => $this->replLogger,
+                       'errorLogger' => $this->errorLogger
+               ];
+       }
+
+       /**
+        * @param ILoadBalancer $lb
+        */
+       protected function initLoadBalancer( ILoadBalancer $lb ) {
+               if ( $this->trxRoundId !== false ) {
+                       $lb->beginMasterChanges( $this->trxRoundId ); // set 
DBO_TRX
+               }
+       }
+
+       /**
+        * Close all open database connections on all open load balancers.
+        * @since 1.28
+        */
+       public function closeAll() {
+               $this->forEachLBCallMethod( 'closeAll', [] );
+       }
+}

-- 
To view, visit https://gerrit.wikimedia.org/r/310536
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: newchange
Gerrit-Change-Id: I325f59b7467ab9c2137731d1ce69816f5a020f03
Gerrit-PatchSet: 1
Gerrit-Project: mediawiki/core
Gerrit-Branch: master
Gerrit-Owner: Aaron Schulz <asch...@wikimedia.org>

_______________________________________________
MediaWiki-commits mailing list
MediaWiki-commits@lists.wikimedia.org
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to