From: Aaron Schulz Date: Tue, 19 Feb 2013 02:45:23 +0000 (-0800) Subject: [LockManager] Added a RedisLockManager class. X-Git-Tag: 1.31.0-rc.0~19945 X-Git-Url: https://git.cyclocoop.org/admin/?a=commitdiff_plain;h=ed01e813591acaec424e482791f774d800a466bb;p=lhc%2Fweb%2Fwiklou.git [LockManager] Added a RedisLockManager class. Change-Id: I7ade74eb307a5075533f36836768af60f106a6b9 --- diff --git a/includes/AutoLoader.php b/includes/AutoLoader.php index 88bb21b1b6..4a8ecaa69e 100644 --- a/includes/AutoLoader.php +++ b/includes/AutoLoader.php @@ -573,8 +573,9 @@ $wgAutoloadLocalClasses = array( 'LSLockManager' => 'includes/filebackend/lockmanager/LSLockManager.php', 'MemcLockManager' => 'includes/filebackend/lockmanager/MemcLockManager.php', 'QuorumLockManager' => 'includes/filebackend/lockmanager/QuorumLockManager.php', - 'MySqlLockManager' => 'includes/filebackend/lockmanager/DBLockManager.php', - 'PostgreSqlLockManager' => 'includes/filebackend/lockmanager/DBLockManager.php', + 'MySqlLockManager'=> 'includes/filebackend/lockmanager/DBLockManager.php', + 'PostgreSqlLockManager'=> 'includes/filebackend/lockmanager/DBLockManager.php', + 'RedisLockManager'=> 'includes/filebackend/lockmanager/RedisLockManager.php', 'NullLockManager' => 'includes/filebackend/lockmanager/LockManager.php', 'FileOp' => 'includes/filebackend/FileOp.php', 'FileOpBatch' => 'includes/filebackend/FileOpBatch.php', diff --git a/includes/filebackend/lockmanager/LockManager.php b/includes/filebackend/lockmanager/LockManager.php index 9b7a7ff6c7..9c778c7e24 100644 --- a/includes/filebackend/lockmanager/LockManager.php +++ b/includes/filebackend/lockmanager/LockManager.php @@ -151,7 +151,7 @@ abstract class LockManager { * * @param array $paths List of storage paths * @param $type integer LockManager::LOCK_* constant - * @return string + * @return Status */ abstract protected function doLock( array $paths, $type ); @@ -160,7 +160,7 @@ abstract class LockManager { * * @param array $paths List of storage paths * @param $type integer LockManager::LOCK_* constant - * @return string + * @return Status */ abstract protected function doUnlock( array $paths, $type ); } diff --git a/includes/filebackend/lockmanager/RedisLockManager.php b/includes/filebackend/lockmanager/RedisLockManager.php new file mode 100644 index 0000000000..e8e5996692 --- /dev/null +++ b/includes/filebackend/lockmanager/RedisLockManager.php @@ -0,0 +1,255 @@ + self::LOCK_SH, + self::LOCK_UW => self::LOCK_SH, + self::LOCK_EX => self::LOCK_EX + ); + + /** @var RedisConnectionPool */ + protected $redisPool; + /** @var Array Map server names to hostname/IP and port numbers */ + protected $lockServers = array(); + + protected $session = ''; // string; random UUID + + /** + * Construct a new instance from configuration. + * + * $config paramaters include: + * - lockServers : Associative array of server names to ":" strings. + * - srvsByBucket : Array of 1-16 consecutive integer keys, starting from 0, + * each having an odd-numbered list of server names (peers) as values. + * - redisConfig : Configuration for RedisConnectionPool::__construct(). + * + * @param Array $config + * @throws MWException + */ + public function __construct( array $config ) { + parent::__construct( $config ); + + $this->lockServers = $config['lockServers']; + // Sanitize srvsByBucket config to prevent PHP errors + $this->srvsByBucket = array_filter( $config['srvsByBucket'], 'is_array' ); + $this->srvsByBucket = array_values( $this->srvsByBucket ); // consecutive + + $config['redisConfig']['serializer'] = 'none'; + $this->redisPool = RedisConnectionPool::singleton( $config['redisConfig'] ); + + $this->session = wfRandomString( 32 ); + } + + protected function getLocksOnServer( $lockSrv, array $paths, $type ) { + $status = Status::newGood(); + + $server = $this->lockServers[$lockSrv]; + $conn = $this->redisPool->getConnection( $server ); + if ( !$conn ) { + foreach ( $paths as $path ) { + $status->fatal( 'lockmanager-fail-acquirelock', $path ); + } + return $status; + } + + $keys = array_map( array( $this, 'recordKeyForPath' ), $paths ); // lock records + + try { + static $script = +<<luaEval( $script, + array_merge( + $keys, // KEYS[0], KEYS[1],...KEYS[N] + array( + $type === self::LOCK_SH ? 'SH' : 'EX', // ARGV[1] + $this->session, // ARGV[2] + $this->lockTTL, // ARGV[3] + time() // ARGV[4] + ) + ), + count( $keys ) # number of first argument(s) that are keys + ); + } catch ( RedisException $e ) { + $res = false; + $this->redisPool->handleException( $server, $conn, $e ); + } + + if ( $res === false ) { + foreach ( $paths as $path ) { + $status->fatal( 'lockmanager-fail-acquirelock', $path ); + } + } else { + $pathsByKey = array_combine( $keys, $paths ); + foreach ( $res as $key ) { + $status->fatal( 'lockmanager-fail-acquirelock', $pathsByKey[$key] ); + } + } + + return $status; + } + + protected function freeLocksOnServer( $lockSrv, array $paths, $type ) { + $status = Status::newGood(); + + $server = $this->lockServers[$lockSrv]; + $conn = $this->redisPool->getConnection( $server ); + if ( !$conn ) { + foreach ( $paths as $path ) { + $status->fatal( 'lockmanager-fail-releaselock', $path ); + } + return $status; + } + + $keys = array_map( array( $this, 'recordKeyForPath' ), $paths ); // lock records + + try { + static $script = +<< 0 then + -- Remove the whole structure if it is now empty + if redis.call('hLen',resourceKey) == 0 then + redis.call('del',resourceKey) + end + else + failed[#failed+1] = resourceKey + end + end + return failed +LUA; + $res = $conn->luaEval( $script, + array_merge( + $keys, // KEYS[0], KEYS[1],...KEYS[N] + array( + $type === self::LOCK_SH ? 'SH' : 'EX', // ARGV[1] + $this->session // ARGV[2] + ) + ), + count( $keys ) # number of first argument(s) that are keys + ); + } catch ( RedisException $e ) { + $res = false; + $this->redisPool->handleException( $server, $conn, $e ); + } + + if ( $res === false ) { + foreach ( $paths as $path ) { + $status->fatal( 'lockmanager-fail-releaselock', $path ); + } + } else { + $pathsByKey = array_combine( $keys, $paths ); + foreach ( $res as $key ) { + $status->fatal( 'lockmanager-fail-releaselock', $pathsByKey[$key] ); + } + } + + return $status; + } + + protected function releaseAllLocks() { + return Status::newGood(); // not supported + } + + protected function isServerUp( $lockSrv ) { + return (bool)$this->redisPool->getConnection( $this->lockServers[$lockSrv] ); + } + + /** + * @param $path string + * @return string + */ + protected function recordKeyForPath( $path ) { + return implode( ':', array( __CLASS__, 'locks', $this->sha1Base36Absolute( $path ) ) ); + } + + /** + * Make sure remaining locks get cleared for sanity + */ + function __destruct() { + while ( count( $this->locksHeld ) ) { + foreach ( $this->locksHeld as $path => $locks ) { + $this->doUnlock( array( $path ), self::LOCK_EX ); + $this->doUnlock( array( $path ), self::LOCK_SH ); + } + } + } +}