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