]> scripts.mit.edu Git - autoinstallsdev/mediawiki.git/blobdiff - vendor/wikimedia/wait-condition-loop/src/WaitConditionLoop.php
MediaWiki 1.30.2
[autoinstallsdev/mediawiki.git] / vendor / wikimedia / wait-condition-loop / src / WaitConditionLoop.php
diff --git a/vendor/wikimedia/wait-condition-loop/src/WaitConditionLoop.php b/vendor/wikimedia/wait-condition-loop/src/WaitConditionLoop.php
new file mode 100644 (file)
index 0000000..583ebb3
--- /dev/null
@@ -0,0 +1,189 @@
+<?php
+/**
+ * Wait loop that reaches a condition or times out.
+ *
+ * Copyright (C) 2016 Aaron Schulz <aschulz@wikimedia.org>
+ *
+ * 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
+ */
+
+namespace Wikimedia;
+
+/**
+ * Wait loop that reaches a condition or times out
+ */
+class WaitConditionLoop {
+       /** @var callable */
+       private $condition;
+       /** @var callable[] */
+       private $busyCallbacks = [];
+       /** @var float Seconds */
+       private $timeout;
+       /** @var float Seconds */
+       private $lastWaitTime;
+       /** @var integer|null */
+       private $rusageMode;
+
+       const CONDITION_REACHED = 1;
+       const CONDITION_CONTINUE = 0; // evaluates as falsey
+       const CONDITION_FAILED = -1;
+       const CONDITION_TIMED_OUT = -2;
+       const CONDITION_ABORTED = -3;
+
+       /**
+        * @param callable $condition Callback that returns a WaitConditionLoop::CONDITION_ constant
+        * @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;
+
+               if ( defined( 'HHVM_VERSION' ) && PHP_OS === 'Linux' ) {
+                       $this->rusageMode = 2; // RUSAGE_THREAD
+               } elseif ( function_exists( 'getrusage' ) ) {
+                       $this->rusageMode = 0; // RUSAGE_SELF
+               }
+       }
+
+       /**
+        * Invoke the loop and continue until either:
+        *   - a) The condition callback returns neither CONDITION_CONTINUE nor false
+        *   - b) The timeout is reached
+        * This a condition callback can return true (stop) or false (continue) for convenience.
+        * In such cases, the halting result of "true" will be converted to CONDITION_REACHED.
+        *
+        * If $timeout is 0, then only the condition callback will be called (no busy callbacks),
+        * and this will immediately return CONDITION_FAILED if the condition was not met.
+        *
+        * Exceptions in callbacks will be caught and the callback will be swapped with
+        * one that simply rethrows that exception back to the caller when invoked.
+        *
+        * @return integer WaitConditionLoop::CONDITION_* constant
+        * @throws \Exception Any error from the condition callback
+        */
+       public function invoke() {
+               $elapsed = 0.0; // seconds
+               $sleepUs = 0; // microseconds to sleep each time
+               $lastCheck = false;
+               $finalResult = self::CONDITION_TIMED_OUT;
+               do {
+                       $checkStartTime = $this->getWallTime();
+                       // Check if the condition is met yet
+                       $realStart = $this->getWallTime();
+                       $cpuStart = $this->getCpuTime();
+                       $checkResult = call_user_func( $this->condition );
+                       $cpu = $this->getCpuTime() - $cpuStart;
+                       $real = $this->getWallTime() - $realStart;
+                       // Exit if the condition is reached, an error occurs, or this is non-blocking
+                       if ( $this->timeout <= 0 ) {
+                               $finalResult = $checkResult ? self::CONDITION_REACHED : self::CONDITION_FAILED;
+                               break;
+                       } elseif ( (int)$checkResult !== self::CONDITION_CONTINUE ) {
+                               if ( is_int( $checkResult ) ) {
+                                       $finalResult = $checkResult;
+                               } else {
+                                       $finalResult = self::CONDITION_REACHED;
+                               }
+                               break;
+                       } elseif ( $lastCheck ) {
+                               break; // timeout reached
+                       }
+                       // Detect if condition callback seems to block or if justs burns CPU
+                       $conditionUsesInterrupts = ( $real > 0.100 && $cpu <= $real * .03 );
+                       if ( !$this->popAndRunBusyCallback() && !$conditionUsesInterrupts ) {
+                               // 10 queries = 10(10+100)/2 ms = 550ms, 14 queries = 1050ms
+                               $sleepUs = min( $sleepUs + 10 * 1e3, 1e6 ); // stop incrementing at ~1s
+                               $this->usleep( $sleepUs );
+                       }
+                       $checkEndTime = $this->getWallTime();
+                       // The max() protects against the clock getting set back
+                       $elapsed += max( $checkEndTime - $checkStartTime, 0.010 );
+                       // Do not let slow callbacks timeout without checking the condition one more time
+                       $lastCheck = ( $elapsed >= $this->timeout );
+               } while ( true );
+
+               $this->lastWaitTime = $elapsed;
+
+               return $finalResult;
+       }
+
+       /**
+        * @return float Seconds
+        */
+       public function getLastWaitTime() {
+               return $this->lastWaitTime;
+       }
+
+       /**
+        * @param integer $microseconds
+        */
+       protected function usleep( $microseconds ) {
+               usleep( $microseconds );
+       }
+
+       /**
+        * @return float
+        */
+       protected function getWallTime() {
+               return microtime( true );
+       }
+
+       /**
+        * @return float Returns 0.0 if not supported (Windows on PHP < 7)
+        */
+       protected function getCpuTime() {
+               if ( $this->rusageMode === null ) {
+                       return microtime( true ); // assume worst case (all time is CPU)
+               }
+
+               $ru = getrusage( $this->rusageMode );
+               $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 one of the callbacks that does work ahead of time for another caller
+        *
+        * @return bool Whether a callback was executed
+        */
+       private function popAndRunBusyCallback() {
+               if ( $this->busyCallbacks ) {
+                       reset( $this->busyCallbacks );
+                       $key = key( $this->busyCallbacks );
+                       /** @var callable $workCallback */
+                       $workCallback =& $this->busyCallbacks[$key];
+                       try {
+                               $workCallback();
+                       } catch ( \Exception $e ) {
+                               $workCallback = function () use ( $e ) {
+                                       throw $e;
+                               };
+                       }
+                       unset( $this->busyCallbacks[$key] ); // consume
+
+                       return true;
+               }
+
+               return false;
+       }
+}