[ Index ] |
PHP Cross Reference of MediaWiki-1.24.0 |
[Summary view] [Print] [Text view]
1 <?php 2 /** 3 * Version of LockManager based on using memcached servers. 4 * 5 * This program is free software; you can redistribute it and/or modify 6 * it under the terms of the GNU General Public License as published by 7 * the Free Software Foundation; either version 2 of the License, or 8 * (at your option) any later version. 9 * 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU General Public License for more details. 14 * 15 * You should have received a copy of the GNU General Public License along 16 * with this program; if not, write to the Free Software Foundation, Inc., 17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 18 * http://www.gnu.org/copyleft/gpl.html 19 * 20 * @file 21 * @ingroup LockManager 22 */ 23 24 /** 25 * Manage locks using memcached servers. 26 * 27 * Version of LockManager based on using memcached servers. 28 * This is meant for multi-wiki systems that may share files. 29 * All locks are non-blocking, which avoids deadlocks. 30 * 31 * All lock requests for a resource, identified by a hash string, will map to one 32 * bucket. Each bucket maps to one or several peer servers, each running memcached. 33 * A majority of peers must agree for a lock to be acquired. 34 * 35 * @ingroup LockManager 36 * @since 1.20 37 */ 38 class MemcLockManager extends QuorumLockManager { 39 /** @var array Mapping of lock types to the type actually used */ 40 protected $lockTypeMap = array( 41 self::LOCK_SH => self::LOCK_SH, 42 self::LOCK_UW => self::LOCK_SH, 43 self::LOCK_EX => self::LOCK_EX 44 ); 45 46 /** @var array Map server names to MemcachedBagOStuff objects */ 47 protected $bagOStuffs = array(); 48 49 /** @var array (server name => bool) */ 50 protected $serversUp = array(); 51 52 /** @var string Random UUID */ 53 protected $session = ''; 54 55 /** 56 * Construct a new instance from configuration. 57 * 58 * @param array $config Paramaters include: 59 * - lockServers : Associative array of server names to "<IP>:<port>" strings. 60 * - srvsByBucket : Array of 1-16 consecutive integer keys, starting from 0, 61 * each having an odd-numbered list of server names (peers) as values. 62 * - memcConfig : Configuration array for ObjectCache::newFromParams. [optional] 63 * If set, this must use one of the memcached classes. 64 * @throws MWException 65 */ 66 public function __construct( array $config ) { 67 parent::__construct( $config ); 68 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 73 $memcConfig = isset( $config['memcConfig'] ) 74 ? $config['memcConfig'] 75 : array( 'class' => 'MemcachedPhpBagOStuff' ); 76 77 foreach ( $config['lockServers'] as $name => $address ) { 78 $params = array( 'servers' => array( $address ) ) + $memcConfig; 79 $cache = ObjectCache::newFromParams( $params ); 80 if ( $cache instanceof MemcachedBagOStuff ) { 81 $this->bagOStuffs[$name] = $cache; 82 } else { 83 throw new MWException( 84 'Only MemcachedBagOStuff classes are supported by MemcLockManager.' ); 85 } 86 } 87 88 $this->session = wfRandomString( 32 ); 89 } 90 91 // @todo Change this code to work in one batch 92 protected function getLocksOnServer( $lockSrv, array $pathsByType ) { 93 $status = Status::newGood(); 94 95 $lockedPaths = array(); 96 foreach ( $pathsByType as $type => $paths ) { 97 $status->merge( $this->doGetLocksOnServer( $lockSrv, $paths, $type ) ); 98 if ( $status->isOK() ) { 99 $lockedPaths[$type] = isset( $lockedPaths[$type] ) 100 ? array_merge( $lockedPaths[$type], $paths ) 101 : $paths; 102 } else { 103 foreach ( $lockedPaths as $lType => $lPaths ) { 104 $status->merge( $this->doFreeLocksOnServer( $lockSrv, $lPaths, $lType ) ); 105 } 106 break; 107 } 108 } 109 110 return $status; 111 } 112 113 // @todo Change this code to work in one batch 114 protected function freeLocksOnServer( $lockSrv, array $pathsByType ) { 115 $status = Status::newGood(); 116 117 foreach ( $pathsByType as $type => $paths ) { 118 $status->merge( $this->doFreeLocksOnServer( $lockSrv, $paths, $type ) ); 119 } 120 121 return $status; 122 } 123 124 /** 125 * @see QuorumLockManager::getLocksOnServer() 126 * @param string $lockSrv 127 * @param array $paths 128 * @param string $type 129 * @return Status 130 */ 131 protected function doGetLocksOnServer( $lockSrv, array $paths, $type ) { 132 $status = Status::newGood(); 133 134 $memc = $this->getCache( $lockSrv ); 135 $keys = array_map( array( $this, 'recordKeyForPath' ), $paths ); // lock records 136 137 // Lock all of the active lock record keys... 138 if ( !$this->acquireMutexes( $memc, $keys ) ) { 139 foreach ( $paths as $path ) { 140 $status->fatal( 'lockmanager-fail-acquirelock', $path ); 141 } 142 143 return $status; 144 } 145 146 // Fetch all the existing lock records... 147 $lockRecords = $memc->getMulti( $keys ); 148 149 $now = time(); 150 // Check if the requested locks conflict with existing ones... 151 foreach ( $paths as $path ) { 152 $locksKey = $this->recordKeyForPath( $path ); 153 $locksHeld = isset( $lockRecords[$locksKey] ) 154 ? self::sanitizeLockArray( $lockRecords[$locksKey] ) 155 : self::newLockArray(); // init 156 foreach ( $locksHeld[self::LOCK_EX] as $session => $expiry ) { 157 if ( $expiry < $now ) { // stale? 158 unset( $locksHeld[self::LOCK_EX][$session] ); 159 } elseif ( $session !== $this->session ) { 160 $status->fatal( 'lockmanager-fail-acquirelock', $path ); 161 } 162 } 163 if ( $type === self::LOCK_EX ) { 164 foreach ( $locksHeld[self::LOCK_SH] as $session => $expiry ) { 165 if ( $expiry < $now ) { // stale? 166 unset( $locksHeld[self::LOCK_SH][$session] ); 167 } elseif ( $session !== $this->session ) { 168 $status->fatal( 'lockmanager-fail-acquirelock', $path ); 169 } 170 } 171 } 172 if ( $status->isOK() ) { 173 // Register the session in the lock record array 174 $locksHeld[$type][$this->session] = $now + $this->lockTTL; 175 // We will update this record if none of the other locks conflict 176 $lockRecords[$locksKey] = $locksHeld; 177 } 178 } 179 180 // If there were no lock conflicts, update all the lock records... 181 if ( $status->isOK() ) { 182 foreach ( $paths as $path ) { 183 $locksKey = $this->recordKeyForPath( $path ); 184 $locksHeld = $lockRecords[$locksKey]; 185 $ok = $memc->set( $locksKey, $locksHeld, 7 * 86400 ); 186 if ( !$ok ) { 187 $status->fatal( 'lockmanager-fail-acquirelock', $path ); 188 } else { 189 wfDebug( __METHOD__ . ": acquired lock on key $locksKey.\n" ); 190 } 191 } 192 } 193 194 // Unlock all of the active lock record keys... 195 $this->releaseMutexes( $memc, $keys ); 196 197 return $status; 198 } 199 200 /** 201 * @see QuorumLockManager::freeLocksOnServer() 202 * @param string $lockSrv 203 * @param array $paths 204 * @param string $type 205 * @return Status 206 */ 207 protected function doFreeLocksOnServer( $lockSrv, array $paths, $type ) { 208 $status = Status::newGood(); 209 210 $memc = $this->getCache( $lockSrv ); 211 $keys = array_map( array( $this, 'recordKeyForPath' ), $paths ); // lock records 212 213 // Lock all of the active lock record keys... 214 if ( !$this->acquireMutexes( $memc, $keys ) ) { 215 foreach ( $paths as $path ) { 216 $status->fatal( 'lockmanager-fail-releaselock', $path ); 217 } 218 219 return $status; 220 } 221 222 // Fetch all the existing lock records... 223 $lockRecords = $memc->getMulti( $keys ); 224 225 // Remove the requested locks from all records... 226 foreach ( $paths as $path ) { 227 $locksKey = $this->recordKeyForPath( $path ); // lock record 228 if ( !isset( $lockRecords[$locksKey] ) ) { 229 $status->warning( 'lockmanager-fail-releaselock', $path ); 230 continue; // nothing to do 231 } 232 $locksHeld = self::sanitizeLockArray( $lockRecords[$locksKey] ); 233 if ( isset( $locksHeld[$type][$this->session] ) ) { 234 unset( $locksHeld[$type][$this->session] ); // unregister this session 235 if ( $locksHeld === self::newLockArray() ) { 236 $ok = $memc->delete( $locksKey ); 237 } else { 238 $ok = $memc->set( $locksKey, $locksHeld ); 239 } 240 if ( !$ok ) { 241 $status->fatal( 'lockmanager-fail-releaselock', $path ); 242 } 243 } else { 244 $status->warning( 'lockmanager-fail-releaselock', $path ); 245 } 246 wfDebug( __METHOD__ . ": released lock on key $locksKey.\n" ); 247 } 248 249 // Unlock all of the active lock record keys... 250 $this->releaseMutexes( $memc, $keys ); 251 252 return $status; 253 } 254 255 /** 256 * @see QuorumLockManager::releaseAllLocks() 257 * @return Status 258 */ 259 protected function releaseAllLocks() { 260 return Status::newGood(); // not supported 261 } 262 263 /** 264 * @see QuorumLockManager::isServerUp() 265 * @param string $lockSrv 266 * @return bool 267 */ 268 protected function isServerUp( $lockSrv ) { 269 return (bool)$this->getCache( $lockSrv ); 270 } 271 272 /** 273 * Get the MemcachedBagOStuff object for a $lockSrv 274 * 275 * @param string $lockSrv Server name 276 * @return MemcachedBagOStuff|null 277 */ 278 protected function getCache( $lockSrv ) { 279 $memc = null; 280 if ( isset( $this->bagOStuffs[$lockSrv] ) ) { 281 $memc = $this->bagOStuffs[$lockSrv]; 282 if ( !isset( $this->serversUp[$lockSrv] ) ) { 283 $this->serversUp[$lockSrv] = $memc->set( __CLASS__ . ':ping', 1, 1 ); 284 if ( !$this->serversUp[$lockSrv] ) { 285 trigger_error( __METHOD__ . ": Could not contact $lockSrv.", E_USER_WARNING ); 286 } 287 } 288 if ( !$this->serversUp[$lockSrv] ) { 289 return null; // server appears to be down 290 } 291 } 292 293 return $memc; 294 } 295 296 /** 297 * @param string $path 298 * @return string 299 */ 300 protected function recordKeyForPath( $path ) { 301 return implode( ':', array( __CLASS__, 'locks', $this->sha1Base36Absolute( $path ) ) ); 302 } 303 304 /** 305 * @return array An empty lock structure for a key 306 */ 307 protected static function newLockArray() { 308 return array( self::LOCK_SH => array(), self::LOCK_EX => array() ); 309 } 310 311 /** 312 * @param array $a 313 * @return array An empty lock structure for a key 314 */ 315 protected static function sanitizeLockArray( $a ) { 316 if ( is_array( $a ) && isset( $a[self::LOCK_EX] ) && isset( $a[self::LOCK_SH] ) ) { 317 return $a; 318 } else { 319 trigger_error( __METHOD__ . ": reset invalid lock array.", E_USER_WARNING ); 320 321 return self::newLockArray(); 322 } 323 } 324 325 /** 326 * @param MemcachedBagOStuff $memc 327 * @param array $keys List of keys to acquire 328 * @return bool 329 */ 330 protected function acquireMutexes( MemcachedBagOStuff $memc, array $keys ) { 331 $lockedKeys = array(); 332 333 // Acquire the keys in lexicographical order, to avoid deadlock problems. 334 // If P1 is waiting to acquire a key P2 has, P2 can't also be waiting for a key P1 has. 335 sort( $keys ); 336 337 // Try to quickly loop to acquire the keys, but back off after a few rounds. 338 // This reduces memcached spam, especially in the rare case where a server acquires 339 // some lock keys and dies without releasing them. Lock keys expire after a few minutes. 340 $rounds = 0; 341 $start = microtime( true ); 342 do { 343 if ( ( ++$rounds % 4 ) == 0 ) { 344 usleep( 1000 * 50 ); // 50 ms 345 } 346 foreach ( array_diff( $keys, $lockedKeys ) as $key ) { 347 if ( $memc->add( "$key:mutex", 1, 180 ) ) { // lock record 348 $lockedKeys[] = $key; 349 } else { 350 continue; // acquire in order 351 } 352 } 353 } while ( count( $lockedKeys ) < count( $keys ) && ( microtime( true ) - $start ) <= 3 ); 354 355 if ( count( $lockedKeys ) != count( $keys ) ) { 356 $this->releaseMutexes( $memc, $lockedKeys ); // failed; release what was locked 357 return false; 358 } 359 360 return true; 361 } 362 363 /** 364 * @param MemcachedBagOStuff $memc 365 * @param array $keys List of acquired keys 366 */ 367 protected function releaseMutexes( MemcachedBagOStuff $memc, array $keys ) { 368 foreach ( $keys as $key ) { 369 $memc->delete( "$key:mutex" ); 370 } 371 } 372 373 /** 374 * Make sure remaining locks get cleared for sanity 375 */ 376 function __destruct() { 377 while ( count( $this->locksHeld ) ) { 378 foreach ( $this->locksHeld as $path => $locks ) { 379 $this->doUnlock( array( $path ), self::LOCK_EX ); 380 $this->doUnlock( array( $path ), self::LOCK_SH ); 381 } 382 } 383 } 384 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Fri Nov 28 14:03:12 2014 | Cross-referenced by PHPXref 0.7.1 |