From: Aaron Schulz Date: Sun, 18 Sep 2016 04:42:56 +0000 (-0700) Subject: Make LockManager use StatusValue and move classes to /libs X-Git-Tag: 1.31.0-rc.0~5494 X-Git-Url: http://git.cyclocoop.org/%40spipnet%40?a=commitdiff_plain;h=25a1651aadad734c5124a52225c972b00e92de96;p=lhc%2Fweb%2Fwiklou.git Make LockManager use StatusValue and move classes to /libs Change-Id: Ifa41fc2939f3515d4a056746b0fcbff79786d25b --- diff --git a/autoload.php b/autoload.php index a07df966cc..ff7d488205 100644 --- a/autoload.php +++ b/autoload.php @@ -438,7 +438,7 @@ $wgAutoloadLocalClasses = [ 'FSFileBackendFileList' => __DIR__ . '/includes/filebackend/FSFileBackend.php', 'FSFileBackendList' => __DIR__ . '/includes/filebackend/FSFileBackend.php', 'FSFileOpHandle' => __DIR__ . '/includes/filebackend/FSFileBackend.php', - 'FSLockManager' => __DIR__ . '/includes/filebackend/lockmanager/FSLockManager.php', + 'FSLockManager' => __DIR__ . '/includes/libs/lockmanager/FSLockManager.php', 'FSRepo' => __DIR__ . '/includes/filerepo/FSRepo.php', 'FakeAuthTemplate' => __DIR__ . '/includes/specialpage/LoginSignupSpecialPage.php', 'FakeConverter' => __DIR__ . '/languages/FakeConverter.php', @@ -747,7 +747,7 @@ $wgAutoloadLocalClasses = [ 'LocalSettingsGenerator' => __DIR__ . '/includes/installer/LocalSettingsGenerator.php', 'LocalisationCache' => __DIR__ . '/includes/cache/localisation/LocalisationCache.php', 'LocalisationCacheBulkLoad' => __DIR__ . '/includes/cache/localisation/LocalisationCacheBulkLoad.php', - 'LockManager' => __DIR__ . '/includes/filebackend/lockmanager/LockManager.php', + 'LockManager' => __DIR__ . '/includes/libs/lockmanager/LockManager.php', 'LockManagerGroup' => __DIR__ . '/includes/filebackend/lockmanager/LockManagerGroup.php', 'LogEntry' => __DIR__ . '/includes/logging/LogEntry.php', 'LogEntryBase' => __DIR__ . '/includes/logging/LogEntry.php', @@ -979,7 +979,7 @@ $wgAutoloadLocalClasses = [ 'NullFileOp' => __DIR__ . '/includes/filebackend/FileOp.php', 'NullIndexField' => __DIR__ . '/includes/search/NullIndexField.php', 'NullJob' => __DIR__ . '/includes/jobqueue/jobs/NullJob.php', - 'NullLockManager' => __DIR__ . '/includes/filebackend/lockmanager/LockManager.php', + 'NullLockManager' => __DIR__ . '/includes/libs/lockmanager/NullLockManager.php', 'NullRepo' => __DIR__ . '/includes/filerepo/NullRepo.php', 'NullStatsdDataFactory' => __DIR__ . '/includes/libs/stats/NullStatsdDataFactory.php', 'NumericUppercaseCollation' => __DIR__ . '/includes/collation/NumericUppercaseCollation.php', @@ -1110,7 +1110,7 @@ $wgAutoloadLocalClasses = [ 'PurgeParserCache' => __DIR__ . '/maintenance/purgeParserCache.php', 'QueryPage' => __DIR__ . '/includes/specialpage/QueryPage.php', 'QuickTemplate' => __DIR__ . '/includes/skins/QuickTemplate.php', - 'QuorumLockManager' => __DIR__ . '/includes/filebackend/lockmanager/QuorumLockManager.php', + 'QuorumLockManager' => __DIR__ . '/includes/libs/lockmanager/QuorumLockManager.php', 'RCCacheEntry' => __DIR__ . '/includes/changes/RCCacheEntry.php', 'RCCacheEntryFactory' => __DIR__ . '/includes/changes/RCCacheEntryFactory.php', 'RCDatabaseLogEntry' => __DIR__ . '/includes/logging/LogEntry.php', diff --git a/includes/filebackend/FileBackend.php b/includes/filebackend/FileBackend.php index 1f91b3f13a..ed2bdcc140 100644 --- a/includes/filebackend/FileBackend.php +++ b/includes/filebackend/FileBackend.php @@ -1260,7 +1260,7 @@ abstract class FileBackend { final public function lockFiles( array $paths, $type, $timeout = 0 ) { $paths = array_map( 'FileBackend::normalizeStoragePath', $paths ); - return $this->lockManager->lock( $paths, $type, $timeout ); + return $this->wrapStatus( $this->lockManager->lock( $paths, $type, $timeout ) ); } /** @@ -1273,7 +1273,7 @@ abstract class FileBackend { final public function unlockFiles( array $paths, $type ) { $paths = array_map( 'FileBackend::normalizeStoragePath', $paths ); - return $this->lockManager->unlock( $paths, $type ); + return $this->wrapStatus( $this->lockManager->unlock( $paths, $type ) ); } /** diff --git a/includes/filebackend/lockmanager/DBLockManager.php b/includes/filebackend/lockmanager/DBLockManager.php index cccf71a929..4667dde450 100644 --- a/includes/filebackend/lockmanager/DBLockManager.php +++ b/includes/filebackend/lockmanager/DBLockManager.php @@ -104,7 +104,7 @@ abstract class DBLockManager extends QuorumLockManager { // @todo change this code to work in one batch protected function getLocksOnServer( $lockSrv, array $pathsByType ) { - $status = Status::newGood(); + $status = StatusValue::newGood(); foreach ( $pathsByType as $type => $paths ) { $status->merge( $this->doGetLocksOnServer( $lockSrv, $paths, $type ) ); } @@ -115,7 +115,7 @@ abstract class DBLockManager extends QuorumLockManager { abstract protected function doGetLocksOnServer( $lockSrv, array $paths, $type ); protected function freeLocksOnServer( $lockSrv, array $pathsByType ) { - return Status::newGood(); + return StatusValue::newGood(); } /** diff --git a/includes/filebackend/lockmanager/FSLockManager.php b/includes/filebackend/lockmanager/FSLockManager.php deleted file mode 100644 index b6629aa846..0000000000 --- a/includes/filebackend/lockmanager/FSLockManager.php +++ /dev/null @@ -1,253 +0,0 @@ - self::LOCK_SH, - self::LOCK_UW => self::LOCK_SH, - self::LOCK_EX => self::LOCK_EX - ]; - - /** @var string Global dir for all servers */ - protected $lockDir; - - /** @var array Map of (locked key => lock file handle) */ - protected $handles = []; - - /** @var bool */ - protected $isWindows; - - /** - * Construct a new instance from configuration. - * - * @param array $config Includes: - * - lockDirectory : Directory containing the lock files - */ - function __construct( array $config ) { - parent::__construct( $config ); - - $this->lockDir = $config['lockDirectory']; - $this->isWindows = ( strtoupper( substr( PHP_OS, 0, 3 ) ) === 'WIN' ); - } - - /** - * @see LockManager::doLock() - * @param array $paths - * @param int $type - * @return StatusValue - */ - protected function doLock( array $paths, $type ) { - $status = Status::newGood(); - - $lockedPaths = []; // files locked in this attempt - foreach ( $paths as $path ) { - $status->merge( $this->doSingleLock( $path, $type ) ); - if ( $status->isOK() ) { - $lockedPaths[] = $path; - } else { - // Abort and unlock everything - $status->merge( $this->doUnlock( $lockedPaths, $type ) ); - - return $status; - } - } - - return $status; - } - - /** - * @see LockManager::doUnlock() - * @param array $paths - * @param int $type - * @return StatusValue - */ - protected function doUnlock( array $paths, $type ) { - $status = Status::newGood(); - - foreach ( $paths as $path ) { - $status->merge( $this->doSingleUnlock( $path, $type ) ); - } - - return $status; - } - - /** - * Lock a single resource key - * - * @param string $path - * @param int $type - * @return StatusValue - */ - protected function doSingleLock( $path, $type ) { - $status = Status::newGood(); - - if ( isset( $this->locksHeld[$path][$type] ) ) { - ++$this->locksHeld[$path][$type]; - } elseif ( isset( $this->locksHeld[$path][self::LOCK_EX] ) ) { - $this->locksHeld[$path][$type] = 1; - } else { - if ( isset( $this->handles[$path] ) ) { - $handle = $this->handles[$path]; - } else { - MediaWiki\suppressWarnings(); - $handle = fopen( $this->getLockPath( $path ), 'a+' ); - if ( !$handle ) { // lock dir missing? - mkdir( $this->lockDir, 0777, true ); - $handle = fopen( $this->getLockPath( $path ), 'a+' ); // try again - } - MediaWiki\restoreWarnings(); - } - if ( $handle ) { - // Either a shared or exclusive lock - $lock = ( $type == self::LOCK_SH ) ? LOCK_SH : LOCK_EX; - if ( flock( $handle, $lock | LOCK_NB ) ) { - // Record this lock as active - $this->locksHeld[$path][$type] = 1; - $this->handles[$path] = $handle; - } else { - fclose( $handle ); - $status->fatal( 'lockmanager-fail-acquirelock', $path ); - } - } else { - $status->fatal( 'lockmanager-fail-openlock', $path ); - } - } - - return $status; - } - - /** - * Unlock a single resource key - * - * @param string $path - * @param int $type - * @return StatusValue - */ - protected function doSingleUnlock( $path, $type ) { - $status = Status::newGood(); - - if ( !isset( $this->locksHeld[$path] ) ) { - $status->warning( 'lockmanager-notlocked', $path ); - } elseif ( !isset( $this->locksHeld[$path][$type] ) ) { - $status->warning( 'lockmanager-notlocked', $path ); - } else { - $handlesToClose = []; - --$this->locksHeld[$path][$type]; - if ( $this->locksHeld[$path][$type] <= 0 ) { - unset( $this->locksHeld[$path][$type] ); - } - if ( !count( $this->locksHeld[$path] ) ) { - unset( $this->locksHeld[$path] ); // no locks on this path - if ( isset( $this->handles[$path] ) ) { - $handlesToClose[] = $this->handles[$path]; - unset( $this->handles[$path] ); - } - } - // Unlock handles to release locks and delete - // any lock files that end up with no locks on them... - if ( $this->isWindows ) { - // Windows: for any process, including this one, - // calling unlink() on a locked file will fail - $status->merge( $this->closeLockHandles( $path, $handlesToClose ) ); - $status->merge( $this->pruneKeyLockFiles( $path ) ); - } else { - // Unix: unlink() can be used on files currently open by this - // process and we must do so in order to avoid race conditions - $status->merge( $this->pruneKeyLockFiles( $path ) ); - $status->merge( $this->closeLockHandles( $path, $handlesToClose ) ); - } - } - - return $status; - } - - /** - * @param string $path - * @param array $handlesToClose - * @return StatusValue - */ - private function closeLockHandles( $path, array $handlesToClose ) { - $status = Status::newGood(); - foreach ( $handlesToClose as $handle ) { - if ( !flock( $handle, LOCK_UN ) ) { - $status->fatal( 'lockmanager-fail-releaselock', $path ); - } - if ( !fclose( $handle ) ) { - $status->warning( 'lockmanager-fail-closelock', $path ); - } - } - - return $status; - } - - /** - * @param string $path - * @return StatusValue - */ - private function pruneKeyLockFiles( $path ) { - $status = Status::newGood(); - if ( !isset( $this->locksHeld[$path] ) ) { - # No locks are held for the lock file anymore - if ( !unlink( $this->getLockPath( $path ) ) ) { - $status->warning( 'lockmanager-fail-deletelock', $path ); - } - unset( $this->handles[$path] ); - } - - return $status; - } - - /** - * Get the path to the lock file for a key - * @param string $path - * @return string - */ - protected function getLockPath( $path ) { - return "{$this->lockDir}/{$this->sha1Base36Absolute( $path )}.lock"; - } - - /** - * Make sure remaining locks get cleared for sanity - */ - function __destruct() { - while ( count( $this->locksHeld ) ) { - foreach ( $this->locksHeld as $path => $locks ) { - $this->doSingleUnlock( $path, self::LOCK_EX ); - $this->doSingleUnlock( $path, self::LOCK_SH ); - } - } - } -} diff --git a/includes/filebackend/lockmanager/LockManager.php b/includes/filebackend/lockmanager/LockManager.php deleted file mode 100644 index e7f37ed981..0000000000 --- a/includes/filebackend/lockmanager/LockManager.php +++ /dev/null @@ -1,258 +0,0 @@ - self::LOCK_SH, - self::LOCK_UW => self::LOCK_EX, // subclasses may use self::LOCK_SH - self::LOCK_EX => self::LOCK_EX - ]; - - /** @var array Map of (resource path => lock type => count) */ - protected $locksHeld = []; - - protected $domain; // string; domain (usually wiki ID) - protected $lockTTL; // integer; maximum time locks can be held - - /** 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) - - /** - * Construct a new instance from configuration - * - * @param array $config Parameters include: - * - domain : Domain (usually wiki ID) that all resources are relative to [optional] - * - lockTTL : Age (in seconds) at which resource locks should expire. - * This only applies if locks are not tied to a connection/process. - */ - public function __construct( array $config ) { - $this->domain = isset( $config['domain'] ) ? $config['domain'] : 'global'; - if ( isset( $config['lockTTL'] ) ) { - $this->lockTTL = max( 5, $config['lockTTL'] ); - } elseif ( PHP_SAPI === 'cli' ) { - $this->lockTTL = 3600; - } else { - $met = ini_get( 'max_execution_time' ); // this is 0 in CLI mode - $this->lockTTL = max( 5 * 60, 2 * (int)$met ); - } - } - - /** - * Lock the resources at the given abstract paths - * - * @param array $paths List of resource names - * @param int $type LockManager::LOCK_* constant - * @param int $timeout Timeout in seconds (0 means non-blocking) (since 1.21) - * @return StatusValue - */ - final public function lock( array $paths, $type = self::LOCK_EX, $timeout = 0 ) { - return $this->lockByType( [ $type => $paths ], $timeout ); - } - - /** - * Lock the resources at the given abstract paths - * - * @param array $pathsByType Map of LockManager::LOCK_* constants to lists of paths - * @param int $timeout Timeout in seconds (0 means non-blocking) (since 1.21) - * @return StatusValue - * @since 1.22 - */ - final public function lockByType( array $pathsByType, $timeout = 0 ) { - $pathsByType = $this->normalizePathsByType( $pathsByType ); - - $status = null; - $loop = new WaitConditionLoop( - function () use ( &$status, $pathsByType ) { - $status = $this->doLockByType( $pathsByType ); - - return $status->isOK() ?: WaitConditionLoop::CONDITION_CONTINUE; - }, - $timeout - ); - $loop->invoke(); - - return $status; - } - - /** - * Unlock the resources at the given abstract paths - * - * @param array $paths List of paths - * @param int $type LockManager::LOCK_* constant - * @return StatusValue - */ - final public function unlock( array $paths, $type = self::LOCK_EX ) { - return $this->unlockByType( [ $type => $paths ] ); - } - - /** - * Unlock the resources at the given abstract paths - * - * @param array $pathsByType Map of LockManager::LOCK_* constants to lists of paths - * @return StatusValue - * @since 1.22 - */ - final public function unlockByType( array $pathsByType ) { - $pathsByType = $this->normalizePathsByType( $pathsByType ); - $status = $this->doUnlockByType( $pathsByType ); - - return $status; - } - - /** - * Get the base 36 SHA-1 of a string, padded to 31 digits. - * Before hashing, the path will be prefixed with the domain ID. - * This should be used interally for lock key or file names. - * - * @param string $path - * @return string - */ - final protected function sha1Base36Absolute( $path ) { - return Wikimedia\base_convert( sha1( "{$this->domain}:{$path}" ), 16, 36, 31 ); - } - - /** - * Get the base 16 SHA-1 of a string, padded to 31 digits. - * Before hashing, the path will be prefixed with the domain ID. - * This should be used interally for lock key or file names. - * - * @param string $path - * @return string - */ - final protected function sha1Base16Absolute( $path ) { - return sha1( "{$this->domain}:{$path}" ); - } - - /** - * Normalize the $paths array by converting LOCK_UW locks into the - * appropriate type and removing any duplicated paths for each lock type. - * - * @param array $pathsByType Map of LockManager::LOCK_* constants to lists of paths - * @return array - * @since 1.22 - */ - final protected function normalizePathsByType( array $pathsByType ) { - $res = []; - foreach ( $pathsByType as $type => $paths ) { - $res[$this->lockTypeMap[$type]] = array_unique( $paths ); - } - - return $res; - } - - /** - * @see LockManager::lockByType() - * @param array $pathsByType Map of LockManager::LOCK_* constants to lists of paths - * @return StatusValue - * @since 1.22 - */ - protected function doLockByType( array $pathsByType ) { - $status = Status::newGood(); - $lockedByType = []; // 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 $lType => $lPaths ) { - $status->merge( $this->doUnlock( $lPaths, $lType ) ); - } - break; - } - } - - return $status; - } - - /** - * Lock resources with the given keys and lock type - * - * @param array $paths List of paths - * @param int $type LockManager::LOCK_* constant - * @return StatusValue - */ - abstract protected function doLock( array $paths, $type ); - - /** - * @see LockManager::unlockByType() - * @param array $pathsByType Map of LockManager::LOCK_* constants to lists of paths - * @return StatusValue - * @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 paths - * @param int $type LockManager::LOCK_* constant - * @return StatusValue - */ - abstract protected function doUnlock( array $paths, $type ); -} - -/** - * Simple version of LockManager that does nothing - * @since 1.19 - */ -class NullLockManager extends LockManager { - protected function doLock( array $paths, $type ) { - return Status::newGood(); - } - - protected function doUnlock( array $paths, $type ) { - return Status::newGood(); - } -} diff --git a/includes/filebackend/lockmanager/MemcLockManager.php b/includes/filebackend/lockmanager/MemcLockManager.php index 2e2d0a3533..81ce424b50 100644 --- a/includes/filebackend/lockmanager/MemcLockManager.php +++ b/includes/filebackend/lockmanager/MemcLockManager.php @@ -90,7 +90,7 @@ class MemcLockManager extends QuorumLockManager { // @todo Change this code to work in one batch protected function getLocksOnServer( $lockSrv, array $pathsByType ) { - $status = Status::newGood(); + $status = StatusValue::newGood(); $lockedPaths = []; foreach ( $pathsByType as $type => $paths ) { @@ -112,7 +112,7 @@ class MemcLockManager extends QuorumLockManager { // @todo Change this code to work in one batch protected function freeLocksOnServer( $lockSrv, array $pathsByType ) { - $status = Status::newGood(); + $status = StatusValue::newGood(); foreach ( $pathsByType as $type => $paths ) { $status->merge( $this->doFreeLocksOnServer( $lockSrv, $paths, $type ) ); @@ -129,7 +129,7 @@ class MemcLockManager extends QuorumLockManager { * @return StatusValue */ protected function doGetLocksOnServer( $lockSrv, array $paths, $type ) { - $status = Status::newGood(); + $status = StatusValue::newGood(); $memc = $this->getCache( $lockSrv ); $keys = array_map( [ $this, 'recordKeyForPath' ], $paths ); // lock records @@ -205,7 +205,7 @@ class MemcLockManager extends QuorumLockManager { * @return StatusValue */ protected function doFreeLocksOnServer( $lockSrv, array $paths, $type ) { - $status = Status::newGood(); + $status = StatusValue::newGood(); $memc = $this->getCache( $lockSrv ); $keys = array_map( [ $this, 'recordKeyForPath' ], $paths ); // lock records @@ -257,7 +257,7 @@ class MemcLockManager extends QuorumLockManager { * @return StatusValue */ protected function releaseAllLocks() { - return Status::newGood(); // not supported + return StatusValue::newGood(); // not supported } /** diff --git a/includes/filebackend/lockmanager/MySqlLockManager.php b/includes/filebackend/lockmanager/MySqlLockManager.php index 896e0ffd64..124d41038a 100644 --- a/includes/filebackend/lockmanager/MySqlLockManager.php +++ b/includes/filebackend/lockmanager/MySqlLockManager.php @@ -38,7 +38,7 @@ class MySqlLockManager extends DBLockManager { * @return StatusValue */ protected function doGetLocksOnServer( $lockSrv, array $paths, $type ) { - $status = Status::newGood(); + $status = StatusValue::newGood(); $db = $this->getConnection( $lockSrv ); // checked in isServerUp() @@ -108,7 +108,7 @@ class MySqlLockManager extends DBLockManager { * @return StatusValue */ protected function releaseAllLocks() { - $status = Status::newGood(); + $status = StatusValue::newGood(); foreach ( $this->conns as $lockDb => $db ) { if ( $db->trxLevel() ) { // in transaction diff --git a/includes/filebackend/lockmanager/PostgreSqlLockManager.php b/includes/filebackend/lockmanager/PostgreSqlLockManager.php index 307c16447e..d6b1ce822d 100644 --- a/includes/filebackend/lockmanager/PostgreSqlLockManager.php +++ b/includes/filebackend/lockmanager/PostgreSqlLockManager.php @@ -14,7 +14,7 @@ class PostgreSqlLockManager extends DBLockManager { ]; protected function doGetLocksOnServer( $lockSrv, array $paths, $type ) { - $status = Status::newGood(); + $status = StatusValue::newGood(); if ( !count( $paths ) ) { return $status; // nothing to lock } @@ -64,7 +64,7 @@ class PostgreSqlLockManager extends DBLockManager { * @return StatusValue */ protected function releaseAllLocks() { - $status = Status::newGood(); + $status = StatusValue::newGood(); foreach ( $this->conns as $lockDb => $db ) { try { diff --git a/includes/filebackend/lockmanager/QuorumLockManager.php b/includes/filebackend/lockmanager/QuorumLockManager.php deleted file mode 100644 index 0db9e815fe..0000000000 --- a/includes/filebackend/lockmanager/QuorumLockManager.php +++ /dev/null @@ -1,248 +0,0 @@ - (lsrv1, lsrv2, ...)) - - /** @var array Map of degraded buckets */ - protected $degradedBuckets = []; // (buckey index => UNIX timestamp) - - final protected function doLock( array $paths, $type ) { - return $this->doLockByType( [ $type => $paths ] ); - } - - final protected function doUnlock( array $paths, $type ) { - return $this->doUnlockByType( [ $type => $paths ] ); - } - - protected function doLockByType( array $pathsByType ) { - $status = Status::newGood(); - - $pathsToLock = []; // (bucket => type => paths) - // Get locks that need to be acquired (buckets => locks)... - 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 = []; // files locked in this attempt (type => paths) - // Attempt to acquire these locks... - foreach ( $pathsToLock as $bucket => $pathsToLockByType ) { - // Try to acquire the locks for this bucket - $status->merge( $this->doLockingRequestBucket( $bucket, $pathsToLockByType ) ); - if ( !$status->isOK() ) { - $status->merge( $this->doUnlockByType( $lockedPaths ) ); - - return $status; - } - // Record these locks as active - 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; - } - } - } - - return $status; - } - - protected function doUnlockByType( array $pathsByType ) { - $status = Status::newGood(); - - $pathsToUnlock = []; // (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 => $pathsToUnlockByType ) { - $status->merge( $this->doUnlockingRequestBucket( $bucket, $pathsToUnlockByType ) ); - } - if ( !count( $this->locksHeld ) ) { - $status->merge( $this->releaseAllLocks() ); - $this->degradedBuckets = []; // safe to retry the normal quorum - } - - return $status; - } - - /** - * Attempt to acquire locks with the peers for a bucket. - * This is all or nothing; if any key is locked then this totally fails. - * - * @param int $bucket - * @param array $pathsByType Map of LockManager::LOCK_* constants to lists of paths - * @return StatusValue - */ - 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 - // 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, $pathsByType ) ); - if ( !$status->isOK() ) { - return $status; // vetoed; resource locked - } - ++$yesVotes; // success for this peer - if ( $yesVotes >= $quorum ) { - return $status; // lock obtained - } - --$votesLeft; - $votesNeeded = $quorum - $yesVotes; - if ( $votesNeeded > $votesLeft ) { - break; // short-circuit - } - } - // At this point, we must not have met the quorum - $status->setResult( false ); - - return $status; - } - - /** - * Attempt to release locks with the peers for a bucket - * - * @param int $bucket - * @param array $pathsByType Map of LockManager::LOCK_* constants to lists of paths - * @return StatusValue - */ - 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->warning( 'lockmanager-fail-svr-release', $lockSrv ); - } else { - // Attempt to release the lock on this peer - $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 StatusValue if the quorum was not met. - // Assumes the same "up" servers as during the acquire step. - $status->setResult( $yesVotes >= $quorum ); - - return $status; - } - - /** - * Get the bucket for resource path. - * This should avoid throwing any exceptions. - * - * @param string $path - * @return int - */ - protected function getBucketFromPath( $path ) { - $prefix = substr( sha1( $path ), 0, 2 ); // first 2 hex chars (8 bits) - return (int)base_convert( $prefix, 16, 10 ) % count( $this->srvsByBucket ); - } - - /** - * Check if a lock server is up. - * This should process cache results to reduce RTT. - * - * @param string $lockSrv - * @return bool - */ - abstract protected function isServerUp( $lockSrv ); - - /** - * Get a connection to a lock server and acquire locks - * - * @param string $lockSrv - * @param array $pathsByType Map of LockManager::LOCK_* constants to lists of paths - * @return StatusValue - */ - abstract protected function getLocksOnServer( $lockSrv, array $pathsByType ); - - /** - * Get a connection to a lock server and release locks on $paths. - * - * Subclasses must effectively implement this or releaseAllLocks(). - * - * @param string $lockSrv - * @param array $pathsByType Map of LockManager::LOCK_* constants to lists of paths - * @return StatusValue - */ - abstract protected function freeLocksOnServer( $lockSrv, array $pathsByType ); - - /** - * Release all locks that this session is holding. - * - * Subclasses must effectively implement this or freeLocksOnServer(). - * - * @return StatusValue - */ - abstract protected function releaseAllLocks(); -} diff --git a/includes/filebackend/lockmanager/RedisLockManager.php b/includes/filebackend/lockmanager/RedisLockManager.php index 4121ecb29d..6fd819d637 100644 --- a/includes/filebackend/lockmanager/RedisLockManager.php +++ b/includes/filebackend/lockmanager/RedisLockManager.php @@ -79,7 +79,7 @@ class RedisLockManager extends QuorumLockManager { } protected function getLocksOnServer( $lockSrv, array $pathsByType ) { - $status = Status::newGood(); + $status = StatusValue::newGood(); $pathList = call_user_func_array( 'array_merge', array_values( $pathsByType ) ); @@ -172,7 +172,7 @@ LUA; } protected function freeLocksOnServer( $lockSrv, array $pathsByType ) { - $status = Status::newGood(); + $status = StatusValue::newGood(); $pathList = call_user_func_array( 'array_merge', array_values( $pathsByType ) ); @@ -242,7 +242,7 @@ LUA; } protected function releaseAllLocks() { - return Status::newGood(); // not supported + return StatusValue::newGood(); // not supported } protected function isServerUp( $lockSrv ) { diff --git a/includes/libs/StatusValue.php b/includes/libs/StatusValue.php index 45185c5130..bff9abd61f 100644 --- a/includes/libs/StatusValue.php +++ b/includes/libs/StatusValue.php @@ -58,7 +58,7 @@ class StatusValue { * Factory function for fatal errors * * @param string|MessageSpecifier $message Message key or object - * @return StatusValue + * @return static */ public static function newFatal( $message /*, parameters...*/ ) { $params = func_get_args(); @@ -71,7 +71,7 @@ class StatusValue { * Factory function for good results * * @param mixed $value - * @return StatusValue + * @return static */ public static function newGood( $value = null ) { $result = new static(); diff --git a/includes/libs/lockmanager/FSLockManager.php b/includes/libs/lockmanager/FSLockManager.php new file mode 100644 index 0000000000..7f33a0abdf --- /dev/null +++ b/includes/libs/lockmanager/FSLockManager.php @@ -0,0 +1,253 @@ + self::LOCK_SH, + self::LOCK_UW => self::LOCK_SH, + self::LOCK_EX => self::LOCK_EX + ]; + + /** @var string Global dir for all servers */ + protected $lockDir; + + /** @var array Map of (locked key => lock file handle) */ + protected $handles = []; + + /** @var bool */ + protected $isWindows; + + /** + * Construct a new instance from configuration. + * + * @param array $config Includes: + * - lockDirectory : Directory containing the lock files + */ + function __construct( array $config ) { + parent::__construct( $config ); + + $this->lockDir = $config['lockDirectory']; + $this->isWindows = ( strtoupper( substr( PHP_OS, 0, 3 ) ) === 'WIN' ); + } + + /** + * @see LockManager::doLock() + * @param array $paths + * @param int $type + * @return StatusValue + */ + protected function doLock( array $paths, $type ) { + $status = StatusValue::newGood(); + + $lockedPaths = []; // files locked in this attempt + foreach ( $paths as $path ) { + $status->merge( $this->doSingleLock( $path, $type ) ); + if ( $status->isOK() ) { + $lockedPaths[] = $path; + } else { + // Abort and unlock everything + $status->merge( $this->doUnlock( $lockedPaths, $type ) ); + + return $status; + } + } + + return $status; + } + + /** + * @see LockManager::doUnlock() + * @param array $paths + * @param int $type + * @return StatusValue + */ + protected function doUnlock( array $paths, $type ) { + $status = StatusValue::newGood(); + + foreach ( $paths as $path ) { + $status->merge( $this->doSingleUnlock( $path, $type ) ); + } + + return $status; + } + + /** + * Lock a single resource key + * + * @param string $path + * @param int $type + * @return StatusValue + */ + protected function doSingleLock( $path, $type ) { + $status = StatusValue::newGood(); + + if ( isset( $this->locksHeld[$path][$type] ) ) { + ++$this->locksHeld[$path][$type]; + } elseif ( isset( $this->locksHeld[$path][self::LOCK_EX] ) ) { + $this->locksHeld[$path][$type] = 1; + } else { + if ( isset( $this->handles[$path] ) ) { + $handle = $this->handles[$path]; + } else { + MediaWiki\suppressWarnings(); + $handle = fopen( $this->getLockPath( $path ), 'a+' ); + if ( !$handle ) { // lock dir missing? + mkdir( $this->lockDir, 0777, true ); + $handle = fopen( $this->getLockPath( $path ), 'a+' ); // try again + } + MediaWiki\restoreWarnings(); + } + if ( $handle ) { + // Either a shared or exclusive lock + $lock = ( $type == self::LOCK_SH ) ? LOCK_SH : LOCK_EX; + if ( flock( $handle, $lock | LOCK_NB ) ) { + // Record this lock as active + $this->locksHeld[$path][$type] = 1; + $this->handles[$path] = $handle; + } else { + fclose( $handle ); + $status->fatal( 'lockmanager-fail-acquirelock', $path ); + } + } else { + $status->fatal( 'lockmanager-fail-openlock', $path ); + } + } + + return $status; + } + + /** + * Unlock a single resource key + * + * @param string $path + * @param int $type + * @return StatusValue + */ + protected function doSingleUnlock( $path, $type ) { + $status = StatusValue::newGood(); + + if ( !isset( $this->locksHeld[$path] ) ) { + $status->warning( 'lockmanager-notlocked', $path ); + } elseif ( !isset( $this->locksHeld[$path][$type] ) ) { + $status->warning( 'lockmanager-notlocked', $path ); + } else { + $handlesToClose = []; + --$this->locksHeld[$path][$type]; + if ( $this->locksHeld[$path][$type] <= 0 ) { + unset( $this->locksHeld[$path][$type] ); + } + if ( !count( $this->locksHeld[$path] ) ) { + unset( $this->locksHeld[$path] ); // no locks on this path + if ( isset( $this->handles[$path] ) ) { + $handlesToClose[] = $this->handles[$path]; + unset( $this->handles[$path] ); + } + } + // Unlock handles to release locks and delete + // any lock files that end up with no locks on them... + if ( $this->isWindows ) { + // Windows: for any process, including this one, + // calling unlink() on a locked file will fail + $status->merge( $this->closeLockHandles( $path, $handlesToClose ) ); + $status->merge( $this->pruneKeyLockFiles( $path ) ); + } else { + // Unix: unlink() can be used on files currently open by this + // process and we must do so in order to avoid race conditions + $status->merge( $this->pruneKeyLockFiles( $path ) ); + $status->merge( $this->closeLockHandles( $path, $handlesToClose ) ); + } + } + + return $status; + } + + /** + * @param string $path + * @param array $handlesToClose + * @return StatusValue + */ + private function closeLockHandles( $path, array $handlesToClose ) { + $status = StatusValue::newGood(); + foreach ( $handlesToClose as $handle ) { + if ( !flock( $handle, LOCK_UN ) ) { + $status->fatal( 'lockmanager-fail-releaselock', $path ); + } + if ( !fclose( $handle ) ) { + $status->warning( 'lockmanager-fail-closelock', $path ); + } + } + + return $status; + } + + /** + * @param string $path + * @return StatusValue + */ + private function pruneKeyLockFiles( $path ) { + $status = StatusValue::newGood(); + if ( !isset( $this->locksHeld[$path] ) ) { + # No locks are held for the lock file anymore + if ( !unlink( $this->getLockPath( $path ) ) ) { + $status->warning( 'lockmanager-fail-deletelock', $path ); + } + unset( $this->handles[$path] ); + } + + return $status; + } + + /** + * Get the path to the lock file for a key + * @param string $path + * @return string + */ + protected function getLockPath( $path ) { + return "{$this->lockDir}/{$this->sha1Base36Absolute( $path )}.lock"; + } + + /** + * Make sure remaining locks get cleared for sanity + */ + function __destruct() { + while ( count( $this->locksHeld ) ) { + foreach ( $this->locksHeld as $path => $locks ) { + $this->doSingleUnlock( $path, self::LOCK_EX ); + $this->doSingleUnlock( $path, self::LOCK_SH ); + } + } + } +} diff --git a/includes/libs/lockmanager/LockManager.php b/includes/libs/lockmanager/LockManager.php new file mode 100644 index 0000000000..80add5b8b7 --- /dev/null +++ b/includes/libs/lockmanager/LockManager.php @@ -0,0 +1,244 @@ + self::LOCK_SH, + self::LOCK_UW => self::LOCK_EX, // subclasses may use self::LOCK_SH + self::LOCK_EX => self::LOCK_EX + ]; + + /** @var array Map of (resource path => lock type => count) */ + protected $locksHeld = []; + + protected $domain; // string; domain (usually wiki ID) + protected $lockTTL; // integer; maximum time locks can be held + + /** 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) + + /** + * Construct a new instance from configuration + * + * @param array $config Parameters include: + * - domain : Domain (usually wiki ID) that all resources are relative to [optional] + * - lockTTL : Age (in seconds) at which resource locks should expire. + * This only applies if locks are not tied to a connection/process. + */ + public function __construct( array $config ) { + $this->domain = isset( $config['domain'] ) ? $config['domain'] : 'global'; + if ( isset( $config['lockTTL'] ) ) { + $this->lockTTL = max( 5, $config['lockTTL'] ); + } elseif ( PHP_SAPI === 'cli' ) { + $this->lockTTL = 3600; + } else { + $met = ini_get( 'max_execution_time' ); // this is 0 in CLI mode + $this->lockTTL = max( 5 * 60, 2 * (int)$met ); + } + } + + /** + * Lock the resources at the given abstract paths + * + * @param array $paths List of resource names + * @param int $type LockManager::LOCK_* constant + * @param int $timeout Timeout in seconds (0 means non-blocking) (since 1.21) + * @return StatusValue + */ + final public function lock( array $paths, $type = self::LOCK_EX, $timeout = 0 ) { + return $this->lockByType( [ $type => $paths ], $timeout ); + } + + /** + * Lock the resources at the given abstract paths + * + * @param array $pathsByType Map of LockManager::LOCK_* constants to lists of paths + * @param int $timeout Timeout in seconds (0 means non-blocking) (since 1.21) + * @return StatusValue + * @since 1.22 + */ + final public function lockByType( array $pathsByType, $timeout = 0 ) { + $pathsByType = $this->normalizePathsByType( $pathsByType ); + + $status = null; + $loop = new WaitConditionLoop( + function () use ( &$status, $pathsByType ) { + $status = $this->doLockByType( $pathsByType ); + + return $status->isOK() ?: WaitConditionLoop::CONDITION_CONTINUE; + }, + $timeout + ); + $loop->invoke(); + + return $status; + } + + /** + * Unlock the resources at the given abstract paths + * + * @param array $paths List of paths + * @param int $type LockManager::LOCK_* constant + * @return StatusValue + */ + final public function unlock( array $paths, $type = self::LOCK_EX ) { + return $this->unlockByType( [ $type => $paths ] ); + } + + /** + * Unlock the resources at the given abstract paths + * + * @param array $pathsByType Map of LockManager::LOCK_* constants to lists of paths + * @return StatusValue + * @since 1.22 + */ + final public function unlockByType( array $pathsByType ) { + $pathsByType = $this->normalizePathsByType( $pathsByType ); + $status = $this->doUnlockByType( $pathsByType ); + + return $status; + } + + /** + * Get the base 36 SHA-1 of a string, padded to 31 digits. + * Before hashing, the path will be prefixed with the domain ID. + * This should be used interally for lock key or file names. + * + * @param string $path + * @return string + */ + final protected function sha1Base36Absolute( $path ) { + return Wikimedia\base_convert( sha1( "{$this->domain}:{$path}" ), 16, 36, 31 ); + } + + /** + * Get the base 16 SHA-1 of a string, padded to 31 digits. + * Before hashing, the path will be prefixed with the domain ID. + * This should be used interally for lock key or file names. + * + * @param string $path + * @return string + */ + final protected function sha1Base16Absolute( $path ) { + return sha1( "{$this->domain}:{$path}" ); + } + + /** + * Normalize the $paths array by converting LOCK_UW locks into the + * appropriate type and removing any duplicated paths for each lock type. + * + * @param array $pathsByType Map of LockManager::LOCK_* constants to lists of paths + * @return array + * @since 1.22 + */ + final protected function normalizePathsByType( array $pathsByType ) { + $res = []; + foreach ( $pathsByType as $type => $paths ) { + $res[$this->lockTypeMap[$type]] = array_unique( $paths ); + } + + return $res; + } + + /** + * @see LockManager::lockByType() + * @param array $pathsByType Map of LockManager::LOCK_* constants to lists of paths + * @return StatusValue + * @since 1.22 + */ + protected function doLockByType( array $pathsByType ) { + $status = StatusValue::newGood(); + $lockedByType = []; // 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 $lType => $lPaths ) { + $status->merge( $this->doUnlock( $lPaths, $lType ) ); + } + break; + } + } + + return $status; + } + + /** + * Lock resources with the given keys and lock type + * + * @param array $paths List of paths + * @param int $type LockManager::LOCK_* constant + * @return StatusValue + */ + abstract protected function doLock( array $paths, $type ); + + /** + * @see LockManager::unlockByType() + * @param array $pathsByType Map of LockManager::LOCK_* constants to lists of paths + * @return StatusValue + * @since 1.22 + */ + protected function doUnlockByType( array $pathsByType ) { + $status = StatusValue::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 paths + * @param int $type LockManager::LOCK_* constant + * @return StatusValue + */ + abstract protected function doUnlock( array $paths, $type ); +} diff --git a/includes/libs/lockmanager/NullLockManager.php b/includes/libs/lockmanager/NullLockManager.php new file mode 100644 index 0000000000..5ad558fa74 --- /dev/null +++ b/includes/libs/lockmanager/NullLockManager.php @@ -0,0 +1,37 @@ + (lsrv1, lsrv2, ...)) + + /** @var array Map of degraded buckets */ + protected $degradedBuckets = []; // (buckey index => UNIX timestamp) + + final protected function doLock( array $paths, $type ) { + return $this->doLockByType( [ $type => $paths ] ); + } + + final protected function doUnlock( array $paths, $type ) { + return $this->doUnlockByType( [ $type => $paths ] ); + } + + protected function doLockByType( array $pathsByType ) { + $status = StatusValue::newGood(); + + $pathsToLock = []; // (bucket => type => paths) + // Get locks that need to be acquired (buckets => locks)... + 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 = []; // files locked in this attempt (type => paths) + // Attempt to acquire these locks... + foreach ( $pathsToLock as $bucket => $pathsToLockByType ) { + // Try to acquire the locks for this bucket + $status->merge( $this->doLockingRequestBucket( $bucket, $pathsToLockByType ) ); + if ( !$status->isOK() ) { + $status->merge( $this->doUnlockByType( $lockedPaths ) ); + + return $status; + } + // Record these locks as active + 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; + } + } + } + + return $status; + } + + protected function doUnlockByType( array $pathsByType ) { + $status = StatusValue::newGood(); + + $pathsToUnlock = []; // (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 => $pathsToUnlockByType ) { + $status->merge( $this->doUnlockingRequestBucket( $bucket, $pathsToUnlockByType ) ); + } + if ( !count( $this->locksHeld ) ) { + $status->merge( $this->releaseAllLocks() ); + $this->degradedBuckets = []; // safe to retry the normal quorum + } + + return $status; + } + + /** + * Attempt to acquire locks with the peers for a bucket. + * This is all or nothing; if any key is locked then this totally fails. + * + * @param int $bucket + * @param array $pathsByType Map of LockManager::LOCK_* constants to lists of paths + * @return StatusValue + */ + final protected function doLockingRequestBucket( $bucket, array $pathsByType ) { + $status = StatusValue::newGood(); + + $yesVotes = 0; // locks made on trustable servers + $votesLeft = count( $this->srvsByBucket[$bucket] ); // remaining peers + $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, $pathsByType ) ); + if ( !$status->isOK() ) { + return $status; // vetoed; resource locked + } + ++$yesVotes; // success for this peer + if ( $yesVotes >= $quorum ) { + return $status; // lock obtained + } + --$votesLeft; + $votesNeeded = $quorum - $yesVotes; + if ( $votesNeeded > $votesLeft ) { + break; // short-circuit + } + } + // At this point, we must not have met the quorum + $status->setResult( false ); + + return $status; + } + + /** + * Attempt to release locks with the peers for a bucket + * + * @param int $bucket + * @param array $pathsByType Map of LockManager::LOCK_* constants to lists of paths + * @return StatusValue + */ + final protected function doUnlockingRequestBucket( $bucket, array $pathsByType ) { + $status = StatusValue::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->warning( 'lockmanager-fail-svr-release', $lockSrv ); + } else { + // Attempt to release the lock on this peer + $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 StatusValue if the quorum was not met. + // Assumes the same "up" servers as during the acquire step. + $status->setResult( $yesVotes >= $quorum ); + + return $status; + } + + /** + * Get the bucket for resource path. + * This should avoid throwing any exceptions. + * + * @param string $path + * @return int + */ + protected function getBucketFromPath( $path ) { + $prefix = substr( sha1( $path ), 0, 2 ); // first 2 hex chars (8 bits) + return (int)base_convert( $prefix, 16, 10 ) % count( $this->srvsByBucket ); + } + + /** + * Check if a lock server is up. + * This should process cache results to reduce RTT. + * + * @param string $lockSrv + * @return bool + */ + abstract protected function isServerUp( $lockSrv ); + + /** + * Get a connection to a lock server and acquire locks + * + * @param string $lockSrv + * @param array $pathsByType Map of LockManager::LOCK_* constants to lists of paths + * @return StatusValue + */ + abstract protected function getLocksOnServer( $lockSrv, array $pathsByType ); + + /** + * Get a connection to a lock server and release locks on $paths. + * + * Subclasses must effectively implement this or releaseAllLocks(). + * + * @param string $lockSrv + * @param array $pathsByType Map of LockManager::LOCK_* constants to lists of paths + * @return StatusValue + */ + abstract protected function freeLocksOnServer( $lockSrv, array $pathsByType ); + + /** + * Release all locks that this session is holding. + * + * Subclasses must effectively implement this or freeLocksOnServer(). + * + * @return StatusValue + */ + abstract protected function releaseAllLocks(); +}