Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
96.97% covered (success)
96.97%
32 / 33
80.00% covered (warning)
80.00%
4 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
CriticalSectionProvider
96.97% covered (success)
96.97%
32 / 33
80.00% covered (warning)
80.00%
4 / 5
7
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 enter
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
1
 exit
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 scopedEnter
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
1
 getEmergencyLimit
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3namespace Wikimedia\RequestTimeout;
4
5/**
6 * A class providing a named critical section concept. When the code is inside
7 * a critical section, if a request timeout occurs, it is queued and then
8 * delivered when the critical section exits.
9 *
10 * The class stores configuration for "emergency timeouts". This is a second
11 * timeout which limits the amount of time a critical section may be open.
12 */
13class CriticalSectionProvider {
14    /** @var RequestTimeout */
15    private $requestTimeout;
16
17    /** @var float */
18    private $emergencyLimit;
19
20    /** @var callable|null */
21    private $emergencyCallback;
22
23    /** @var callable|null */
24    private $implicitExitCallback;
25
26    /** @var array */
27    private $stack = [];
28
29    /**
30     * @internal Use RequestTimeout::createCriticalSectionProvider()
31     *
32     * @param RequestTimeout $requestTimeout The parent object
33     * @param float $emergencyLimit The emergency timeout in seconds
34     * @param callable|null $emergencyCallback A callback to call when the
35     *   emergency timeout expires. If null, an exception will be thrown.
36     * @param callable|null $implicitExitCallback A callback to call when a scoped
37     *   critical section is exited implicitly by scope destruction, rather than
38     *   by CriticalSectionScope::exit().
39     */
40    public function __construct(
41        RequestTimeout $requestTimeout,
42        $emergencyLimit,
43        $emergencyCallback,
44        $implicitExitCallback
45    ) {
46        $this->requestTimeout = $requestTimeout;
47        $this->emergencyLimit = $emergencyLimit;
48        $this->emergencyCallback = $emergencyCallback;
49        $this->implicitExitCallback = $implicitExitCallback;
50    }
51
52    /**
53     * Enter a critical section, giving it a name. The name should uniquely
54     * identify the calling code.
55     *
56     * Multiple critical sections may be active at a given time. Critical
57     * sections created by this method must be exited in the reverse order to
58     * which they were created, i.e. there is a stack of named critical
59     * sections.
60     *
61     * @param string $name
62     * @param float|null $emergencyLimit If non-null, this will override the
63     *   configured emergency timeout
64     * @param callable|null $emergencyCallback If non-null, this will override
65     *   the configured emergency timeout callback.
66     */
67    public function enter( $name, $emergencyLimit = null, $emergencyCallback = null ) {
68        $id = $this->requestTimeout->enterCriticalSection(
69            $name,
70            $emergencyLimit ?? $this->emergencyLimit,
71            $emergencyCallback ?? $this->emergencyCallback
72        );
73        $this->stack[ count( $this->stack ) ] = [
74            'name' => $name,
75            'id' => $id
76        ];
77    }
78
79    /**
80     * Exit a named critical section. If the name does not match the most recent
81     * call to enter(), an exception will be thrown.
82     *
83     * @throws CriticalSectionMismatchException
84     * @throws TimeoutException
85     * @param string $name
86     */
87    public function exit( $name ) {
88        $i = count( $this->stack ) - 1;
89        if ( $i === -1 ) {
90            throw new CriticalSectionMismatchException( $name, '[none]' );
91        }
92        if ( $this->stack[$i]['name'] !== $name ) {
93            throw new CriticalSectionMismatchException( $name, $this->stack[$i]['name'] );
94        }
95        $this->requestTimeout->exitCriticalSection( $this->stack[$i]['id'] );
96        unset( $this->stack[$i] );
97    }
98
99    /**
100     * Enter a critical section, and return a scope variable. The critical
101     * section will automatically exit when the scope variable is destroyed.
102     *
103     * Multiple critical sections may be active at a given time. There is no
104     * restriction on the order in which critical sections created by this
105     * method are exited.
106     *
107     * NOTE: Callers should typically call CriticalSectionScope::exit() instead
108     * of waiting for __destruct() to be called, since exiting a critical
109     * section may throw a timeout exception, but it is a fatal error to throw
110     * an exception from a destructor during request shutdown.
111     *
112     * @param string $name A name for the critical section, used in error messages
113     * @param float|null $emergencyLimit If non-null, this will override the
114     *   configured emergency timeout
115     * @param callable|null $emergencyCallback If non-null, this will override
116     *   the configured emergency timeout callback.
117     * @param callable|null $implicitExitCallback If non-null, this will override
118     *   the configured implicit exit callback. The callback will be called if the
119     *   section is exited in __destruct() instead of by calling exit().
120     * @return CriticalSectionScope
121     */
122    public function scopedEnter( $name, $emergencyLimit = null,
123        $emergencyCallback = null, $implicitExitCallback = null
124    ) {
125        $id = $this->requestTimeout->enterCriticalSection(
126            $name,
127            $emergencyLimit ?? $this->emergencyLimit,
128            $emergencyCallback ?? $this->emergencyCallback
129        );
130
131        return new CriticalSectionScope(
132            $id,
133            function ( $id ) {
134                $this->requestTimeout->exitCriticalSection( $id );
135            },
136            $implicitExitCallback ?? $this->implicitExitCallback
137        );
138    }
139
140    /**
141     * Get the configured emergency time limit
142     *
143     * @since 1.1.0
144     * @return float
145     */
146    public function getEmergencyLimit() {
147        return $this->emergencyLimit;
148    }
149}