<?php
+/**
+ * Version of LockManager based on using DB table 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 based on using DB table locks.
* Caching is used to avoid hitting servers that are down.
*
* @ingroup LockManager
+ * @since 1.19
*/
class DBLockManager extends LockManager {
/** @var Array Map of DB names to server config */
protected $safeDelay; // integer number of seconds
protected $session = 0; // random integer
- /** @var Array Map of (locked key => lock type => count) */
- protected $locksHeld = array();
/** @var Array Map Database connections (DB name => Database) */
protected $conns = array();
/**
* Construct a new instance from configuration.
+ *
* $config paramaters include:
* 'dbServers' : Associative array of DB names to server configuration.
* Configuration is an associative array that includes:
* @param Array $config
*/
public function __construct( array $config ) {
- $this->dbServers = $config['dbServers'];
+ parent::__construct( $config );
+
+ $this->dbServers = isset( $config['dbServers'] )
+ ? $config['dbServers']
+ : array(); // likely just using 'localDBMaster'
// Sanitize dbsByBucket config to prevent PHP errors
$this->dbsByBucket = array_filter( $config['dbsByBucket'], 'is_array' );
$this->dbsByBucket = array_values( $this->dbsByBucket ); // consecutive
$this->session = wfBaseConvert( sha1( $this->session ), 16, 36, 31 );
}
- protected function doLock( array $keys, $type ) {
+ /**
+ * @see LockManager::doLock()
+ * @param $paths array
+ * @param $type int
+ * @return Status
+ */
+ protected function doLock( array $paths, $type ) {
$status = Status::newGood();
- $keysToLock = array();
+ $pathsToLock = array();
// Get locks that need to be acquired (buckets => locks)...
- foreach ( $keys as $key ) {
- if ( isset( $this->locksHeld[$key][$type] ) ) {
- ++$this->locksHeld[$key][$type];
- } elseif ( isset( $this->locksHeld[$key][self::LOCK_EX] ) ) {
- $this->locksHeld[$key][$type] = 1;
+ 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( $key );
- $keysToLock[$bucket][] = $key;
+ $bucket = $this->getBucketFromKey( $path );
+ $pathsToLock[$bucket][] = $path;
}
}
- $lockedKeys = array(); // files locked in this attempt
+ $lockedPaths = array(); // files locked in this attempt
// Attempt to acquire these locks...
- foreach ( $keysToLock as $bucket => $keys ) {
+ foreach ( $pathsToLock as $bucket => $paths ) {
// Try to acquire the locks for this bucket
- $res = $this->doLockingQueryAll( $bucket, $keys, $type );
+ $res = $this->doLockingQueryAll( $bucket, $paths, $type );
if ( $res === 'cantacquire' ) {
// Resources already locked by another process.
// Abort and unlock everything we just locked.
- $status->fatal( 'lockmanager-fail-acquirelocks', implode( ', ', $keys ) );
- $status->merge( $this->doUnlock( $lockedKeys, $type ) );
+ foreach ( $paths as $path ) {
+ $status->fatal( 'lockmanager-fail-acquirelock', $path );
+ }
+ $status->merge( $this->doUnlock( $lockedPaths, $type ) );
return $status;
} elseif ( $res !== true ) {
// Couldn't contact any DBs for this bucket.
// Abort and unlock everything we just locked.
$status->fatal( 'lockmanager-fail-db-bucket', $bucket );
- $status->merge( $this->doUnlock( $lockedKeys, $type ) );
+ $status->merge( $this->doUnlock( $lockedPaths, $type ) );
return $status;
}
// Record these locks as active
- foreach ( $keys as $key ) {
- $this->locksHeld[$key][$type] = 1; // locked
+ foreach ( $paths as $path ) {
+ $this->locksHeld[$path][$type] = 1; // locked
}
// Keep track of what locks were made in this attempt
- $lockedKeys = array_merge( $lockedKeys, $keys );
+ $lockedPaths = array_merge( $lockedPaths, $paths );
}
return $status;
}
- protected function doUnlock( array $keys, $type ) {
+ /**
+ * @see LockManager::doUnlock()
+ * @param $paths array
+ * @param $type int
+ * @return Status
+ */
+ protected function doUnlock( array $paths, $type ) {
$status = Status::newGood();
- foreach ( $keys as $key ) {
- if ( !isset( $this->locksHeld[$key] ) ) {
- $status->warning( 'lockmanager-notlocked', $key );
- } elseif ( !isset( $this->locksHeld[$key][$type] ) ) {
- $status->warning( 'lockmanager-notlocked', $key );
+ foreach ( $paths as $path ) {
+ if ( !isset( $this->locksHeld[$path] ) ) {
+ $status->warning( 'lockmanager-notlocked', $path );
+ } elseif ( !isset( $this->locksHeld[$path][$type] ) ) {
+ $status->warning( 'lockmanager-notlocked', $path );
} else {
- --$this->locksHeld[$key][$type];
- if ( $this->locksHeld[$key][$type] <= 0 ) {
- unset( $this->locksHeld[$key][$type] );
+ --$this->locksHeld[$path][$type];
+ if ( $this->locksHeld[$path][$type] <= 0 ) {
+ unset( $this->locksHeld[$path][$type] );
}
- if ( !count( $this->locksHeld[$key] ) ) {
- unset( $this->locksHeld[$key] ); // no SH or EX locks left for key
+ if ( !count( $this->locksHeld[$path] ) ) {
+ unset( $this->locksHeld[$path] ); // no SH or EX locks left for key
}
}
}
}
/**
- * Get a connection to a lock DB and acquire locks on $keys.
+ * 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.
*
* @param $lockDb string
- * @param $keys Array
+ * @param $paths Array
* @param $type integer LockManager::LOCK_EX or LockManager::LOCK_SH
* @return bool Resources able to be locked
* @throws DBError
*/
- protected function doLockingQuery( $lockDb, array $keys, $type ) {
+ protected function doLockingQuery( $lockDb, array $paths, $type ) {
if ( $type == self::LOCK_EX ) { // writer locks
$db = $this->getConnection( $lockDb );
if ( !$db ) {
return false; // bad config
}
+ $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 );
* This should avoid throwing any exceptions.
*
* @param $bucket integer
- * @param $keys Array List of resource keys to lock
+ * @param $paths Array List of resource keys to lock
* @param $type integer LockManager::LOCK_EX or LockManager::LOCK_SH
* @return bool|string One of (true, 'cantacquire', 'dberrors')
*/
- protected function doLockingQueryAll( $bucket, array $keys, $type ) {
+ protected function doLockingQueryAll( $bucket, array $paths, $type ) {
$yesVotes = 0; // locks made on trustable DBs
$votesLeft = count( $this->dbsByBucket[$bucket] ); // remaining DBs
$quorum = floor( $votesLeft/2 + 1 ); // simple majority
// Get votes for each DB, in order, until we have enough...
- foreach ( $this->dbsByBucket[$bucket] as $index => $lockDb ) {
+ foreach ( $this->dbsByBucket[$bucket] as $lockDb ) {
// Check that DB is not *known* to be down
if ( $this->cacheCheckFailures( $lockDb ) ) {
try {
// Attempt to acquire the lock on this DB
- if ( !$this->doLockingQuery( $lockDb, $keys, $type ) ) {
+ if ( !$this->doLockingQuery( $lockDb, $paths, $type ) ) {
return 'cantacquire'; // vetoed; resource locked
}
++$yesVotes; // success for this peer
}
}
}
- $votesLeft--;
+ --$votesLeft;
$votesNeeded = $quorum - $yesVotes;
if ( $votesNeeded > $votesLeft ) {
// In "trust cache" mode we don't have to meet the quorum
* Get (or reuse) a connection to a lock DB
*
* @param $lockDb string
- * @return Database
+ * @return DatabaseBase
* @throws DBError
*/
protected function getConnection( $lockDb ) {
$this->initConnection( $lockDb, $this->conns[$lockDb] );
}
if ( !$this->conns[$lockDb]->trxLevel() ) {
- $this->conns[$lockDb]->begin(); // start transaction
+ $this->conns[$lockDb]->begin( __METHOD__ ); // start transaction
}
return $this->conns[$lockDb];
}
foreach ( $this->conns as $lockDb => $db ) {
if ( $db->trxLevel() ) { // in transaction
try {
- $db->rollback(); // finish transaction and kill any rows
+ $db->rollback( __METHOD__ ); // finish transaction and kill any rows
} catch ( DBError $e ) {
$status->fatal( 'lockmanager-fail-db-release', $lockDb );
}
*/
protected function cacheCheckFailures( $lockDb ) {
if ( $this->statusCache && $this->safeDelay > 0 ) {
- $key = $this->getMissKey( $lockDb );
- $misses = $this->statusCache->get( $key );
+ $path = $this->getMissKey( $lockDb );
+ $misses = $this->statusCache->get( $path );
return !$misses;
}
return true;
*/
protected function cacheRecordFailure( $lockDb ) {
if ( $this->statusCache && $this->safeDelay > 0 ) {
- $key = $this->getMissKey( $lockDb );
- $misses = $this->statusCache->get( $key );
+ $path = $this->getMissKey( $lockDb );
+ $misses = $this->statusCache->get( $path );
if ( $misses ) {
- return $this->statusCache->incr( $key );
+ return $this->statusCache->incr( $path );
} else {
- return $this->statusCache->add( $key, 1, $this->safeDelay );
+ return $this->statusCache->add( $path, 1, $this->safeDelay );
}
}
return true;
}
/**
- * Get the bucket for lock key.
+ * Get the bucket for resource path.
* This should avoid throwing any exceptions.
*
- * @param $key string (31 char hex key)
+ * @param $path string
* @return integer
*/
- protected function getBucketFromKey( $key ) {
- $prefix = substr( $key, 0, 2 ); // first 2 hex chars (8 bits)
+ protected function getBucketFromKey( $path ) {
+ $prefix = substr( sha1( $path ), 0, 2 ); // first 2 hex chars (8 bits)
return intval( base_convert( $prefix, 16, 10 ) ) % count( $this->dbsByBucket );
}
foreach ( $this->conns as $lockDb => $db ) {
if ( $db->trxLevel() ) { // in transaction
try {
- $db->rollback(); // finish transaction and kill any rows
+ $db->rollback( __METHOD__ ); // finish transaction and kill any rows
} catch ( DBError $e ) {
// oh well
}
self::LOCK_EX => self::LOCK_EX
);
+ /**
+ * @param $lockDb string
+ * @param $db DatabaseBase
+ */
protected function initConnection( $lockDb, DatabaseBase $db ) {
# Let this transaction see lock rows from other transactions
$db->query( "SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;" );
}
- protected function doLockingQuery( $lockDb, array $keys, $type ) {
+ /**
+ * @param $lockDb string
+ * @param $paths array
+ * @param $type int
+ * @return bool
+ */
+ protected function doLockingQuery( $lockDb, array $paths, $type ) {
$db = $this->getConnection( $lockDb );
if ( !$db ) {
return false;
}
+ $keys = array_unique( array_map( 'LockManager::sha1Base36', $paths ) );
+ # Build up values for INSERT clause
$data = array();
foreach ( $keys as $key ) {
$data[] = array( 'fls_key' => $key, 'fls_session' => $this->session );
__METHOD__
);
if ( !$blocked ) {
+ # Build up values for INSERT clause
$data = array();
foreach ( $keys as $key ) {
$data[] = array( 'fle_key' => $key );