[ Index ]

PHP Cross Reference of MediaWiki-1.24.0

title

Body

[close]

/includes/filebackend/lockmanager/ -> MemcLockManager.php (source)

   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  }


Generated: Fri Nov 28 14:03:12 2014 Cross-referenced by PHPXref 0.7.1