]> scripts.mit.edu Git - autoinstallsdev/mediawiki.git/blob - vendor/wikimedia/wait-condition-loop/src/WaitConditionLoop.php
MediaWiki 1.30.2
[autoinstallsdev/mediawiki.git] / vendor / wikimedia / wait-condition-loop / src / WaitConditionLoop.php
1 <?php
2 /**
3  * Wait loop that reaches a condition or times out.
4  *
5  * Copyright (C) 2016 Aaron Schulz <aschulz@wikimedia.org>
6  *
7  * This program is free software; you can redistribute it and/or modify
8  * it under the terms of the GNU General Public License as published by
9  * the Free Software Foundation; either version 2 of the License, or
10  * (at your option) any later version.
11  *
12  * This program is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15  * GNU General Public License for more details.
16  *
17  * You should have received a copy of the GNU General Public License along
18  * with this program; if not, write to the Free Software Foundation, Inc.,
19  * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
20  * http://www.gnu.org/copyleft/gpl.html
21  *
22  * @file
23  * @author Aaron Schulz
24  */
25
26 namespace Wikimedia;
27
28 /**
29  * Wait loop that reaches a condition or times out
30  */
31 class WaitConditionLoop {
32         /** @var callable */
33         private $condition;
34         /** @var callable[] */
35         private $busyCallbacks = [];
36         /** @var float Seconds */
37         private $timeout;
38         /** @var float Seconds */
39         private $lastWaitTime;
40         /** @var integer|null */
41         private $rusageMode;
42
43         const CONDITION_REACHED = 1;
44         const CONDITION_CONTINUE = 0; // evaluates as falsey
45         const CONDITION_FAILED = -1;
46         const CONDITION_TIMED_OUT = -2;
47         const CONDITION_ABORTED = -3;
48
49         /**
50          * @param callable $condition Callback that returns a WaitConditionLoop::CONDITION_ constant
51          * @param float $timeout Timeout in seconds
52          * @param array &$busyCallbacks List of callbacks to do useful work (by reference)
53          */
54         public function __construct( callable $condition, $timeout = 5.0, &$busyCallbacks = [] ) {
55                 $this->condition = $condition;
56                 $this->timeout = $timeout;
57                 $this->busyCallbacks =& $busyCallbacks;
58
59                 if ( defined( 'HHVM_VERSION' ) && PHP_OS === 'Linux' ) {
60                         $this->rusageMode = 2; // RUSAGE_THREAD
61                 } elseif ( function_exists( 'getrusage' ) ) {
62                         $this->rusageMode = 0; // RUSAGE_SELF
63                 }
64         }
65
66         /**
67          * Invoke the loop and continue until either:
68          *   - a) The condition callback returns neither CONDITION_CONTINUE nor false
69          *   - b) The timeout is reached
70          * This a condition callback can return true (stop) or false (continue) for convenience.
71          * In such cases, the halting result of "true" will be converted to CONDITION_REACHED.
72          *
73          * If $timeout is 0, then only the condition callback will be called (no busy callbacks),
74          * and this will immediately return CONDITION_FAILED if the condition was not met.
75          *
76          * Exceptions in callbacks will be caught and the callback will be swapped with
77          * one that simply rethrows that exception back to the caller when invoked.
78          *
79          * @return integer WaitConditionLoop::CONDITION_* constant
80          * @throws \Exception Any error from the condition callback
81          */
82         public function invoke() {
83                 $elapsed = 0.0; // seconds
84                 $sleepUs = 0; // microseconds to sleep each time
85                 $lastCheck = false;
86                 $finalResult = self::CONDITION_TIMED_OUT;
87                 do {
88                         $checkStartTime = $this->getWallTime();
89                         // Check if the condition is met yet
90                         $realStart = $this->getWallTime();
91                         $cpuStart = $this->getCpuTime();
92                         $checkResult = call_user_func( $this->condition );
93                         $cpu = $this->getCpuTime() - $cpuStart;
94                         $real = $this->getWallTime() - $realStart;
95                         // Exit if the condition is reached, an error occurs, or this is non-blocking
96                         if ( $this->timeout <= 0 ) {
97                                 $finalResult = $checkResult ? self::CONDITION_REACHED : self::CONDITION_FAILED;
98                                 break;
99                         } elseif ( (int)$checkResult !== self::CONDITION_CONTINUE ) {
100                                 if ( is_int( $checkResult ) ) {
101                                         $finalResult = $checkResult;
102                                 } else {
103                                         $finalResult = self::CONDITION_REACHED;
104                                 }
105                                 break;
106                         } elseif ( $lastCheck ) {
107                                 break; // timeout reached
108                         }
109                         // Detect if condition callback seems to block or if justs burns CPU
110                         $conditionUsesInterrupts = ( $real > 0.100 && $cpu <= $real * .03 );
111                         if ( !$this->popAndRunBusyCallback() && !$conditionUsesInterrupts ) {
112                                 // 10 queries = 10(10+100)/2 ms = 550ms, 14 queries = 1050ms
113                                 $sleepUs = min( $sleepUs + 10 * 1e3, 1e6 ); // stop incrementing at ~1s
114                                 $this->usleep( $sleepUs );
115                         }
116                         $checkEndTime = $this->getWallTime();
117                         // The max() protects against the clock getting set back
118                         $elapsed += max( $checkEndTime - $checkStartTime, 0.010 );
119                         // Do not let slow callbacks timeout without checking the condition one more time
120                         $lastCheck = ( $elapsed >= $this->timeout );
121                 } while ( true );
122
123                 $this->lastWaitTime = $elapsed;
124
125                 return $finalResult;
126         }
127
128         /**
129          * @return float Seconds
130          */
131         public function getLastWaitTime() {
132                 return $this->lastWaitTime;
133         }
134
135         /**
136          * @param integer $microseconds
137          */
138         protected function usleep( $microseconds ) {
139                 usleep( $microseconds );
140         }
141
142         /**
143          * @return float
144          */
145         protected function getWallTime() {
146                 return microtime( true );
147         }
148
149         /**
150          * @return float Returns 0.0 if not supported (Windows on PHP < 7)
151          */
152         protected function getCpuTime() {
153                 if ( $this->rusageMode === null ) {
154                         return microtime( true ); // assume worst case (all time is CPU)
155                 }
156
157                 $ru = getrusage( $this->rusageMode );
158                 $time = $ru['ru_utime.tv_sec'] + $ru['ru_utime.tv_usec'] / 1e6;
159                 $time += $ru['ru_stime.tv_sec'] + $ru['ru_stime.tv_usec'] / 1e6;
160
161                 return $time;
162         }
163
164         /**
165          * Run one of the callbacks that does work ahead of time for another caller
166          *
167          * @return bool Whether a callback was executed
168          */
169         private function popAndRunBusyCallback() {
170                 if ( $this->busyCallbacks ) {
171                         reset( $this->busyCallbacks );
172                         $key = key( $this->busyCallbacks );
173                         /** @var callable $workCallback */
174                         $workCallback =& $this->busyCallbacks[$key];
175                         try {
176                                 $workCallback();
177                         } catch ( \Exception $e ) {
178                                 $workCallback = function () use ( $e ) {
179                                         throw $e;
180                                 };
181                         }
182                         unset( $this->busyCallbacks[$key] ); // consume
183
184                         return true;
185                 }
186
187                 return false;
188         }
189 }