MediaWiki master
MemcLockManager.php
Go to the documentation of this file.
1<?php
7
8use Exception;
9use InvalidArgumentException;
10use StatusValue;
14use Wikimedia\WaitConditionLoop;
15
32 protected $lockTypeMap = [
33 self::LOCK_SH => self::LOCK_SH,
34 self::LOCK_UW => self::LOCK_SH,
35 self::LOCK_EX => self::LOCK_EX
36 ];
37
39 protected $cacheServers = [];
41 protected $statusCache;
42
56 public function __construct( array $config ) {
57 parent::__construct( $config );
58
59 if ( isset( $config['srvsByBucket'] ) ) {
60 // Sanitize srvsByBucket config to prevent PHP errors
61 $this->srvsByBucket = array_filter( $config['srvsByBucket'], 'is_array' );
62 $this->srvsByBucket = array_values( $this->srvsByBucket ); // consecutive
63 } else {
64 $this->srvsByBucket = [ array_keys( $config['lockServers'] ) ];
65 }
66
67 $memcConfig = $config['memcConfig'] ?? [];
68 $memcConfig += [ 'class' => MemcachedPhpBagOStuff::class ]; // default
69
70 $class = $memcConfig['class'];
71 if ( !is_subclass_of( $class, MemcachedBagOStuff::class ) ) {
72 throw new InvalidArgumentException( "$class is not of type MemcachedBagOStuff." );
73 }
74
75 foreach ( $config['lockServers'] as $name => $address ) {
76 $params = [ 'servers' => [ $address ] ] + $memcConfig;
77 $this->cacheServers[$name] = new $class( $params );
78 }
79
80 $this->statusCache = new MapCacheLRU( 100 );
81 }
82
84 protected function getLocksOnServer( $lockSrv, array $pathsByType ) {
85 $status = StatusValue::newGood();
86
87 $memc = $this->getCache( $lockSrv );
88 // List of affected paths
89 $paths = array_merge( ...array_values( $pathsByType ) );
90 $paths = array_unique( $paths );
91 // List of affected lock record keys
92 $keys = array_map( $this->recordKeyForPath( ... ), $paths );
93
94 // Lock all of the active lock record keys...
95 if ( !$this->acquireMutexes( $memc, $keys ) ) {
96 $status->fatal( 'lockmanager-fail-conflict' );
97 return $status;
98 }
99
100 // Fetch all the existing lock records...
101 $lockRecords = $memc->getMulti( $keys );
102
103 $now = time();
104 // Check if the requested locks conflict with existing ones...
105 foreach ( $pathsByType as $type => $paths2 ) {
106 foreach ( $paths2 as $path ) {
107 $locksKey = $this->recordKeyForPath( $path );
108 $locksHeld = isset( $lockRecords[$locksKey] )
109 ? self::sanitizeLockArray( $lockRecords[$locksKey] )
110 : self::newLockArray(); // init
111 foreach ( $locksHeld[self::LOCK_EX] as $session => $expiry ) {
112 if ( $expiry < $now ) { // stale?
113 unset( $locksHeld[self::LOCK_EX][$session] );
114 } elseif ( $session !== $this->session ) {
115 $status->fatal( 'lockmanager-fail-conflict' );
116 }
117 }
118 if ( $type === self::LOCK_EX ) {
119 foreach ( $locksHeld[self::LOCK_SH] as $session => $expiry ) {
120 if ( $expiry < $now ) { // stale?
121 unset( $locksHeld[self::LOCK_SH][$session] );
122 } elseif ( $session !== $this->session ) {
123 $status->fatal( 'lockmanager-fail-conflict' );
124 }
125 }
126 }
127 if ( $status->isOK() ) {
128 // Register the session in the lock record array
130 // We will update this record if none of the other locks conflict
131 $lockRecords[$locksKey] = $locksHeld;
132 }
133 }
134 }
135
136 // If there were no lock conflicts, update all the lock records...
137 if ( $status->isOK() ) {
138 foreach ( $paths as $path ) {
139 $locksKey = $this->recordKeyForPath( $path );
140 $locksHeld = $lockRecords[$locksKey];
141 $ok = $memc->set( $locksKey, $locksHeld, self::MAX_LOCK_TTL );
142 if ( !$ok ) {
143 $status->fatal( 'lockmanager-fail-acquirelock', $path );
144 } else {
145 $this->logger->debug( __METHOD__ . ": acquired lock on key $locksKey." );
146 }
147 }
148 }
149
150 // Unlock all of the active lock record keys...
151 $this->releaseMutexes( $memc, $keys );
152
153 return $status;
154 }
155
157 protected function freeLocksOnServer( $lockSrv, array $pathsByType ) {
158 $status = StatusValue::newGood();
159
160 $memc = $this->getCache( $lockSrv );
161 // List of affected paths
162 $paths = array_merge( ...array_values( $pathsByType ) );
163 $paths = array_unique( $paths );
164 // List of affected lock record keys
165 $keys = array_map( $this->recordKeyForPath( ... ), $paths );
166
167 // Lock all of the active lock record keys...
168 if ( !$this->acquireMutexes( $memc, $keys ) ) {
169 foreach ( $paths as $path ) {
170 $status->fatal( 'lockmanager-fail-releaselock', $path );
171 }
172
173 return $status;
174 }
175
176 // Fetch all the existing lock records...
177 $lockRecords = $memc->getMulti( $keys );
178
179 // Remove the requested locks from all records...
180 foreach ( $pathsByType as $type => $paths2 ) {
181 foreach ( $paths2 as $path ) {
182 $locksKey = $this->recordKeyForPath( $path ); // lock record
183 if ( !isset( $lockRecords[$locksKey] ) ) {
184 $status->warning( 'lockmanager-fail-releaselock', $path );
185 continue; // nothing to do
186 }
187 $locksHeld = $this->sanitizeLockArray( $lockRecords[$locksKey] );
188 if ( isset( $locksHeld[$type][$this->session] ) ) {
189 unset( $locksHeld[$type][$this->session] ); // unregister this session
190 $lockRecords[$locksKey] = $locksHeld;
191 } else {
192 $status->warning( 'lockmanager-fail-releaselock', $path );
193 }
194 }
195 }
196
197 // Persist the new lock record values...
198 foreach ( $paths as $path ) {
199 $locksKey = $this->recordKeyForPath( $path );
200 if ( !isset( $lockRecords[$locksKey] ) ) {
201 continue; // nothing to do
202 }
203 $locksHeld = $lockRecords[$locksKey];
204 if ( $locksHeld === $this->newLockArray() ) {
205 $ok = $memc->delete( $locksKey );
206 } else {
207 $ok = $memc->set( $locksKey, $locksHeld, self::MAX_LOCK_TTL );
208 }
209 if ( $ok ) {
210 $this->logger->debug( __METHOD__ . ": released lock on key $locksKey." );
211 } else {
212 $status->fatal( 'lockmanager-fail-releaselock', $path );
213 }
214 }
215
216 // Unlock all of the active lock record keys...
217 $this->releaseMutexes( $memc, $keys );
218
219 return $status;
220 }
221
226 protected function releaseAllLocks() {
227 return StatusValue::newGood(); // not supported
228 }
229
235 protected function isServerUp( $lockSrv ) {
236 return (bool)$this->getCache( $lockSrv );
237 }
238
245 protected function getCache( $lockSrv ) {
246 if ( !isset( $this->cacheServers[$lockSrv] ) ) {
247 throw new InvalidArgumentException( "Invalid cache server '$lockSrv'." );
248 }
249
250 $online = $this->statusCache->get( "online:$lockSrv", 30 );
251 if ( $online === null ) {
252 $online = $this->cacheServers[$lockSrv]->set( __CLASS__ . ':ping', 1, 1 );
253 if ( !$online ) { // server down?
254 $this->logger->warning( __METHOD__ . ": Could not contact $lockSrv." );
255 }
256 $this->statusCache->set( "online:$lockSrv", (int)$online );
257 }
258
259 return $online ? $this->cacheServers[$lockSrv] : null;
260 }
261
266 protected function recordKeyForPath( $path ) {
267 return implode( ':', [ __CLASS__, 'locks', $this->sha1Base36Absolute( $path ) ] );
268 }
269
273 protected function newLockArray() {
274 return [ self::LOCK_SH => [], self::LOCK_EX => [] ];
275 }
276
281 protected function sanitizeLockArray( $a ) {
282 if ( is_array( $a ) && isset( $a[self::LOCK_EX] ) && isset( $a[self::LOCK_SH] ) ) {
283 return $a;
284 }
285
286 $this->logger->error( __METHOD__ . ": reset invalid lock array." );
287
288 return $this->newLockArray();
289 }
290
296 protected function acquireMutexes( MemcachedBagOStuff $memc, array $keys ) {
297 $lockedKeys = [];
298
299 // Acquire the keys in lexicographical order, to avoid deadlock problems.
300 // If P1 is waiting to acquire a key P2 has, P2 can't also be waiting for a key P1 has.
301 sort( $keys );
302
303 // Try to quickly loop to acquire the keys, but back off after a few rounds.
304 // This reduces memcached spam, especially in the rare case where a server acquires
305 // some lock keys and dies without releasing them. Lock keys expire after a few minutes.
306 $loop = new WaitConditionLoop(
307 static function () use ( $memc, $keys, &$lockedKeys ) {
308 foreach ( array_diff( $keys, $lockedKeys ) as $key ) {
309 if ( $memc->add( "$key:mutex", 1, 180 ) ) { // lock record
310 $lockedKeys[] = $key;
311 }
312 }
313
314 return array_diff( $keys, $lockedKeys )
315 ? WaitConditionLoop::CONDITION_CONTINUE
316 : true;
317 },
318 3.0 // timeout
319 );
320 $loop->invoke();
321
322 if ( count( $lockedKeys ) != count( $keys ) ) {
323 $this->releaseMutexes( $memc, $lockedKeys ); // failed; release what was locked
324 return false;
325 }
326
327 return true;
328 }
329
334 protected function releaseMutexes( MemcachedBagOStuff $memc, array $keys ) {
335 foreach ( $keys as $key ) {
336 $memc->delete( "$key:mutex" );
337 }
338 }
339
343 public function __destruct() {
344 $pathsByType = [];
345 foreach ( $this->locksHeld as $path => $locks ) {
346 foreach ( $locks as $type => $count ) {
347 $pathsByType[$type][] = $path;
348 }
349 }
350 if ( $pathsByType ) {
351 $this->unlockByType( $pathsByType );
352 }
353 }
354}
356class_alias( MemcLockManager::class, 'MemcLockManager' );
Generic operation result class Has warning/error list, boolean status and arbitrary value.
int $lockTTL
maximum time locks can be held
const LOCK_SH
Lock types; stronger locks have higher values.
string $session
Random 32-char hex number.
array $locksHeld
Map of (resource path => lock type => count)
sha1Base36Absolute( $path)
Get the base 36 SHA-1 of a string, padded to 31 digits.
unlockByType(array $pathsByType)
Unlock the resources at the given abstract paths.
Manage locks using memcached servers.
__destruct()
Make sure remaining locks get cleared.
releaseMutexes(MemcachedBagOStuff $memc, array $keys)
__construct(array $config)
Construct a new instance from configuration.
MemcachedBagOStuff[] $cacheServers
Map of (server name => MemcachedBagOStuff)
array $lockTypeMap
Mapping of lock types to the type actually used.
getLocksOnServer( $lockSrv, array $pathsByType)
Get a connection to a lock server and acquire locks.StatusValue
acquireMutexes(MemcachedBagOStuff $memc, array $keys)
getCache( $lockSrv)
Get the MemcachedBagOStuff object for a $lockSrv.
MapCacheLRU $statusCache
Server status cache.
freeLocksOnServer( $lockSrv, array $pathsByType)
Get a connection to a lock server and release locks on $paths.Subclasses must effectively implement t...
Base class for lock managers that use a quorum of peer servers for locks.
Store key-value entries in a size-limited in-memory LRU cache.
add( $key, $value, $exptime=0, $flags=0)
Insert an item if it does not already exist.bool Success (item created)
delete( $key, $flags=0)
Delete an item if it exists.For large values set with WRITE_ALLOW_SEGMENTS, this only deletes the pla...
Store data in a memcached server or memcached cluster.
Store data on memcached servers(s) via a pure-PHP memcached client.