MediaWiki  REL1_24
RedisLockManager.php
Go to the documentation of this file.
00001 <?php
00040 class RedisLockManager extends QuorumLockManager {
00042     protected $lockTypeMap = array(
00043         self::LOCK_SH => self::LOCK_SH,
00044         self::LOCK_UW => self::LOCK_SH,
00045         self::LOCK_EX => self::LOCK_EX
00046     );
00047 
00049     protected $redisPool;
00050 
00052     protected $lockServers = array();
00053 
00055     protected $session = '';
00056 
00067     public function __construct( array $config ) {
00068         parent::__construct( $config );
00069 
00070         $this->lockServers = $config['lockServers'];
00071         // Sanitize srvsByBucket config to prevent PHP errors
00072         $this->srvsByBucket = array_filter( $config['srvsByBucket'], 'is_array' );
00073         $this->srvsByBucket = array_values( $this->srvsByBucket ); // consecutive
00074 
00075         $config['redisConfig']['serializer'] = 'none';
00076         $this->redisPool = RedisConnectionPool::singleton( $config['redisConfig'] );
00077 
00078         $this->session = wfRandomString( 32 );
00079     }
00080 
00081     protected function getLocksOnServer( $lockSrv, array $pathsByType ) {
00082         $status = Status::newGood();
00083 
00084         $server = $this->lockServers[$lockSrv];
00085         $conn = $this->redisPool->getConnection( $server );
00086         if ( !$conn ) {
00087             foreach ( array_merge( array_values( $pathsByType ) ) as $path ) {
00088                 $status->fatal( 'lockmanager-fail-acquirelock', $path );
00089             }
00090 
00091             return $status;
00092         }
00093 
00094         $pathsByKey = array(); // (type:hash => path) map
00095         foreach ( $pathsByType as $type => $paths ) {
00096             $typeString = ( $type == LockManager::LOCK_SH ) ? 'SH' : 'EX';
00097             foreach ( $paths as $path ) {
00098                 $pathsByKey[$this->recordKeyForPath( $path, $typeString )] = $path;
00099             }
00100         }
00101 
00102         try {
00103             static $script =
00104 <<<LUA
00105             local failed = {}
00106             -- Load input params (e.g. session, ttl, time of request)
00107             local rSession, rTTL, rTime = unpack(ARGV)
00108             -- Check that all the locks can be acquired
00109             for i,requestKey in ipairs(KEYS) do
00110                 local _, _, rType, resourceKey = string.find(requestKey,"(%w+):(%w+)$")
00111                 local keyIsFree = true
00112                 local currentLocks = redis.call('hKeys',resourceKey)
00113                 for i,lockKey in ipairs(currentLocks) do
00114                     -- Get the type and session of this lock
00115                     local _, _, type, session = string.find(lockKey,"(%w+):(%w+)")
00116                     -- Check any locks that are not owned by this session
00117                     if session ~= rSession then
00118                         local lockExpiry = redis.call('hGet',resourceKey,lockKey)
00119                         if 1*lockExpiry < 1*rTime then
00120                             -- Lock is stale, so just prune it out
00121                             redis.call('hDel',resourceKey,lockKey)
00122                         elseif rType == 'EX' or type == 'EX' then
00123                             keyIsFree = false
00124                             break
00125                         end
00126                     end
00127                 end
00128                 if not keyIsFree then
00129                     failed[#failed+1] = requestKey
00130                 end
00131             end
00132             -- If all locks could be acquired, then do so
00133             if #failed == 0 then
00134                 for i,requestKey in ipairs(KEYS) do
00135                     local _, _, rType, resourceKey = string.find(requestKey,"(%w+):(%w+)$")
00136                     redis.call('hSet',resourceKey,rType .. ':' .. rSession,rTime + rTTL)
00137                     -- In addition to invalidation logic, be sure to garbage collect
00138                     redis.call('expire',resourceKey,rTTL)
00139                 end
00140             end
00141             return failed
00142 LUA;
00143             $res = $conn->luaEval( $script,
00144                 array_merge(
00145                     array_keys( $pathsByKey ), // KEYS[0], KEYS[1],...,KEYS[N]
00146                     array(
00147                         $this->session, // ARGV[1]
00148                         $this->lockTTL, // ARGV[2]
00149                         time() // ARGV[3]
00150                     )
00151                 ),
00152                 count( $pathsByKey ) # number of first argument(s) that are keys
00153             );
00154         } catch ( RedisException $e ) {
00155             $res = false;
00156             $this->redisPool->handleError( $conn, $e );
00157         }
00158 
00159         if ( $res === false ) {
00160             foreach ( array_merge( array_values( $pathsByType ) ) as $path ) {
00161                 $status->fatal( 'lockmanager-fail-acquirelock', $path );
00162             }
00163         } else {
00164             foreach ( $res as $key ) {
00165                 $status->fatal( 'lockmanager-fail-acquirelock', $pathsByKey[$key] );
00166             }
00167         }
00168 
00169         return $status;
00170     }
00171 
00172     protected function freeLocksOnServer( $lockSrv, array $pathsByType ) {
00173         $status = Status::newGood();
00174 
00175         $server = $this->lockServers[$lockSrv];
00176         $conn = $this->redisPool->getConnection( $server );
00177         if ( !$conn ) {
00178             foreach ( array_merge( array_values( $pathsByType ) ) as $path ) {
00179                 $status->fatal( 'lockmanager-fail-releaselock', $path );
00180             }
00181 
00182             return $status;
00183         }
00184 
00185         $pathsByKey = array(); // (type:hash => path) map
00186         foreach ( $pathsByType as $type => $paths ) {
00187             $typeString = ( $type == LockManager::LOCK_SH ) ? 'SH' : 'EX';
00188             foreach ( $paths as $path ) {
00189                 $pathsByKey[$this->recordKeyForPath( $path, $typeString )] = $path;
00190             }
00191         }
00192 
00193         try {
00194             static $script =
00195 <<<LUA
00196             local failed = {}
00197             -- Load input params (e.g. session)
00198             local rSession = unpack(ARGV)
00199             for i,requestKey in ipairs(KEYS) do
00200                 local _, _, rType, resourceKey = string.find(requestKey,"(%w+):(%w+)$")
00201                 local released = redis.call('hDel',resourceKey,rType .. ':' .. rSession)
00202                 if released > 0 then
00203                     -- Remove the whole structure if it is now empty
00204                     if redis.call('hLen',resourceKey) == 0 then
00205                         redis.call('del',resourceKey)
00206                     end
00207                 else
00208                     failed[#failed+1] = requestKey
00209                 end
00210             end
00211             return failed
00212 LUA;
00213             $res = $conn->luaEval( $script,
00214                 array_merge(
00215                     array_keys( $pathsByKey ), // KEYS[0], KEYS[1],...,KEYS[N]
00216                     array(
00217                         $this->session, // ARGV[1]
00218                     )
00219                 ),
00220                 count( $pathsByKey ) # number of first argument(s) that are keys
00221             );
00222         } catch ( RedisException $e ) {
00223             $res = false;
00224             $this->redisPool->handleError( $conn, $e );
00225         }
00226 
00227         if ( $res === false ) {
00228             foreach ( array_merge( array_values( $pathsByType ) ) as $path ) {
00229                 $status->fatal( 'lockmanager-fail-releaselock', $path );
00230             }
00231         } else {
00232             foreach ( $res as $key ) {
00233                 $status->fatal( 'lockmanager-fail-releaselock', $pathsByKey[$key] );
00234             }
00235         }
00236 
00237         return $status;
00238     }
00239 
00240     protected function releaseAllLocks() {
00241         return Status::newGood(); // not supported
00242     }
00243 
00244     protected function isServerUp( $lockSrv ) {
00245         return (bool)$this->redisPool->getConnection( $this->lockServers[$lockSrv] );
00246     }
00247 
00253     protected function recordKeyForPath( $path, $type ) {
00254         return implode( ':',
00255             array( __CLASS__, 'locks', "$type:" . $this->sha1Base36Absolute( $path ) ) );
00256     }
00257 
00261     function __destruct() {
00262         while ( count( $this->locksHeld ) ) {
00263             $pathsByType = array();
00264             foreach ( $this->locksHeld as $path => $locks ) {
00265                 foreach ( $locks as $type => $count ) {
00266                     $pathsByType[$type][] = $path;
00267                 }
00268             }
00269             $this->unlockByType( $pathsByType );
00270         }
00271     }
00272 }