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