MediaWiki  REL1_22
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;
00051     protected $lockServers = array();
00052 
00053     protected $session = ''; // string; random UUID
00054 
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     // @TODO: change this code to work in one batch
00082     protected function getLocksOnServer( $lockSrv, array $pathsByType ) {
00083         $status = Status::newGood();
00084 
00085         $lockedPaths = array();
00086         foreach ( $pathsByType as $type => $paths ) {
00087             $status->merge( $this->doGetLocksOnServer( $lockSrv, $paths, $type ) );
00088             if ( $status->isOK() ) {
00089                 $lockedPaths[$type] = isset( $lockedPaths[$type] )
00090                     ? array_merge( $lockedPaths[$type], $paths )
00091                     : $paths;
00092             } else {
00093                 foreach ( $lockedPaths as $type => $paths ) {
00094                     $status->merge( $this->doFreeLocksOnServer( $lockSrv, $paths, $type ) );
00095                 }
00096                 break;
00097             }
00098         }
00099 
00100         return $status;
00101     }
00102 
00103     // @TODO: change this code to work in one batch
00104     protected function freeLocksOnServer( $lockSrv, array $pathsByType ) {
00105         $status = Status::newGood();
00106 
00107         foreach ( $pathsByType as $type => $paths ) {
00108             $status->merge( $this->doFreeLocksOnServer( $lockSrv, $paths, $type ) );
00109         }
00110 
00111         return $status;
00112     }
00113 
00114     protected function doGetLocksOnServer( $lockSrv, array $paths, $type ) {
00115         $status = Status::newGood();
00116 
00117         $server = $this->lockServers[$lockSrv];
00118         $conn = $this->redisPool->getConnection( $server );
00119         if ( !$conn ) {
00120             foreach ( $paths as $path ) {
00121                 $status->fatal( 'lockmanager-fail-acquirelock', $path );
00122             }
00123             return $status;
00124         }
00125 
00126         $keys = array_map( array( $this, 'recordKeyForPath' ), $paths ); // lock records
00127 
00128         try {
00129             static $script =
00130 <<<LUA
00131             if ARGV[1] ~= 'EX' and ARGV[1] ~= 'SH' then
00132                 return redis.error_reply('Unrecognized lock type given (must be EX or SH)')
00133             end
00134             local failed = {}
00135             -- Check that all the locks can be acquired
00136             for i,resourceKey in ipairs(KEYS) do
00137                 local keyIsFree = true
00138                 local currentLocks = redis.call('hKeys',resourceKey)
00139                 for i,lockKey in ipairs(currentLocks) do
00140                     local _, _, type, session = string.find(lockKey,"(%w+):(%w+)")
00141                     -- Check any locks that are not owned by this session
00142                     if session ~= ARGV[2] then
00143                         local lockTimestamp = redis.call('hGet',resourceKey,lockKey)
00144                         if 1*lockTimestamp < ( ARGV[4] - ARGV[3] ) then
00145                             -- Lock is stale, so just prune it out
00146                             redis.call('hDel',resourceKey,lockKey)
00147                         elseif ARGV[1] == 'EX' or type == 'EX' then
00148                             keyIsFree = false
00149                             break
00150                         end
00151                     end
00152                 end
00153                 if not keyIsFree then
00154                     failed[#failed+1] = resourceKey
00155                 end
00156             end
00157             -- If all locks could be acquired, then do so
00158             if #failed == 0 then
00159                 for i,resourceKey in ipairs(KEYS) do
00160                     redis.call('hSet',resourceKey,ARGV[1] .. ':' .. ARGV[2],ARGV[4])
00161                     -- In addition to invalidation logic, be sure to garbage collect
00162                     redis.call('expire',resourceKey,ARGV[3])
00163                 end
00164             end
00165             return failed
00166 LUA;
00167             $res = $conn->luaEval( $script,
00168                 array_merge(
00169                     $keys, // KEYS[0], KEYS[1],...KEYS[N]
00170                     array(
00171                         $type === self::LOCK_SH ? 'SH' : 'EX', // ARGV[1]
00172                         $this->session, // ARGV[2]
00173                         $this->lockTTL, // ARGV[3]
00174                         time() // ARGV[4]
00175                     )
00176                 ),
00177                 count( $keys ) # number of first argument(s) that are keys
00178             );
00179         } catch ( RedisException $e ) {
00180             $res = false;
00181             $this->redisPool->handleException( $server, $conn, $e );
00182         }
00183 
00184         if ( $res === false ) {
00185             foreach ( $paths as $path ) {
00186                 $status->fatal( 'lockmanager-fail-acquirelock', $path );
00187             }
00188         } else {
00189             $pathsByKey = array_combine( $keys, $paths );
00190             foreach ( $res as $key ) {
00191                 $status->fatal( 'lockmanager-fail-acquirelock', $pathsByKey[$key] );
00192             }
00193         }
00194 
00195         return $status;
00196     }
00197 
00198     protected function doFreeLocksOnServer( $lockSrv, array $paths, $type ) {
00199         $status = Status::newGood();
00200 
00201         $server = $this->lockServers[$lockSrv];
00202         $conn = $this->redisPool->getConnection( $server );
00203         if ( !$conn ) {
00204             foreach ( $paths as $path ) {
00205                 $status->fatal( 'lockmanager-fail-releaselock', $path );
00206             }
00207             return $status;
00208         }
00209 
00210         $keys = array_map( array( $this, 'recordKeyForPath' ), $paths ); // lock records
00211 
00212         try {
00213             static $script =
00214 <<<LUA
00215             if ARGV[1] ~= 'EX' and ARGV[1] ~= 'SH' then
00216                 return redis.error_reply('Unrecognized lock type given (must be EX or SH)')
00217             end
00218             local failed = {}
00219             for i,resourceKey in ipairs(KEYS) do
00220                 local released = redis.call('hDel',resourceKey,ARGV[1] .. ':' .. ARGV[2])
00221                 if released > 0 then
00222                     -- Remove the whole structure if it is now empty
00223                     if redis.call('hLen',resourceKey) == 0 then
00224                         redis.call('del',resourceKey)
00225                     end
00226                 else
00227                     failed[#failed+1] = resourceKey
00228                 end
00229             end
00230             return failed
00231 LUA;
00232             $res = $conn->luaEval( $script,
00233                 array_merge(
00234                     $keys, // KEYS[0], KEYS[1],...KEYS[N]
00235                     array(
00236                         $type === self::LOCK_SH ? 'SH' : 'EX', // ARGV[1]
00237                         $this->session // ARGV[2]
00238                     )
00239                 ),
00240                 count( $keys ) # number of first argument(s) that are keys
00241             );
00242         } catch ( RedisException $e ) {
00243             $res = false;
00244             $this->redisPool->handleException( $server, $conn, $e );
00245         }
00246 
00247         if ( $res === false ) {
00248             foreach ( $paths as $path ) {
00249                 $status->fatal( 'lockmanager-fail-releaselock', $path );
00250             }
00251         } else {
00252             $pathsByKey = array_combine( $keys, $paths );
00253             foreach ( $res as $key ) {
00254                 $status->fatal( 'lockmanager-fail-releaselock', $pathsByKey[$key] );
00255             }
00256         }
00257 
00258         return $status;
00259     }
00260 
00261     protected function releaseAllLocks() {
00262         return Status::newGood(); // not supported
00263     }
00264 
00265     protected function isServerUp( $lockSrv ) {
00266         return (bool)$this->redisPool->getConnection( $this->lockServers[$lockSrv] );
00267     }
00268 
00273     protected function recordKeyForPath( $path ) {
00274         return implode( ':', array( __CLASS__, 'locks', $this->sha1Base36Absolute( $path ) ) );
00275     }
00276 
00280     function __destruct() {
00281         while ( count( $this->locksHeld ) ) {
00282             foreach ( $this->locksHeld as $path => $locks ) {
00283                 $this->doUnlock( array( $path ), self::LOCK_EX );
00284                 $this->doUnlock( array( $path ), self::LOCK_SH );
00285             }
00286         }
00287     }
00288 }