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