MediaWiki  master
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
145 LUA;
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
217 LUA;
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 }
RedisLockManager\$lockTypeMap
array $lockTypeMap
Mapping of lock types to the type actually used.
Definition: RedisLockManager.php:42
RedisConnectionPool\singleton
static singleton(array $options)
Definition: RedisConnectionPool.php:149
LockManager\LOCK_SH
const LOCK_SH
Lock types; stronger locks have higher values.
Definition: LockManager.php:70
RedisLockManager\isServerUp
isServerUp( $lockSrv)
Check if a lock server is up.
Definition: RedisLockManager.php:249
RedisLockManager
Manage locks using redis servers.
Definition: RedisLockManager.php:40
RedisLockManager\releaseAllLocks
releaseAllLocks()
Release all locks that this session is holding.
Definition: RedisLockManager.php:245
RedisLockManager\recordKeyForPath
recordKeyForPath( $path, $type)
Definition: RedisLockManager.php:260
$res
$res
Definition: testCompression.php:57
RedisLockManager\$lockServers
array $lockServers
Map server names to hostname/IP and port numbers.
Definition: RedisLockManager.php:52
QuorumLockManager
Version of LockManager that uses a quorum from peer servers for locks.
Definition: QuorumLockManager.php:32
LockManager\sha1Base36Absolute
sha1Base36Absolute( $path)
Get the base 36 SHA-1 of a string, padded to 31 digits.
Definition: LockManager.php:191
RedisLockManager\freeLocksOnServer
freeLocksOnServer( $lockSrv, array $pathsByType)
Get a connection to a lock server and release locks on $paths.
Definition: RedisLockManager.php:174
RedisLockManager\__destruct
__destruct()
Make sure remaining locks get cleared.
Definition: RedisLockManager.php:268
LockManager\lock
lock(array $paths, $type=self::LOCK_EX, $timeout=0)
Lock the resources at the given abstract paths.
Definition: LockManager.php:129
LockManager\unlockByType
unlockByType(array $pathsByType)
Unlock the resources at the given abstract paths.
Definition: LockManager.php:176
RedisConnectionPool
Helper class to manage Redis connections.
Definition: RedisConnectionPool.php:41
RedisLockManager\$redisPool
RedisConnectionPool $redisPool
Definition: RedisLockManager.php:49
StatusValue\newGood
static newGood( $value=null)
Factory function for good results.
Definition: StatusValue.php:82
$path
$path
Definition: NoLocalSettings.php:25
LockManager\LOCK_EX
const LOCK_EX
Definition: LockManager.php:72
RedisLockManager\__construct
__construct(array $config)
Construct a new instance from configuration.
Definition: RedisLockManager.php:65
RedisLockManager\getLocksOnServer
getLocksOnServer( $lockSrv, array $pathsByType)
Get a connection to a lock server and acquire locks.
Definition: RedisLockManager.php:81
$type
$type
Definition: testCompression.php:52