Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
96.77% covered (success)
96.77%
30 / 31
85.71% covered (warning)
85.71%
6 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
ExcimerTimerWrapper
96.77% covered (success)
96.77%
30 / 31
85.71% covered (warning)
85.71%
6 / 7
14
0.00% covered (danger)
0.00%
0 / 1
 enterCriticalSection
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 exitCriticalSection
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 setWallTimeLimit
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
3
 onTimeout
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 getWallTimeRemaining
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
2.15
 getWallTimeLimit
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 stop
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2
3namespace Wikimedia\RequestTimeout\Detail;
4
5use ExcimerTimer;
6use Wikimedia\RequestTimeout\RequestTimeoutException;
7use 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 */
18class 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}