Aaron Schulz has uploaded a new change for review.

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

Change subject: objectcache: allow for callbacks to mask SYNC_WRITE latency
......................................................................

objectcache: allow for callbacks to mask SYNC_WRITE latency

Also made RedisBagOStuff respect it and made a common
wait loop class.

Change-Id: I908222ad3788ebe330aa58831cda139da32becd8
---
M autoload.php
A includes/libs/WaitConditionLoop.php
M includes/libs/objectcache/BagOStuff.php
M includes/objectcache/RedisBagOStuff.php
M includes/objectcache/SqlBagOStuff.php
5 files changed, 252 insertions(+), 25 deletions(-)


  git pull ssh://gerrit.wikimedia.org:29418/mediawiki/core 
refs/changes/29/307329/1

diff --git a/autoload.php b/autoload.php
index 39102fd..0f9d2db 100644
--- a/autoload.php
+++ b/autoload.php
@@ -1498,6 +1498,7 @@
        'VirtualRESTService' => __DIR__ . 
'/includes/libs/virtualrest/VirtualRESTService.php',
        'VirtualRESTServiceClient' => __DIR__ . 
'/includes/libs/virtualrest/VirtualRESTServiceClient.php',
        'WANObjectCache' => __DIR__ . 
'/includes/libs/objectcache/WANObjectCache.php',
+       'WaitConditionLoop' => __DIR__ . '/includes/libs/WaitConditionLoop.php',
        'WantedCategoriesPage' => __DIR__ . 
'/includes/specials/SpecialWantedcategories.php',
        'WantedFilesPage' => __DIR__ . 
'/includes/specials/SpecialWantedfiles.php',
        'WantedPagesPage' => __DIR__ . 
