MediaWiki REL1_39
RedisLockManager.php
Go to the documentation of this file.
1<?php
42 protected $lockTypeMap = [
43 self::LOCK_SH => self::LOCK_SH,
44 self::LOCK_UW => self::LOCK_SH,
45 self::LOCK_EX => self::LOCK_EX
46 ];
47
49 protected $redisPool;
50
52 protected $lockServers = [];
53
65 public function __construct( array $config ) {
66 parent::__construct( $config );
67
68 $this->lockServers = $config['lockServers'];
69 if ( isset( $config['srvsByBucket'] ) ) {
70 // Sanitize srvsByBucket config to prevent PHP errors
71 $this->srvsByBucket = array_filter( $config['srvsByBucket'], 'is_array' );
72 $this->srvsByBucket = array_values( $this->srvsByBucket ); // consecutive
73 } else {
74 $this->srvsByBucket = [ array_keys( $this->lockServers ) ];
75 }
76
77 $config['redisConfig']['serializer'] = 'none';
78 $this->redisPool = RedisConnectionPool::singleton( $config['redisConfig'] );
79 }
80
81 protected function getLocksOnServer( $lockSrv, array $pathsByType ) {
82 $status = StatusValue::newGood();
83
84 $pathList = array_merge( ...array_values( $pathsByType ) );
85
86 $server = $this->lockServers[$lockSrv];
87 $conn = $this->redisPool->getConnection( $server, $this->logger );
88 if ( !$conn ) {
89 foreach ( $pathList as $path ) {
90 $status->fatal( 'lockmanager-fail-acquirelock', $path );
91 }
92
93 return $status;
94 }
95
96 $pathsByKey = []; // (type:hash => path) map
97 foreach ( $pathsByType as $type => $paths ) {
98 $typeString = ( $type == LockManager::LOCK_SH ) ? 'SH' : 'EX';
99 foreach ( $paths as $path ) {
100 $pathsByKey[$this->recordKeyForPath( $path, $typeString )] = $path;
101 }
102 }
103
104 try {
105 static $script =
107<<<LUA
108 local failed = {}
109 -- Load input params (e.g. session, ttl, time of request)
110 local rSession, rTTL, rMaxTTL, rTime = unpack(ARGV)
111 -- Check that all the locks can be acquired
112 for i,requestKey in ipairs(KEYS) do
113 local _, _, rType, resourceKey = string.find(requestKey,"(%w+):(%w+)$")
114 local keyIsFree = true
115 local currentLocks = redis.call('hKeys',resourceKey)
116 for i,lockKey in ipairs(currentLocks) do
117 -- Get the type and session of this lock
118 local _, _, type, session = string.find(lockKey,"(%w+):(%w+)")
119 -- Check any locks that are not owned by this session
120 if session ~= rSession then
121 local lockExpiry = redis.call('hGet',resourceKey,lockKey)
122 if 1*lockExpiry < 1*rTime then
123 -- Lock is stale, so just prune it out
124 redis.call('hDel',resourceKey,lockKey)
125 elseif rType == 'EX' or type == 'EX' then
126 keyIsFree = false
127 break
128 end
129 end
130 end
131 if not keyIsFree then
132 failed[#failed+1] = requestKey
133 end
134 end
135 -- If all locks could be acquired, then do so
136 if #failed == 0 then
137 for i,requestKey in ipairs(KEYS) do
138 local _, _, rType, resourceKey = string.find(requestKey,"(%w+):(%w+)$")
139 redis.call('hSet',resourceKey,rType .. ':' .. rSession,rTime + rTTL)
140 -- In addition to invalidation logic, be sure to garbage collect
141 redis.call('expire',resourceKey,rMaxTTL)
142 end
143 end
144 return failed
145LUA;
146 $res = $conn->luaEval( $script,
147 array_merge(
148 array_keys( $pathsByKey ), // KEYS[0], KEYS[1],...,KEYS[N]
149 [
150 $this->session, // ARGV[1]
151 $this->lockTTL, // ARGV[2]
152 self::MAX_LOCK_TTL, // ARGV[3]
153 time() // ARGV[4]
154 ]
155 ),
156 count( $pathsByKey ) # number of first argument(s) that are keys
157 );
158 } catch ( RedisException $e ) {
159 $res = false;
160 $this->redisPool->handleError( $conn, $e );
161 }
162
163 if ( $res === false ) {
164 foreach ( $pathList as $path ) {
165 $status->fatal( 'lockmanager-fail-acquirelock', $path );
166 }
167 } elseif ( count( $res ) ) {
168 $status->fatal( 'lockmanager-fail-conflict' );
169 }
170
171 return $status;
172 }
173
174 protected function freeLocksOnServer( $lockSrv, array $pathsByType ) {
175 $status = StatusValue::newGood();
176
177 $pathList = array_merge( ...array_values( $pathsByType ) );
178
179 $server = $this->lockServers[$lockSrv];
180 $conn = $this->redisPool->getConnection( $server, $this->logger );
181 if ( !$conn ) {
182 foreach ( $pathList as $path ) {
183 $status->fatal( 'lockmanager-fail-releaselock', $path );
184 }
185
186 return $status;
187 }
188
189 $pathsByKey = []; // (type:hash => path) map
190 foreach ( $pathsByType as $type => $paths ) {
191 $typeString = ( $type == LockManager::LOCK_SH ) ? 'SH' : 'EX';
192 foreach ( $paths as $path ) {
193 $pathsByKey[$this->recordKeyForPath( $path, $typeString )] = $path;
194 }
195 }
196
197 try {
198 static $script =
200<<<LUA
201 local failed = {}
202 -- Load input params (e.g. session)
203 local rSession = unpack(ARGV)
204 for i,requestKey in ipairs(KEYS) do
205 local _, _, rType, resourceKey = string.find(requestKey,"(%w+):(%w+)$")
206 local released = redis.call('hDel',resourceKey,rType .. ':' .. rSession)
207 if released > 0 then
208 -- Remove the whole structure if it is now empty
209 if redis.call('hLen',resourceKey) == 0 then
210 redis.call('del',resourceKey)
211 end
212 else
213 failed[#failed+1] = requestKey
214 end
215 end
216 return failed
217LUA;
218 $res = $conn->luaEval( $script,
219 array_merge(
220 array_keys( $pathsByKey ), // KEYS[0], KEYS[1],...,KEYS[N]
221 [
222 $this->session, // ARGV[1]
223 ]
224 ),
225 count( $pathsByKey ) # number of first argument(s) that are keys
226 );
227 } catch ( RedisException $e ) {
228 $res = false;
229 $this->redisPool->handleError( $conn, $e );
230 }
231
232 if ( $res === false ) {
233 foreach ( $pathList as $path ) {
234 $status->fatal( 'lockmanager-fail-releaselock', $path );
235 }
236 } else {
237 foreach ( $res as $key ) {
238 $status->fatal( 'lockmanager-fail-releaselock', $pathsByKey[$key] );
239 }
240 }
241
242 return $status;
243 }
244
245 protected function releaseAllLocks() {
246 return StatusValue::newGood(); // not supported
247 }
248
249 protected function isServerUp( $lockSrv ) {
250 $conn = $this->redisPool->getConnection( $this->lockServers[$lockSrv], $this->logger );
251
252 return (bool)$conn;
253 }
254
260 protected function recordKeyForPath( $path, $type ) {
261 return implode( ':',
262 [ __CLASS__, 'locks', "$type:" . $this->sha1Base36Absolute( $path ) ] );
263 }
264
268 public function __destruct() {
269 while ( count( $this->locksHeld ) ) {
270 $pathsByType = [];
271 foreach ( $this->locksHeld as $path => $locks ) {
272 foreach ( $locks as $type => $count ) {
273 $pathsByType[$type][] = $path;
274 }
275 }
276 $this->unlockByType( $pathsByType );
277 }
278 }
279}
lock(array $paths, $type=self::LOCK_EX, $timeout=0)
Lock the resources at the given abstract paths.
const LOCK_SH
Lock types; stronger locks have higher values.
sha1Base36Absolute( $path)
Get the base 36 SHA-1 of a string, padded to 31 digits.
unlockByType(array $pathsByType)
Unlock the resources at the given abstract paths.
Version of LockManager that uses a quorum from peer servers for locks.
Helper class to manage Redis connections.
static singleton(array $options)
Manage locks using redis servers.
__construct(array $config)
Construct a new instance from configuration.
recordKeyForPath( $path, $type)
freeLocksOnServer( $lockSrv, array $pathsByType)
Get a connection to a lock server and release locks on $paths.
RedisConnectionPool $redisPool
getLocksOnServer( $lockSrv, array $pathsByType)
Get a connection to a lock server and acquire locks.
array $lockServers
Map server names to hostname/IP and port numbers.
releaseAllLocks()
Release all locks that this session is holding.
__destruct()
Make sure remaining locks get cleared.
array $lockTypeMap
Mapping of lock types to the type actually used.
isServerUp( $lockSrv)
Check if a lock server is up.