Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
100.00% |
46 / 46 |
|
100.00% |
4 / 4 |
CRAP | |
100.00% |
1 / 1 |
WaitConditionLoop | |
100.00% |
46 / 46 |
|
100.00% |
4 / 4 |
19 | |
100.00% |
1 / 1 |
__construct | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
invoke | |
100.00% |
30 / 30 |
|
100.00% |
1 / 1 |
9 | |||
getLastWaitTime | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
usleep | n/a |
0 / 0 |
n/a |
0 / 0 |
1 | |||||
getWallTime | n/a |
0 / 0 |
n/a |
0 / 0 |
1 | |||||
getCpuTime | n/a |
0 / 0 |
n/a |
0 / 0 |
2 | |||||
popAndRunBusyCallback | |
100.00% |
12 / 12 |
|
100.00% |
1 / 1 |
3 |
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 int|null */ |
41 | private $rusageMode; |
42 | |
43 | public const CONDITION_REACHED = 1; |
44 | // evaluates as falsey |
45 | public const CONDITION_CONTINUE = 0; |
46 | public const CONDITION_FAILED = -1; |
47 | public const CONDITION_TIMED_OUT = -2; |
48 | public const CONDITION_ABORTED = -3; |
49 | |
50 | /** |
51 | * @param callable $condition Callback that returns a WaitConditionLoop::CONDITION_ constant |
52 | * @param float $timeout Timeout in seconds |
53 | * @param array &$busyCallbacks List of callbacks to do useful work (by reference) |
54 | */ |
55 | public function __construct( callable $condition, $timeout = 5.0, &$busyCallbacks = [] ) { |
56 | $this->condition = $condition; |
57 | $this->timeout = $timeout; |
58 | $this->busyCallbacks =& $busyCallbacks; |
59 | |
60 | // @codeCoverageIgnoreStart |
61 | if ( function_exists( 'getrusage' ) ) { |
62 | // RUSAGE_SELF |
63 | $this->rusageMode = 0; |
64 | } |
65 | // @codeCoverageIgnoreEnd |
66 | } |
67 | |
68 | /** |
69 | * Invoke the loop and continue until either: |
70 | * - a) The condition callback returns neither CONDITION_CONTINUE nor false |
71 | * - b) The timeout is reached |
72 | * Thus a condition callback can return true (stop) or false (continue) for convenience. |
73 | * In such cases, the halting result of "true" will be converted to CONDITION_REACHED. |
74 | * |
75 | * If $timeout is 0, then only the condition callback will be called (no busy callbacks), |
76 | * and this will immediately return CONDITION_FAILED if the condition was not met. |
77 | * |
78 | * Exceptions in callbacks will be caught and the callback will be swapped with |
79 | * one that simply rethrows that exception back to the caller when invoked. |
80 | * |
81 | * @return int WaitConditionLoop::CONDITION_* constant |
82 | * @throws \Exception Any error from the condition callback |
83 | */ |
84 | public function invoke() { |
85 | // seconds |
86 | $elapsed = 0.0; |
87 | // microseconds to sleep each time |
88 | $sleepUs = 0; |
89 | $lastCheck = false; |
90 | $finalResult = self::CONDITION_TIMED_OUT; |
91 | do { |
92 | $checkStartTime = $this->getWallTime(); |
93 | // Check if the condition is met yet |
94 | $realStart = $this->getWallTime(); |
95 | $cpuStart = $this->getCpuTime(); |
96 | $checkResult = call_user_func( $this->condition ); |
97 | $cpu = $this->getCpuTime() - $cpuStart; |
98 | $real = $this->getWallTime() - $realStart; |
99 | // Exit if the condition is reached, an error occurs, or this is non-blocking |
100 | if ( $this->timeout <= 0 ) { |
101 | // Accepts boolean true or self::CONDITION_REACHED |
102 | $finalResult = $checkResult > 0 ? self::CONDITION_REACHED : self::CONDITION_FAILED; |
103 | break; |
104 | } elseif ( (int)$checkResult !== self::CONDITION_CONTINUE ) { |
105 | if ( is_int( $checkResult ) ) { |
106 | $finalResult = $checkResult; |
107 | } else { |
108 | $finalResult = self::CONDITION_REACHED; |
109 | } |
110 | break; |
111 | } elseif ( $lastCheck ) { |
112 | // timeout reached |
113 | break; |
114 | } |
115 | // Detect if condition callback seems to block or if justs burns CPU |
116 | $conditionUsesInterrupts = ( $real > 0.100 && $cpu <= $real * 0.03 ); |
117 | if ( !$this->popAndRunBusyCallback() && !$conditionUsesInterrupts ) { |
118 | // 10 queries = 10(10+100)/2 ms = 550ms, 14 queries = 1050ms |
119 | // stop incrementing at ~1s |
120 | $sleepUs = min( $sleepUs + 10 * 1e3, 1e6 ); |
121 | $this->usleep( $sleepUs ); |
122 | } |
123 | $checkEndTime = $this->getWallTime(); |
124 | // The max() protects against the clock getting set back |
125 | $elapsed += max( $checkEndTime - $checkStartTime, 0.010 ); |
126 | // Do not let slow callbacks timeout without checking the condition one more time |
127 | $lastCheck = ( $elapsed >= $this->timeout ); |
128 | } while ( true ); |
129 | |
130 | $this->lastWaitTime = $elapsed; |
131 | |
132 | return $finalResult; |
133 | } |
134 | |
135 | /** |
136 | * @return float Seconds |
137 | */ |
138 | public function getLastWaitTime() { |
139 | return $this->lastWaitTime; |
140 | } |
141 | |
142 | /** |
143 | * @param int $microseconds |
144 | * @codeCoverageIgnore |
145 | */ |
146 | protected function usleep( $microseconds ) { |
147 | usleep( $microseconds ); |
148 | } |
149 | |
150 | /** |
151 | * @return float |
152 | * @codeCoverageIgnore |
153 | */ |
154 | protected function getWallTime() { |
155 | return microtime( true ); |
156 | } |
157 | |
158 | /** |
159 | * @return float Returns 0.0 if not supported (Windows on PHP < 7) |
160 | * @codeCoverageIgnore |
161 | */ |
162 | protected function getCpuTime() { |
163 | if ( $this->rusageMode === null ) { |
164 | // assume worst case (all time is CPU) |
165 | return microtime( true ); |
166 | } |
167 | |
168 | $ru = getrusage( $this->rusageMode ); |
169 | $time = $ru['ru_utime.tv_sec'] + $ru['ru_utime.tv_usec'] / 1e6; |
170 | $time += $ru['ru_stime.tv_sec'] + $ru['ru_stime.tv_usec'] / 1e6; |
171 | |
172 | return $time; |
173 | } |
174 | |
175 | /** |
176 | * Run one of the callbacks that does work ahead of time for another caller |
177 | * |
178 | * @return bool Whether a callback was executed |
179 | */ |
180 | private function popAndRunBusyCallback() { |
181 | if ( $this->busyCallbacks ) { |
182 | reset( $this->busyCallbacks ); |
183 | $key = key( $this->busyCallbacks ); |
184 | /** @var callable $workCallback */ |
185 | $workCallback =& $this->busyCallbacks[$key]; |
186 | try { |
187 | $workCallback(); |
188 | } catch ( \Exception $e ) { |
189 | /** @return never */ |
190 | $workCallback = static function () use ( $e ) { |
191 | throw $e; |
192 | }; |
193 | } |
194 | // consume |
195 | unset( $this->busyCallbacks[$key] ); |
196 | |
197 | return true; |
198 | } |
199 | |
200 | return false; |
201 | } |
202 | } |