MediaWiki master
LockManager.php
Go to the documentation of this file.
1<?php
7
8use InvalidArgumentException;
9use Psr\Log\LoggerInterface;
10use Psr\Log\NullLogger;
11use StatusValue;
12use Wikimedia\RequestTimeout\RequestTimeout;
13use Wikimedia\WaitConditionLoop;
14
36abstract class LockManager {
38 protected $logger;
39
41 protected $lockTypeMap = [
42 self::LOCK_SH => self::LOCK_SH,
43 self::LOCK_UW => self::LOCK_EX, // subclasses may use self::LOCK_SH
44 self::LOCK_EX => self::LOCK_EX
45 ];
46
48 protected $locksHeld = [];
49
51 protected $domain;
53 protected $lockTTL;
54
56 protected $session;
57
59 public const LOCK_SH = 1; // shared lock (for reads)
60 public const LOCK_UW = 2; // shared lock (for reads used to write elsewhere)
61 public const LOCK_EX = 3; // exclusive lock (for writes)
62
64 protected const MAX_LOCK_TTL = 2 * 3600; // 2 hours
65
67 protected const MIN_LOCK_TTL = 5; // seconds
68
82 public function __construct( array $config ) {
83 $this->domain = $config['domain'] ?? 'global';
84 if ( isset( $config['lockTTL'] ) ) {
85 $this->lockTTL = max( self::MIN_LOCK_TTL, $config['lockTTL'] );
86 } elseif ( PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg' ) {
87 $this->lockTTL = 3600; // 1 hour
88 } else {
89 $ttl = 2 * ceil( RequestTimeout::singleton()->getWallTimeLimit() );
90 $minMaxTTL = 5 * 60; // 5 minutes
91 $this->lockTTL = ( $ttl === INF || $ttl < $minMaxTTL ) ? $minMaxTTL : $ttl;
92 }
93
94 $this->lockTTL = min( $this->lockTTL, self::MAX_LOCK_TTL );
95
96 $random = [];
97 for ( $i = 1; $i <= 5; ++$i ) {
98 $random[] = mt_rand( 0, 0xFFFFFFF );
99 }
100 $this->session = md5( implode( '-', $random ) );
101
102 $this->logger = $config['logger'] ?? new NullLogger();
103 }
104
113 final public function lock( array $paths, $type = self::LOCK_EX, $timeout = 0 ) {
114 return $this->lockByType( [ $type => $paths ], $timeout );
115 }
116
125 final public function lockByType( array $pathsByType, $timeout = 0 ) {
126 $pathsByType = $this->normalizePathsByType( $pathsByType );
127
128 $status = null;
129 $loop = new WaitConditionLoop(
130 function () use ( &$status, $pathsByType ) {
131 $status = $this->doLockByType( $pathsByType );
132
133 return $status->isOK() ?: WaitConditionLoop::CONDITION_CONTINUE;
134 },
135 $timeout
136 );
137 $loop->invoke();
138
139 // @phan-suppress-next-line PhanTypeMismatchReturn WaitConditionLoop throws or status is set
140 return $status;
141 }
142
150 final public function unlock( array $paths, $type = self::LOCK_EX ) {
151 return $this->unlockByType( [ $type => $paths ] );
152 }
153
161 final public function unlockByType( array $pathsByType ) {
162 $pathsByType = $this->normalizePathsByType( $pathsByType );
163 $status = $this->doUnlockByType( $pathsByType );
164
165 return $status;
166 }
167
176 final protected function sha1Base36Absolute( $path ) {
177 return \Wikimedia\base_convert( sha1( "{$this->domain}:{$path}" ), 16, 36, 31 );
178 }
179
188 final protected function normalizePathsByType( array $pathsByType ) {
189 $res = [];
190 foreach ( $pathsByType as $type => $paths ) {
191 foreach ( $paths as $path ) {
192 if ( (string)$path === '' ) {
193 throw new InvalidArgumentException( __METHOD__ . ": got empty path." );
194 }
195 }
196 $res[$this->lockTypeMap[$type]] = array_unique( $paths );
197 }
198
199 return $res;
200 }
201
209 protected function doLockByType( array $pathsByType ) {
210 $status = StatusValue::newGood();
211 $lockedByType = []; // map of (type => paths)
212 foreach ( $pathsByType as $type => $paths ) {
213 $status->merge( $this->doLock( $paths, $type ) );
214 if ( $status->isOK() ) {
215 $lockedByType[$type] = $paths;
216 } else {
217 // Release the subset of locks that were acquired
218 foreach ( $lockedByType as $lType => $lPaths ) {
219 $status->merge( $this->doUnlock( $lPaths, $lType ) );
220 }
221 break;
222 }
223 }
224
225 return $status;
226 }
227
235 abstract protected function doLock( array $paths, $type );
236
244 protected function doUnlockByType( array $pathsByType ) {
245 $status = StatusValue::newGood();
246 foreach ( $pathsByType as $type => $paths ) {
247 $status->merge( $this->doUnlock( $paths, $type ) );
248 }
249
250 return $status;
251 }
252
260 abstract protected function doUnlock( array $paths, $type );
261}
263class_alias( LockManager::class, 'LockManager' );
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Resource locking handling.
int $lockTTL
maximum time locks can be held
unlock(array $paths, $type=self::LOCK_EX)
Unlock the resources at the given abstract paths.
const MIN_LOCK_TTL
Minimum lock TTL.
string $domain
domain (usually wiki ID)
doUnlockByType(array $pathsByType)
const LOCK_SH
Lock types; stronger locks have higher values.
lock(array $paths, $type=self::LOCK_EX, $timeout=0)
Lock the resources at the given abstract paths.
string $session
Random 32-char hex number.
doLock(array $paths, $type)
Lock resources with the given keys and lock type.
doUnlock(array $paths, $type)
Unlock resources with the given keys and lock type.
normalizePathsByType(array $pathsByType)
Normalize the $paths array by converting LOCK_UW locks into the appropriate type and removing any dup...
array $locksHeld
Map of (resource path => lock type => count)
__construct(array $config)
Construct a new instance from configuration.
sha1Base36Absolute( $path)
Get the base 36 SHA-1 of a string, padded to 31 digits.
lockByType(array $pathsByType, $timeout=0)
Lock the resources at the given abstract paths.
array $lockTypeMap
Mapping of lock types to the type actually used.
const MAX_LOCK_TTL
Max expected lock expiry in any context.
unlockByType(array $pathsByType)
Unlock the resources at the given abstract paths.