[ Index ]

PHP Cross Reference of MediaWiki-1.24.0

title

Body

[close]

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

   1  <?php
   2  /**
   3   * Version of LockManager based on using redis 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 redis servers.
  26   *
  27   * Version of LockManager based on using redis 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 redis.
  33   * A majority of peers must agree for a lock to be acquired.
  34   *
  35   * This class requires Redis 2.6 as it makes use Lua scripts for fast atomic operations.
  36   *
  37   * @ingroup LockManager
  38   * @since 1.22
  39   */
  40  class RedisLockManager extends QuorumLockManager {
  41      /** @var array Mapping of lock types to the type actually used */
  42      protected $lockTypeMap = array(
  43          self::LOCK_SH => self::LOCK_SH,
  44          self::LOCK_UW => self::LOCK_SH,
  45          self::LOCK_EX => self::LOCK_EX
  46      );
  47  
  48      /** @var RedisConnectionPool */
  49      protected $redisPool;
  50  
  51      /** @var array Map server names to hostname/IP and port numbers */
  52      protected $lockServers = array();
  53  
  54      /** @var string Random UUID */
  55      protected $session = '';
  56  
  57      /**
  58       * Construct a new instance from configuration.
  59       *
  60       * @param array $config Parameters include:
  61       *   - lockServers  : Associative array of server names to "<IP>:<port>" strings.
  62       *   - srvsByBucket : Array of 1-16 consecutive integer keys, starting from 0,
  63       *                    each having an odd-numbered list of server names (peers) as values.
  64       *   - redisConfig  : Configuration for RedisConnectionPool::__construct().
  65       * @throws MWException
  66       */
  67  	public function __construct( array $config ) {
  68          parent::__construct( $config );
  69  
  70          $this->lockServers = $config['lockServers'];
  71          // Sanitize srvsByBucket config to prevent PHP errors
  72          $this->srvsByBucket = array_filter( $config['srvsByBucket'], 'is_array' );
  73          $this->srvsByBucket = array_values( $this->srvsByBucket ); // consecutive
  74  
  75          $config['redisConfig']['serializer'] = 'none';
  76          $this->redisPool = RedisConnectionPool::singleton( $config['redisConfig'] );
  77  
  78          $this->session = wfRandomString( 32 );
  79      }
  80  
  81  	protected function getLocksOnServer( $lockSrv, array $pathsByType ) {
  82          $status = Status::newGood();
  83  
  84          $server = $this->lockServers[$lockSrv];
  85          $conn = $this->redisPool->getConnection( $server );
  86          if ( !$conn ) {
  87              foreach ( array_merge( array_values( $pathsByType ) ) as $path ) {
  88                  $status->fatal( 'lockmanager-fail-acquirelock', $path );
  89              }
  90  
  91              return $status;
  92          }
  93  
  94          $pathsByKey = array(); // (type:hash => path) map
  95          foreach ( $pathsByType as $type => $paths ) {
  96              $typeString = ( $type == LockManager::LOCK_SH ) ? 'SH' : 'EX';
  97              foreach ( $paths as $path ) {
  98                  $pathsByKey[$this->recordKeyForPath( $path, $typeString )] = $path;
  99              }
 100          }
 101  
 102          try {
 103              static $script =
 104  <<<LUA
 105              local failed = {}
 106              -- Load input params (e.g. session, ttl, time of request)
 107              local rSession, rTTL, rTime = unpack(ARGV)
 108              -- Check that all the locks can be acquired
 109              for i,requestKey in ipairs(KEYS) do
 110                  local _, _, rType, resourceKey = string.find(requestKey,"(%w+):(%w+)$")
 111                  local keyIsFree = true
 112                  local currentLocks = redis.call('hKeys',resourceKey)
 113                  for i,lockKey in ipairs(currentLocks) do
 114                      -- Get the type and session of this lock
 115                      local _, _, type, session = string.find(lockKey,"(%w+):(%w+)")
 116                      -- Check any locks that are not owned by this session
 117                      if session ~= rSession then
 118                          local lockExpiry = redis.call('hGet',resourceKey,lockKey)
 119                          if 1*lockExpiry < 1*rTime then
 120                              -- Lock is stale, so just prune it out
 121                              redis.call('hDel',resourceKey,lockKey)
 122                          elseif rType == 'EX' or type == 'EX' then
 123                              keyIsFree = false
 124                              break
 125                          end
 126                      end
 127                  end
 128                  if not keyIsFree then
 129                      failed[#failed+1] = requestKey
 130                  end
 131              end
 132              -- If all locks could be acquired, then do so
 133              if #failed == 0 then
 134                  for i,requestKey in ipairs(KEYS) do
 135                      local _, _, rType, resourceKey = string.find(requestKey,"(%w+):(%w+)$")
 136                      redis.call('hSet',resourceKey,rType .. ':' .. rSession,rTime + rTTL)
 137                      -- In addition to invalidation logic, be sure to garbage collect
 138                      redis.call('expire',resourceKey,rTTL)
 139                  end
 140              end
 141              return failed
 142  LUA;
 143              $res = $conn->luaEval( $script,
 144                  array_merge(
 145                      array_keys( $pathsByKey ), // KEYS[0], KEYS[1],...,KEYS[N]
 146                      array(
 147                          $this->session, // ARGV[1]
 148                          $this->lockTTL, // ARGV[2]
 149                          time() // ARGV[3]
 150                      )
 151                  ),
 152                  count( $pathsByKey ) # number of first argument(s) that are keys
 153              );
 154          } catch ( RedisException $e ) {
 155              $res = false;
 156              $this->redisPool->handleError( $conn, $e );
 157          }
 158  
 159          if ( $res === false ) {
 160              foreach ( array_merge( array_values( $pathsByType ) ) as $path ) {
 161                  $status->fatal( 'lockmanager-fail-acquirelock', $path );
 162              }
 163          } else {
 164              foreach ( $res as $key ) {
 165                  $status->fatal( 'lockmanager-fail-acquirelock', $pathsByKey[$key] );
 166              }
 167          }
 168  
 169          return $status;
 170      }
 171  
 172  	protected function freeLocksOnServer( $lockSrv, array $pathsByType ) {
 173          $status = Status::newGood();
 174  
 175          $server = $this->lockServers[$lockSrv];
 176          $conn = $this->redisPool->getConnection( $server );
 177          if ( !$conn ) {
 178              foreach ( array_merge( array_values( $pathsByType ) ) as $path ) {
 179                  $status->fatal( 'lockmanager-fail-releaselock', $path );
 180              }
 181  
 182              return $status;
 183          }
 184  
 185          $pathsByKey = array(); // (type:hash => path) map
 186          foreach ( $pathsByType as $type => $paths ) {
 187              $typeString = ( $type == LockManager::LOCK_SH ) ? 'SH' : 'EX';
 188              foreach ( $paths as $path ) {
 189                  $pathsByKey[$this->recordKeyForPath( $path, $typeString )] = $path;
 190              }
 191          }
 192  
 193          try {
 194              static $script =
 195  <<<LUA
 196              local failed = {}
 197              -- Load input params (e.g. session)
 198              local rSession = unpack(ARGV)
 199              for i,requestKey in ipairs(KEYS) do
 200                  local _, _, rType, resourceKey = string.find(requestKey,"(%w+):(%w+)$")
 201                  local released = redis.call('hDel',resourceKey,rType .. ':' .. rSession)
 202                  if released > 0 then
 203                      -- Remove the whole structure if it is now empty
 204                      if redis.call('hLen',resourceKey) == 0 then
 205                          redis.call('del',resourceKey)
 206                      end
 207                  else
 208                      failed[#failed+1] = requestKey
 209                  end
 210              end
 211              return failed
 212  LUA;
 213              $res = $conn->luaEval( $script,
 214                  array_merge(
 215                      array_keys( $pathsByKey ), // KEYS[0], KEYS[1],...,KEYS[N]
 216                      array(
 217                          $this->session, // ARGV[1]
 218                      )
 219                  ),
 220                  count( $pathsByKey ) # number of first argument(s) that are keys
 221              );
 222          } catch ( RedisException $e ) {
 223              $res = false;
 224              $this->redisPool->handleError( $conn, $e );
 225          }
 226  
 227          if ( $res === false ) {
 228              foreach ( array_merge( array_values( $pathsByType ) ) as $path ) {
 229                  $status->fatal( 'lockmanager-fail-releaselock', $path );
 230              }
 231          } else {
 232              foreach ( $res as $key ) {
 233                  $status->fatal( 'lockmanager-fail-releaselock', $pathsByKey[$key] );
 234              }
 235          }
 236  
 237          return $status;
 238      }
 239  
 240  	protected function releaseAllLocks() {
 241          return Status::newGood(); // not supported
 242      }
 243  
 244  	protected function isServerUp( $lockSrv ) {
 245          return (bool)$this->redisPool->getConnection( $this->lockServers[$lockSrv] );
 246      }
 247  
 248      /**
 249       * @param string $path
 250       * @param string $type One of (EX,SH)
 251       * @return string
 252       */
 253  	protected function recordKeyForPath( $path, $type ) {
 254          return implode( ':',
 255              array( __CLASS__, 'locks', "$type:" . $this->sha1Base36Absolute( $path ) ) );
 256      }
 257  
 258      /**
 259       * Make sure remaining locks get cleared for sanity
 260       */
 261  	function __destruct() {
 262          while ( count( $this->locksHeld ) ) {
 263              $pathsByType = array();
 264              foreach ( $this->locksHeld as $path => $locks ) {
 265                  foreach ( $locks as $type => $count ) {
 266                      $pathsByType[$type][] = $path;
 267                  }
 268              }
 269              $this->unlockByType( $pathsByType );
 270          }
 271      }
 272  }


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