https://www.mediawiki.org/wiki/Special:Code/MediaWiki/105819
Revision: 105819 Author: aaron Date: 2011-12-11 19:23:40 +0000 (Sun, 11 Dec 2011) Log Message: ----------- In LockManager: * Added LockManager::LOCK_UW for readers that are using the data to write elsewhere. In DBLockManager: * Removed broken shared lock query. * Split out MySQL lock manager subclass with shared locks (that don't lock the whole table). * Only call cacheRecordFailure() if we have a DB connection error. Modified Paths: -------------- branches/FileBackend/phase3/includes/AutoLoader.php branches/FileBackend/phase3/includes/filerepo/backend/LockManager.php Modified: branches/FileBackend/phase3/includes/AutoLoader.php =================================================================== --- branches/FileBackend/phase3/includes/AutoLoader.php 2011-12-11 19:15:04 UTC (rev 105818) +++ branches/FileBackend/phase3/includes/AutoLoader.php 2011-12-11 19:23:40 UTC (rev 105819) @@ -499,6 +499,7 @@ 'LockManager' => 'includes/filerepo/backend/LockManager.php', 'FSLockManager' => 'includes/filerepo/backend/LockManager.php', 'DBLockManager' => 'includes/filerepo/backend/LockManager.php', + 'MySqlLockManager'=> 'includes/filerepo/backend/LockManager.php', 'NullLockManager' => 'includes/filerepo/backend/LockManager.php', 'FileOp' => 'includes/filerepo/backend/FileOp.php', 'StoreFileOp' => 'includes/filerepo/backend/FileOp.php', Modified: branches/FileBackend/phase3/includes/filerepo/backend/LockManager.php =================================================================== --- branches/FileBackend/phase3/includes/filerepo/backend/LockManager.php 2011-12-11 19:15:04 UTC (rev 105818) +++ branches/FileBackend/phase3/includes/filerepo/backend/LockManager.php 2011-12-11 19:23:40 UTC (rev 105819) @@ -3,18 +3,28 @@ * FileBackend helper class for handling file locking. * Locks on resource keys can either be shared or exclusive. * - * Implementations can keep track of what is locked in the process cache. - * This can reduce hits to external resources for lock()/unlock() calls. + * 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 FileBackend */ abstract class LockManager { - /* Lock types; stronger locks have high values */ + /* Lock types; stronger locks have higher values */ const LOCK_SH = 1; // shared lock (for reads) - const LOCK_EX = 2; // exclusive lock (for writes) + const LOCK_UW = 2; // shared lock (for reads used to write elsewhere) + const LOCK_EX = 3; // exclusive lock (for writes) + /** @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_EX, // subclasses may use self::LOCK_SH + self::LOCK_EX => self::LOCK_EX + ); + /** * Construct a new instance from configuration * @@ -26,31 +36,31 @@ * Lock the resources at the given abstract paths * * @param $paths Array List of resource names - * @param $type integer LockManager::LOCK_EX, LockManager::LOCK_SH + * @param $type integer LockManager::LOCK_* constant * @return Status */ final public function lock( array $paths, $type = self::LOCK_EX ) { $keys = array_unique( array_map( 'sha1', $paths ) ); - return $this->doLock( $keys, $type ); + return $this->doLock( $keys, $this->lockTypeMap[$type] ); } /** * Unlock the resources at the given abstract paths * * @param $paths Array List of storage paths - * @param $type integer LockManager::LOCK_EX, LockManager::LOCK_SH + * @param $type integer LockManager::LOCK_* constant * @return Status */ final public function unlock( array $paths, $type = self::LOCK_EX ) { $keys = array_unique( array_map( 'sha1', $paths ) ); - return $this->doUnlock( $keys, $type ); + return $this->doUnlock( $keys, $this->lockTypeMap[$type] ); } /** * Lock resources with the given keys and lock type * * @param $key Array List of keys to lock (40 char hex hashes) - * @param $type integer LockManager::LOCK_EX, LockManager::LOCK_SH + * @param $type integer LockManager::LOCK_* constant * @return string */ abstract protected function doLock( array $keys, $type ); @@ -59,7 +69,7 @@ * Unlock resources with the given keys and lock type * * @param $key Array List of keys to unlock (40 char hex hashes) - * @param $type integer LockManager::LOCK_EX, LockManager::LOCK_SH + * @param $type integer LockManager::LOCK_* constant * @return string */ abstract protected function doUnlock( array $keys, $type ); @@ -74,6 +84,13 @@ * locks will be ignored; see http://nfs.sourceforge.net/#section_d. */ 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 => count) */ @@ -259,7 +276,7 @@ * * All lock requests for a resource, identified by a hash string, will * map to one bucket. Each bucket maps to one or several peer DB servers, - * each having a `file_locks` table with row-level locking. + * each having a the file_locks.sql tables with row-level locking. * This does not use GET_LOCK() per http://bugs.mysql.com/bug.php?id=1118. * * A majority of peer servers must agree for a lock to be acquired. @@ -284,7 +301,7 @@ /** @var Array Map of (locked key => lock type => count) */ protected $locksHeld = array(); - /** $var Array Map Lock-active database connections (server name => Database) */ + /** @var Array Map Lock-active database connections (DB name => Database) */ protected $activeConns = array(); /** @@ -363,7 +380,7 @@ $status->merge( $this->doUnlock( $lockedKeys, $type ) ); return $status; } elseif ( $res !== true ) { - // Couldn't contact any servers for this bucket. + // 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 ) ); @@ -408,47 +425,26 @@ } /** - * Get a DB connection to a lock server and acquire locks on $keys. + * Get a connection to a lock DB and acquire locks on $keys * * @param $server string * @param $keys Array * @param $type integer LockManager::LOCK_EX or LockManager::LOCK_SH - * @return void + * @return bool Resources able to be locked + * @throws DBError */ protected function doLockingQuery( $server, array $keys, $type ) { - if ( !isset( $this->activeConns[$server] ) ) { - $this->activeConns[$server] = wfGetDB( DB_MASTER, array(), $server ); - $this->activeConns[$server]->begin(); // start transaction - # If the connection drops, try to avoid letting the DB rollback - # and release the locks before the file operations are finished. - # This won't handle the case of server reboots however. - $options = array(); - if ( php_sapi_name() == 'cli' ) { // maintenance scripts - if ( $this->cliTimeout > 0 ) { - $options['connTimeout'] = $this->cliTimeout; - } - } else { // web requests - if ( $this->webTimeout > 0 ) { - $options['connTimeout'] = $this->webTimeout; - } - } - $this->activeConns[$server]->setSessionOptions( $options ); - } - $db = $this->activeConns[$server]; - # Try to get the locks...this should be the last query of this function - if ( $type == self::LOCK_SH ) { // reader locks - $db->select( 'file_locks', '1', - array( 'fl_key' => $keys ), - __METHOD__, - array( 'LOCK IN SHARE MODE' ) // single-row gap locks - ); - } else { // writer locks + if ( $type == self::LOCK_EX ) { // writer locks + $db = $this->getConnection( $server ); + # Actually do the locking queries... $data = array(); foreach ( $keys as $key ) { - $data[] = array( 'fl_key' => $key ); + $data[] = array( 'fle_key' => $key ); } - $db->insert( 'file_locks', $data, __METHOD__ ); + # Wait on any existing writers and block new ones if we get in + $db->insert( 'file_locks_exclusive', $data, __METHOD__ ); } + return true; } /** @@ -462,7 +458,7 @@ */ protected function doLockingQueryAll( $bucket, array $keys, $type ) { $yesVotes = 0; // locks made on trustable servers - $votesLeft = count( $this->dbsByBucket[$bucket] ); // remaining servers + $votesLeft = count( $this->dbsByBucket[$bucket] ); // remaining DBs $quorum = floor( $votesLeft/2 + 1 ); // simple majority // Get votes for each server, in order, until we have enough... foreach ( $this->dbsByBucket[$bucket] as $index => $server ) { @@ -478,7 +474,9 @@ if ( $this->cacheCheckFailures( $server ) ) { try { // Attempt to acquire the lock on this server - $this->doLockingQuery( $server, $keys, $type ); + if ( !$this->doLockingQuery( $server, $keys, $type ) ) { + return 'cantacquire'; // vetoed; resource locked + } // Check that server has no signs of lock loss if ( $this->checkUptime( $server ) ) { ++$yesVotes; // success for this peer @@ -490,11 +488,11 @@ return true; // lock obtained } } + } catch ( DBConnectionError $e ) { + $this->cacheRecordFailure( $server ); } catch ( DBError $e ) { if ( $this->lastErrorIndicatesLocked( $server ) ) { return 'cantacquire'; // vetoed; resource locked - } else { // can't connect? - $this->cacheRecordFailure( $server ); } } } @@ -513,6 +511,46 @@ } /** + * Get a new connection to a lock DB + * + * @param $server string + * @return Database + * @throws DBError + */ + protected function getConnection( $server ) { + if ( !isset( $this->activeConns[$server] ) ) { + $this->activeConns[$server] = wfGetDB( DB_MASTER, array(), $server ); + $this->activeConns[$server]->begin(); // start transaction + # If the connection drops, try to avoid letting the DB rollback + # and release the locks before the file operations are finished. + # This won't handle the case of server reboots however. + $options = array(); + if ( php_sapi_name() == 'cli' ) { // maintenance scripts + if ( $this->cliTimeout > 0 ) { + $options['connTimeout'] = $this->cliTimeout; + } + } else { // web requests + if ( $this->webTimeout > 0 ) { + $options['connTimeout'] = $this->webTimeout; + } + } + $this->activeConns[$server]->setSessionOptions( $options ); + $this->initConnection( $server, $this->activeConns[$server] ); + } + return $this->activeConns[$server]; + } + + /** + * Do additional initialization for new lock DB connection + * + * @param $server string + * @param $db Database + * @return void + * @throws DBError + */ + protected function initConnection( $server, DatabaseBase $db ) {} + + /** * Commit all changes to lock-active databases. * This should avoid throwing any exceptions. * @@ -554,6 +592,7 @@ * * @param $server string * @return bool + * @throws DBError */ protected function checkUptime( $server ) { if ( isset( $this->activeConns[$server] ) ) { // sanity @@ -679,12 +718,70 @@ return intval( base_convert( $prefix, 16, 10 ) ) % count( $this->dbsByBucket ); } + /** + * Make sure remaining locks get cleared for sanity + */ function __destruct() { - // Make sure remaining locks get cleared for sanity $this->finishLockTransactions(); } } +class MySqlLockManager 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 + ); + + /** @var Array Map of (DB name => original transaction isolation) */ + protected $trxIso = array(); + + protected function initConnection( $server, DatabaseBase $db ) { + # Get the original transaction level for the server. + $row = $db->query( "SELECT @@tx_isolation AS tx_iso;" )->fetchObject(); + # Convert "REPEATABLE-READ" => "REPEATABLE READ" for SET query + $this->trxIso[$server] = str_replace( '-', ' ', $row->tx_iso ); + } + + protected function doLockingQuery( $server, array $keys, $type ) { + $ok = true; + # Actually do the locking queries... + if ( $type == self::LOCK_SH ) { // reader locks + $db = $this->getConnection( $server ); + $data = array(); + foreach ( $keys as $key ) { + $data[] = array( 'fls_key' => $key ); + } + # Block new writers... + $db->insert( 'file_locks_shared', $data, __METHOD__ ); + # Wait on any existing writers... + $db->query( "SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED" ); + $ok = !$db->selectField( 'file_locks_exclusive', '1', + array( 'fle_key' => $keys ), + __METHOD__ + ); + $db->query( "SET SESSION TRANSACTION ISOLATION LEVEL {$this->trxIso[$server]}" ); + } elseif ( $type == self::LOCK_EX ) { // writer locks + $db = $this->getConnection( $server ); + $data = array(); + foreach ( $keys as $key ) { + $data[] = array( 'fle_key' => $key ); + } + # Block new readers/writers and wait on any existing writers + $db->insert( 'file_locks_exclusive', $data, __METHOD__ ); + # Wait on any existing readers... + $db->query( "SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED" ); + $ok = !$db->selectField( 'file_locks_shared', '1', + array( 'fls_key' => $keys ), + __METHOD__ + ); + $db->query( "SET SESSION TRANSACTION ISOLATION LEVEL {$this->trxIso[$server]}" ); + } + return $ok; + } +} + /** * Simple version of LockManager that does nothing */ _______________________________________________ MediaWiki-CVS mailing list MediaWiki-CVS@lists.wikimedia.org https://lists.wikimedia.org/mailman/listinfo/mediawiki-cvs