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