From: Aaron Schulz Date: Sat, 20 Aug 2016 17:26:01 +0000 (-0700) Subject: Split DBLockManager classes into their own files X-Git-Tag: 1.31.0-rc.0~5973^2 X-Git-Url: http://git.cyclocoop.org/%40spipnet%40?a=commitdiff_plain;h=7f3faf091fa87e3c2678cc8fa551308a0cb58bfa;p=lhc%2Fweb%2Fwiklou.git Split DBLockManager classes into their own files Change-Id: If903a90a5be2d6ff11504d34eb125e86c1ab1191 --- diff --git a/autoload.php b/autoload.php index e37a011093..c718dec9e6 100644 --- a/autoload.php +++ b/autoload.php @@ -950,7 +950,7 @@ $wgAutoloadLocalClasses = [ 'MwSql' => __DIR__ . '/maintenance/sql.php', 'MySQLField' => __DIR__ . '/includes/db/DatabaseMysqlBase.php', 'MySQLMasterPos' => __DIR__ . '/includes/db/DatabaseMysqlBase.php', - 'MySqlLockManager' => __DIR__ . '/includes/filebackend/lockmanager/DBLockManager.php', + 'MySqlLockManager' => __DIR__ . '/includes/filebackend/lockmanager/MySqlLockManager.php', 'MysqlInstaller' => __DIR__ . '/includes/installer/MysqlInstaller.php', 'MysqlUpdater' => __DIR__ . '/includes/installer/MysqlUpdater.php', 'NaiveForeignTitleFactory' => __DIR__ . '/includes/title/NaiveForeignTitleFactory.php', @@ -1058,7 +1058,7 @@ $wgAutoloadLocalClasses = [ 'PopulateRecentChangesSource' => __DIR__ . '/maintenance/populateRecentChangesSource.php', 'PopulateRevisionLength' => __DIR__ . '/maintenance/populateRevisionLength.php', 'PopulateRevisionSha1' => __DIR__ . '/maintenance/populateRevisionSha1.php', - 'PostgreSqlLockManager' => __DIR__ . '/includes/filebackend/lockmanager/DBLockManager.php', + 'PostgreSqlLockManager' => __DIR__ . '/includes/filebackend/lockmanager/PostgreSqlLockManager.php', 'PostgresBlob' => __DIR__ . '/includes/db/DatabasePostgres.php', 'PostgresField' => __DIR__ . '/includes/db/DatabasePostgres.php', 'PostgresInstaller' => __DIR__ . '/includes/installer/PostgresInstaller.php', diff --git a/includes/filebackend/lockmanager/DBLockManager.php b/includes/filebackend/lockmanager/DBLockManager.php index c9aad43a48..0aefbfa623 100644 --- a/includes/filebackend/lockmanager/DBLockManager.php +++ b/includes/filebackend/lockmanager/DBLockManager.php @@ -239,205 +239,3 @@ abstract class DBLockManager extends QuorumLockManager { } } } - -/** - * MySQL version of DBLockManager that supports shared locks. - * - * All lock servers must have the innodb table defined in locking/filelocks.sql. - * All locks are non-blocking, which avoids deadlocks. - * - * @ingroup LockManager - */ -class MySqlLockManager extends DBLockManager { - /** @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 - ]; - - protected function getLocalLB() { - // Use a separate connection so releaseAllLocks() doesn't rollback the main trx - return wfGetLBFactory()->newMainLB( $this->domain ); - } - - protected function initConnection( $lockDb, IDatabase $db ) { - # Let this transaction see lock rows from other transactions - $db->query( "SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;" ); - } - - /** - * 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 DBLockManager::getLocksOnServer() - * @param string $lockSrv - * @param array $paths - * @param string $type - * @return Status - */ - protected function doGetLocksOnServer( $lockSrv, array $paths, $type ) { - $status = Status::newGood(); - - $db = $this->getConnection( $lockSrv ); // checked in isServerUp() - - $keys = []; // list of hash keys for the paths - $data = []; // list of rows to insert - $checkEXKeys = []; // list of hash keys that this has no EX lock on - # Build up values for INSERT clause - foreach ( $paths as $path ) { - $key = $this->sha1Base36Absolute( $path ); - $keys[] = $key; - $data[] = [ 'fls_key' => $key, 'fls_session' => $this->session ]; - if ( !isset( $this->locksHeld[$path][self::LOCK_EX] ) ) { - $checkEXKeys[] = $key; - } - } - - # Block new writers (both EX and SH locks leave entries here)... - $db->insert( 'filelocks_shared', $data, __METHOD__, [ 'IGNORE' ] ); - # Actually do the locking queries... - if ( $type == self::LOCK_SH ) { // reader locks - $blocked = false; - # Bail if there are any existing writers... - if ( count( $checkEXKeys ) ) { - $blocked = $db->selectField( 'filelocks_exclusive', '1', - [ '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... - # 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', - [ 'fls_key' => $keys, "fls_session != $encSession" ], - __METHOD__ - ); - if ( !$blocked ) { - # Build up values for INSERT clause - $data = []; - foreach ( $keys as $key ) { - $data[] = [ 'fle_key' => $key ]; - } - # Block new readers/writers... - $db->insert( 'filelocks_exclusive', $data, __METHOD__ ); - # Bail if there are any existing readers... - $blocked = $db->selectField( 'filelocks_shared', '1', - [ 'fls_key' => $keys, "fls_session != $encSession" ], - __METHOD__ - ); - } - } - - if ( $blocked ) { - foreach ( $paths as $path ) { - $status->fatal( 'lockmanager-fail-acquirelock', $path ); - } - } - - return $status; - } - - /** - * @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 = [ - self::LOCK_SH => self::LOCK_SH, - self::LOCK_UW => self::LOCK_SH, - self::LOCK_EX => self::LOCK_EX - ]; - - protected function doGetLocksOnServer( $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 Wikimedia\base_convert( substr( $key, 0, 15 ), 16, 10 ); - }, - array_map( [ $this, 'sha1Base16Absolute' ], $paths ) - ) ); - - // Try to acquire all the locks... - $fields = []; - 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 = $res->fetchRow(); - - if ( in_array( 'f', $row ) ) { - // Release any acquired locks if some could not be acquired... - $fields = []; - 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::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/MySqlLockManager.php b/includes/filebackend/lockmanager/MySqlLockManager.php new file mode 100644 index 0000000000..c51df16b89 --- /dev/null +++ b/includes/filebackend/lockmanager/MySqlLockManager.php @@ -0,0 +1,123 @@ + self::LOCK_SH, + self::LOCK_UW => self::LOCK_SH, + self::LOCK_EX => self::LOCK_EX + ]; + + protected function getLocalLB() { + // Use a separate connection so releaseAllLocks() doesn't rollback the main trx + return wfGetLBFactory()->newMainLB( $this->domain ); + } + + protected function initConnection( $lockDb, IDatabase $db ) { + # Let this transaction see lock rows from other transactions + $db->query( "SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;" ); + } + + /** + * 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 DBLockManager::getLocksOnServer() + * @param string $lockSrv + * @param array $paths + * @param string $type + * @return Status + */ + protected function doGetLocksOnServer( $lockSrv, array $paths, $type ) { + $status = Status::newGood(); + + $db = $this->getConnection( $lockSrv ); // checked in isServerUp() + + $keys = []; // list of hash keys for the paths + $data = []; // list of rows to insert + $checkEXKeys = []; // list of hash keys that this has no EX lock on + # Build up values for INSERT clause + foreach ( $paths as $path ) { + $key = $this->sha1Base36Absolute( $path ); + $keys[] = $key; + $data[] = [ 'fls_key' => $key, 'fls_session' => $this->session ]; + if ( !isset( $this->locksHeld[$path][self::LOCK_EX] ) ) { + $checkEXKeys[] = $key; + } + } + + # Block new writers (both EX and SH locks leave entries here)... + $db->insert( 'filelocks_shared', $data, __METHOD__, [ 'IGNORE' ] ); + # Actually do the locking queries... + if ( $type == self::LOCK_SH ) { // reader locks + $blocked = false; + # Bail if there are any existing writers... + if ( count( $checkEXKeys ) ) { + $blocked = $db->selectField( 'filelocks_exclusive', '1', + [ '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... + # 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', + [ 'fls_key' => $keys, "fls_session != $encSession" ], + __METHOD__ + ); + if ( !$blocked ) { + # Build up values for INSERT clause + $data = []; + foreach ( $keys as $key ) { + $data[] = [ 'fle_key' => $key ]; + } + # Block new readers/writers... + $db->insert( 'filelocks_exclusive', $data, __METHOD__ ); + # Bail if there are any existing readers... + $blocked = $db->selectField( 'filelocks_shared', '1', + [ 'fls_key' => $keys, "fls_session != $encSession" ], + __METHOD__ + ); + } + } + + if ( $blocked ) { + foreach ( $paths as $path ) { + $status->fatal( 'lockmanager-fail-acquirelock', $path ); + } + } + + return $status; + } + + /** + * @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; + } +} diff --git a/includes/filebackend/lockmanager/PostgreSqlLockManager.php b/includes/filebackend/lockmanager/PostgreSqlLockManager.php new file mode 100644 index 0000000000..d55b5ae060 --- /dev/null +++ b/includes/filebackend/lockmanager/PostgreSqlLockManager.php @@ -0,0 +1,79 @@ + self::LOCK_SH, + self::LOCK_UW => self::LOCK_SH, + self::LOCK_EX => self::LOCK_EX + ]; + + protected function doGetLocksOnServer( $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 Wikimedia\base_convert( substr( $key, 0, 15 ), 16, 10 ); + }, + array_map( [ $this, 'sha1Base16Absolute' ], $paths ) + ) ); + + // Try to acquire all the locks... + $fields = []; + 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 = $res->fetchRow(); + + if ( in_array( 'f', $row ) ) { + // Release any acquired locks if some could not be acquired... + $fields = []; + 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::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; + } +}