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