[LockManager] Created PostgreSqlLockManager class.
authorAaron Schulz <aschulz@wikimedia.org>
Sun, 3 Feb 2013 08:00:50 +0000 (00:00 -0800)
committerGerrit Code Review <gerrit@wikimedia.org>
Fri, 15 Feb 2013 04:06:10 +0000 (04:06 +0000)
* Made DBLockManager abstract instead of a hacky blocking implementation.
  With a PG and MySQL option, that option is no longer useful.

Change-Id: I939551bd2283608f2d017d9d2fca1334a533c005

includes/AutoLoader.php
includes/filebackend/lockmanager/DBLockManager.php
includes/filebackend/lockmanager/LockManager.php

index 06e3f22..23cf411 100644 (file)
@@ -583,6 +583,7 @@ $wgAutoloadLocalClasses = array(
        'MemcLockManager' => 'includes/filebackend/lockmanager/MemcLockManager.php',
        'QuorumLockManager' => 'includes/filebackend/lockmanager/QuorumLockManager.php',
        'MySqlLockManager'=> 'includes/filebackend/lockmanager/DBLockManager.php',
+       'PostgreSqlLockManager'=> 'includes/filebackend/lockmanager/DBLockManager.php',
        'NullLockManager' => 'includes/filebackend/lockmanager/LockManager.php',
        'FileOp' => 'includes/filebackend/FileOp.php',
        'FileOpBatch' => 'includes/filebackend/FileOpBatch.php',
index 7b365c1..90f4ccd 100644 (file)
  */
 
 /**
- * Version of LockManager based on using DB table row locks.
+ * Version of LockManager based on using named/row DB locks.
  *
  * This is meant for multi-wiki systems that may share files.
- * All locks are blocking, so it might be useful to set a small
- * lock-wait timeout via server config to curtail deadlocks.
  *
  * All lock requests for a resource, identified by a hash string, will map
  * to one bucket. Each bucket maps to one or several peer DBs, each on their
@@ -38,7 +36,7 @@
  * @ingroup LockManager
  * @since 1.19
  */
-class DBLockManager extends QuorumLockManager {
+abstract class DBLockManager extends QuorumLockManager {
        /** @var Array Map of DB names to server config */
        protected $dbServers; // (DB name => server config array)
        /** @var BagOStuff */
@@ -112,65 +110,6 @@ class DBLockManager extends QuorumLockManager {
                $this->session = wfRandomString( 31 );
        }
 
-       /**
-        * 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 QuorumLockManager::getLocksOnServer()
-        * @return Status
-        */
-       protected function getLocksOnServer( $lockSrv, array $paths, $type ) {
-               $status = Status::newGood();
-
-               if ( $type == self::LOCK_EX ) { // writer locks
-                       try {
-                               $keys = array_unique( array_map( array( $this, 'sha1Base36Absolute' ), $paths ) );
-                               # Build up values for INSERT clause
-                               $data = array();
-                               foreach ( $keys as $key ) {
-                                       $data[] = array( 'fle_key' => $key );
-                               }
-                               # Wait on any existing writers and block new ones if we get in
-                               $db = $this->getConnection( $lockSrv ); // checked in isServerUp()
-                               $db->insert( 'filelocks_exclusive', $data, __METHOD__ );
-                       } catch ( DBError $e ) {
-                               foreach ( $paths as $path ) {
-                                       $status->fatal( 'lockmanager-fail-acquirelock', $path );
-                               }
-                       }
-               }
-
-               return $status;
-       }
-
-       /**
-        * @see QuorumLockManager::freeLocksOnServer()
-        * @return Status
-        */
-       protected function freeLocksOnServer( $lockSrv, array $paths, $type ) {
-               return Status::newGood(); // not supported
-       }
-
-       /**
-        * @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;
-       }
-
        /**
         * @see QuorumLockManager::isServerUp()
         * @return bool
@@ -276,14 +215,8 @@ class DBLockManager extends QuorumLockManager {
         * Make sure remaining locks get cleared for sanity
         */
        function __destruct() {
+               $this->releaseAllLocks();
                foreach ( $this->conns as $db ) {
-                       if ( $db->trxLevel() ) { // in transaction
-                               try {
-                                       $db->rollback( __METHOD__ ); // finish transaction and kill any rows
-                               } catch ( DBError $e ) {
-                                       // oh well
-                               }
-                       }
                        $db->close();
                }
        }
@@ -373,4 +306,117 @@ class MySqlLockManager extends DBLockManager {
 
                return $status;
        }
+
+       /**
+        * @see QuorumLockManager::freeLocksOnServer()
+        * @return Status
+        */
+       protected function freeLocksOnServer( $lockSrv, array $paths, $type ) {
+               return Status::newGood(); // not supported
+       }
+
+       /**
+        * @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 = array(
+               self::LOCK_SH => self::LOCK_SH,
+               self::LOCK_UW => self::LOCK_SH,
+               self::LOCK_EX => self::LOCK_EX
+       );
+
+       protected function getLocksOnServer( $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 wfBaseConvert( substr( $key, 0, 15 ), 16, 10 ); },
+                       array_map( array( $this, 'sha1Base16Absolute' ), $paths )
+               ) );
+
+               // Try to acquire all the locks...
+               $fields = array();
+               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 = (array)$res->fetchObject();
+
+               if ( in_array( 'f', $row ) ) {
+                       // Release any acquired locks if some could not be acquired...
+                       $fields = array();
+                       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::freeLocksOnServer()
+        * @return Status
+        */
+       protected function freeLocksOnServer( $lockSrv, array $paths, $type ) {
+               return Status::newGood(); // not supported
+       }
+
+       /**
+        * @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;
+       }
 }
index f988ff4..d222bab 100644 (file)
@@ -112,6 +112,18 @@ abstract class LockManager {
                return wfBaseConvert( sha1( "{$this->domain}:{$path}" ), 16, 36, 31 );
        }
 
+       /**
+        * Get the base 16 SHA-1 of a string, padded to 31 digits.
+        * Before hashing, the path will be prefixed with the domain ID.
+        * This should be used interally for lock key or file names.
+        *
+        * @param $path string
+        * @return string
+        */
+       final protected function sha1Base16Absolute( $path ) {
+               return sha1( "{$this->domain}:{$path}" );
+       }
+
        /**
         * Lock resources with the given keys and lock type
         *