Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
84.62% |
44 / 52 |
|
66.67% |
6 / 9 |
CRAP | |
0.00% |
0 / 1 |
LockManager | |
84.62% |
44 / 52 |
|
66.67% |
6 / 9 |
24.93 | |
0.00% |
0 / 1 |
__construct | |
71.43% |
10 / 14 |
|
0.00% |
0 / 1 |
8.14 | |||
lock | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
lockByType | |
100.00% |
11 / 11 |
|
100.00% |
1 / 1 |
2 | |||
unlock | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
unlockByType | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
sha1Base36Absolute | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
normalizePathsByType | |
85.71% |
6 / 7 |
|
0.00% |
0 / 1 |
4.05 | |||
doLockByType | |
70.00% |
7 / 10 |
|
0.00% |
0 / 1 |
4.43 | |||
doLock | n/a |
0 / 0 |
n/a |
0 / 0 |
0 | |||||
doUnlockByType | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
doUnlock | n/a |
0 / 0 |
n/a |
0 / 0 |
0 |
1 | <?php |
2 | /** |
3 | * This program is free software; you can redistribute it and/or modify |
4 | * it under the terms of the GNU General Public License as published by |
5 | * the Free Software Foundation; either version 2 of the License, or |
6 | * (at your option) any later version. |
7 | * |
8 | * This program is distributed in the hope that it will be useful, |
9 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
10 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
11 | * GNU General Public License for more details. |
12 | * |
13 | * You should have received a copy of the GNU General Public License along |
14 | * with this program; if not, write to the Free Software Foundation, Inc., |
15 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
16 | * http://www.gnu.org/copyleft/gpl.html |
17 | * |
18 | * @file |
19 | */ |
20 | |
21 | use Psr\Log\LoggerInterface; |
22 | use Psr\Log\NullLogger; |
23 | use Wikimedia\RequestTimeout\RequestTimeout; |
24 | use Wikimedia\WaitConditionLoop; |
25 | |
26 | /** |
27 | * @defgroup LockManager Lock management |
28 | * @ingroup FileBackend |
29 | */ |
30 | |
31 | /** |
32 | * Resource locking handling. |
33 | * |
34 | * Locks on resource keys can either be shared or exclusive. |
35 | * |
36 | * Implementations must keep track of what is locked by this process |
37 | * in-memory and support nested locking calls (using reference counting). |
38 | * At least LOCK_UW and LOCK_EX must be implemented. LOCK_SH can be a no-op. |
39 | * Locks should either be non-blocking or have low wait timeouts. |
40 | * |
41 | * Subclasses should avoid throwing exceptions at all costs. |
42 | * |
43 | * @stable to extend |
44 | * @ingroup LockManager |
45 | * @since 1.19 |
46 | */ |
47 | abstract class LockManager { |
48 | /** @var LoggerInterface */ |
49 | protected $logger; |
50 | |
51 | /** @var array Mapping of lock types to the type actually used */ |
52 | protected $lockTypeMap = [ |
53 | self::LOCK_SH => self::LOCK_SH, |
54 | self::LOCK_UW => self::LOCK_EX, // subclasses may use self::LOCK_SH |
55 | self::LOCK_EX => self::LOCK_EX |
56 | ]; |
57 | |
58 | /** @var array Map of (resource path => lock type => count) */ |
59 | protected $locksHeld = []; |
60 | |
61 | /** @var string domain (usually wiki ID) */ |
62 | protected $domain; |
63 | /** @var int maximum time locks can be held */ |
64 | protected $lockTTL; |
65 | |
66 | /** @var string Random 32-char hex number */ |
67 | protected $session; |
68 | |
69 | /** Lock types; stronger locks have higher values */ |
70 | public const LOCK_SH = 1; // shared lock (for reads) |
71 | public const LOCK_UW = 2; // shared lock (for reads used to write elsewhere) |
72 | public const LOCK_EX = 3; // exclusive lock (for writes) |
73 | |
74 | /** Max expected lock expiry in any context */ |
75 | protected const MAX_LOCK_TTL = 2 * 3600; // 2 hours |
76 | |
77 | /** Default lock TTL in CLI mode */ |
78 | protected const CLI_LOCK_TTL = 3600; // 1 hour |
79 | |
80 | /** Minimum lock TTL. The configured lockTTL is ignored if it is less than this value. */ |
81 | protected const MIN_LOCK_TTL = 5; // seconds |
82 | |
83 | /** The minimum lock TTL if it is guessed from max_execution_time rather than configured. */ |
84 | protected const MIN_GUESSED_LOCK_TTL = 5 * 60; // 5 minutes |
85 | |
86 | /** |
87 | * Construct a new instance from configuration |
88 | * @stable to call |
89 | * |
90 | * @param array $config Parameters include: |
91 | * - domain : Domain (usually wiki ID) that all resources are relative to [optional] |
92 | * - lockTTL : Age (in seconds) at which resource locks should expire. |
93 | * This only applies if locks are not tied to a connection/process. |
94 | */ |
95 | public function __construct( array $config ) { |
96 | $this->domain = $config['domain'] ?? 'global'; |
97 | if ( isset( $config['lockTTL'] ) ) { |
98 | $this->lockTTL = max( self::MIN_LOCK_TTL, $config['lockTTL'] ); |
99 | } elseif ( PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg' ) { |
100 | $this->lockTTL = self::CLI_LOCK_TTL; |
101 | } else { |
102 | $ttl = 2 * ceil( RequestTimeout::singleton()->getWallTimeLimit() ); |
103 | $this->lockTTL = ( $ttl === INF || $ttl < self::MIN_GUESSED_LOCK_TTL ) |
104 | ? self::MIN_GUESSED_LOCK_TTL : $ttl; |
105 | } |
106 | |
107 | // Upper bound on how long to keep lock structures around. This is useful when setting |
108 | // TTLs, as the "lockTTL" value may vary based on CLI mode and app server group. This is |
109 | // a "safe" value that can be used to avoid clobbering other locks that use high TTLs. |
110 | $this->lockTTL = min( $this->lockTTL, self::MAX_LOCK_TTL ); |
111 | |
112 | $random = []; |
113 | for ( $i = 1; $i <= 5; ++$i ) { |
114 | $random[] = mt_rand( 0, 0xFFFFFFF ); |
115 | } |
116 | $this->session = md5( implode( '-', $random ) ); |
117 | |
118 | $this->logger = $config['logger'] ?? new NullLogger(); |
119 | } |
120 | |
121 | /** |
122 | * Lock the resources at the given abstract paths |
123 | * |
124 | * @param array $paths List of resource names |
125 | * @param int $type LockManager::LOCK_* constant |
126 | * @param int $timeout Timeout in seconds (0 means non-blocking) (since 1.21) |
127 | * @return StatusValue |
128 | */ |
129 | final public function lock( array $paths, $type = self::LOCK_EX, $timeout = 0 ) { |
130 | return $this->lockByType( [ $type => $paths ], $timeout ); |
131 | } |
132 | |
133 | /** |
134 | * Lock the resources at the given abstract paths |
135 | * |
136 | * @param array $pathsByType Map of LockManager::LOCK_* constants to lists of paths |
137 | * @param int $timeout Timeout in seconds (0 means non-blocking) (since 1.21) |
138 | * @return StatusValue |
139 | * @since 1.22 |
140 | */ |
141 | final public function lockByType( array $pathsByType, $timeout = 0 ) { |
142 | $pathsByType = $this->normalizePathsByType( $pathsByType ); |
143 | |
144 | $status = null; |
145 | $loop = new WaitConditionLoop( |
146 | function () use ( &$status, $pathsByType ) { |
147 | $status = $this->doLockByType( $pathsByType ); |
148 | |
149 | return $status->isOK() ?: WaitConditionLoop::CONDITION_CONTINUE; |
150 | }, |
151 | $timeout |
152 | ); |
153 | $loop->invoke(); |
154 | |
155 | // @phan-suppress-next-line PhanTypeMismatchReturn WaitConditionLoop throws or status is set |
156 | return $status; |
157 | } |
158 | |
159 | /** |
160 | * Unlock the resources at the given abstract paths |
161 | * |
162 | * @param array $paths List of paths |
163 | * @param int $type LockManager::LOCK_* constant |
164 | * @return StatusValue |
165 | */ |
166 | final public function unlock( array $paths, $type = self::LOCK_EX ) { |
167 | return $this->unlockByType( [ $type => $paths ] ); |
168 | } |
169 | |
170 | /** |
171 | * Unlock the resources at the given abstract paths |
172 | * |
173 | * @param array $pathsByType Map of LockManager::LOCK_* constants to lists of paths |
174 | * @return StatusValue |
175 | * @since 1.22 |
176 | */ |
177 | final public function unlockByType( array $pathsByType ) { |
178 | $pathsByType = $this->normalizePathsByType( $pathsByType ); |
179 | $status = $this->doUnlockByType( $pathsByType ); |
180 | |
181 | return $status; |
182 | } |
183 | |
184 | /** |
185 | * Get the base 36 SHA-1 of a string, padded to 31 digits. |
186 | * Before hashing, the path will be prefixed with the domain ID. |
187 | * This should be used internally for lock key or file names. |
188 | * |
189 | * @param string $path |
190 | * @return string |
191 | */ |
192 | final protected function sha1Base36Absolute( $path ) { |
193 | return Wikimedia\base_convert( sha1( "{$this->domain}:{$path}" ), 16, 36, 31 ); |
194 | } |
195 | |
196 | /** |
197 | * Normalize the $paths array by converting LOCK_UW locks into the |
198 | * appropriate type and removing any duplicated paths for each lock type. |
199 | * |
200 | * @param array $pathsByType Map of LockManager::LOCK_* constants to lists of paths |
201 | * @return array |
202 | * @since 1.22 |
203 | */ |
204 | final protected function normalizePathsByType( array $pathsByType ) { |
205 | $res = []; |
206 | foreach ( $pathsByType as $type => $paths ) { |
207 | foreach ( $paths as $path ) { |
208 | if ( (string)$path === '' ) { |
209 | throw new InvalidArgumentException( __METHOD__ . ": got empty path." ); |
210 | } |
211 | } |
212 | $res[$this->lockTypeMap[$type]] = array_unique( $paths ); |
213 | } |
214 | |
215 | return $res; |
216 | } |
217 | |
218 | /** |
219 | * @see LockManager::lockByType() |
220 | * @stable to override |
221 | * @param array $pathsByType Map of LockManager::LOCK_* constants to lists of paths |
222 | * @return StatusValue |
223 | * @since 1.22 |
224 | */ |
225 | protected function doLockByType( array $pathsByType ) { |
226 | $status = StatusValue::newGood(); |
227 | $lockedByType = []; // map of (type => paths) |
228 | foreach ( $pathsByType as $type => $paths ) { |
229 | $status->merge( $this->doLock( $paths, $type ) ); |
230 | if ( $status->isOK() ) { |
231 | $lockedByType[$type] = $paths; |
232 | } else { |
233 | // Release the subset of locks that were acquired |
234 | foreach ( $lockedByType as $lType => $lPaths ) { |
235 | $status->merge( $this->doUnlock( $lPaths, $lType ) ); |
236 | } |
237 | break; |
238 | } |
239 | } |
240 | |
241 | return $status; |
242 | } |
243 | |
244 | /** |
245 | * Lock resources with the given keys and lock type |
246 | * |
247 | * @param array $paths List of paths |
248 | * @param int $type LockManager::LOCK_* constant |
249 | * @return StatusValue |
250 | */ |
251 | abstract protected function doLock( array $paths, $type ); |
252 | |
253 | /** |
254 | * @see LockManager::unlockByType() |
255 | * @stable to override |
256 | * @param array $pathsByType Map of LockManager::LOCK_* constants to lists of paths |
257 | * @return StatusValue |
258 | * @since 1.22 |
259 | */ |
260 | protected function doUnlockByType( array $pathsByType ) { |
261 | $status = StatusValue::newGood(); |
262 | foreach ( $pathsByType as $type => $paths ) { |
263 | $status->merge( $this->doUnlock( $paths, $type ) ); |
264 | } |
265 | |
266 | return $status; |
267 | } |
268 | |
269 | /** |
270 | * Unlock resources with the given keys and lock type |
271 | * |
272 | * @param array $paths List of paths |
273 | * @param int $type LockManager::LOCK_* constant |
274 | * @return StatusValue |
275 | */ |
276 | abstract protected function doUnlock( array $paths, $type ); |
277 | } |