MediaWiki
REL1_22
|
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 }