Use standard exceptions in ObjectCache and always handle CACHE_NONE/"hash"
[lhc/web/wiklou.git] / includes / objectcache / SqlBagOStuff.php
index 9ab28aa..d06213f 100644 (file)
@@ -44,10 +44,12 @@ class SqlBagOStuff extends BagOStuff {
        /** @var string */
        protected $tableName = 'objectcache';
        /** @var bool */
-       protected $slaveOnly = false;
+       protected $replicaOnly = false;
        /** @var int */
        protected $syncTimeout = 3;
 
+       /** @var LoadBalancer|null */
+       protected $separateMainLB;
        /** @var array */
        protected $conns;
        /** @var array UNIX timestamps */
@@ -83,11 +85,11 @@ class SqlBagOStuff extends BagOStuff {
         *                  required to hold the largest shard index. Data will be
         *                  distributed across all tables by key hash. This is for
         *                  MySQL bugs 61735 and 61736.
-        *   - slaveOnly:   Whether to only use slave DBs and avoid triggering
+        *   - slaveOnly:   Whether to only use replica DBs and avoid triggering
         *                  garbage collection logic of expired items. This only
         *                  makes sense if the primary DB is used and only if get()
         *                  calls will be used. This is used by ReplicatedBagOStuff.
-        *   - syncTimeout: Max seconds to wait for slaves to catch up for WRITE_SYNC.
+        *   - syncTimeout: Max seconds to wait for replica DBs to catch up for WRITE_SYNC.
         *
         * @param array $params
         */
@@ -95,6 +97,7 @@ class SqlBagOStuff extends BagOStuff {
                parent::__construct( $params );
 
                $this->attrMap[self::ATTR_EMULATION] = self::QOS_EMULATION_SQL;
+               $this->attrMap[self::ATTR_SYNCWRITES] = self::QOS_SYNCWRITES_NONE;
 
                if ( isset( $params['servers'] ) ) {
                        $this->serverInfos = [];
@@ -114,8 +117,10 @@ class SqlBagOStuff extends BagOStuff {
                        $this->serverInfos = [ $params['server'] ];
                        $this->numServers = count( $this->serverInfos );
                } else {
+                       // Default to using the main wiki's database servers
                        $this->serverInfos = false;
                        $this->numServers = 1;
+                       $this->attrMap[self::ATTR_SYNCWRITES] = self::QOS_SYNCWRITES_BE;
                }
                if ( isset( $params['purgePeriod'] ) ) {
                        $this->purgePeriod = intval( $params['purgePeriod'] );
@@ -129,7 +134,24 @@ class SqlBagOStuff extends BagOStuff {
                if ( isset( $params['syncTimeout'] ) ) {
                        $this->syncTimeout = $params['syncTimeout'];
                }
-               $this->slaveOnly = !empty( $params['slaveOnly'] );
+               $this->replicaOnly = !empty( $params['slaveOnly'] );
+       }
+
+       protected function getSeparateMainLB() {
+               global $wgDBtype;
+
+               if ( $wgDBtype === 'mysql' && $this->usesMainDB() ) {
+                       if ( !$this->separateMainLB ) {
+                               // We must keep a separate connection to MySQL in order to avoid deadlocks
+                               $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
+                               $this->separateMainLB = $lbFactory->newMainLB();
+                       }
+                       return $this->separateMainLB;
+               } else {
+                       // However, SQLite has an opposite behavior. And PostgreSQL needs to know
+                       // if we are in transaction or not (@TODO: find some PostgreSQL work-around).
+                       return null;
+               }
        }
 
        /**
@@ -163,16 +185,13 @@ class SqlBagOStuff extends BagOStuff {
                                $db = DatabaseBase::factory( $type, $info );
                                $db->clearFlag( DBO_TRX );
                        } else {
-                               // We must keep a separate connection to MySQL in order to avoid deadlocks
-                               // However, SQLite has an opposite behavior. And PostgreSQL needs to know
-                               // if we are in transaction or not (@TODO: find some work-around).
-                               $index = $this->slaveOnly ? DB_SLAVE : DB_MASTER;
-                               if ( wfGetDB( $index )->getType() == 'mysql' ) {
-                                       $lb = wfGetLBFactory()->newMainLB();
-                                       $db = $lb->getConnection( $index );
+                               $index = $this->replicaOnly ? DB_REPLICA : DB_MASTER;
+                               if ( $this->getSeparateMainLB() ) {
+                                       $db = $this->getSeparateMainLB()->getConnection( $index );
                                        $db->clearFlag( DBO_TRX ); // auto-commit mode
                                } else {
                                        $db = wfGetDB( $index );
+                                       // Can't mess with transaction rounds (DBO_TRX) :(
                                }
                        }
                        $this->logger->debug( sprintf( "Connection %s will be used for SqlBagOStuff", $db ) );
@@ -360,7 +379,7 @@ class SqlBagOStuff extends BagOStuff {
        public function set( $key, $value, $exptime = 0, $flags = 0 ) {
                $ok = $this->setMulti( [ $key => $value ], $exptime );
                if ( ( $flags & self::WRITE_SYNC ) == self::WRITE_SYNC ) {
-                       $ok = $ok && $this->waitForSlaves();
+                       $ok = $this->waitForReplication() && $ok;
                }
 
                return $ok;
@@ -472,7 +491,7 @@ class SqlBagOStuff extends BagOStuff {
        public function merge( $key, callable $callback, $exptime = 0, $attempts = 10, $flags = 0 ) {
                $ok = $this->mergeViaCas( $key, $callback, $exptime, $attempts );
                if ( ( $flags & self::WRITE_SYNC ) == self::WRITE_SYNC ) {
-                       $ok = $ok && $this->waitForSlaves();
+                       $ok = $this->waitForReplication() && $ok;
                }
 
                return $ok;
@@ -522,7 +541,7 @@ class SqlBagOStuff extends BagOStuff {
        }
 
        protected function garbageCollect() {
-               if ( !$this->purgePeriod || $this->slaveOnly ) {
+               if ( !$this->purgePeriod || $this->replicaOnly ) {
                        // Disabled
                        return;
                }
@@ -780,19 +799,30 @@ class SqlBagOStuff extends BagOStuff {
                return !$this->serverInfos;
        }
 
-       protected function waitForSlaves() {
-               if ( $this->usesMainDB() ) {
-                       // Main LB is used; wait for any slaves to catch up
-                       try {
-                               $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
-                               $lbFactory->waitForReplication( [ 'wiki' => wfWikiID() ] );
-                               return true;
-                       } catch ( DBReplicationWaitError $e ) {
-                               return false;
-                       }
-               } else {
+       protected function waitForReplication() {
+               if ( !$this->usesMainDB() ) {
                        // Custom DB server list; probably doesn't use replication
                        return true;
                }
+
+               $lb = $this->getSeparateMainLB()
+                       ?: MediaWikiServices::getInstance()->getDBLoadBalancer();
+
+               if ( $lb->getServerCount() <= 1 ) {
+                       return true; // no replica DBs
+               }
+
+               // Main LB is used; wait for any replica DBs to catch up
+               $masterPos = $lb->getMasterPos();
+
+               $loop = new WaitConditionLoop(
+                       function () use ( $lb, $masterPos ) {
+                               return $lb->waitForAll( $masterPos, 1 );
+                       },
+                       $this->syncTimeout,
+                       $this->busyCallbacks
+               );
+
+               return ( $loop->invoke() === $loop::CONDITION_REACHED );
        }
 }