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