Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
96.77% |
30 / 31 |
|
85.71% |
6 / 7 |
CRAP | |
0.00% |
0 / 1 |
ExcimerTimerWrapper | |
96.77% |
30 / 31 |
|
85.71% |
6 / 7 |
14 | |
0.00% |
0 / 1 |
enterCriticalSection | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
exitCriticalSection | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
3 | |||
setWallTimeLimit | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
3 | |||
onTimeout | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
getWallTimeRemaining | |
66.67% |
2 / 3 |
|
0.00% |
0 / 1 |
2.15 | |||
getWallTimeLimit | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
stop | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 |
1 | <?php |
2 | |
3 | namespace Wikimedia\RequestTimeout\Detail; |
4 | |
5 | use ExcimerTimer; |
6 | use Wikimedia\RequestTimeout\RequestTimeoutException; |
7 | use Wikimedia\RequestTimeout\TimeoutException; |
8 | |
9 | /** |
10 | * It's difficult to avoid the circular reference in $this->timer due to the |
11 | * callback closure, which means this object is not destroyed implicitly when |
12 | * it goes out of scope. So ExcimerRequestTimeout is split into an implicitly |
13 | * destructible part (ExcimerRequestTimeout) and a part which must be |
14 | * explicitly destroyed (this class). |
15 | * |
16 | * @internal |
17 | */ |
18 | class ExcimerTimerWrapper { |
19 | /** @var ExcimerTimer|null */ |
20 | private $timer; |
21 | |
22 | /** @var int The next critical section ID to use */ |
23 | private $nextCriticalId = 1; |
24 | |
25 | /** @var CriticalSection[] */ |
26 | private $criticalSections = []; |
27 | |
28 | /** @var array|null Data about the pending timeout, or null if no timeout is pending */ |
29 | private $pending; |
30 | |
31 | /** @var float */ |
32 | private $limit = INF; |
33 | |
34 | /** |
35 | * @param string $name |
36 | * @param float $emergencyLimit |
37 | * @param callable|null $emergencyCallback |
38 | * @return int |
39 | */ |
40 | public function enterCriticalSection( $name, $emergencyLimit, $emergencyCallback ) { |
41 | $id = $this->nextCriticalId++; |
42 | $this->criticalSections[$id] = new CriticalSection( |
43 | $name, $emergencyLimit, $emergencyCallback ); |
44 | return $id; |
45 | } |
46 | |
47 | /** |
48 | * @param int $id |
49 | * @throws TimeoutException |
50 | */ |
51 | public function exitCriticalSection( $id ) { |
52 | if ( isset( $this->criticalSections[$id] ) ) { |
53 | $this->criticalSections[$id]->stop(); |
54 | unset( $this->criticalSections[$id] ); |
55 | } |
56 | if ( $this->pending ) { |
57 | $limit = $this->pending['limit']; |
58 | $this->pending = null; |
59 | throw new RequestTimeoutException( $limit ); |
60 | } |
61 | } |
62 | |
63 | /** |
64 | * @param float $limit The limit in seconds |
65 | */ |
66 | public function setWallTimeLimit( $limit ) { |
67 | if ( $limit > 0 && $limit !== INF ) { |
68 | $this->limit = (float)$limit; |
69 | $this->timer = new ExcimerTimer; |
70 | $this->timer->setInterval( $limit ); |
71 | $this->timer->setCallback( function () use ( $limit ) { |
72 | $this->onTimeout( $limit ); |
73 | } ); |
74 | $this->timer->start(); |
75 | } else { |
76 | $this->stop(); |
77 | $this->limit = INF; |
78 | } |
79 | } |
80 | |
81 | /** |
82 | * Callback function for the main request timeout. If any critical section |
83 | * is open, queue the event. Otherwise, throw the exception now. |
84 | * |
85 | * The limit is passed as a parameter, instead of using an object property, |
86 | * to provide greater assurance that the reported limit is the one that is |
87 | * actually timing out and not the result of a separate call to |
88 | * setWallTimeLimit(). |
89 | * |
90 | * @param float $limit |
91 | * @throws TimeoutException |
92 | */ |
93 | private function onTimeout( $limit ) { |
94 | if ( count( $this->criticalSections ) ) { |
95 | $this->pending = [ 'limit' => $limit ]; |
96 | } else { |
97 | throw new RequestTimeoutException( $limit ); |
98 | } |
99 | } |
100 | |
101 | /** |
102 | * Get the amount of time remaining of the limit. |
103 | * |
104 | * @return float |
105 | */ |
106 | public function getWallTimeRemaining() { |
107 | if ( $this->timer ) { |
108 | return $this->timer->getTime(); |
109 | } else { |
110 | return INF; |
111 | } |
112 | } |
113 | |
114 | /** |
115 | * Get the current wall time limit, or INF if there is no limit |
116 | * |
117 | * @return float |
118 | */ |
119 | public function getWallTimeLimit() { |
120 | return $this->limit; |
121 | } |
122 | |
123 | /** |
124 | * Stop and destroy the underlying timer. |
125 | */ |
126 | public function stop() { |
127 | if ( $this->timer ) { |
128 | $this->timer->stop(); |
129 | } |
130 | $this->timer = null; |
131 | } |
132 | } |