2011-12-20 03:52:06 +00:00
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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 'lockDir' 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
|
|
|
|
|
*/
|
|
|
|
|
class FSLockManager extends LockManager {
|
|
|
|
|
/** @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 $lockDir; // global dir for all servers
|
|
|
|
|
|
|
|
|
|
/** @var Array Map of (locked key => lock type => lock file handle) */
|
|
|
|
|
protected $handles = array();
|
|
|
|
|
|
|
|
|
|
function __construct( array $config ) {
|
|
|
|
|
$this->lockDir = $config['lockDirectory'];
|
|
|
|
|
}
|
|
|
|
|
|
2011-12-24 00:16:06 +00:00
|
|
|
protected function doLock( array $paths, $type ) {
|
2011-12-20 03:52:06 +00:00
|
|
|
$status = Status::newGood();
|
|
|
|
|
|
2011-12-24 00:16:06 +00:00
|
|
|
$lockedPaths = array(); // files locked in this attempt
|
|
|
|
|
foreach ( $paths as $path ) {
|
|
|
|
|
$subStatus = $this->doSingleLock( $path, $type );
|
2011-12-20 03:52:06 +00:00
|
|
|
$status->merge( $subStatus );
|
|
|
|
|
if ( $status->isOK() ) {
|
2011-12-24 00:16:06 +00:00
|
|
|
// Don't append to $lockedPaths if $path is already locked.
|
2011-12-20 03:52:06 +00:00
|
|
|
// We do NOT want to unlock the key if we have to rollback.
|
|
|
|
|
if ( $subStatus->isGood() ) { // no warnings/fatals?
|
2011-12-24 00:16:06 +00:00
|
|
|
$lockedPaths[] = $path;
|
2011-12-20 03:52:06 +00:00
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// Abort and unlock everything
|
2011-12-24 00:16:06 +00:00
|
|
|
$status->merge( $this->doUnlock( $lockedPaths, $type ) );
|
2011-12-20 03:52:06 +00:00
|
|
|
return $status;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $status;
|
|
|
|
|
}
|
|
|
|
|
|
2011-12-24 00:16:06 +00:00
|
|
|
protected function doUnlock( array $paths, $type ) {
|
2011-12-20 03:52:06 +00:00
|
|
|
$status = Status::newGood();
|
|
|
|
|
|
2011-12-24 00:16:06 +00:00
|
|
|
foreach ( $paths as $path ) {
|
|
|
|
|
$status->merge( $this->doSingleUnlock( $path, $type ) );
|
2011-12-20 03:52:06 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $status;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Lock a single resource key
|
|
|
|
|
*
|
2011-12-24 00:16:06 +00:00
|
|
|
* @param $path string
|
2011-12-20 03:52:06 +00:00
|
|
|
* @param $type integer
|
|
|
|
|
* @return Status
|
|
|
|
|
*/
|
2011-12-24 00:16:06 +00:00
|
|
|
protected function doSingleLock( $path, $type ) {
|
2011-12-20 03:52:06 +00:00
|
|
|
$status = Status::newGood();
|
|
|
|
|
|
2011-12-24 00:16:06 +00:00
|
|
|
if ( isset( $this->locksHeld[$path][$type] ) ) {
|
|
|
|
|
++$this->locksHeld[$path][$type];
|
|
|
|
|
} elseif ( isset( $this->locksHeld[$path][self::LOCK_EX] ) ) {
|
|
|
|
|
$this->locksHeld[$path][$type] = 1;
|
2011-12-20 03:52:06 +00:00
|
|
|
} else {
|
|
|
|
|
wfSuppressWarnings();
|
2011-12-24 00:16:06 +00:00
|
|
|
$handle = fopen( $this->getLockPath( $path ), 'a+' );
|
2011-12-20 03:52:06 +00:00
|
|
|
wfRestoreWarnings();
|
|
|
|
|
if ( !$handle ) { // lock dir missing?
|
|
|
|
|
wfMkdirParents( $this->lockDir );
|
2011-12-24 00:16:06 +00:00
|
|
|
$handle = fopen( $this->getLockPath( $path ), 'a+' ); // try again
|
2011-12-20 03:52:06 +00:00
|
|
|
}
|
|
|
|
|
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
|
2011-12-24 00:16:06 +00:00
|
|
|
$this->locksHeld[$path][$type] = 1;
|
|
|
|
|
$this->handles[$path][$type] = $handle;
|
2011-12-20 03:52:06 +00:00
|
|
|
} else {
|
|
|
|
|
fclose( $handle );
|
2011-12-24 00:16:06 +00:00
|
|
|
$status->fatal( 'lockmanager-fail-acquirelock', $path );
|
2011-12-20 03:52:06 +00:00
|
|
|
}
|
|
|
|
|
} else {
|
2011-12-24 00:16:06 +00:00
|
|
|
$status->fatal( 'lockmanager-fail-openlock', $path );
|
2011-12-20 03:52:06 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $status;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Unlock a single resource key
|
|
|
|
|
*
|
2011-12-24 00:16:06 +00:00
|
|
|
* @param $path string
|
2011-12-20 03:52:06 +00:00
|
|
|
* @param $type integer
|
|
|
|
|
* @return Status
|
|
|
|
|
*/
|
2011-12-24 00:16:06 +00:00
|
|
|
protected function doSingleUnlock( $path, $type ) {
|
2011-12-20 03:52:06 +00:00
|
|
|
$status = Status::newGood();
|
|
|
|
|
|
2011-12-24 00:16:06 +00:00
|
|
|
if ( !isset( $this->locksHeld[$path] ) ) {
|
|
|
|
|
$status->warning( 'lockmanager-notlocked', $path );
|
|
|
|
|
} elseif ( !isset( $this->locksHeld[$path][$type] ) ) {
|
|
|
|
|
$status->warning( 'lockmanager-notlocked', $path );
|
2011-12-20 03:52:06 +00:00
|
|
|
} else {
|
|
|
|
|
$handlesToClose = array();
|
2011-12-24 00:16:06 +00:00
|
|
|
--$this->locksHeld[$path][$type];
|
|
|
|
|
if ( $this->locksHeld[$path][$type] <= 0 ) {
|
|
|
|
|
unset( $this->locksHeld[$path][$type] );
|
2011-12-20 03:52:06 +00:00
|
|
|
// If a LOCK_SH comes in while we have a LOCK_EX, we don't
|
|
|
|
|
// actually add a handler, so check for handler existence.
|
2011-12-24 00:16:06 +00:00
|
|
|
if ( isset( $this->handles[$path][$type] ) ) {
|
2011-12-20 03:52:06 +00:00
|
|
|
// Mark this handle to be unlocked and closed
|
2011-12-24 00:16:06 +00:00
|
|
|
$handlesToClose[] = $this->handles[$path][$type];
|
|
|
|
|
unset( $this->handles[$path][$type] );
|
2011-12-20 03:52:06 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// Unlock handles to release locks and delete
|
|
|
|
|
// any lock files that end up with no locks on them...
|
|
|
|
|
if ( wfIsWindows() ) {
|
|
|
|
|
// Windows: for any process, including this one,
|
|
|
|
|
// calling unlink() on a locked file will fail
|
2011-12-24 00:16:06 +00:00
|
|
|
$status->merge( $this->closeLockHandles( $path, $handlesToClose ) );
|
|
|
|
|
$status->merge( $this->pruneKeyLockFiles( $path ) );
|
2011-12-20 03:52:06 +00:00
|
|
|
} else {
|
|
|
|
|
// Unix: unlink() can be used on files currently open by this
|
|
|
|
|
// process and we must do so in order to avoid race conditions
|
2011-12-24 00:16:06 +00:00
|
|
|
$status->merge( $this->pruneKeyLockFiles( $path ) );
|
|
|
|
|
$status->merge( $this->closeLockHandles( $path, $handlesToClose ) );
|
2011-12-20 03:52:06 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $status;
|
|
|
|
|
}
|
|
|
|
|
|
2011-12-24 00:16:06 +00:00
|
|
|
private function closeLockHandles( $path, array $handlesToClose ) {
|
2011-12-20 03:52:06 +00:00
|
|
|
$status = Status::newGood();
|
|
|
|
|
foreach ( $handlesToClose as $handle ) {
|
|
|
|
|
wfSuppressWarnings();
|
|
|
|
|
if ( !flock( $handle, LOCK_UN ) ) {
|
2011-12-24 00:16:06 +00:00
|
|
|
$status->fatal( 'lockmanager-fail-releaselock', $path );
|
2011-12-20 03:52:06 +00:00
|
|
|
}
|
|
|
|
|
if ( !fclose( $handle ) ) {
|
2011-12-24 00:16:06 +00:00
|
|
|
$status->warning( 'lockmanager-fail-closelock', $path );
|
2011-12-20 03:52:06 +00:00
|
|
|
}
|
|
|
|
|
wfRestoreWarnings();
|
|
|
|
|
}
|
|
|
|
|
return $status;
|
|
|
|
|
}
|
|
|
|
|
|
2011-12-24 00:16:06 +00:00
|
|
|
private function pruneKeyLockFiles( $path ) {
|
2011-12-20 03:52:06 +00:00
|
|
|
$status = Status::newGood();
|
2011-12-24 00:16:06 +00:00
|
|
|
if ( !count( $this->locksHeld[$path] ) ) {
|
2011-12-20 03:52:06 +00:00
|
|
|
wfSuppressWarnings();
|
|
|
|
|
# No locks are held for the lock file anymore
|
2011-12-24 00:16:06 +00:00
|
|
|
if ( !unlink( $this->getLockPath( $path ) ) ) {
|
|
|
|
|
$status->warning( 'lockmanager-fail-deletelock', $path );
|
2011-12-20 03:52:06 +00:00
|
|
|
}
|
|
|
|
|
wfRestoreWarnings();
|
2011-12-24 00:16:06 +00:00
|
|
|
unset( $this->locksHeld[$path] );
|
|
|
|
|
unset( $this->handles[$path] );
|
2011-12-20 03:52:06 +00:00
|
|
|
}
|
|
|
|
|
return $status;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get the path to the lock file for a key
|
2011-12-24 00:16:06 +00:00
|
|
|
* @param $path string
|
2011-12-20 03:52:06 +00:00
|
|
|
* @return string
|
|
|
|
|
*/
|
2011-12-24 00:16:06 +00:00
|
|
|
protected function getLockPath( $path ) {
|
|
|
|
|
$hash = self::sha1Base36( $path );
|
|
|
|
|
return "{$this->lockDir}/{$hash}.lock";
|
2011-12-20 03:52:06 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function __destruct() {
|
|
|
|
|
// Make sure remaining locks get cleared for sanity
|
2011-12-24 00:16:06 +00:00
|
|
|
foreach ( $this->locksHeld as $path => $locks ) {
|
|
|
|
|
$this->doSingleUnlock( $path, self::LOCK_EX );
|
|
|
|
|
$this->doSingleUnlock( $path, self::LOCK_SH );
|
2011-12-20 03:52:06 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|