summaryrefslogtreecommitdiff
path: root/www/wiki/includes/filebackend/lockmanager/MySqlLockManager.php
diff options
context:
space:
mode:
Diffstat (limited to 'www/wiki/includes/filebackend/lockmanager/MySqlLockManager.php')
-rw-r--r--www/wiki/includes/filebackend/lockmanager/MySqlLockManager.php141
1 files changed, 141 insertions, 0 deletions
diff --git a/www/wiki/includes/filebackend/lockmanager/MySqlLockManager.php b/www/wiki/includes/filebackend/lockmanager/MySqlLockManager.php
new file mode 100644
index 00000000..2108aed4
--- /dev/null
+++ b/www/wiki/includes/filebackend/lockmanager/MySqlLockManager.php
@@ -0,0 +1,141 @@
+<?php
+
+use Wikimedia\Rdbms\IDatabase;
+use Wikimedia\Rdbms\DBError;
+
+/**
+ * MySQL version of DBLockManager that supports shared locks.
+ *
+ * Do NOT use this on connection handles that are also being used for anything
+ * else as the transaction isolation will be wrong and all the other changes will
+ * get rolled back when the locks release!
+ *
+ * All lock servers must have the innodb table defined in maintenance/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
+ ];
+
+ public function __construct( array $config ) {
+ parent::__construct( $config );
+
+ $this->session = substr( $this->session, 0, 31 ); // fit to field
+ }
+
+ protected function initConnection( $lockDb, IDatabase $db ) {
+ # Let this transaction see lock rows from other transactions
+ $db->query( "SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;" );
+ # Do everything in a transaction as it all gets rolled back eventually
+ $db->startAtomic( __CLASS__ );
+ }
+
+ /**
+ * Get a connection to a lock DB and acquire locks on $paths.
+ * This does not use GET_LOCK() per https://bugs.mysql.com/bug.php?id=1118.
+ *
+ * @see DBLockManager::getLocksOnServer()
+ * @param string $lockSrv
+ * @param array $paths
+ * @param string $type
+ * @return StatusValue
+ */
+ protected function doGetLocksOnServer( $lockSrv, array $paths, $type ) {
+ $status = StatusValue::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; // this has no EX lock on $key itself
+ }
+ }
+
+ # 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
+ # Bail if there are any existing writers...
+ if ( count( $checkEXKeys ) ) {
+ $blocked = $db->selectField(
+ 'filelocks_exclusive',
+ '1',
+ [ 'fle_key' => $checkEXKeys ],
+ __METHOD__
+ );
+ } else {
+ $blocked = false;
+ }
+ # 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 StatusValue
+ */
+ protected function releaseAllLocks() {
+ $status = StatusValue::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;
+ }
+}