MediaWiki  master
MemcLockManager.php
Go to the documentation of this file.
1 <?php
23 use Wikimedia\WaitConditionLoop;
24 
41  protected $lockTypeMap = [
42  self::LOCK_SH => self::LOCK_SH,
43  self::LOCK_UW => self::LOCK_SH,
44  self::LOCK_EX => self::LOCK_EX
45  ];
46 
48  protected $cacheServers = [];
50  protected $statusCache;
51 
65  public function __construct( array $config ) {
66  parent::__construct( $config );
67 
68  if ( isset( $config['srvsByBucket'] ) ) {
69  // Sanitize srvsByBucket config to prevent PHP errors
70  $this->srvsByBucket = array_filter( $config['srvsByBucket'], 'is_array' );
71  $this->srvsByBucket = array_values( $this->srvsByBucket ); // consecutive
72  } else {
73  $this->srvsByBucket = [ array_keys( $config['lockServers'] ) ];
74  }
75 
76  $memcConfig = $config['memcConfig'] ?? [];
77  $memcConfig += [ 'class' => MemcachedPhpBagOStuff::class ]; // default
78 
79  $class = $memcConfig['class'];
80  if ( !is_subclass_of( $class, MemcachedBagOStuff::class ) ) {
81  throw new InvalidArgumentException( "$class is not of type MemcachedBagOStuff." );
82  }
83 
84  foreach ( $config['lockServers'] as $name => $address ) {
85  $params = [ 'servers' => [ $address ] ] + $memcConfig;
86  $this->cacheServers[$name] = new $class( $params );
87  }
88 
89  $this->statusCache = new MapCacheLRU( 100 );
90  }
91 
92  protected function getLocksOnServer( $lockSrv, array $pathsByType ) {
93  $status = StatusValue::newGood();
94 
95  $memc = $this->getCache( $lockSrv );
96  // List of affected paths
97  $paths = array_merge( ...array_values( $pathsByType ) );
98  $paths = array_unique( $paths );
99  // List of affected lock record keys
100  $keys = array_map( [ $this, 'recordKeyForPath' ], $paths );
101 
102  // Lock all of the active lock record keys...
103  if ( !$this->acquireMutexes( $memc, $keys ) ) {
104  $status->fatal( 'lockmanager-fail-conflict' );
105  return $status;
106  }
107 
108  // Fetch all the existing lock records...
109  $lockRecords = $memc->getMulti( $keys );
110 
111  $now = time();
112  // Check if the requested locks conflict with existing ones...
113  foreach ( $pathsByType as $type => $paths ) {
114  foreach ( $paths as $path ) {
115  $locksKey = $this->recordKeyForPath( $path );
116  $locksHeld = isset( $lockRecords[$locksKey] )
117  ? self::sanitizeLockArray( $lockRecords[$locksKey] )
118  : self::newLockArray(); // init
119  foreach ( $locksHeld[self::LOCK_EX] as $session => $expiry ) {
120  if ( $expiry < $now ) { // stale?
121  unset( $locksHeld[self::LOCK_EX][$session] );
122  } elseif ( $session !== $this->session ) {
123  $status->fatal( 'lockmanager-fail-conflict' );
124  }
125  }
126  if ( $type === self::LOCK_EX ) {
127  foreach ( $locksHeld[self::LOCK_SH] as $session => $expiry ) {
128  if ( $expiry < $now ) { // stale?
129  unset( $locksHeld[self::LOCK_SH][$session] );
130  } elseif ( $session !== $this->session ) {
131  $status->fatal( 'lockmanager-fail-conflict' );
132  }
133  }
134  }
135  if ( $status->isOK() ) {
136  // Register the session in the lock record array
138  // We will update this record if none of the other locks conflict
139  $lockRecords[$locksKey] = $locksHeld;
140  }
141  }
142  }
143 
144  // If there were no lock conflicts, update all the lock records...
145  if ( $status->isOK() ) {
146  foreach ( $paths as $path ) {
147  $locksKey = $this->recordKeyForPath( $path );
148  $locksHeld = $lockRecords[$locksKey];
149  $ok = $memc->set( $locksKey, $locksHeld, self::MAX_LOCK_TTL );
150  if ( !$ok ) {
151  $status->fatal( 'lockmanager-fail-acquirelock', $path );
152  } else {
153  $this->logger->debug( __METHOD__ . ": acquired lock on key $locksKey." );
154  }
155  }
156  }
157 
158  // Unlock all of the active lock record keys...
159  $this->releaseMutexes( $memc, $keys );
160 
161  return $status;
162  }
163 
164  protected function freeLocksOnServer( $lockSrv, array $pathsByType ) {
165  $status = StatusValue::newGood();
166 
167  $memc = $this->getCache( $lockSrv );
168  // List of affected paths
169  $paths = array_merge( ...array_values( $pathsByType ) );
170  $paths = array_unique( $paths );
171  // List of affected lock record keys
172  $keys = array_map( [ $this, 'recordKeyForPath' ], $paths );
173 
174  // Lock all of the active lock record keys...
175  if ( !$this->acquireMutexes( $memc, $keys ) ) {
176  foreach ( $paths as $path ) {
177  $status->fatal( 'lockmanager-fail-releaselock', $path );
178  }
179 
180  return $status;
181  }
182 
183  // Fetch all the existing lock records...
184  $lockRecords = $memc->getMulti( $keys );
185 
186  // Remove the requested locks from all records...
187  foreach ( $pathsByType as $type => $paths ) {
188  foreach ( $paths as $path ) {
189  $locksKey = $this->recordKeyForPath( $path ); // lock record
190  if ( !isset( $lockRecords[$locksKey] ) ) {
191  $status->warning( 'lockmanager-fail-releaselock', $path );
192  continue; // nothing to do
193  }
194  $locksHeld = $this->sanitizeLockArray( $lockRecords[$locksKey] );
195  if ( isset( $locksHeld[$type][$this->session] ) ) {
196  unset( $locksHeld[$type][$this->session] ); // unregister this session
197  $lockRecords[$locksKey] = $locksHeld;
198  } else {
199  $status->warning( 'lockmanager-fail-releaselock', $path );
200  }
201  }
202  }
203 
204  // Persist the new lock record values...
205  foreach ( $paths as $path ) {
206  $locksKey = $this->recordKeyForPath( $path );
207  if ( !isset( $lockRecords[$locksKey] ) ) {
208  continue; // nothing to do
209  }
210  $locksHeld = $lockRecords[$locksKey];
211  if ( $locksHeld === $this->newLockArray() ) {
212  $ok = $memc->delete( $locksKey );
213  } else {
214  $ok = $memc->set( $locksKey, $locksHeld, self::MAX_LOCK_TTL );
215  }
216  if ( $ok ) {
217  $this->logger->debug( __METHOD__ . ": released lock on key $locksKey." );
218  } else {
219  $status->fatal( 'lockmanager-fail-releaselock', $path );
220  }
221  }
222 
223  // Unlock all of the active lock record keys...
224  $this->releaseMutexes( $memc, $keys );
225 
226  return $status;
227  }
228 
233  protected function releaseAllLocks() {
234  return StatusValue::newGood(); // not supported
235  }
236 
242  protected function isServerUp( $lockSrv ) {
243  return (bool)$this->getCache( $lockSrv );
244  }
245 
252  protected function getCache( $lockSrv ) {
253  if ( !isset( $this->cacheServers[$lockSrv] ) ) {
254  throw new InvalidArgumentException( "Invalid cache server '$lockSrv'." );
255  }
256 
257  $online = $this->statusCache->get( "online:$lockSrv", 30 );
258  if ( $online === null ) {
259  $online = $this->cacheServers[$lockSrv]->set( __CLASS__ . ':ping', 1, 1 );
260  if ( !$online ) { // server down?
261  $this->logger->warning( __METHOD__ . ": Could not contact $lockSrv." );
262  }
263  $this->statusCache->set( "online:$lockSrv", (int)$online );
264  }
265 
266  return $online ? $this->cacheServers[$lockSrv] : null;
267  }
268 
273  protected function recordKeyForPath( $path ) {
274  return implode( ':', [ __CLASS__, 'locks', $this->sha1Base36Absolute( $path ) ] );
275  }
276 
280  protected function newLockArray() {
281  return [ self::LOCK_SH => [], self::LOCK_EX => [] ];
282  }
283 
288  protected function sanitizeLockArray( $a ) {
289  if ( is_array( $a ) && isset( $a[self::LOCK_EX] ) && isset( $a[self::LOCK_SH] ) ) {
290  return $a;
291  }
292 
293  $this->logger->error( __METHOD__ . ": reset invalid lock array." );
294 
295  return $this->newLockArray();
296  }
297 
303  protected function acquireMutexes( MemcachedBagOStuff $memc, array $keys ) {
304  $lockedKeys = [];
305 
306  // Acquire the keys in lexicographical order, to avoid deadlock problems.
307  // If P1 is waiting to acquire a key P2 has, P2 can't also be waiting for a key P1 has.
308  sort( $keys );
309 
310  // Try to quickly loop to acquire the keys, but back off after a few rounds.
311  // This reduces memcached spam, especially in the rare case where a server acquires
312  // some lock keys and dies without releasing them. Lock keys expire after a few minutes.
313  $loop = new WaitConditionLoop(
314  static function () use ( $memc, $keys, &$lockedKeys ) {
315  foreach ( array_diff( $keys, $lockedKeys ) as $key ) {
316  if ( $memc->add( "$key:mutex", 1, 180 ) ) { // lock record
317  $lockedKeys[] = $key;
318  }
319  }
320 
321  return array_diff( $keys, $lockedKeys )
322  ? WaitConditionLoop::CONDITION_CONTINUE
323  : true;
324  },
325  3.0 // timeout
326  );
327  $loop->invoke();
328 
329  if ( count( $lockedKeys ) != count( $keys ) ) {
330  $this->releaseMutexes( $memc, $lockedKeys ); // failed; release what was locked
331  return false;
332  }
333 
334  return true;
335  }
336 
341  protected function releaseMutexes( MemcachedBagOStuff $memc, array $keys ) {
342  foreach ( $keys as $key ) {
343  $memc->delete( "$key:mutex" );
344  }
345  }
346 
350  public function __destruct() {
351  while ( count( $this->locksHeld ) ) {
352  foreach ( $this->locksHeld as $path => $locks ) {
353  $this->doUnlock( [ $path ], self::LOCK_EX );
354  $this->doUnlock( [ $path ], self::LOCK_SH );
355  }
356  }
357  }
358 }
LockManager\$locksHeld
array $locksHeld
Map of (resource path => lock type => count)
Definition: LockManager.php:61
MemcLockManager\$statusCache
MapCacheLRU $statusCache
Server status cache.
Definition: MemcLockManager.php:50
MemcLockManager\freeLocksOnServer
freeLocksOnServer( $lockSrv, array $pathsByType)
Get a connection to a lock server and release locks on $paths.
Definition: MemcLockManager.php:164
MemcachedBagOStuff
Base class for memcached clients.
Definition: MemcachedBagOStuff.php:29
LockManager\LOCK_SH
const LOCK_SH
Lock types; stronger locks have higher values.
Definition: LockManager.php:70
MemcLockManager\newLockArray
newLockArray()
Definition: MemcLockManager.php:280
MediumSpecificBagOStuff\delete
delete( $key, $flags=0)
Delete an item.
Definition: MediumSpecificBagOStuff.php:203
MemcLockManager\releaseAllLocks
releaseAllLocks()
Definition: MemcLockManager.php:233
MemcLockManager\acquireMutexes
acquireMutexes(MemcachedBagOStuff $memc, array $keys)
Definition: MemcLockManager.php:303
MemcLockManager\__construct
__construct(array $config)
Construct a new instance from configuration.
Definition: MemcLockManager.php:65
LockManager\$lockTTL
$lockTTL
Definition: LockManager.php:64
MemcLockManager\getCache
getCache( $lockSrv)
Get the MemcachedBagOStuff object for a $lockSrv.
Definition: MemcLockManager.php:252
QuorumLockManager
Version of LockManager that uses a quorum from peer servers for locks.
Definition: QuorumLockManager.php:32
LockManager\sha1Base36Absolute
sha1Base36Absolute( $path)
Get the base 36 SHA-1 of a string, padded to 31 digits.
Definition: LockManager.php:191
MapCacheLRU
Handles a simple LRU key/value map with a maximum number of entries.
Definition: MapCacheLRU.php:36
MemcLockManager\isServerUp
isServerUp( $lockSrv)
Definition: MemcLockManager.php:242
QuorumLockManager\doUnlock
doUnlock(array $paths, $type)
Unlock resources with the given keys and lock type.
Definition: QuorumLockManager.php:291
StatusValue\newGood
static newGood( $value=null)
Factory function for good results.
Definition: StatusValue.php:82
MemcLockManager\__destruct
__destruct()
Make sure remaining locks get cleared.
Definition: MemcLockManager.php:350
LockManager\$session
string $session
Random 32-char hex number.
Definition: LockManager.php:67
MemcLockManager\recordKeyForPath
recordKeyForPath( $path)
Definition: MemcLockManager.php:273
$path
$path
Definition: NoLocalSettings.php:25
MemcLockManager\sanitizeLockArray
sanitizeLockArray( $a)
Definition: MemcLockManager.php:288
MediumSpecificBagOStuff\add
add( $key, $value, $exptime=0, $flags=0)
Insert an item if it does not already exist.
Definition: MediumSpecificBagOStuff.php:236
MemcLockManager
Manage locks using memcached servers.
Definition: MemcLockManager.php:39
$keys
$keys
Definition: testCompression.php:72
MemcLockManager\releaseMutexes
releaseMutexes(MemcachedBagOStuff $memc, array $keys)
Definition: MemcLockManager.php:341
LockManager\LOCK_EX
const LOCK_EX
Definition: LockManager.php:72
MemcLockManager\$lockTypeMap
array $lockTypeMap
Mapping of lock types to the type actually used.
Definition: MemcLockManager.php:41
MemcLockManager\getLocksOnServer
getLocksOnServer( $lockSrv, array $pathsByType)
Get a connection to a lock server and acquire locks.
Definition: MemcLockManager.php:92
MemcLockManager\$cacheServers
MemcachedBagOStuff[] $cacheServers
Map of (server name => MemcachedBagOStuff)
Definition: MemcLockManager.php:48
$type
$type
Definition: testCompression.php:52