summaryrefslogtreecommitdiff
path: root/includes/filebackend/lockmanager
diff options
context:
space:
mode:
Diffstat (limited to 'includes/filebackend/lockmanager')
-rw-r--r--includes/filebackend/lockmanager/DBLockManager.php227
-rw-r--r--includes/filebackend/lockmanager/FSLockManager.php42
-rw-r--r--includes/filebackend/lockmanager/LSLockManager.php8
-rw-r--r--includes/filebackend/lockmanager/LockManager.php324
-rw-r--r--includes/filebackend/lockmanager/LockManagerGroup.php54
-rw-r--r--includes/filebackend/lockmanager/MemcLockManager.php94
-rw-r--r--includes/filebackend/lockmanager/QuorumLockManager.php230
-rw-r--r--includes/filebackend/lockmanager/ScopedLock.php102
8 files changed, 633 insertions, 448 deletions
diff --git a/includes/filebackend/lockmanager/DBLockManager.php b/includes/filebackend/lockmanager/DBLockManager.php
index a8fe258b..f02387dc 100644
--- a/includes/filebackend/lockmanager/DBLockManager.php
+++ b/includes/filebackend/lockmanager/DBLockManager.php
@@ -22,10 +22,9 @@
*/
/**
- * Version of LockManager based on using DB table locks.
+ * Version of LockManager based on using named/row DB locks.
+ *
* This is meant for multi-wiki systems that may share files.
- * All locks are blocking, so it might be useful to set a small
- * lock-wait timeout via server config to curtail deadlocks.
*
* All lock requests for a resource, identified by a hash string, will map
* to one bucket. Each bucket maps to one or several peer DBs, each on their
@@ -37,7 +36,7 @@
* @ingroup LockManager
* @since 1.19
*/
-class DBLockManager extends QuorumLockManager {
+abstract class DBLockManager extends QuorumLockManager {
/** @var Array Map of DB names to server config */
protected $dbServers; // (DB name => server config array)
/** @var BagOStuff */
@@ -67,11 +66,12 @@ class DBLockManager extends QuorumLockManager {
* each having an odd-numbered list of DB names (peers) as values.
* Any DB named 'localDBMaster' will automatically use the DB master
* settings for this wiki (without the need for a dbServers entry).
+ * Only use 'localDBMaster' if the domain is a valid wiki ID.
* - lockExpiry : Lock timeout (seconds) for dropped connections. [optional]
* This tells the DB server how long to wait before assuming
* connection failure and releasing all the locks for a session.
*
- * @param Array $config
+ * @param array $config
*/
public function __construct( array $config ) {
parent::__construct( $config );
@@ -111,65 +111,6 @@ class DBLockManager extends QuorumLockManager {
}
/**
- * Get a connection to a lock DB and acquire locks on $paths.
- * This does not use GET_LOCK() per http://bugs.mysql.com/bug.php?id=1118.
- *
- * @see QuorumLockManager::getLocksOnServer()
- * @return Status
- */
- protected function getLocksOnServer( $lockSrv, array $paths, $type ) {
- $status = Status::newGood();
-
- if ( $type == self::LOCK_EX ) { // writer locks
- try {
- $keys = array_unique( array_map( 'LockManager::sha1Base36', $paths ) );
- # Build up values for INSERT clause
- $data = array();
- foreach ( $keys as $key ) {
- $data[] = array( 'fle_key' => $key );
- }
- # Wait on any existing writers and block new ones if we get in
- $db = $this->getConnection( $lockSrv ); // checked in isServerUp()
- $db->insert( 'filelocks_exclusive', $data, __METHOD__ );
- } catch ( DBError $e ) {
- foreach ( $paths as $path ) {
- $status->fatal( 'lockmanager-fail-acquirelock', $path );
- }
- }
- }
-
- return $status;
- }
-
- /**
- * @see QuorumLockManager::freeLocksOnServer()
- * @return Status
- */
- protected function freeLocksOnServer( $lockSrv, array $paths, $type ) {
- return Status::newGood(); // not supported
- }
-
- /**
- * @see QuorumLockManager::releaseAllLocks()
- * @return Status
- */
- protected function releaseAllLocks() {
- $status = Status::newGood();
-
- foreach ( $this->conns as $lockDb => $db ) {
- if ( $db->trxLevel() ) { // in transaction
- try {
- $db->rollback( __METHOD__ ); // finish transaction and kill any rows
- } catch ( DBError $e ) {
- $status->fatal( 'lockmanager-fail-db-release', $lockDb );
- }
- }
- }
-
- return $status;
- }
-
- /**
* @see QuorumLockManager::isServerUp()
* @return bool
*/
@@ -197,8 +138,8 @@ class DBLockManager extends QuorumLockManager {
if ( !isset( $this->conns[$lockDb] ) ) {
$db = null;
if ( $lockDb === 'localDBMaster' ) {
- $lb = wfGetLBFactory()->newMainLB();
- $db = $lb->getConnection( DB_MASTER );
+ $lb = wfGetLBFactory()->getMainLB( $this->domain );
+ $db = $lb->getConnection( DB_MASTER, array(), $this->domain );
} elseif ( isset( $this->dbServers[$lockDb] ) ) {
$config = $this->dbServers[$lockDb];
$db = DatabaseBase::factory( $config['type'], $config );
@@ -274,14 +215,8 @@ class DBLockManager extends QuorumLockManager {
* Make sure remaining locks get cleared for sanity
*/
function __destruct() {
+ $this->releaseAllLocks();
foreach ( $this->conns as $db ) {
- if ( $db->trxLevel() ) { // in transaction
- try {
- $db->rollback( __METHOD__ ); // finish transaction and kill any rows
- } catch ( DBError $e ) {
- // oh well
- }
- }
$db->close();
}
}
@@ -321,27 +256,38 @@ class MySqlLockManager extends DBLockManager {
$status = Status::newGood();
$db = $this->getConnection( $lockSrv ); // checked in isServerUp()
- $keys = array_unique( array_map( 'LockManager::sha1Base36', $paths ) );
+
+ $keys = array(); // list of hash keys for the paths
+ $data = array(); // list of rows to insert
+ $checkEXKeys = array(); // list of hash keys that this has no EX lock on
# Build up values for INSERT clause
- $data = array();
- foreach ( $keys as $key ) {
+ foreach ( $paths as $path ) {
+ $key = $this->sha1Base36Absolute( $path );
+ $keys[] = $key;
$data[] = array( 'fls_key' => $key, 'fls_session' => $this->session );
+ if ( !isset( $this->locksHeld[$path][self::LOCK_EX] ) ) {
+ $checkEXKeys[] = $key;
+ }
}
- # Block new writers...
+
+ # Block new writers (both EX and SH locks leave entries here)...
$db->insert( 'filelocks_shared', $data, __METHOD__, array( 'IGNORE' ) );
# Actually do the locking queries...
if ( $type == self::LOCK_SH ) { // reader locks
+ $blocked = false;
# Bail if there are any existing writers...
- $blocked = $db->selectField( 'filelocks_exclusive', '1',
- array( 'fle_key' => $keys ),
- __METHOD__
- );
- # Prospective writers that haven't yet updated filelocks_exclusive
- # will recheck filelocks_shared after doing so and bail due to our entry.
+ if ( count( $checkEXKeys ) ) {
+ $blocked = $db->selectField( 'filelocks_exclusive', '1',
+ array( 'fle_key' => $checkEXKeys ),
+ __METHOD__
+ );
+ }
+ # Other prospective writers that haven't yet updated filelocks_exclusive
+ # will recheck filelocks_shared after doing so and bail due to this entry.
} else { // writer locks
$encSession = $db->addQuotes( $this->session );
# Bail if there are any existing writers...
- # The may detect readers, but the safe check for them is below.
+ # This may detect readers, but the safe check for them is below.
# Note: if two writers come at the same time, both bail :)
$blocked = $db->selectField( 'filelocks_shared', '1',
array( 'fls_key' => $keys, "fls_session != $encSession" ),
@@ -371,4 +317,117 @@ class MySqlLockManager extends DBLockManager {
return $status;
}
+
+ /**
+ * @see QuorumLockManager::freeLocksOnServer()
+ * @return Status
+ */
+ protected function freeLocksOnServer( $lockSrv, array $paths, $type ) {
+ return Status::newGood(); // not supported
+ }
+
+ /**
+ * @see QuorumLockManager::releaseAllLocks()
+ * @return Status
+ */
+ protected function releaseAllLocks() {
+ $status = Status::newGood();
+
+ foreach ( $this->conns as $lockDb => $db ) {
+ if ( $db->trxLevel() ) { // in transaction
+ try {
+ $db->rollback( __METHOD__ ); // finish transaction and kill any rows
+ } catch ( DBError $e ) {
+ $status->fatal( 'lockmanager-fail-db-release', $lockDb );
+ }
+ }
+ }
+
+ return $status;
+ }
+}
+
+/**
+ * PostgreSQL version of DBLockManager that supports shared locks.
+ * All locks are non-blocking, which avoids deadlocks.
+ *
+ * @ingroup LockManager
+ */
+class PostgreSqlLockManager extends DBLockManager {
+ /** @var Array Mapping of lock types to the type actually used */
+ protected $lockTypeMap = array(
+ self::LOCK_SH => self::LOCK_SH,
+ self::LOCK_UW => self::LOCK_SH,
+ self::LOCK_EX => self::LOCK_EX
+ );
+
+ protected function getLocksOnServer( $lockSrv, array $paths, $type ) {
+ $status = Status::newGood();
+ if ( !count( $paths ) ) {
+ return $status; // nothing to lock
+ }
+
+ $db = $this->getConnection( $lockSrv ); // checked in isServerUp()
+ $bigints = array_unique( array_map(
+ function( $key ) { return wfBaseConvert( substr( $key, 0, 15 ), 16, 10 ); },
+ array_map( array( $this, 'sha1Base16Absolute' ), $paths )
+ ) );
+
+ // Try to acquire all the locks...
+ $fields = array();
+ foreach ( $bigints as $bigint ) {
+ $fields[] = ( $type == self::LOCK_SH )
+ ? "pg_try_advisory_lock_shared({$db->addQuotes( $bigint )}) AS K$bigint"
+ : "pg_try_advisory_lock({$db->addQuotes( $bigint )}) AS K$bigint";
+ }
+ $res = $db->query( 'SELECT ' . implode( ', ', $fields ), __METHOD__ );
+ $row = (array)$res->fetchObject();
+
+ if ( in_array( 'f', $row ) ) {
+ // Release any acquired locks if some could not be acquired...
+ $fields = array();
+ foreach ( $row as $kbigint => $ok ) {
+ if ( $ok === 't' ) { // locked
+ $bigint = substr( $kbigint, 1 ); // strip off the "K"
+ $fields[] = ( $type == self::LOCK_SH )
+ ? "pg_advisory_unlock_shared({$db->addQuotes( $bigint )})"
+ : "pg_advisory_unlock({$db->addQuotes( $bigint )})";
+ }
+ }
+ if ( count( $fields ) ) {
+ $db->query( 'SELECT ' . implode( ', ', $fields ), __METHOD__ );
+ }
+ foreach ( $paths as $path ) {
+ $status->fatal( 'lockmanager-fail-acquirelock', $path );
+ }
+ }
+
+ return $status;
+ }
+
+ /**
+ * @see QuorumLockManager::freeLocksOnServer()
+ * @return Status
+ */
+ protected function freeLocksOnServer( $lockSrv, array $paths, $type ) {
+ return Status::newGood(); // not supported
+ }
+
+ /**
+ * @see QuorumLockManager::releaseAllLocks()
+ * @return Status
+ */
+ protected function releaseAllLocks() {
+ $status = Status::newGood();
+
+ foreach ( $this->conns as $lockDb => $db ) {
+ try {
+ $db->query( "SELECT pg_advisory_unlock_all()", __METHOD__ );
+ } catch ( DBError $e ) {
+ $status->fatal( 'lockmanager-fail-db-release', $lockDb );
+ }
+ }
+
+ return $status;
+ }
}
diff --git a/includes/filebackend/lockmanager/FSLockManager.php b/includes/filebackend/lockmanager/FSLockManager.php
index 9a6206fd..eacba704 100644
--- a/includes/filebackend/lockmanager/FSLockManager.php
+++ b/includes/filebackend/lockmanager/FSLockManager.php
@@ -43,7 +43,7 @@ class FSLockManager extends LockManager {
protected $lockDir; // global dir for all servers
- /** @var Array Map of (locked key => lock type => lock file handle) */
+ /** @var Array Map of (locked key => lock file handle) */
protected $handles = array();
/**
@@ -115,12 +115,16 @@ class FSLockManager extends LockManager {
} elseif ( isset( $this->locksHeld[$path][self::LOCK_EX] ) ) {
$this->locksHeld[$path][$type] = 1;
} else {
- wfSuppressWarnings();
- $handle = fopen( $this->getLockPath( $path ), 'a+' );
- wfRestoreWarnings();
- if ( !$handle ) { // lock dir missing?
- wfMkdirParents( $this->lockDir );
- $handle = fopen( $this->getLockPath( $path ), 'a+' ); // try again
+ if ( isset( $this->handles[$path] ) ) {
+ $handle = $this->handles[$path];
+ } else {
+ wfSuppressWarnings();
+ $handle = fopen( $this->getLockPath( $path ), 'a+' );
+ wfRestoreWarnings();
+ if ( !$handle ) { // lock dir missing?
+ wfMkdirParents( $this->lockDir );
+ $handle = fopen( $this->getLockPath( $path ), 'a+' ); // try again
+ }
}
if ( $handle ) {
// Either a shared or exclusive lock
@@ -128,7 +132,7 @@ class FSLockManager extends LockManager {
if ( flock( $handle, $lock | LOCK_NB ) ) {
// Record this lock as active
$this->locksHeld[$path][$type] = 1;
- $this->handles[$path][$type] = $handle;
+ $this->handles[$path] = $handle;
} else {
fclose( $handle );
$status->fatal( 'lockmanager-fail-acquirelock', $path );
@@ -160,24 +164,13 @@ class FSLockManager extends LockManager {
--$this->locksHeld[$path][$type];
if ( $this->locksHeld[$path][$type] <= 0 ) {
unset( $this->locksHeld[$path][$type] );
- // If a LOCK_SH comes in while we have a LOCK_EX, we don't
- // actually add a handler, so check for handler existence.
- if ( isset( $this->handles[$path][$type] ) ) {
- if ( $type === self::LOCK_EX
- && isset( $this->locksHeld[$path][self::LOCK_SH] )
- && !isset( $this->handles[$path][self::LOCK_SH] ) )
- {
- // EX lock came first: move this handle to the SH one
- $this->handles[$path][self::LOCK_SH] = $this->handles[$path][$type];
- } else {
- // Mark this handle to be unlocked and closed
- $handlesToClose[] = $this->handles[$path][$type];
- }
- unset( $this->handles[$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...
@@ -237,8 +230,7 @@ class FSLockManager extends LockManager {
* @return string
*/
protected function getLockPath( $path ) {
- $hash = self::sha1Base36( $path );
- return "{$this->lockDir}/{$hash}.lock";
+ return "{$this->lockDir}/{$this->sha1Base36Absolute( $path )}.lock";
}
/**
diff --git a/includes/filebackend/lockmanager/LSLockManager.php b/includes/filebackend/lockmanager/LSLockManager.php
index 89428182..97de8dca 100644
--- a/includes/filebackend/lockmanager/LSLockManager.php
+++ b/includes/filebackend/lockmanager/LSLockManager.php
@@ -66,7 +66,7 @@ class LSLockManager extends QuorumLockManager {
* each having an odd-numbered list of server names (peers) as values.
* - connTimeout : Lock server connection attempt timeout. [optional]
*
- * @param Array $config
+ * @param array $config
*/
public function __construct( array $config ) {
parent::__construct( $config );
@@ -94,7 +94,7 @@ class LSLockManager extends QuorumLockManager {
// Send out the command and get the response...
$type = ( $type == self::LOCK_SH ) ? 'SH' : 'EX';
- $keys = array_unique( array_map( 'LockManager::sha1Base36', $paths ) );
+ $keys = array_unique( array_map( array( $this, 'sha1Base36Absolute' ), $paths ) );
$response = $this->sendCommand( $lockSrv, 'ACQUIRE', $type, $keys );
if ( $response !== 'ACQUIRED' ) {
@@ -115,7 +115,7 @@ class LSLockManager extends QuorumLockManager {
// Send out the command and get the response...
$type = ( $type == self::LOCK_SH ) ? 'SH' : 'EX';
- $keys = array_unique( array_map( 'LockManager::sha1Base36', $paths ) );
+ $keys = array_unique( array_map( array( $this, 'sha1Base36Absolute' ), $paths ) );
$response = $this->sendCommand( $lockSrv, 'RELEASE', $type, $keys );
if ( $response !== 'RELEASED' ) {
@@ -169,7 +169,7 @@ class LSLockManager extends QuorumLockManager {
$authKey = $this->lockServers[$lockSrv]['authKey'];
// Build of the command as a flat string...
$values = implode( '|', $values );
- $key = sha1( $this->session . $action . $type . $values . $authKey );
+ $key = hash_hmac( 'sha1', "{$this->session}\n{$action}\n{$type}\n{$values}", $authKey );
// Send out the command...
if ( fwrite( $conn, "{$this->session}:$key:$action:$type:$values\n" ) === false ) {
return false;
diff --git a/includes/filebackend/lockmanager/LockManager.php b/includes/filebackend/lockmanager/LockManager.php
index 07853f87..0512a01b 100644
--- a/includes/filebackend/lockmanager/LockManager.php
+++ b/includes/filebackend/lockmanager/LockManager.php
@@ -53,6 +53,9 @@ abstract class LockManager {
/** @var Array Map of (resource path => lock type => count) */
protected $locksHeld = array();
+ 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)
@@ -61,14 +64,29 @@ abstract class LockManager {
/**
* Construct a new instance from configuration
*
+ * $config paramaters 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.
+ *
* @param $config Array
*/
- public function __construct( array $config ) {}
+ public function __construct( array $config ) {
+ $this->domain = isset( $config['domain'] ) ? $config['domain'] : wfWikiID();
+ if ( isset( $config['lockTTL'] ) ) {
+ $this->lockTTL = max( 1, $config['lockTTL'] );
+ } elseif ( PHP_SAPI === 'cli' ) {
+ $this->lockTTL = 2*3600;
+ } else {
+ $met = ini_get( 'max_execution_time' ); // this is 0 in CLI mode
+ $this->lockTTL = max( 5*60, 2*(int)$met );
+ }
+ }
/**
* Lock the resources at the given abstract paths
*
- * @param $paths Array List of resource names
+ * @param array $paths List of resource names
* @param $type integer LockManager::LOCK_* constant
* @return Status
*/
@@ -82,7 +100,7 @@ abstract class LockManager {
/**
* Unlock the resources at the given abstract paths
*
- * @param $paths Array List of storage paths
+ * @param array $paths List of storage paths
* @param $type integer LockManager::LOCK_* constant
* @return Status
*/
@@ -94,308 +112,46 @@ abstract class LockManager {
}
/**
- * Get the base 36 SHA-1 of a string, padded to 31 digits
+ * 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 $path string
* @return string
*/
- final protected static function sha1Base36( $path ) {
- return wfBaseConvert( sha1( $path ), 16, 36, 31 );
+ final protected function sha1Base36Absolute( $path ) {
+ return wfBaseConvert( sha1( "{$this->domain}:{$path}" ), 16, 36, 31 );
}
/**
- * Lock resources with the given keys and lock type
+ * 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 $paths Array List of storage paths
- * @param $type integer LockManager::LOCK_* constant
+ * @param $path string
* @return string
*/
- abstract protected function doLock( array $paths, $type );
+ final protected function sha1Base16Absolute( $path ) {
+ return sha1( "{$this->domain}:{$path}" );
+ }
/**
- * Unlock resources with the given keys and lock type
+ * Lock resources with the given keys and lock type
*
- * @param $paths Array List of storage paths
+ * @param array $paths List of storage paths
* @param $type integer LockManager::LOCK_* constant
* @return string
*/
- abstract protected function doUnlock( array $paths, $type );
-}
-
-/**
- * Self-releasing locks
- *
- * LockManager helper class to handle scoped locks, which
- * release when an object is destroyed or goes out of scope.
- *
- * @ingroup LockManager
- * @since 1.19
- */
-class ScopedLock {
- /** @var LockManager */
- protected $manager;
- /** @var Status */
- protected $status;
- /** @var Array List of resource paths*/
- protected $paths;
-
- protected $type; // integer lock type
-
- /**
- * @param $manager LockManager
- * @param $paths Array List of storage paths
- * @param $type integer LockManager::LOCK_* constant
- * @param $status Status
- */
- protected function __construct(
- LockManager $manager, array $paths, $type, Status $status
- ) {
- $this->manager = $manager;
- $this->paths = $paths;
- $this->status = $status;
- $this->type = $type;
- }
+ abstract protected function doLock( array $paths, $type );
/**
- * Get a ScopedLock object representing a lock on resource paths.
- * Any locks are released once this object goes out of scope.
- * The status object is updated with any errors or warnings.
+ * Unlock resources with the given keys and lock type
*
- * @param $manager LockManager
- * @param $paths Array List of storage paths
+ * @param array $paths List of storage paths
* @param $type integer LockManager::LOCK_* constant
- * @param $status Status
- * @return ScopedLock|null Returns null on failure
- */
- public static function factory(
- LockManager $manager, array $paths, $type, Status $status
- ) {
- $lockStatus = $manager->lock( $paths, $type );
- $status->merge( $lockStatus );
- if ( $lockStatus->isOK() ) {
- return new self( $manager, $paths, $type, $status );
- }
- return null;
- }
-
- function __destruct() {
- $wasOk = $this->status->isOK();
- $this->status->merge( $this->manager->unlock( $this->paths, $this->type ) );
- if ( $wasOk ) {
- // Make sure status is OK, despite any unlockFiles() fatals
- $this->status->setResult( true, $this->status->value );
- }
- }
-}
-
-/**
- * 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 = array(); // (bucket index => (lsrv1, lsrv2, ...))
-
- /**
- * @see LockManager::doLock()
- * @param $paths array
- * @param $type int
- * @return Status
- */
- final protected function doLock( array $paths, $type ) {
- $status = Status::newGood();
-
- $pathsToLock = array(); // (bucket => paths)
- // Get locks that need to be acquired (buckets => locks)...
- foreach ( $paths as $path ) {
- if ( isset( $this->locksHeld[$path][$type] ) ) {
- ++$this->locksHeld[$path][$type];
- } elseif ( isset( $this->locksHeld[$path][self::LOCK_EX] ) ) {
- $this->locksHeld[$path][$type] = 1;
- } else {
- $bucket = $this->getBucketFromKey( $path );
- $pathsToLock[$bucket][] = $path;
- }
- }
-
- $lockedPaths = array(); // files locked in this attempt
- // Attempt to acquire these locks...
- foreach ( $pathsToLock as $bucket => $paths ) {
- // Try to acquire the locks for this bucket
- $status->merge( $this->doLockingRequestBucket( $bucket, $paths, $type ) );
- if ( !$status->isOK() ) {
- $status->merge( $this->doUnlock( $lockedPaths, $type ) );
- return $status;
- }
- // Record these locks as active
- foreach ( $paths as $path ) {
- $this->locksHeld[$path][$type] = 1; // locked
- }
- // Keep track of what locks were made in this attempt
- $lockedPaths = array_merge( $lockedPaths, $paths );
- }
-
- return $status;
- }
-
- /**
- * @see LockManager::doUnlock()
- * @param $paths array
- * @param $type int
- * @return Status
- */
- final protected function doUnlock( array $paths, $type ) {
- $status = Status::newGood();
-
- $pathsToUnlock = array();
- foreach ( $paths as $path ) {
- if ( !isset( $this->locksHeld[$path][$type] ) ) {
- $status->warning( 'lockmanager-notlocked', $path );
- } else {
- --$this->locksHeld[$path][$type];
- // Reference count the locks held and release locks when zero
- if ( $this->locksHeld[$path][$type] <= 0 ) {
- unset( $this->locksHeld[$path][$type] );
- $bucket = $this->getBucketFromKey( $path );
- $pathsToUnlock[$bucket][] = $path;
- }
- if ( !count( $this->locksHeld[$path] ) ) {
- unset( $this->locksHeld[$path] ); // no SH or EX locks left for key
- }
- }
- }
-
- // Remove these specific locks if possible, or at least release
- // all locks once this process is currently not holding any locks.
- foreach ( $pathsToUnlock as $bucket => $paths ) {
- $status->merge( $this->doUnlockingRequestBucket( $bucket, $paths, $type ) );
- }
- if ( !count( $this->locksHeld ) ) {
- $status->merge( $this->releaseAllLocks() );
- }
-
- 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 $bucket integer
- * @param $paths Array List of resource keys to lock
- * @param $type integer LockManager::LOCK_EX or LockManager::LOCK_SH
- * @return Status
- */
- final protected function doLockingRequestBucket( $bucket, array $paths, $type ) {
- $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 );
- continue; // server down?
- }
- // Attempt to acquire the lock on this peer
- $status->merge( $this->getLocksOnServer( $lockSrv, $paths, $type ) );
- 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 $bucket integer
- * @param $paths Array List of resource keys to lock
- * @param $type integer LockManager::LOCK_EX or LockManager::LOCK_SH
- * @return Status
- */
- final protected function doUnlockingRequestBucket( $bucket, array $paths, $type ) {
- $status = Status::newGood();
-
- foreach ( $this->srvsByBucket[$bucket] as $lockSrv ) {
- if ( !$this->isServerUp( $lockSrv ) ) {
- $status->fatal( 'lockmanager-fail-svr-release', $lockSrv );
- // Attempt to release the lock on this peer
- } else {
- $status->merge( $this->freeLocksOnServer( $lockSrv, $paths, $type ) );
- }
- }
-
- return $status;
- }
-
- /**
- * Get the bucket for resource path.
- * This should avoid throwing any exceptions.
- *
- * @param $path string
- * @return integer
- */
- protected function getBucketFromKey( $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
- *
- * @param $lockSrv string
- * @return bool
- */
- abstract protected function isServerUp( $lockSrv );
-
- /**
- * Get a connection to a lock server and acquire locks on $paths
- *
- * @param $lockSrv string
- * @param $paths array
- * @param $type integer
- * @return Status
- */
- abstract protected function getLocksOnServer( $lockSrv, array $paths, $type );
-
- /**
- * Get a connection to a lock server and release locks on $paths.
- *
- * Subclasses must effectively implement this or releaseAllLocks().
- *
- * @param $lockSrv string
- * @param $paths array
- * @param $type integer
- * @return Status
- */
- abstract protected function freeLocksOnServer( $lockSrv, array $paths, $type );
-
- /**
- * Release all locks that this session is holding.
- *
- * Subclasses must effectively implement this or freeLocksOnServer().
- *
- * @return Status
+ * @return string
*/
- abstract protected function releaseAllLocks();
+ abstract protected function doUnlock( array $paths, $type );
}
/**
diff --git a/includes/filebackend/lockmanager/LockManagerGroup.php b/includes/filebackend/lockmanager/LockManagerGroup.php
index 8c8c940a..ac0bd49b 100644
--- a/includes/filebackend/lockmanager/LockManagerGroup.php
+++ b/includes/filebackend/lockmanager/LockManagerGroup.php
@@ -29,33 +29,41 @@
* @since 1.19
*/
class LockManagerGroup {
- /**
- * @var LockManagerGroup
- */
- protected static $instance = null;
+ /** @var Array (domain => LockManager) */
+ protected static $instances = array();
+
+ protected $domain; // string; domain (usually wiki ID)
- /** @var Array of (name => ('class' =>, 'config' =>, 'instance' =>)) */
+ /** @var Array of (name => ('class' => ..., 'config' => ..., 'instance' => ...)) */
protected $managers = array();
- protected function __construct() {}
+ /**
+ * @param string $domain Domain (usually wiki ID)
+ */
+ protected function __construct( $domain ) {
+ $this->domain = $domain;
+ }
/**
+ * @param string $domain Domain (usually wiki ID)
* @return LockManagerGroup
*/
- public static function singleton() {
- if ( self::$instance == null ) {
- self::$instance = new self();
- self::$instance->initFromGlobals();
+ public static function singleton( $domain = false ) {
+ $domain = ( $domain === false ) ? wfWikiID() : $domain;
+ if ( !isset( self::$instances[$domain] ) ) {
+ self::$instances[$domain] = new self( $domain );
+ self::$instances[$domain]->initFromGlobals();
}
- return self::$instance;
+ return self::$instances[$domain];
}
/**
- * Destroy the singleton instance, so that a new one will be created next
- * time singleton() is called.
+ * Destroy the singleton instances
+ *
+ * @return void
*/
- public static function destroySingleton() {
- self::$instance = null;
+ public static function destroySingletons() {
+ self::$instances = array();
}
/**
@@ -78,6 +86,7 @@ class LockManagerGroup {
*/
protected function register( array $configs ) {
foreach ( $configs as $config ) {
+ $config['domain'] = $this->domain;
if ( !isset( $config['name'] ) ) {
throw new MWException( "Cannot register a lock manager with no name." );
}
@@ -116,6 +125,21 @@ class LockManagerGroup {
}
/**
+ * Get the config array for a lock manager object with a given name
+ *
+ * @param $name string
+ * @return Array
+ * @throws MWException
+ */
+ public function config( $name ) {
+ if ( !isset( $this->managers[$name] ) ) {
+ throw new MWException( "No lock manager defined with the name `$name`." );
+ }
+ $class = $this->managers[$name]['class'];
+ return array( 'class' => $class ) + $this->managers[$name]['config'];
+ }
+
+ /**
* Get the default lock manager configured for the site.
* Returns NullLockManager if no lock manager could be found.
*
diff --git a/includes/filebackend/lockmanager/MemcLockManager.php b/includes/filebackend/lockmanager/MemcLockManager.php
index 57c0463d..fafc588a 100644
--- a/includes/filebackend/lockmanager/MemcLockManager.php
+++ b/includes/filebackend/lockmanager/MemcLockManager.php
@@ -28,8 +28,8 @@
* This is meant for multi-wiki systems that may share files.
* All locks are non-blocking, which avoids deadlocks.
*
- * All lock requests for a resource, identified by a hash string, will map
- * to one bucket. Each bucket maps to one or several peer servers, each running memcached.
+ * All lock requests for a resource, identified by a hash string, will map to one
+ * bucket. Each bucket maps to one or several peer servers, each running memcached.
* A majority of peers must agree for a lock to be acquired.
*
* @ingroup LockManager
@@ -48,9 +48,7 @@ class MemcLockManager extends QuorumLockManager {
/** @var Array */
protected $serversUp = array(); // (server name => bool)
- protected $lockExpiry; // integer; maximum time locks can be held
- protected $session = ''; // string; random SHA-1 UUID
- protected $wikiId = ''; // string
+ protected $session = ''; // string; random UUID
/**
* Construct a new instance from configuration.
@@ -61,9 +59,9 @@ class MemcLockManager extends QuorumLockManager {
* each having an odd-numbered list of server names (peers) as values.
* - memcConfig : Configuration array for ObjectCache::newFromParams. [optional]
* If set, this must use one of the memcached classes.
- * - wikiId : Wiki ID string that all resources are relative to. [optional]
*
- * @param Array $config
+ * @param array $config
+ * @throws MWException
*/
public function __construct( array $config ) {
parent::__construct( $config );
@@ -87,11 +85,6 @@ class MemcLockManager extends QuorumLockManager {
}
}
- $this->wikiId = isset( $config['wikiId'] ) ? $config['wikiId'] : wfWikiID();
-
- $met = ini_get( 'max_execution_time' ); // this is 0 in CLI mode
- $this->lockExpiry = $met ? 2*(int)$met : 2*3600;
-
$this->session = wfRandomString( 32 );
}
@@ -110,7 +103,7 @@ class MemcLockManager extends QuorumLockManager {
foreach ( $paths as $path ) {
$status->fatal( 'lockmanager-fail-acquirelock', $path );
}
- return;
+ return $status;
}
// Fetch all the existing lock records...
@@ -121,8 +114,8 @@ class MemcLockManager extends QuorumLockManager {
foreach ( $paths as $path ) {
$locksKey = $this->recordKeyForPath( $path );
$locksHeld = isset( $lockRecords[$locksKey] )
- ? $lockRecords[$locksKey]
- : array( self::LOCK_SH => array(), self::LOCK_EX => array() ); // init
+ ? self::sanitizeLockArray( $lockRecords[$locksKey] )
+ : self::newLockArray(); // init
foreach ( $locksHeld[self::LOCK_EX] as $session => $expiry ) {
if ( $expiry < $now ) { // stale?
unset( $locksHeld[self::LOCK_EX][$session] );
@@ -141,7 +134,7 @@ class MemcLockManager extends QuorumLockManager {
}
if ( $status->isOK() ) {
// Register the session in the lock record array
- $locksHeld[$type][$this->session] = $now + $this->lockExpiry;
+ $locksHeld[$type][$this->session] = $now + $this->lockTTL;
// We will update this record if none of the other locks conflict
$lockRecords[$locksKey] = $locksHeld;
}
@@ -149,9 +142,15 @@ class MemcLockManager extends QuorumLockManager {
// If there were no lock conflicts, update all the lock records...
if ( $status->isOK() ) {
- foreach ( $lockRecords as $locksKey => $locksHeld ) {
- $memc->set( $locksKey, $locksHeld );
- wfDebug( __METHOD__ . ": acquired lock on key $locksKey.\n" );
+ foreach ( $paths as $path ) {
+ $locksKey = $this->recordKeyForPath( $path );
+ $locksHeld = $lockRecords[$locksKey];
+ $ok = $memc->set( $locksKey, $locksHeld, 7*86400 );
+ if ( !$ok ) {
+ $status->fatal( 'lockmanager-fail-acquirelock', $path );
+ } else {
+ wfDebug( __METHOD__ . ": acquired lock on key $locksKey.\n" );
+ }
}
}
@@ -186,17 +185,22 @@ class MemcLockManager extends QuorumLockManager {
foreach ( $paths as $path ) {
$locksKey = $this->recordKeyForPath( $path ); // lock record
if ( !isset( $lockRecords[$locksKey] ) ) {
+ $status->warning( 'lockmanager-fail-releaselock', $path );
continue; // nothing to do
}
- $locksHeld = $lockRecords[$locksKey];
- if ( is_array( $locksHeld ) && isset( $locksHeld[$type] ) ) {
- unset( $locksHeld[$type][$this->session] );
- $ok = $memc->set( $locksKey, $locksHeld );
+ $locksHeld = self::sanitizeLockArray( $lockRecords[$locksKey] );
+ if ( isset( $locksHeld[$type][$this->session] ) ) {
+ unset( $locksHeld[$type][$this->session] ); // unregister this session
+ if ( $locksHeld === self::newLockArray() ) {
+ $ok = $memc->delete( $locksKey );
+ } else {
+ $ok = $memc->set( $locksKey, $locksHeld );
+ }
+ if ( !$ok ) {
+ $status->fatal( 'lockmanager-fail-releaselock', $path );
+ }
} else {
- $ok = true;
- }
- if ( !$ok ) {
- $status->fatal( 'lockmanager-fail-releaselock', $path );
+ $status->warning( 'lockmanager-fail-releaselock', $path );
}
wfDebug( __METHOD__ . ": released lock on key $locksKey.\n" );
}
@@ -226,7 +230,7 @@ class MemcLockManager extends QuorumLockManager {
/**
* Get the MemcachedBagOStuff object for a $lockSrv
*
- * @param $lockSrv string Server name
+ * @param string $lockSrv Server name
* @return MemcachedBagOStuff|null
*/
protected function getCache( $lockSrv ) {
@@ -234,7 +238,7 @@ class MemcLockManager extends QuorumLockManager {
if ( isset( $this->bagOStuffs[$lockSrv] ) ) {
$memc = $this->bagOStuffs[$lockSrv];
if ( !isset( $this->serversUp[$lockSrv] ) ) {
- $this->serversUp[$lockSrv] = $memc->set( 'MemcLockManager:ping', 1, 1 );
+ $this->serversUp[$lockSrv] = $memc->set( __CLASS__ . ':ping', 1, 1 );
if ( !$this->serversUp[$lockSrv] ) {
trigger_error( __METHOD__ . ": Could not contact $lockSrv.", E_USER_WARNING );
}
@@ -251,14 +255,32 @@ class MemcLockManager extends QuorumLockManager {
* @return string
*/
protected function recordKeyForPath( $path ) {
- $hash = LockManager::sha1Base36( $path );
- list( $db, $prefix ) = wfSplitWikiID( $this->wikiId );
- return wfForeignMemcKey( $db, $prefix, __CLASS__, 'locks', $hash );
+ return implode( ':', array( __CLASS__, 'locks', $this->sha1Base36Absolute( $path ) ) );
+ }
+
+ /**
+ * @return Array An empty lock structure for a key
+ */
+ protected static function newLockArray() {
+ return array( self::LOCK_SH => array(), self::LOCK_EX => array() );
+ }
+
+ /**
+ * @param $a array
+ * @return Array An empty lock structure for a key
+ */
+ protected static function sanitizeLockArray( $a ) {
+ if ( is_array( $a ) && isset( $a[self::LOCK_EX] ) && isset( $a[self::LOCK_SH] ) ) {
+ return $a;
+ } else {
+ trigger_error( __METHOD__ . ": reset invalid lock array.", E_USER_WARNING );
+ return self::newLockArray();
+ }
}
/**
* @param $memc MemcachedBagOStuff
- * @param $keys Array List of keys to acquire
+ * @param array $keys List of keys to acquire
* @return bool
*/
protected function acquireMutexes( MemcachedBagOStuff $memc, array $keys ) {
@@ -284,10 +306,10 @@ class MemcLockManager extends QuorumLockManager {
continue; // acquire in order
}
}
- } while ( count( $lockedKeys ) < count( $keys ) && ( microtime( true ) - $start ) <= 6 );
+ } while ( count( $lockedKeys ) < count( $keys ) && ( microtime( true ) - $start ) <= 3 );
if ( count( $lockedKeys ) != count( $keys ) ) {
- $this->releaseMutexes( $lockedKeys ); // failed; release what was locked
+ $this->releaseMutexes( $memc, $lockedKeys ); // failed; release what was locked
return false;
}
@@ -296,7 +318,7 @@ class MemcLockManager extends QuorumLockManager {
/**
* @param $memc MemcachedBagOStuff
- * @param $keys Array List of acquired keys
+ * @param array $keys List of acquired keys
* @return void
*/
protected function releaseMutexes( MemcachedBagOStuff $memc, array $keys ) {
diff --git a/includes/filebackend/lockmanager/QuorumLockManager.php b/includes/filebackend/lockmanager/QuorumLockManager.php
new file mode 100644
index 00000000..b331b540
--- /dev/null
+++ b/includes/filebackend/lockmanager/QuorumLockManager.php
@@ -0,0 +1,230 @@
+<?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 = array(); // (bucket index => (lsrv1, lsrv2, ...))
+
+ /**
+ * @see LockManager::doLock()
+ * @param $paths array
+ * @param $type int
+ * @return Status
+ */
+ final protected function doLock( array $paths, $type ) {
+ $status = Status::newGood();
+
+ $pathsToLock = array(); // (bucket => paths)
+ // Get locks that need to be acquired (buckets => locks)...
+ foreach ( $paths as $path ) {
+ if ( isset( $this->locksHeld[$path][$type] ) ) {
+ ++$this->locksHeld[$path][$type];
+ } else {
+ $bucket = $this->getBucketFromPath( $path );
+ $pathsToLock[$bucket][] = $path;
+ }
+ }
+
+ $lockedPaths = array(); // files locked in this attempt
+ // Attempt to acquire these locks...
+ foreach ( $pathsToLock as $bucket => $paths ) {
+ // Try to acquire the locks for this bucket
+ $status->merge( $this->doLockingRequestBucket( $bucket, $paths, $type ) );
+ if ( !$status->isOK() ) {
+ $status->merge( $this->doUnlock( $lockedPaths, $type ) );
+ return $status;
+ }
+ // Record these locks as active
+ foreach ( $paths as $path ) {
+ $this->locksHeld[$path][$type] = 1; // locked
+ }
+ // Keep track of what locks were made in this attempt
+ $lockedPaths = array_merge( $lockedPaths, $paths );
+ }
+
+ return $status;
+ }
+
+ /**
+ * @see LockManager::doUnlock()
+ * @param $paths array
+ * @param $type int
+ * @return Status
+ */
+ final protected function doUnlock( array $paths, $type ) {
+ $status = Status::newGood();
+
+ $pathsToUnlock = array();
+ foreach ( $paths as $path ) {
+ if ( !isset( $this->locksHeld[$path][$type] ) ) {
+ $status->warning( 'lockmanager-notlocked', $path );
+ } else {
+ --$this->locksHeld[$path][$type];
+ // Reference count the locks held and release locks when zero
+ if ( $this->locksHeld[$path][$type] <= 0 ) {
+ unset( $this->locksHeld[$path][$type] );
+ $bucket = $this->getBucketFromPath( $path );
+ $pathsToUnlock[$bucket][] = $path;
+ }
+ if ( !count( $this->locksHeld[$path] ) ) {
+ unset( $this->locksHeld[$path] ); // no SH or EX locks left for key
+ }
+ }
+ }
+
+ // Remove these specific locks if possible, or at least release
+ // all locks once this process is currently not holding any locks.
+ foreach ( $pathsToUnlock as $bucket => $paths ) {
+ $status->merge( $this->doUnlockingRequestBucket( $bucket, $paths, $type ) );
+ }
+ if ( !count( $this->locksHeld ) ) {
+ $status->merge( $this->releaseAllLocks() );
+ }
+
+ 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 $bucket integer
+ * @param array $paths List of resource keys to lock
+ * @param $type integer LockManager::LOCK_EX or LockManager::LOCK_SH
+ * @return Status
+ */
+ final protected function doLockingRequestBucket( $bucket, array $paths, $type ) {
+ $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 );
+ continue; // server down?
+ }
+ // Attempt to acquire the lock on this peer
+ $status->merge( $this->getLocksOnServer( $lockSrv, $paths, $type ) );
+ 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 $bucket integer
+ * @param array $paths List of resource keys to lock
+ * @param $type integer LockManager::LOCK_EX or LockManager::LOCK_SH
+ * @return Status
+ */
+ final protected function doUnlockingRequestBucket( $bucket, array $paths, $type ) {
+ $status = Status::newGood();
+
+ foreach ( $this->srvsByBucket[$bucket] as $lockSrv ) {
+ if ( !$this->isServerUp( $lockSrv ) ) {
+ $status->fatal( 'lockmanager-fail-svr-release', $lockSrv );
+ // Attempt to release the lock on this peer
+ } else {
+ $status->merge( $this->freeLocksOnServer( $lockSrv, $paths, $type ) );
+ }
+ }
+
+ return $status;
+ }
+
+ /**
+ * Get the bucket for resource path.
+ * This should avoid throwing any exceptions.
+ *
+ * @param $path string
+ * @return integer
+ */
+ 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
+ *
+ * @param $lockSrv string
+ * @return bool
+ */
+ abstract protected function isServerUp( $lockSrv );
+
+ /**
+ * Get a connection to a lock server and acquire locks on $paths
+ *
+ * @param $lockSrv string
+ * @param $paths array
+ * @param $type integer
+ * @return Status
+ */
+ abstract protected function getLocksOnServer( $lockSrv, array $paths, $type );
+
+ /**
+ * Get a connection to a lock server and release locks on $paths.
+ *
+ * Subclasses must effectively implement this or releaseAllLocks().
+ *
+ * @param $lockSrv string
+ * @param $paths array
+ * @param $type integer
+ * @return Status
+ */
+ abstract protected function freeLocksOnServer( $lockSrv, array $paths, $type );
+
+ /**
+ * Release all locks that this session is holding.
+ *
+ * Subclasses must effectively implement this or freeLocksOnServer().
+ *
+ * @return Status
+ */
+ abstract protected function releaseAllLocks();
+}
diff --git a/includes/filebackend/lockmanager/ScopedLock.php b/includes/filebackend/lockmanager/ScopedLock.php
new file mode 100644
index 00000000..edcb1d65
--- /dev/null
+++ b/includes/filebackend/lockmanager/ScopedLock.php
@@ -0,0 +1,102 @@
+<?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
+ */
+
+/**
+ * Self-releasing locks
+ *
+ * LockManager helper class to handle scoped locks, which
+ * release when an object is destroyed or goes out of scope.
+ *
+ * @ingroup LockManager
+ * @since 1.19
+ */
+class ScopedLock {
+ /** @var LockManager */
+ protected $manager;
+ /** @var Status */
+ protected $status;
+ /** @var Array List of resource paths*/
+ protected $paths;
+
+ protected $type; // integer lock type
+
+ /**
+ * @param $manager LockManager
+ * @param array $paths List of storage paths
+ * @param $type integer LockManager::LOCK_* constant
+ * @param $status Status
+ */
+ protected function __construct(
+ LockManager $manager, array $paths, $type, Status $status
+ ) {
+ $this->manager = $manager;
+ $this->paths = $paths;
+ $this->status = $status;
+ $this->type = $type;
+ }
+
+ /**
+ * Get a ScopedLock object representing a lock on resource paths.
+ * Any locks are released once this object goes out of scope.
+ * The status object is updated with any errors or warnings.
+ *
+ * @param $manager LockManager
+ * @param array $paths List of storage paths
+ * @param $type integer LockManager::LOCK_* constant
+ * @param $status Status
+ * @return ScopedLock|null Returns null on failure
+ */
+ public static function factory(
+ LockManager $manager, array $paths, $type, Status $status
+ ) {
+ $lockStatus = $manager->lock( $paths, $type );
+ $status->merge( $lockStatus );
+ if ( $lockStatus->isOK() ) {
+ return new self( $manager, $paths, $type, $status );
+ }
+ return null;
+ }
+
+ /**
+ * Release a scoped lock and set any errors in the attatched Status object.
+ * This is useful for early release of locks before function scope is destroyed.
+ * This is the same as setting the lock object to null.
+ *
+ * @param ScopedLock $lock
+ * @return void
+ * @since 1.21
+ */
+ public static function release( ScopedLock &$lock = null ) {
+ $lock = null;
+ }
+
+ function __destruct() {
+ $wasOk = $this->status->isOK();
+ $this->status->merge( $this->manager->unlock( $this->paths, $this->type ) );
+ if ( $wasOk ) {
+ // Make sure status is OK, despite any unlockFiles() fatals
+ $this->status->setResult( true, $this->status->value );
+ }
+ }
+}