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