Completed
Branch master (90e9fc)
by
unknown
29:23
created

WaitConditionLoop::invoke()   C

Complexity

Conditions 8
Paths 7

Size

Total Lines 38
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 8
eloc 27
nc 7
nop 0
dl 0
loc 38
rs 5.3846
c 1
b 0
f 0
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
38
	const CONDITION_REACHED = 1;
39
	const CONDITION_CONTINUE = 0; // evaluates as falsey
40
	const CONDITION_TIMED_OUT = -1;
41
	const CONDITION_ABORTED = -2;
42
43
	/**
44
	 * @param callable $condition Callback that returns a WaitConditionLoop::CONDITION_ constant
45
	 * @param float $timeout Timeout in seconds
46
	 * @param array &$busyCallbacks List of callbacks to do useful work (by reference)
47
	 */
48
	public function __construct( callable $condition, $timeout = 5.0, &$busyCallbacks = [] ) {
49
		$this->condition = $condition;
50
		$this->timeout = $timeout;
51
		$this->busyCallbacks =& $busyCallbacks;
52
	}
53
54
	/**
55
	 * Invoke the loop and continue until either:
56
	 *   - a) The condition callback does not return either CONDITION_CONTINUE or true
57
	 *   - b) The timeout is reached
58
	 * This a condition callback can return true (stop) or false (continue) for convenience.
59
	 * In such cases, the halting result of "true" will be converted to CONDITION_REACHED.
60
	 *
61
	 * Exceptions in callbacks will be caught and the callback will be swapped with
62
	 * one that simply rethrows that exception back to the caller when invoked.
63
	 *
64
	 * @return integer WaitConditionLoop::CONDITION_* constant
65
	 * @throws Exception Any error from the condition callback
66
	 */
67
	public function invoke() {
68
		$elapsed = 0.0; // seconds
69
		$sleepUs = 0; // microseconds to sleep each time
70
		$lastCheck = false;
71
		$finalResult = self::CONDITION_TIMED_OUT;
72
		do {
73
			$checkStartTime = $this->getWallTime();
74
			// Check if the condition is met yet
75
			$realStart = $this->getWallTime();
76
			$cpuStart = $this->getCpuTime();
77
			$checkResult = call_user_func( $this->condition );
78
			$cpu = $this->getCpuTime() - $cpuStart;
79
			$real = $this->getWallTime() - $realStart;
80
			// Exit if the condition is reached
81
			if ( (int)$checkResult !== self::CONDITION_CONTINUE ) {
82
				$finalResult = is_int( $checkResult ) ? $checkResult : self::CONDITION_REACHED;
83
				break;
84
			} elseif ( $lastCheck ) {
85
				break; // timeout
86
			}
87
			// Detect if condition callback seems to block or if justs burns CPU
88
			$conditionUsesInterrupts = ( $real > 0.100 && $cpu <= $real * .03 );
89
			if ( !$this->popAndRunBusyCallback() && !$conditionUsesInterrupts ) {
90
				// 10 queries = 10(10+100)/2 ms = 550ms, 14 queries = 1050ms
91
				$sleepUs = min( $sleepUs + 10 * 1e3, 1e6 ); // stop incrementing at ~1s
92
				$this->usleep( $sleepUs );
93
			}
94
			$checkEndTime = $this->getWallTime();
95
			// The max() protects against the clock getting set back
96
			$elapsed += max( $checkEndTime - $checkStartTime, 0.010 );
97
			// Do not let slow callbacks timeout without checking the condition one more time
98
			$lastCheck = ( $elapsed >= $this->timeout );
99
		} while ( true );
100
101
		$this->lastWaitTime = $elapsed;
102
103
		return $finalResult;
104
	}
105
106
	/**
107
	 * @return float Seconds
108
	 */
109
	public function getLastWaitTime() {
110
		return $this->lastWaitTime;
111
	}
112
113
	/**
114
	 * @param integer $microseconds
115
	 */
116
	protected function usleep( $microseconds ) {
117
		usleep( $microseconds );
118
	}
119
120
	/**
121
	 * @return float
122
	 */
123
	protected function getWallTime() {
124
		return microtime( true );
125
	}
126
127
	/**
128
	 * @return float Returns 0.0 if not supported (Windows on PHP < 7)
129
	 */
130
	protected function getCpuTime() {
131
		$time = 0.0;
132
133
		if ( defined( 'HHVM_VERSION' ) && PHP_OS === 'Linux' ) {
134
			$ru = getrusage( 2 /* RUSAGE_THREAD */ );
135
		} else {
136
			$ru = getrusage( 0 /* RUSAGE_SELF */ );
137
		}
138
		if ( $ru ) {
139
			$time += $ru['ru_utime.tv_sec'] + $ru['ru_utime.tv_usec'] / 1e6;
140
			$time += $ru['ru_stime.tv_sec'] + $ru['ru_stime.tv_usec'] / 1e6;
141
		}
142
143
		return $time;
144
	}
145
146
	/**
147
	 * Run one of the callbacks that does work ahead of time for another caller
148
	 *
149
	 * @return bool Whether a callback was executed
150
	 */
151
	private function popAndRunBusyCallback() {
152
		if ( $this->busyCallbacks ) {
153
			reset( $this->busyCallbacks );
154
			$key = key( $this->busyCallbacks );
155
			/** @var callable $workCallback */
156
			$workCallback =& $this->busyCallbacks[$key];
157
			try {
158
				$workCallback();
159
			} catch ( Exception $e ) {
160
				$workCallback = function () use ( $e ) {
161
					throw $e;
162
				};
163
			}
164
			unset( $this->busyCallbacks[$key] ); // consume
165
166
			return true;
167
		}
168
169
		return false;
170
	}
171
}
172