Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
83.02% |
44 / 53 |
|
60.00% |
6 / 10 |
CRAP | |
0.00% |
0 / 1 |
LockManager | |
83.02% |
44 / 53 |
|
60.00% |
6 / 10 |
26.82 | |
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 | |||
sha1Base16Absolute | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
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 | protected $domain; // string; domain (usually wiki ID) |
62 | protected $lockTTL; // integer; maximum time locks can be held |
63 | |
64 | /** @var string Random 32-char hex number */ |
65 | protected $session; |
66 | |
67 | /** Lock types; stronger locks have higher values */ |
68 | public const LOCK_SH = 1; // shared lock (for reads) |
69 | public const LOCK_UW = 2; // shared lock (for reads used to write elsewhere) |
70 | public const LOCK_EX = 3; // exclusive lock (for writes) |
71 | |
72 | /** Max expected lock expiry in any context */ |
73 | protected const MAX_LOCK_TTL = 2 * 3600; // 2 hours |
74 | |
75 | /** Default lock TTL in CLI mode */ |
76 | protected const CLI_LOCK_TTL = 3600; // 1 hour |
77 | |
78 | /** Minimum lock TTL. The configured lockTTL is ignored if it is less than this value. */ |
79 | protected const MIN_LOCK_TTL = 5; // seconds |
80 | |
81 | /** The minimum lock TTL if it is guessed from max_execution_time rather than configured. */ |
82 | protected const MIN_GUESSED_LOCK_TTL = 5 * 60; // 5 minutes |
83 | |
84 | /** |
85 | * Construct a new instance from configuration |
86 | * @stable to call |
87 | * |
88 | * @param array $config Parameters include: |
89 | * - domain : Domain (usually wiki ID) that all resources are relative to [optional] |
90 | * - lockTTL : Age (in seconds) at which resource locks should expire. |
91 | * This only applies if locks are not tied to a connection/process. |
92 | */ |
93 | public function __construct( array $config ) { |
94 | $this->domain = $config['domain'] ?? 'global'; |
95 | if ( isset( $config['lockTTL'] ) ) { |
96 | $this->lockTTL = max( self::MIN_LOCK_TTL, $config['lockTTL'] ); |
97 | } elseif ( PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg' ) { |
98 | $this->lockTTL = self::CLI_LOCK_TTL; |
99 | } else { |
100 | $ttl = 2 * ceil( RequestTimeout::singleton()->getWallTimeLimit() ); |
101 | $this->lockTTL = ( $ttl === INF || $ttl < self::MIN_GUESSED_LOCK_TTL ) |
102 | ? self::MIN_GUESSED_LOCK_TTL : $ttl; |
103 | } |
104 | |
105 | // Upper bound on how long to keep lock structures around. This is useful when setting |
106 | // TTLs, as the "lockTTL" value may vary based on CLI mode and app server group. This is |
107 | // a "safe" value that can be used to avoid clobbering other locks that use high TTLs. |
108 | $this->lockTTL = min( $this->lockTTL, self::MAX_LOCK_TTL ); |
109 | |
110 | $random = []; |
111 | for ( $i = 1; $i <= 5; ++$i ) { |
112 | $random[] = mt_rand( 0, 0xFFFFFFF ); |
113 | } |
114 | $this->session = md5( implode( '-', $random ) ); |
115 | |
116 | $this->logger = $config['logger'] ?? new NullLogger(); |
117 | } |
118 | |
119 | /** |
120 | * Lock the resources at the given abstract paths |
121 | * |
122 | * @param array $paths List of resource names |
123 | * @param int $type LockManager::LOCK_* constant |
124 | * @param int $timeout Timeout in seconds (0 means non-blocking) (since 1.21) |
125 | * @return StatusValue |
126 | */ |
127 | final public function lock( array $paths, $type = self::LOCK_EX, $timeout = 0 ) { |
128 | return $this->lockByType( [ $type => $paths ], $timeout ); |
129 | } |
130 | |
131 | /** |
132 | * Lock the resources at the given abstract paths |
133 | * |
134 | * @param array $pathsByType Map of LockManager::LOCK_* constants to lists of paths |
135 | * @param int $timeout Timeout in seconds (0 means non-blocking) (since 1.21) |
136 | * @return StatusValue |
137 | * @since 1.22 |
138 | */ |
139 | final public function lockByType( array $pathsByType, $timeout = 0 ) { |
140 | $pathsByType = $this->normalizePathsByType( $pathsByType ); |
141 | |
142 | $status = null; |
143 | $loop = new WaitConditionLoop( |
144 | function () use ( &$status, $pathsByType ) { |
145 | $status = $this->doLockByType( $pathsByType ); |
146 | |
147 | return $status->isOK() ?: WaitConditionLoop::CONDITION_CONTINUE; |
148 | }, |
149 | $timeout |
150 | ); |
151 | $loop->invoke(); |
152 | |
153 | // @phan-suppress-next-line PhanTypeMismatchReturn WaitConditionLoop throws or status is set |
154 | return $status; |
155 | } |
156 | |
157 | /** |
158 | * Unlock the resources at the given abstract paths |
159 | * |
160 | * @param array $paths List of paths |
161 | * @param int $type LockManager::LOCK_* constant |
162 | * @return StatusValue |
163 | */ |
164 | final public function unlock( array $paths, $type = self::LOCK_EX ) { |
165 | return $this->unlockByType( [ $type => $paths ] ); |
166 | } |
167 | |
168 | /** |
169 | * Unlock the resources at the given abstract paths |
170 | * |
171 | * @param array $pathsByType Map of LockManager::LOCK_* constants to lists of paths |
172 | * @return StatusValue |
173 | * @since 1.22 |
174 | */ |
175 | final public function unlockByType( array $pathsByType ) { |
176 | $pathsByType = $this->normalizePathsByType( $pathsByType ); |
177 | $status = $this->doUnlockByType( $pathsByType ); |
178 | |
179 | return $status; |
180 | } |
181 | |
182 | /** |
183 | * Get the base 36 SHA-1 of a string, padded to 31 digits. |
184 | * Before hashing, the path will be prefixed with the domain ID. |
185 | * This should be used internally for lock key or file names. |
186 | * |
187 | * @param string $path |
188 | * @return string |
189 | */ |
190 | final protected function sha1Base36Absolute( $path ) { |
191 | return Wikimedia\base_convert( sha1( "{$this->domain}:{$path}" ), 16, 36, 31 ); |
192 | } |
193 | |
194 | /** |
195 | * Get the base 16 SHA-1 of a string, padded to 31 digits. |
196 | * Before hashing, the path will be prefixed with the domain ID. |
197 | * This should be used internally for lock key or file names. |
198 | * |
199 | * @param string $path |
200 | * @return string |
201 | */ |
202 | final protected function sha1Base16Absolute( $path ) { |
203 | return sha1( "{$this->domain}:{$path}" ); |
204 | } |
205 | |
206 | /** |
207 | * Normalize the $paths array by converting LOCK_UW locks into the |
208 | * appropriate type and removing any duplicated paths for each lock type. |
209 | * |
210 | * @param array $pathsByType Map of LockManager::LOCK_* constants to lists of paths |
211 | * @return array |
212 | * @since 1.22 |
213 | */ |
214 | final protected function normalizePathsByType( array $pathsByType ) { |
215 | $res = []; |
216 | foreach ( $pathsByType as $type => $paths ) { |
217 | foreach ( $paths as $path ) { |
218 | if ( (string)$path === '' ) { |
219 | throw new InvalidArgumentException( __METHOD__ . ": got empty path." ); |
220 | } |
221 | } |
222 | $res[$this->lockTypeMap[$type]] = array_unique( $paths ); |
223 | } |
224 | |
225 | return $res; |
226 | } |
227 | |
228 | /** |
229 | * @see LockManager::lockByType() |
230 | * @stable to override |
231 | * @param array $pathsByType Map of LockManager::LOCK_* constants to lists of paths |
232 | * @return StatusValue |
233 | * @since 1.22 |
234 | */ |
235 | protected function doLockByType( array $pathsByType ) { |
236 | $status = StatusValue::newGood(); |
237 | $lockedByType = []; // map of (type => paths) |
238 | foreach ( $pathsByType as $type => $paths ) { |
239 | $status->merge( $this->doLock( $paths, $type ) ); |
240 | if ( $status->isOK() ) { |
241 | $lockedByType[$type] = $paths; |
242 | } else { |
243 | // Release the subset of locks that were acquired |
244 | foreach ( $lockedByType as $lType => $lPaths ) { |
245 | $status->merge( $this->doUnlock( $lPaths, $lType ) ); |
246 | } |
247 | break; |
248 | } |
249 | } |
250 | |
251 | return $status; |
252 | } |
253 | |
254 | /** |
255 | * Lock resources with the given keys and lock type |
256 | * |
257 | * @param array $paths List of paths |
258 | * @param int $type LockManager::LOCK_* constant |
259 | * @return StatusValue |
260 | */ |
261 | abstract protected function doLock( array $paths, $type ); |
262 | |
263 | /** |
264 | * @see LockManager::unlockByType() |
265 | * @stable to override |
266 | * @param array $pathsByType Map of LockManager::LOCK_* constants to lists of paths |
267 | * @return StatusValue |
268 | * @since 1.22 |
269 | */ |
270 | protected function doUnlockByType( array $pathsByType ) { |
271 | $status = StatusValue::newGood(); |
272 | foreach ( $pathsByType as $type => $paths ) { |
273 | $status->merge( $this->doUnlock( $paths, $type ) ); |
274 | } |
275 | |
276 | return $status; |
277 | } |
278 | |
279 | /** |
280 | * Unlock resources with the given keys and lock type |
281 | * |
282 | * @param array $paths List of paths |
283 | * @param int $type LockManager::LOCK_* constant |
284 | * @return StatusValue |
285 | */ |
286 | abstract protected function doUnlock( array $paths, $type ); |
287 | } |