'/includes/specials/SpecialWantedpages.php',
diff --git a/includes/libs/WaitConditionLoop.php 
b/includes/libs/WaitConditionLoop.php
new file mode 100644
index 0000000..1448e32
--- /dev/null
+++ b/includes/libs/WaitConditionLoop.php
@@ -0,0 +1,144 @@
+<?php
+/**
+ * Wait loop that reaches a condition or times out.
+ *
+ * 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
+ * @author Aaron Schulz
+ */
+
+/**
+ * Wait loop that reaches a condition or times out
+ * @since 1.29
+ */
+class WaitConditionLoop {
+       /** @var callable */
+       private $condition;
+       /** @var callable[] */
+       private $busyCallbacks = [];
+       /** @var float Seconds */
+       private $timeout;
+       /** @var float Seconds */
+       private $lastWaitTime;
+
+       /**
+        * Keep checking $condition() until the StatusValue it returns is 
"good" or not "OK"
+        * per isGood() and isOK(), or until the timeout specified by $timeout 
is reached
+        *
+        * @param callable $condition Callback that returns a StatusValue
+        * @param float $timeout Timeout in seconds
+        * @param array &$busyCallbacks List of callbacks to do useful work (by 
reference)
+        */
+       public function __construct( callable $condition, $timeout = 5.0, 
&$busyCallbacks = [] ) {
+               $this->condition = $condition;
+               $this->timeout = $timeout;
+               $this->busyCallbacks =& $busyCallbacks;
+       }
+
+       /**
+        * Invoke the loop
+        *
+        * The resulting status will fall into three possibilities:
+        *   - If isGood(), then the condition was reached
+        *   - If not isGood() but isOK(), then a timeout was reached
+        *   - If not isOK(), then some serious error occured in the callback
+        *
+        * @return StatusValue
+        */
+       public function invoke() {
+               $elapsed = 0.0; // seconds
+               $sleepUs = 0; // microseconds to sleep each time
+               do {
+                       $checkStartTime = microtime( true );
+                       // Check if the condition is met yet
+                       $realStart = microtime( true );
+                       $cpuStart = $this->getCpuTime();
+                       /** @var StatusValue $status */
+                       $status = call_user_func( $this->condition );
+                       $cpu = $this->getCpuTime() - $cpuStart;
+                       $real = microtime( true ) - $realStart;
+                       // Exit if the condition is reached
+                       if ( $status->isGood() || !$status->isOK() ) {
+                               $this->lastWaitTime = $elapsed;
+
+                               return $status;
+                       }
+                       // Detect if the callback seems to block or if justs 
burns CPU
+                       $callbackBlocksViaIO = ( $real > 0.100 && $cpu <= $real 
* .03 );
+                       if ( $this->runBusyCallbacks( 1 ) == 0 && 
!$callbackBlocksViaIO ) {
+                               // 10 queries = 10(10+100)/2 ms = 550ms, 14 
queries = 1050ms
+                               $sleepUs = min( $sleepUs + 10 * 1e3, 1e6 ); // 
stop incrementing at ~1s
+                               usleep( $sleepUs );
+                       }
+                       $checkEndTime = microtime( true );
+                       // The max() protects against the clock getting set back
+                       $elapsed += max( $checkEndTime - $checkStartTime, 0.010 
);
+               } while ( $elapsed < $this->timeout );
+
+               $status->error( 'timeout' );
+               $this->lastWaitTime = $elapsed;
+
+               return $status;
+       }
+
+       /**
+        * @return float Seconds
+        */
+       public function getLastWaitTime() {
+               return $this->lastWaitTime;
+       }
+
+       /**
+        * @return float Returns 0.0 if not supported (Windows on PHP < 7)
+        */
+       private function getCpuTime() {
+               $time = 0.0;
+
+               if ( defined( 'HHVM_VERSION' ) && PHP_OS === 'Linux' ) {
+                       $ru = getrusage( 2 /* RUSAGE_THREAD */ );
+               } else {
+                       $ru = getrusage( 0 /* RUSAGE_SELF */ );
+               }
+               if ( $ru ) {
+                       $time += $ru['ru_utime.tv_sec'] + 
$ru['ru_utime.tv_usec'] / 1e6;
+                       $time += $ru['ru_stime.tv_sec'] + 
$ru['ru_stime.tv_usec'] / 1e6;
+               }
+
+               return $time;
+       }
+
+       /**
+        * Run callbacks to due useful things instead of blocking for stuff to 
happen
+        *
+        * @param integer $maxRun Maximum number of tasks to run
+        * @return integer Number of callbacks run this time
+        * @since 1.28
+        */
+       protected function runBusyCallbacks( $maxRun = INF ) {
+               $runCount = 0;
+               foreach ( $this->busyCallbacks as $i => $workCallback ) {
+                       if ( $runCount++ >= $maxRun ) {
+                               break;
+                       }
+                       unset( $this->busyCallbacks[$i] ); // consume
+                       $workCallback();
+               }
+
+               return $runCount;
+       }
+
+}
diff --git a/includes/libs/objectcache/BagOStuff.php 
b/includes/libs/objectcache/BagOStuff.php
index 5472e83..616f07f 100644
--- a/includes/libs/objectcache/BagOStuff.php
+++ b/includes/libs/objectcache/BagOStuff.php
@@ -45,30 +45,28 @@
 abstract class BagOStuff implements IExpiringStore, LoggerAwareInterface {
        /** @var array[] Lock tracking */
        protected $locks = [];
-
        /** @var integer ERR_* class constant */
        protected $lastError = self::ERR_NONE;
-
        /** @var string */
        protected $keyspace = 'local';
-
        /** @var LoggerInterface */
        protected $logger;
-
        /** @var callback|null */
        protected $asyncHandler;
+       /** @var integer Seconds */
+       protected $syncTimeout;
 
        /** @var bool */
        private $debugMode = false;
-
        /** @var array */
        private $duplicateKeyLookups = [];
-
        /** @var bool */
        private $reportDupes = false;
-
        /** @var bool */
        private $dupeTrackScheduled = false;
+
+       /** @var callable[] */
+       protected $busyCallbacks = [];
 
        /** @var integer[] Map of (ATTR_* class constant => QOS_* class 
constant) */
        protected $attrMap = [];
@@ -86,6 +84,8 @@
        const WRITE_SYNC = 1; // synchronously write to all locations for 
replicated stores
        const WRITE_CACHE_ONLY = 2; // Only change state of the in-memory cache
 
+       const SYNC_TIMEOUT_SEC = 3;
+
        /**
         * $params include:
         *   - logger: Psr\Log\LoggerInterface instance
@@ -94,6 +94,7 @@
         *      In CLI mode, it should run the task immediately.
         *   - reportDupes: Whether to emit warning log messages for all keys 
that were
         *      requested more than once (requires an asyncHandler).
+        *   - syncTimeout: How long to wait with WRITE_SYNC in seconds.
         * @param array $params
         */
        public function __construct( array $params = [] ) {
@@ -114,6 +115,10 @@
                if ( !empty( $params['reportDupes'] ) && is_callable( 
$this->asyncHandler ) ) {
                        $this->reportDupes = true;
                }
+
+               $this->syncTimeout = isset( $params['syncTimeout'] )
+                       ? $params['syncTimeout']
+                       : self::SYNC_TIMEOUT_SEC;
        }
 
        /**
@@ -643,6 +648,19 @@
        }
 
        /**
+        * Let a callback be run to avoid wasting time on special blocking calls
+        *
+        * The result of the callback can be fetched by reapBusyCallback() using
+        * the request ID this returns. It will run the method if this had not 
done so yet.
+        *
+        * @param callable $workCallback
+        * @since 1.28
+        */
+       public function addBusyCallback( callable $workCallback ) {
+               $this->busyCallbacks[] = $workCallback;
+       }
+
+       /**
         * Modify a cache update operation array for EventRelayer::notify()
         *
         * This is used for relayed writes, e.g. for broadcasting a change
diff --git a/includes/objectcache/RedisBagOStuff.php 
b/includes/objectcache/RedisBagOStuff.php
index c3e0c96..099c1eb 100644
--- a/includes/objectcache/RedisBagOStuff.php
+++ b/includes/objectcache/RedisBagOStuff.php
@@ -115,6 +115,9 @@
                                // No expiry, that is very different from zero 
expiry in Redis
                                $result = $conn->set( $key, $this->serialize( 
$value ) );
                        }
+                       if ( ( $flags & self::WRITE_SYNC ) == self::WRITE_SYNC 
) {
+                               $result = $this->waitForReplication( $conn ) && 
$result;
+                       }
                } catch ( RedisException $e ) {
                        $result = false;
                        $this->handleException( $conn, $e );
@@ -410,7 +413,7 @@
         * not. The safest response for us is to explicitly destroy the 
connection
         * object and let it be reopened during the next request.
         * @param RedisConnRef $conn
-        * @param Exception $e
+        * @param RedisException $e
         */
        protected function handleException( RedisConnRef $conn, $e ) {
                $this->setLastError( BagOStuff::ERR_UNEXPECTED );
@@ -418,6 +421,55 @@
        }
 
        /**
+        * @param RedisConnRef $conn
+        * @return bool
+        */
+       protected function waitForReplication( RedisConnRef $conn ) {
+               // http://redis.io/commands/info
+               $info = $conn->info( 'replication' );
+               if ( !isset( $info['slave0'] ) || !isset( 
$info['master_repl_offset'] ) ) {
+                       return true; // no slaves configured
+               }
+
+               $masterPos = (int)$info['master_repl_offset'];
+               $loop = new WaitConditionLoop(
+                       function () use ( $conn, $masterPos ) {
+                               $status = StatusValue::newGood();
+                               if ( !$this->wasReplicated( $masterPos, 
$conn->info( 'replication' ) ) ) {
+                                       $status->error( 'notsynced' );
+                               }
+                               return $status;
+                       },
+                       $this->syncTimeout,
+                       $this->busyCallbacks
+               );
+
+               return $loop->invoke()->isGood();
+       }
+
+       /**
+        * @param integer $masterPos
+        * @param array $info Info from 'replication' group
+        * @return bool
+        */
+       private function wasReplicated( $masterPos, array $info ) {
+               foreach ( $info as $key => $value ) {
+                       $m = [];
+                       // https://github.com/antirez/redis/issues/2375
+                       if ( preg_match( '/^slave\d+$/', $key )
+                               && preg_match( '/offset=(\d+)/', $value, $m )
+                       ) {
+                               $slavePos = (int)$m[1];
+                               if ( $slavePos < $masterPos ) {
+                                       return false;
+                               }
+                       }
+               }
+
+               return true;
+       }
+
+       /**
         * Send information about a single request to the debug log
         * @param string $method
         * @param string $key
diff --git a/includes/objectcache/SqlBagOStuff.php 
b/includes/objectcache/SqlBagOStuff.php
index 7a89991..68eeb34 100644
--- a/includes/objectcache/SqlBagOStuff.php
+++ b/includes/objectcache/SqlBagOStuff.php
@@ -377,7 +377,7 @@
        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;
@@ -489,7 +489,7 @@
        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 = $ok && $this->waitForReplication();
                }
 
                return $ok;
@@ -797,22 +797,34 @@
                return !$this->serverInfos;
        }
 
-       protected function waitForSlaves() {
-               if ( $this->usesMainDB() ) {
-                       $lb = $this->getSeparateMainLB()
-                               ?: 
MediaWikiServices::getInstance()->getDBLoadBalancer();
-                       // Main LB is used; wait for any slaves to catch up
-                       try {
-                               $pos = $lb->getMasterPos();
-                               if ( $pos ) {
-                                       return $lb->waitForAll( $pos, 3 );
-                               }
-                       } catch ( DBReplicationWaitError $e ) {
-                               return false;
-                       }
+       protected function waitForReplication() {
+               if ( !$this->usesMainDB() ) {
+                       // Custom DB server list; probably doesn't use 
replication
+                       return true;
                }
 
-               // Custom DB server list; probably doesn't use replication
-               return true;
+               $lb = $this->getSeparateMainLB()
+                       ?: 
MediaWikiServices::getInstance()->getDBLoadBalancer();
+
+               if ( $lb->getServerCount() <= 1 ) {
+                       return true; // no slaves
+               }
+
+               // Main LB is used; wait for any slaves to catch up
+               $masterPos = $lb->getConnection( DB_MASTER )->getMasterPos();
+
+               $loop = new WaitConditionLoop(
+                       function () use ( $lb, $masterPos ) {
+                               $status = StatusValue::newGood();
+                               if ( !$lb->waitForAll( $masterPos, 1 ) ) {
+                                       $status->error( 'not_synced' );
+                               }
+                               return $status;
+                       },
+                       $this->syncTimeout,
+                       $this->busyCallbacks
+               );
+
+               return $loop->invoke()->isGood();
        }
 }

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

Gerrit-MessageType: newchange
Gerrit-Change-Id: I908222ad3788ebe330aa58831cda139da32becd8
Gerrit-PatchSet: 1
Gerrit-Project: mediawiki/core
Gerrit-Branch: master
Gerrit-Owner: Aaron Schulz <[email protected]>

_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to