3 * Wait loop that reaches a condition or times out.
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.
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.
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
21 * @author Aaron Schulz
25 * Wait loop that reaches a condition or times out
28 class WaitConditionLoop
{
31 /** @var callable[] */
32 private $busyCallbacks = [];
33 /** @var float Seconds */
35 /** @var float Seconds */
36 private $lastWaitTime;
38 const CONDITION_REACHED
= 1;
39 const CONDITION_CONTINUE
= 0; // evaluates as falsey
40 const CONDITION_FAILED
= -1;
41 const CONDITION_TIMED_OUT
= -2;
42 const CONDITION_ABORTED
= -3;
45 * @param callable $condition Callback that returns a WaitConditionLoop::CONDITION_ constant
46 * @param float $timeout Timeout in seconds
47 * @param array &$busyCallbacks List of callbacks to do useful work (by reference)
49 public function __construct( callable
$condition, $timeout = 5.0, &$busyCallbacks = [] ) {
50 $this->condition
= $condition;
51 $this->timeout
= $timeout;
52 $this->busyCallbacks
=& $busyCallbacks;
56 * Invoke the loop and continue until either:
57 * - a) The condition callback returns neither CONDITION_CONTINUE nor false
58 * - b) The timeout is reached
59 * This a condition callback can return true (stop) or false (continue) for convenience.
60 * In such cases, the halting result of "true" will be converted to CONDITION_REACHED.
62 * If $timeout is 0, then only the condition callback will be called (no busy callbacks),
63 * and this will immediately return CONDITION_FAILED if the condition was not met.
65 * Exceptions in callbacks will be caught and the callback will be swapped with
66 * one that simply rethrows that exception back to the caller when invoked.
68 * @return integer WaitConditionLoop::CONDITION_* constant
69 * @throws Exception Any error from the condition callback
71 public function invoke() {
72 $elapsed = 0.0; // seconds
73 $sleepUs = 0; // microseconds to sleep each time
75 $finalResult = self
::CONDITION_TIMED_OUT
;
77 $checkStartTime = $this->getWallTime();
78 // Check if the condition is met yet
79 $realStart = $this->getWallTime();
80 $cpuStart = $this->getCpuTime();
81 $checkResult = call_user_func( $this->condition
);
82 $cpu = $this->getCpuTime() - $cpuStart;
83 $real = $this->getWallTime() - $realStart;
84 // Exit if the condition is reached, and error occurs, or this is non-blocking
85 if ( $this->timeout
<= 0 ) {
86 $finalResult = $checkResult ? self
::CONDITION_REACHED
: self
::CONDITION_FAILED
;
88 } elseif ( (int)$checkResult !== self
::CONDITION_CONTINUE
) {
89 if ( is_int( $checkResult ) ) {
90 $finalResult = $checkResult;
92 $finalResult = self
::CONDITION_REACHED
;
95 } elseif ( $lastCheck ) {
96 break; // timeout reached
98 // Detect if condition callback seems to block or if justs burns CPU
99 $conditionUsesInterrupts = ( $real > 0.100 && $cpu <= $real * .03 );
100 if ( !$this->popAndRunBusyCallback() && !$conditionUsesInterrupts ) {
101 // 10 queries = 10(10+100)/2 ms = 550ms, 14 queries = 1050ms
102 $sleepUs = min( $sleepUs +
10 * 1e3
, 1e6
); // stop incrementing at ~1s
103 $this->usleep( $sleepUs );
105 $checkEndTime = $this->getWallTime();
106 // The max() protects against the clock getting set back
107 $elapsed +
= max( $checkEndTime - $checkStartTime, 0.010 );
108 // Do not let slow callbacks timeout without checking the condition one more time
109 $lastCheck = ( $elapsed >= $this->timeout
);
112 $this->lastWaitTime
= $elapsed;
118 * @return float Seconds
120 public function getLastWaitTime() {
121 return $this->lastWaitTime
;
125 * @param integer $microseconds
127 protected function usleep( $microseconds ) {
128 usleep( $microseconds );
134 protected function getWallTime() {
135 return microtime( true );
139 * @return float Returns 0.0 if not supported (Windows on PHP < 7)
141 protected function getCpuTime() {
144 if ( defined( 'HHVM_VERSION' ) && PHP_OS
=== 'Linux' ) {
145 $ru = getrusage( 2 /* RUSAGE_THREAD */ );
147 $ru = getrusage( 0 /* RUSAGE_SELF */ );
150 $time +
= $ru['ru_utime.tv_sec'] +
$ru['ru_utime.tv_usec'] / 1e6
;
151 $time +
= $ru['ru_stime.tv_sec'] +
$ru['ru_stime.tv_usec'] / 1e6
;
158 * Run one of the callbacks that does work ahead of time for another caller
160 * @return bool Whether a callback was executed
162 private function popAndRunBusyCallback() {
163 if ( $this->busyCallbacks
) {
164 reset( $this->busyCallbacks
);
165 $key = key( $this->busyCallbacks
);
166 /** @var callable $workCallback */
167 $workCallback =& $this->busyCallbacks
[$key];
170 } catch ( Exception
$e ) {
171 $workCallback = function () use ( $e ) {
175 unset( $this->busyCallbacks
[$key] ); // consume