MediaWiki  master
RedisLockManager.php
Go to the documentation of this file.
1 <?php
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 
45  protected $redisPool;
46 
48  protected $lockServers = [];
49 
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 =
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 =
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 
256  protected function recordKeyForPath( $path, $type ) {
257  return implode( ':',
258  [ __CLASS__, 'locks', "$type:" . $this->sha1Base36Absolute( $path ) ] );
259  }
260 
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 }
lock(array $paths, $type=self::LOCK_EX, $timeout=0)
Lock the resources at the given abstract paths.
const LOCK_SH
Lock types; stronger locks have higher values.
Definition: LockManager.php:68
const LOCK_EX
Definition: LockManager.php:70
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.
static singleton(array $options)
Manage locks using redis servers.
__construct(array $config)
Construct a new instance from configuration.
recordKeyForPath( $path, $type)
freeLocksOnServer( $lockSrv, array $pathsByType)
Get a connection to a lock server and release locks on $paths.
RedisConnectionPool $redisPool
getLocksOnServer( $lockSrv, array $pathsByType)
Get a connection to a lock server and acquire locks.
array $lockServers
Map server names to hostname/IP and port numbers.
releaseAllLocks()
Release all locks that this session is holding.
__destruct()
Make sure remaining locks get cleared.
array $lockTypeMap
Mapping of lock types to the type actually used.
isServerUp( $lockSrv)
Check if a lock server is up.
static newGood( $value=null)
Factory function for good results.
Definition: StatusValue.php:85