Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 90 |
|
0.00% |
0 / 7 |
CRAP | |
0.00% |
0 / 1 |
RedisLockManager | |
0.00% |
0 / 90 |
|
0.00% |
0 / 7 |
870 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
6 | |||
getLocksOnServer | |
0.00% |
0 / 37 |
|
0.00% |
0 / 1 |
110 | |||
freeLocksOnServer | |
0.00% |
0 / 34 |
|
0.00% |
0 / 1 |
110 | |||
releaseAllLocks | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
isServerUp | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
recordKeyForPath | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
__destruct | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
20 |
1 | <?php |
2 | /** |
3 | * This program is free software; you can redistribute it and/or modify |
4 | * it under the terms of the GNU General Public License as published by |
5 | * the Free Software Foundation; either version 2 of the License, or |
6 | * (at your option) any later version. |
7 | * |
8 | * This program is distributed in the hope that it will be useful, |
9 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
10 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
11 | * GNU General Public License for more details. |
12 | * |
13 | * You should have received a copy of the GNU General Public License along |
14 | * with this program; if not, write to the Free Software Foundation, Inc., |
15 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
16 | * http://www.gnu.org/copyleft/gpl.html |
17 | * |
18 | * @file |
19 | */ |
20 | |
21 | /** |
22 | * Manage locks using redis servers. |
23 | * |
24 | * This is meant for multi-wiki systems that may share files. |
25 | * All locks are non-blocking, which avoids deadlocks. |
26 | * |
27 | * All lock requests for a resource, identified by a hash string, will map to one |
28 | * bucket. Each bucket maps to one or several peer servers, each running redis. |
29 | * A majority of peers must agree for a lock to be acquired. |
30 | * |
31 | * This class requires Redis 2.6 as it makes use of Lua scripts for fast atomic operations. |
32 | * |
33 | * @ingroup LockManager |
34 | * @since 1.22 |
35 | */ |
36 | class RedisLockManager extends QuorumLockManager { |
37 | /** @var array Mapping of lock types to the type actually used */ |
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 | |
44 | /** @var RedisConnectionPool */ |
45 | protected $redisPool; |
46 | |
47 | /** @var array Map server names to hostname/IP and port numbers */ |
48 | protected $lockServers = []; |
49 | |
50 | /** |
51 | * Construct a new instance from configuration. |
52 | * |
53 | * @param array $config Parameters include: |
54 | * - lockServers : Associative array of server names to "<IP>:<port>" strings. |
55 | * - srvsByBucket : An array of up to 16 arrays, each containing the server names |
56 | * in a bucket. Each bucket should have an odd number of servers. |
57 | * If omitted, all servers will be in one bucket. (optional). |
58 | * - redisConfig : Configuration for RedisConnectionPool::singleton() (optional). |
59 | * @throws Exception |
60 | */ |
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 = |
102 | /** @lang Lua */ |
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 |
141 | LUA; |
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 = |
195 | /** @lang Lua */ |
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 |
213 | LUA; |
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 | |
251 | /** |
252 | * @param string $path |
253 | * @param string $type One of (EX,SH) |
254 | * @return string |
255 | */ |
256 | protected function recordKeyForPath( $path, $type ) { |
257 | return implode( ':', |
258 | [ __CLASS__, 'locks', "$type:" . $this->sha1Base36Absolute( $path ) ] ); |
259 | } |
260 | |
261 | /** |
262 | * Make sure remaining locks get cleared |
263 | */ |
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 | } |