diff options
Diffstat (limited to 'includes/filebackend/lockmanager')
-rw-r--r-- | includes/filebackend/lockmanager/DBLockManager.php | 37 | ||||
-rw-r--r-- | includes/filebackend/lockmanager/LockManager.php | 125 | ||||
-rw-r--r-- | includes/filebackend/lockmanager/LockManagerGroup.php | 4 | ||||
-rw-r--r-- | includes/filebackend/lockmanager/MemcLockManager.php | 41 | ||||
-rw-r--r-- | includes/filebackend/lockmanager/QuorumLockManager.php | 140 | ||||
-rw-r--r-- | includes/filebackend/lockmanager/RedisLockManager.php | 288 | ||||
-rw-r--r-- | includes/filebackend/lockmanager/ScopedLock.php | 44 |
7 files changed, 548 insertions, 131 deletions
diff --git a/includes/filebackend/lockmanager/DBLockManager.php b/includes/filebackend/lockmanager/DBLockManager.php index f02387dc..3e934ba5 100644 --- a/includes/filebackend/lockmanager/DBLockManager.php +++ b/includes/filebackend/lockmanager/DBLockManager.php @@ -110,6 +110,19 @@ abstract class DBLockManager extends QuorumLockManager { $this->session = wfRandomString( 31 ); } + // @TODO: change this code to work in one batch + protected function getLocksOnServer( $lockSrv, array $pathsByType ) { + $status = Status::newGood(); + foreach ( $pathsByType as $type => $paths ) { + $status->merge( $this->doGetLocksOnServer( $lockSrv, $paths, $type ) ); + } + return $status; + } + + protected function freeLocksOnServer( $lockSrv, array $pathsByType ) { + return Status::newGood(); + } + /** * @see QuorumLockManager::isServerUp() * @return bool @@ -252,7 +265,7 @@ class MySqlLockManager extends DBLockManager { * @see DBLockManager::getLocksOnServer() * @return Status */ - protected function getLocksOnServer( $lockSrv, array $paths, $type ) { + protected function doGetLocksOnServer( $lockSrv, array $paths, $type ) { $status = Status::newGood(); $db = $this->getConnection( $lockSrv ); // checked in isServerUp() @@ -319,14 +332,6 @@ class MySqlLockManager extends DBLockManager { } /** - * @see QuorumLockManager::freeLocksOnServer() - * @return Status - */ - protected function freeLocksOnServer( $lockSrv, array $paths, $type ) { - return Status::newGood(); // not supported - } - - /** * @see QuorumLockManager::releaseAllLocks() * @return Status */ @@ -361,7 +366,7 @@ class PostgreSqlLockManager extends DBLockManager { self::LOCK_EX => self::LOCK_EX ); - protected function getLocksOnServer( $lockSrv, array $paths, $type ) { + protected function doGetLocksOnServer( $lockSrv, array $paths, $type ) { $status = Status::newGood(); if ( !count( $paths ) ) { return $status; // nothing to lock @@ -369,7 +374,9 @@ class PostgreSqlLockManager extends DBLockManager { $db = $this->getConnection( $lockSrv ); // checked in isServerUp() $bigints = array_unique( array_map( - function( $key ) { return wfBaseConvert( substr( $key, 0, 15 ), 16, 10 ); }, + function( $key ) { + return wfBaseConvert( substr( $key, 0, 15 ), 16, 10 ); + }, array_map( array( $this, 'sha1Base16Absolute' ), $paths ) ) ); @@ -406,14 +413,6 @@ class PostgreSqlLockManager extends DBLockManager { } /** - * @see QuorumLockManager::freeLocksOnServer() - * @return Status - */ - protected function freeLocksOnServer( $lockSrv, array $paths, $type ) { - return Status::newGood(); // not supported - } - - /** * @see QuorumLockManager::releaseAllLocks() * @return Status */ diff --git a/includes/filebackend/lockmanager/LockManager.php b/includes/filebackend/lockmanager/LockManager.php index 0512a01b..dad8a624 100644 --- a/includes/filebackend/lockmanager/LockManager.php +++ b/includes/filebackend/lockmanager/LockManager.php @@ -56,7 +56,7 @@ abstract class LockManager { protected $domain; // string; domain (usually wiki ID) protected $lockTTL; // integer; maximum time locks can be held - /* Lock types; stronger locks have higher values */ + /** Lock types; stronger locks have higher values */ const LOCK_SH = 1; // shared lock (for reads) const LOCK_UW = 2; // shared lock (for reads used to write elsewhere) const LOCK_EX = 3; // exclusive lock (for writes) @@ -76,10 +76,10 @@ abstract class LockManager { if ( isset( $config['lockTTL'] ) ) { $this->lockTTL = max( 1, $config['lockTTL'] ); } elseif ( PHP_SAPI === 'cli' ) { - $this->lockTTL = 2*3600; + $this->lockTTL = 2 * 3600; } else { $met = ini_get( 'max_execution_time' ); // this is 0 in CLI mode - $this->lockTTL = max( 5*60, 2*(int)$met ); + $this->lockTTL = max( 5 * 60, 2 * (int)$met ); } } @@ -88,11 +88,36 @@ abstract class LockManager { * * @param array $paths List of resource names * @param $type integer LockManager::LOCK_* constant + * @param integer $timeout Timeout in seconds (0 means non-blocking) (since 1.21) * @return Status */ - final public function lock( array $paths, $type = self::LOCK_EX ) { + final public function lock( array $paths, $type = self::LOCK_EX, $timeout = 0 ) { + return $this->lockByType( array( $type => $paths ), $timeout ); + } + + /** + * Lock the resources at the given abstract paths + * + * @param array $pathsByType Map of LockManager::LOCK_* constants to lists of paths + * @param integer $timeout Timeout in seconds (0 means non-blocking) (since 1.21) + * @return Status + * @since 1.22 + */ + final public function lockByType( array $pathsByType, $timeout = 0 ) { wfProfileIn( __METHOD__ ); - $status = $this->doLock( array_unique( $paths ), $this->lockTypeMap[$type] ); + $status = Status::newGood(); + $pathsByType = $this->normalizePathsByType( $pathsByType ); + $msleep = array( 0, 50, 100, 300, 500 ); // retry backoff times + $start = microtime( true ); + do { + $status = $this->doLockByType( $pathsByType ); + $elapsed = microtime( true ) - $start; + if ( $status->isOK() || $elapsed >= $timeout || $elapsed < 0 ) { + break; // success, timeout, or clock set back + } + usleep( 1e3 * ( next( $msleep ) ?: 1000 ) ); // use 1 sec after enough times + $elapsed = microtime( true ) - $start; + } while ( $elapsed < $timeout && $elapsed >= 0 ); wfProfileOut( __METHOD__ ); return $status; } @@ -100,13 +125,25 @@ abstract class LockManager { /** * Unlock the resources at the given abstract paths * - * @param array $paths List of storage paths + * @param array $paths List of paths * @param $type integer LockManager::LOCK_* constant * @return Status */ final public function unlock( array $paths, $type = self::LOCK_EX ) { + return $this->unlockByType( array( $type => $paths ) ); + } + + /** + * Unlock the resources at the given abstract paths + * + * @param array $pathsByType Map of LockManager::LOCK_* constants to lists of paths + * @return Status + * @since 1.22 + */ + final public function unlockByType( array $pathsByType ) { wfProfileIn( __METHOD__ ); - $status = $this->doUnlock( array_unique( $paths ), $this->lockTypeMap[$type] ); + $pathsByType = $this->normalizePathsByType( $pathsByType ); + $status = $this->doUnlockByType( $pathsByType ); wfProfileOut( __METHOD__ ); return $status; } @@ -136,20 +173,74 @@ abstract class LockManager { } /** + * Normalize the $paths array by converting LOCK_UW locks into the + * appropriate type and removing any duplicated paths for each lock type. + * + * @param array $paths Map of LockManager::LOCK_* constants to lists of paths + * @return Array + * @since 1.22 + */ + final protected function normalizePathsByType( array $pathsByType ) { + $res = array(); + foreach ( $pathsByType as $type => $paths ) { + $res[$this->lockTypeMap[$type]] = array_unique( $paths ); + } + return $res; + } + + /** + * @see LockManager::lockByType() + * @param array $paths Map of LockManager::LOCK_* constants to lists of paths + * @return Status + * @since 1.22 + */ + protected function doLockByType( array $pathsByType ) { + $status = Status::newGood(); + $lockedByType = array(); // map of (type => paths) + foreach ( $pathsByType as $type => $paths ) { + $status->merge( $this->doLock( $paths, $type ) ); + if ( $status->isOK() ) { + $lockedByType[$type] = $paths; + } else { + // Release the subset of locks that were acquired + foreach ( $lockedByType as $type => $paths ) { + $status->merge( $this->doUnlock( $paths, $type ) ); + } + break; + } + } + return $status; + } + + /** * Lock resources with the given keys and lock type * - * @param array $paths List of storage paths + * @param array $paths List of paths * @param $type integer LockManager::LOCK_* constant - * @return string + * @return Status */ abstract protected function doLock( array $paths, $type ); /** + * @see LockManager::unlockByType() + * @param array $paths Map of LockManager::LOCK_* constants to lists of paths + * @return Status + * @since 1.22 + */ + protected function doUnlockByType( array $pathsByType ) { + $status = Status::newGood(); + foreach ( $pathsByType as $type => $paths ) { + $status->merge( $this->doUnlock( $paths, $type ) ); + } + return $status; + } + + /** * Unlock resources with the given keys and lock type * - * @param array $paths List of storage paths + * @param array $paths List of paths * @param $type integer LockManager::LOCK_* constant - * @return string + * @return Status */ abstract protected function doUnlock( array $paths, $type ); } @@ -159,22 +250,10 @@ abstract class LockManager { * @since 1.19 */ class NullLockManager extends LockManager { - /** - * @see LockManager::doLock() - * @param $paths array - * @param $type int - * @return Status - */ protected function doLock( array $paths, $type ) { return Status::newGood(); } - /** - * @see LockManager::doUnlock() - * @param $paths array - * @param $type int - * @return Status - */ protected function doUnlock( array $paths, $type ) { return Status::newGood(); } diff --git a/includes/filebackend/lockmanager/LockManagerGroup.php b/includes/filebackend/lockmanager/LockManagerGroup.php index ac0bd49b..9aff2415 100644 --- a/includes/filebackend/lockmanager/LockManagerGroup.php +++ b/includes/filebackend/lockmanager/LockManagerGroup.php @@ -97,8 +97,8 @@ class LockManagerGroup { $class = $config['class']; unset( $config['class'] ); // lock manager won't need this $this->managers[$name] = array( - 'class' => $class, - 'config' => $config, + 'class' => $class, + 'config' => $config, 'instance' => null ); } diff --git a/includes/filebackend/lockmanager/MemcLockManager.php b/includes/filebackend/lockmanager/MemcLockManager.php index fafc588a..5eab03ee 100644 --- a/includes/filebackend/lockmanager/MemcLockManager.php +++ b/includes/filebackend/lockmanager/MemcLockManager.php @@ -88,11 +88,44 @@ class MemcLockManager extends QuorumLockManager { $this->session = wfRandomString( 32 ); } + // @TODO: change this code to work in one batch + protected function getLocksOnServer( $lockSrv, array $pathsByType ) { + $status = Status::newGood(); + + $lockedPaths = array(); + foreach ( $pathsByType as $type => $paths ) { + $status->merge( $this->doGetLocksOnServer( $lockSrv, $paths, $type ) ); + if ( $status->isOK() ) { + $lockedPaths[$type] = isset( $lockedPaths[$type] ) + ? array_merge( $lockedPaths[$type], $paths ) + : $paths; + } else { + foreach ( $lockedPaths as $type => $paths ) { + $status->merge( $this->doFreeLocksOnServer( $lockSrv, $paths, $type ) ); + } + break; + } + } + + return $status; + } + + // @TODO: change this code to work in one batch + protected function freeLocksOnServer( $lockSrv, array $pathsByType ) { + $status = Status::newGood(); + + foreach ( $pathsByType as $type => $paths ) { + $status->merge( $this->doFreeLocksOnServer( $lockSrv, $paths, $type ) ); + } + + return $status; + } + /** * @see QuorumLockManager::getLocksOnServer() * @return Status */ - protected function getLocksOnServer( $lockSrv, array $paths, $type ) { + protected function doGetLocksOnServer( $lockSrv, array $paths, $type ) { $status = Status::newGood(); $memc = $this->getCache( $lockSrv ); @@ -145,7 +178,7 @@ class MemcLockManager extends QuorumLockManager { foreach ( $paths as $path ) { $locksKey = $this->recordKeyForPath( $path ); $locksHeld = $lockRecords[$locksKey]; - $ok = $memc->set( $locksKey, $locksHeld, 7*86400 ); + $ok = $memc->set( $locksKey, $locksHeld, 7 * 86400 ); if ( !$ok ) { $status->fatal( 'lockmanager-fail-acquirelock', $path ); } else { @@ -164,7 +197,7 @@ class MemcLockManager extends QuorumLockManager { * @see QuorumLockManager::freeLocksOnServer() * @return Status */ - protected function freeLocksOnServer( $lockSrv, array $paths, $type ) { + protected function doFreeLocksOnServer( $lockSrv, array $paths, $type ) { $status = Status::newGood(); $memc = $this->getCache( $lockSrv ); @@ -297,7 +330,7 @@ class MemcLockManager extends QuorumLockManager { $start = microtime( true ); do { if ( ( ++$rounds % 4 ) == 0 ) { - usleep( 1000*50 ); // 50 ms + usleep( 1000 * 50 ); // 50 ms } foreach ( array_diff( $keys, $lockedKeys ) as $key ) { if ( $memc->add( "$key:mutex", 1, 180 ) ) { // lock record diff --git a/includes/filebackend/lockmanager/QuorumLockManager.php b/includes/filebackend/lockmanager/QuorumLockManager.php index b331b540..8356d32a 100644 --- a/includes/filebackend/lockmanager/QuorumLockManager.php +++ b/includes/filebackend/lockmanager/QuorumLockManager.php @@ -31,81 +31,86 @@ abstract class QuorumLockManager extends LockManager { /** @var Array Map of bucket indexes to peer server lists */ protected $srvsByBucket = array(); // (bucket index => (lsrv1, lsrv2, ...)) + /** @var Array Map of degraded buckets */ + protected $degradedBuckets = array(); // (buckey index => UNIX timestamp) - /** - * @see LockManager::doLock() - * @param $paths array - * @param $type int - * @return Status - */ final protected function doLock( array $paths, $type ) { + return $this->doLockByType( array( $type => $paths ) ); + } + + final protected function doUnlock( array $paths, $type ) { + return $this->doUnlockByType( array( $type => $paths ) ); + } + + protected function doLockByType( array $pathsByType ) { $status = Status::newGood(); - $pathsToLock = array(); // (bucket => paths) + $pathsToLock = array(); // (bucket => type => paths) // Get locks that need to be acquired (buckets => locks)... - foreach ( $paths as $path ) { - if ( isset( $this->locksHeld[$path][$type] ) ) { - ++$this->locksHeld[$path][$type]; - } else { - $bucket = $this->getBucketFromPath( $path ); - $pathsToLock[$bucket][] = $path; + foreach ( $pathsByType as $type => $paths ) { + foreach ( $paths as $path ) { + if ( isset( $this->locksHeld[$path][$type] ) ) { + ++$this->locksHeld[$path][$type]; + } else { + $bucket = $this->getBucketFromPath( $path ); + $pathsToLock[$bucket][$type][] = $path; + } } } - $lockedPaths = array(); // files locked in this attempt + $lockedPaths = array(); // files locked in this attempt (type => paths) // Attempt to acquire these locks... - foreach ( $pathsToLock as $bucket => $paths ) { + foreach ( $pathsToLock as $bucket => $pathsToLockByType ) { // Try to acquire the locks for this bucket - $status->merge( $this->doLockingRequestBucket( $bucket, $paths, $type ) ); + $status->merge( $this->doLockingRequestBucket( $bucket, $pathsToLockByType ) ); if ( !$status->isOK() ) { - $status->merge( $this->doUnlock( $lockedPaths, $type ) ); + $status->merge( $this->doUnlockByType( $lockedPaths ) ); return $status; } // Record these locks as active - foreach ( $paths as $path ) { - $this->locksHeld[$path][$type] = 1; // locked + foreach ( $pathsToLockByType as $type => $paths ) { + foreach ( $paths as $path ) { + $this->locksHeld[$path][$type] = 1; // locked + // Keep track of what locks were made in this attempt + $lockedPaths[$type][] = $path; + } } - // Keep track of what locks were made in this attempt - $lockedPaths = array_merge( $lockedPaths, $paths ); } return $status; } - /** - * @see LockManager::doUnlock() - * @param $paths array - * @param $type int - * @return Status - */ - final protected function doUnlock( array $paths, $type ) { + protected function doUnlockByType( array $pathsByType ) { $status = Status::newGood(); - $pathsToUnlock = array(); - foreach ( $paths as $path ) { - if ( !isset( $this->locksHeld[$path][$type] ) ) { - $status->warning( 'lockmanager-notlocked', $path ); - } else { - --$this->locksHeld[$path][$type]; - // Reference count the locks held and release locks when zero - if ( $this->locksHeld[$path][$type] <= 0 ) { - unset( $this->locksHeld[$path][$type] ); - $bucket = $this->getBucketFromPath( $path ); - $pathsToUnlock[$bucket][] = $path; - } - if ( !count( $this->locksHeld[$path] ) ) { - unset( $this->locksHeld[$path] ); // no SH or EX locks left for key + $pathsToUnlock = array(); // (bucket => type => paths) + foreach ( $pathsByType as $type => $paths ) { + foreach ( $paths as $path ) { + if ( !isset( $this->locksHeld[$path][$type] ) ) { + $status->warning( 'lockmanager-notlocked', $path ); + } else { + --$this->locksHeld[$path][$type]; + // Reference count the locks held and release locks when zero + if ( $this->locksHeld[$path][$type] <= 0 ) { + unset( $this->locksHeld[$path][$type] ); + $bucket = $this->getBucketFromPath( $path ); + $pathsToUnlock[$bucket][$type][] = $path; + } + if ( !count( $this->locksHeld[$path] ) ) { + unset( $this->locksHeld[$path] ); // no SH or EX locks left for key + } } } } // Remove these specific locks if possible, or at least release // all locks once this process is currently not holding any locks. - foreach ( $pathsToUnlock as $bucket => $paths ) { - $status->merge( $this->doUnlockingRequestBucket( $bucket, $paths, $type ) ); + foreach ( $pathsToUnlock as $bucket => $pathsToUnlockByType ) { + $status->merge( $this->doUnlockingRequestBucket( $bucket, $pathsToUnlockByType ) ); } if ( !count( $this->locksHeld ) ) { $status->merge( $this->releaseAllLocks() ); + $this->degradedBuckets = array(); // safe to retry the normal quorum } return $status; @@ -116,25 +121,25 @@ abstract class QuorumLockManager extends LockManager { * This is all or nothing; if any key is locked then this totally fails. * * @param $bucket integer - * @param array $paths List of resource keys to lock - * @param $type integer LockManager::LOCK_EX or LockManager::LOCK_SH + * @param array $pathsByType Map of LockManager::LOCK_* constants to lists of paths * @return Status */ - final protected function doLockingRequestBucket( $bucket, array $paths, $type ) { + final protected function doLockingRequestBucket( $bucket, array $pathsByType ) { $status = Status::newGood(); $yesVotes = 0; // locks made on trustable servers $votesLeft = count( $this->srvsByBucket[$bucket] ); // remaining peers - $quorum = floor( $votesLeft/2 + 1 ); // simple majority + $quorum = floor( $votesLeft / 2 + 1 ); // simple majority // Get votes for each peer, in order, until we have enough... foreach ( $this->srvsByBucket[$bucket] as $lockSrv ) { if ( !$this->isServerUp( $lockSrv ) ) { --$votesLeft; $status->warning( 'lockmanager-fail-svr-acquire', $lockSrv ); + $this->degradedBuckets[$bucket] = time(); continue; // server down? } // Attempt to acquire the lock on this peer - $status->merge( $this->getLocksOnServer( $lockSrv, $paths, $type ) ); + $status->merge( $this->getLocksOnServer( $lockSrv, $pathsByType ) ); if ( !$status->isOK() ) { return $status; // vetoed; resource locked } @@ -158,21 +163,33 @@ abstract class QuorumLockManager extends LockManager { * Attempt to release locks with the peers for a bucket * * @param $bucket integer - * @param array $paths List of resource keys to lock - * @param $type integer LockManager::LOCK_EX or LockManager::LOCK_SH + * @param array $pathsByType Map of LockManager::LOCK_* constants to lists of paths * @return Status */ - final protected function doUnlockingRequestBucket( $bucket, array $paths, $type ) { + final protected function doUnlockingRequestBucket( $bucket, array $pathsByType ) { $status = Status::newGood(); + $yesVotes = 0; // locks freed on trustable servers + $votesLeft = count( $this->srvsByBucket[$bucket] ); // remaining peers + $quorum = floor( $votesLeft / 2 + 1 ); // simple majority + $isDegraded = isset( $this->degradedBuckets[$bucket] ); // not the normal quorum? foreach ( $this->srvsByBucket[$bucket] as $lockSrv ) { if ( !$this->isServerUp( $lockSrv ) ) { - $status->fatal( 'lockmanager-fail-svr-release', $lockSrv ); + $status->warning( 'lockmanager-fail-svr-release', $lockSrv ); // Attempt to release the lock on this peer } else { - $status->merge( $this->freeLocksOnServer( $lockSrv, $paths, $type ) ); + $status->merge( $this->freeLocksOnServer( $lockSrv, $pathsByType ) ); + ++$yesVotes; // success for this peer + // Normally the first peers form the quorum, and the others are ignored. + // Ignore them in this case, but not when an alternative quorum was used. + if ( $yesVotes >= $quorum && !$isDegraded ) { + break; // lock released + } } } + // Set a bad status if the quorum was not met. + // Assumes the same "up" servers as during the acquire step. + $status->setResult( $yesVotes >= $quorum ); return $status; } @@ -190,7 +207,8 @@ abstract class QuorumLockManager extends LockManager { } /** - * Check if a lock server is up + * Check if a lock server is up. + * This should process cache results to reduce RTT. * * @param $lockSrv string * @return bool @@ -198,14 +216,13 @@ abstract class QuorumLockManager extends LockManager { abstract protected function isServerUp( $lockSrv ); /** - * Get a connection to a lock server and acquire locks on $paths + * Get a connection to a lock server and acquire locks * * @param $lockSrv string - * @param $paths array - * @param $type integer + * @param array $pathsByType Map of LockManager::LOCK_* constants to lists of paths * @return Status */ - abstract protected function getLocksOnServer( $lockSrv, array $paths, $type ); + abstract protected function getLocksOnServer( $lockSrv, array $pathsByType ); /** * Get a connection to a lock server and release locks on $paths. @@ -213,11 +230,10 @@ abstract class QuorumLockManager extends LockManager { * Subclasses must effectively implement this or releaseAllLocks(). * * @param $lockSrv string - * @param $paths array - * @param $type integer + * @param array $pathsByType Map of LockManager::LOCK_* constants to lists of paths * @return Status */ - abstract protected function freeLocksOnServer( $lockSrv, array $paths, $type ); + abstract protected function freeLocksOnServer( $lockSrv, array $pathsByType ); /** * Release all locks that this session is holding. diff --git a/includes/filebackend/lockmanager/RedisLockManager.php b/includes/filebackend/lockmanager/RedisLockManager.php new file mode 100644 index 00000000..43b0198a --- /dev/null +++ b/includes/filebackend/lockmanager/RedisLockManager.php @@ -0,0 +1,288 @@ +<?php +/** + * Version of LockManager based on using redis servers. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + * @ingroup LockManager + */ + +/** + * Manage locks using redis servers. + * + * Version of LockManager based on using redis servers. + * This is meant for multi-wiki systems that may share files. + * All locks are non-blocking, which avoids deadlocks. + * + * All lock requests for a resource, identified by a hash string, will map to one + * bucket. Each bucket maps to one or several peer servers, each running redis. + * A majority of peers must agree for a lock to be acquired. + * + * This class requires Redis 2.6 as it makes use Lua scripts for fast atomic operations. + * + * @ingroup LockManager + * @since 1.22 + */ +class RedisLockManager extends QuorumLockManager { + /** @var Array Mapping of lock types to the type actually used */ + protected $lockTypeMap = array( + self::LOCK_SH => 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 "<IP>:<port>" 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 ); + } + + // @TODO: change this code to work in one batch + protected function getLocksOnServer( $lockSrv, array $pathsByType ) { + $status = Status::newGood(); + + $lockedPaths = array(); + foreach ( $pathsByType as $type => $paths ) { + $status->merge( $this->doGetLocksOnServer( $lockSrv, $paths, $type ) ); + if ( $status->isOK() ) { + $lockedPaths[$type] = isset( $lockedPaths[$type] ) + ? array_merge( $lockedPaths[$type], $paths ) + : $paths; + } else { + foreach ( $lockedPaths as $type => $paths ) { + $status->merge( $this->doFreeLocksOnServer( $lockSrv, $paths, $type ) ); + } + break; + } + } + + return $status; + } + + // @TODO: change this code to work in one batch + protected function freeLocksOnServer( $lockSrv, array $pathsByType ) { + $status = Status::newGood(); + + foreach ( $pathsByType as $type => $paths ) { + $status->merge( $this->doFreeLocksOnServer( $lockSrv, $paths, $type ) ); + } + + return $status; + } + + protected function doGetLocksOnServer( $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 = +<<<LUA + if ARGV[1] ~= 'EX' and ARGV[1] ~= 'SH' then + return redis.error_reply('Unrecognized lock type given (must be EX or SH)') + end + local failed = {} + -- Check that all the locks can be acquired + for i,resourceKey in ipairs(KEYS) do + local keyIsFree = true + local currentLocks = redis.call('hKeys',resourceKey) + for i,lockKey in ipairs(currentLocks) do + local _, _, type, session = string.find(lockKey,"(%w+):(%w+)") + -- Check any locks that are not owned by this session + if session ~= ARGV[2] then + local lockTimestamp = redis.call('hGet',resourceKey,lockKey) + if 1*lockTimestamp < ( ARGV[4] - ARGV[3] ) then + -- Lock is stale, so just prune it out + redis.call('hDel',resourceKey,lockKey) + elseif ARGV[1] == 'EX' or type == 'EX' then + keyIsFree = false + break + end + end + end + if not keyIsFree then + failed[#failed+1] = resourceKey + end + end + -- If all locks could be acquired, then do so + if #failed == 0 then + for i,resourceKey in ipairs(KEYS) do + redis.call('hSet',resourceKey,ARGV[1] .. ':' .. ARGV[2],ARGV[4]) + -- In addition to invalidation logic, be sure to garbage collect + redis.call('expire',resourceKey,ARGV[3]) + 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] + $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 doFreeLocksOnServer( $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 = +<<<LUA + if ARGV[1] ~= 'EX' and ARGV[1] ~= 'SH' then + return redis.error_reply('Unrecognized lock type given (must be EX or SH)') + end + local failed = {} + for i,resourceKey in ipairs(KEYS) do + local released = redis.call('hDel',resourceKey,ARGV[1] .. ':' .. ARGV[2]) + if released > 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 ); + } + } + } +} diff --git a/includes/filebackend/lockmanager/ScopedLock.php b/includes/filebackend/lockmanager/ScopedLock.php index edcb1d65..5faad4a6 100644 --- a/includes/filebackend/lockmanager/ScopedLock.php +++ b/includes/filebackend/lockmanager/ScopedLock.php @@ -36,24 +36,18 @@ class ScopedLock { protected $manager; /** @var Status */ protected $status; - /** @var Array List of resource paths*/ - protected $paths; - - protected $type; // integer lock type + /** @var Array Map of lock types to resource paths */ + protected $pathsByType; /** - * @param $manager LockManager - * @param array $paths List of storage paths - * @param $type integer LockManager::LOCK_* constant - * @param $status Status + * @param LockManager $manager + * @param array $pathsByType Map of lock types to path lists + * @param Status $status */ - protected function __construct( - LockManager $manager, array $paths, $type, Status $status - ) { + protected function __construct( LockManager $manager, array $pathsByType, Status $status ) { $this->manager = $manager; - $this->paths = $paths; + $this->pathsByType = $pathsByType; $this->status = $status; - $this->type = $type; } /** @@ -61,19 +55,24 @@ class ScopedLock { * Any locks are released once this object goes out of scope. * The status object is updated with any errors or warnings. * - * @param $manager LockManager - * @param array $paths List of storage paths - * @param $type integer LockManager::LOCK_* constant - * @param $status Status + * $type can be "mixed" and $paths can be a map of types to paths (since 1.22). + * Otherwise $type should be an integer and $paths should be a list of paths. + * + * @param LockManager $manager + * @param array $paths List of storage paths or map of lock types to path lists + * @param integer|string $type LockManager::LOCK_* constant or "mixed" + * @param Status $status + * @param integer $timeout Timeout in seconds (0 means non-blocking) (since 1.22) * @return ScopedLock|null Returns null on failure */ public static function factory( - LockManager $manager, array $paths, $type, Status $status + LockManager $manager, array $paths, $type, Status $status, $timeout = 0 ) { - $lockStatus = $manager->lock( $paths, $type ); + $pathsByType = is_integer( $type ) ? array( $type => $paths ) : $paths; + $lockStatus = $manager->lockByType( $pathsByType, $timeout ); $status->merge( $lockStatus ); if ( $lockStatus->isOK() ) { - return new self( $manager, $paths, $type, $status ); + return new self( $manager, $pathsByType, $status ); } return null; } @@ -91,9 +90,12 @@ class ScopedLock { $lock = null; } + /** + * Release the locks when this goes out of scope + */ function __destruct() { $wasOk = $this->status->isOK(); - $this->status->merge( $this->manager->unlock( $this->paths, $this->type ) ); + $this->status->merge( $this->manager->unlockByType( $this->pathsByType ) ); if ( $wasOk ) { // Make sure status is OK, despite any unlockFiles() fatals $this->status->setResult( true, $this->status->value ); |