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