'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',
'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',
'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',
'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',
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 ) );
}
/**
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 ) );
}
/**
// @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 ) );
}
abstract protected function doGetLocksOnServer( $lockSrv, array $paths, $type );
protected function freeLocksOnServer( $lockSrv, array $pathsByType ) {
- return Status::newGood();
+ return StatusValue::newGood();
}
/**
+++ /dev/null
-<?php
-/**
- * Simple version of LockManager based on using FS lock files.
- *
- * 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
- */
-
-/**
- * Simple version of LockManager based on using FS lock files.
- * All locks are non-blocking, which avoids deadlocks.
- *
- * This should work fine for small sites running off one server.
- * Do not use this with 'lockDirectory' set to an NFS mount unless the
- * NFS client is at least version 2.6.12. Otherwise, the BSD flock()
- * locks will be ignored; see http://nfs.sourceforge.net/#section_d.
- *
- * @ingroup LockManager
- * @since 1.19
- */
-class FSLockManager extends LockManager {
- /** @var array Mapping of lock types to the type actually used */
- protected $lockTypeMap = [
- self::LOCK_SH => 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 );
- }
- }
- }
-}
+++ /dev/null
-<?php
-/**
- * @defgroup LockManager Lock management
- * @ingroup FileBackend
- */
-
-/**
- * Resource locking handling.
- *
- * 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
- * @author Aaron Schulz
- */
-
-/**
- * @brief Class for handling resource locking.
- *
- * Locks on resource keys can either be shared or exclusive.
- *
- * Implementations must keep track of what is locked by this proccess
- * in-memory and support nested locking calls (using reference counting).
- * At least LOCK_UW and LOCK_EX must be implemented. LOCK_SH can be a no-op.
- * Locks should either be non-blocking or have low wait timeouts.
- *
- * Subclasses should avoid throwing exceptions at all costs.
- *
- * @ingroup LockManager
- * @since 1.19
- */
-abstract class LockManager {
- /** @var array Mapping of lock types to the type actually used */
- protected $lockTypeMap = [
- self::LOCK_SH => 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();
- }
-}
// @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 ) {
// @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 ) );
* @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
* @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
* @return StatusValue
*/
protected function releaseAllLocks() {
- return Status::newGood(); // not supported
+ return StatusValue::newGood(); // not supported
}
/**
* @return StatusValue
*/
protected function doGetLocksOnServer( $lockSrv, array $paths, $type ) {
- $status = Status::newGood();
+ $status = StatusValue::newGood();
$db = $this->getConnection( $lockSrv ); // checked in isServerUp()
* @return StatusValue
*/
protected function releaseAllLocks() {
- $status = Status::newGood();
+ $status = StatusValue::newGood();
foreach ( $this->conns as $lockDb => $db ) {
if ( $db->trxLevel() ) { // in transaction
];
protected function doGetLocksOnServer( $lockSrv, array $paths, $type ) {
- $status = Status::newGood();
+ $status = StatusValue::newGood();
if ( !count( $paths ) ) {
return $status; // nothing to lock
}
* @return StatusValue
*/
protected function releaseAllLocks() {
- $status = Status::newGood();
+ $status = StatusValue::newGood();
foreach ( $this->conns as $lockDb => $db ) {
try {
+++ /dev/null
-<?php
-/**
- * Version of LockManager that uses a quorum from peer servers for locks.
- *
- * 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
- */
-
-/**
- * Version of LockManager that uses a quorum from peer servers for locks.
- * The resource space can also be sharded into separate peer groups.
- *
- * @ingroup LockManager
- * @since 1.20
- */
-abstract class QuorumLockManager extends LockManager {
- /** @var array Map of bucket indexes to peer server lists */
- protected $srvsByBucket = []; // (bucket index => (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();
-}
}
protected function getLocksOnServer( $lockSrv, array $pathsByType ) {
- $status = Status::newGood();
+ $status = StatusValue::newGood();
$pathList = call_user_func_array( 'array_merge', array_values( $pathsByType ) );
}
protected function freeLocksOnServer( $lockSrv, array $pathsByType ) {
- $status = Status::newGood();
+ $status = StatusValue::newGood();
$pathList = call_user_func_array( 'array_merge', array_values( $pathsByType ) );
}
protected function releaseAllLocks() {
- return Status::newGood(); // not supported
+ return StatusValue::newGood(); // not supported
}
protected function isServerUp( $lockSrv ) {
* 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();
* Factory function for good results
*
* @param mixed $value
- * @return StatusValue
+ * @return static
*/
public static function newGood( $value = null ) {
$result = new static();
--- /dev/null
+<?php
+/**
+ * Simple version of LockManager based on using FS lock files.
+ *
+ * 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
+ */
+
+/**
+ * Simple version of LockManager based on using FS lock files.
+ * All locks are non-blocking, which avoids deadlocks.
+ *
+ * This should work fine for small sites running off one server.
+ * Do not use this with 'lockDirectory' set to an NFS mount unless the
+ * NFS client is at least version 2.6.12. Otherwise, the BSD flock()
+ * locks will be ignored; see http://nfs.sourceforge.net/#section_d.
+ *
+ * @ingroup LockManager
+ * @since 1.19
+ */
+class FSLockManager extends LockManager {
+ /** @var array Mapping of lock types to the type actually used */
+ protected $lockTypeMap = [
+ self::LOCK_SH => 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 );
+ }
+ }
+ }
+}
--- /dev/null
+<?php
+/**
+ * @defgroup LockManager Lock management
+ * @ingroup FileBackend
+ */
+
+/**
+ * Resource locking handling.
+ *
+ * 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
+ * @author Aaron Schulz
+ */
+
+/**
+ * @brief Class for handling resource locking.
+ *
+ * Locks on resource keys can either be shared or exclusive.
+ *
+ * Implementations must keep track of what is locked by this proccess
+ * in-memory and support nested locking calls (using reference counting).
+ * At least LOCK_UW and LOCK_EX must be implemented. LOCK_SH can be a no-op.
+ * Locks should either be non-blocking or have low wait timeouts.
+ *
+ * Subclasses should avoid throwing exceptions at all costs.
+ *
+ * @ingroup LockManager
+ * @since 1.19
+ */
+abstract class LockManager {
+ /** @var array Mapping of lock types to the type actually used */
+ protected $lockTypeMap = [
+ self::LOCK_SH => 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 );
+}
--- /dev/null
+<?php
+/**
+ * Resource locking handling.
+ *
+ * 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
+ * @author Aaron Schulz
+ */
+
+/**
+ * Simple version of LockManager that does nothing
+ * @since 1.19
+ */
+class NullLockManager extends LockManager {
+ protected function doLock( array $paths, $type ) {
+ return StatusValue::newGood();
+ }
+
+ protected function doUnlock( array $paths, $type ) {
+ return StatusValue::newGood();
+ }
+}
--- /dev/null
+<?php
+/**
+ * Version of LockManager that uses a quorum from peer servers for locks.
+ *
+ * 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
+ */
+
+/**
+ * Version of LockManager that uses a quorum from peer servers for locks.
+ * The resource space can also be sharded into separate peer groups.
+ *
+ * @ingroup LockManager
+ * @since 1.20
+ */
+abstract class QuorumLockManager extends LockManager {
+ /** @var array Map of bucket indexes to peer server lists */
+ protected $srvsByBucket = []; // (bucket index => (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();
+}