https://www.mediawiki.org/wiki/Special:Code/MediaWiki/103790

Revision: 103790
Author:   aaron
Date:     2011-11-20 22:03:58 +0000 (Sun, 20 Nov 2011)
Log Message:
-----------
* Cleanups to DBFileLockManager; improved $config layout
* Moved locking to doOperations() to reduce queries for DB-based locking 
(better batching)
* Renamed $transaction to $fileOp in doOperations()
* Fixed bug in attempt() wrt setting failedAttempt field
* Added missing storagePathsToLock() to FileMoveOp
* Better use of Status::merge() function in various places

Modified Paths:
--------------
    branches/FileBackend/phase3/includes/filerepo/ForeignDBViaLBRepo.php
    branches/FileBackend/phase3/includes/filerepo/backend/FileBackend.php
    
branches/FileBackend/phase3/includes/filerepo/backend/FileBackendMultiWrite.php
    branches/FileBackend/phase3/includes/filerepo/backend/FileLockManager.php

Modified: branches/FileBackend/phase3/includes/filerepo/ForeignDBViaLBRepo.php
===================================================================
--- branches/FileBackend/phase3/includes/filerepo/ForeignDBViaLBRepo.php        
2011-11-20 22:03:13 UTC (rev 103789)
+++ branches/FileBackend/phase3/includes/filerepo/ForeignDBViaLBRepo.php        
2011-11-20 22:03:58 UTC (rev 103790)
@@ -30,6 +30,7 @@
        function getSlaveDB() {
                return wfGetDB( DB_SLAVE, array(), $this->wiki );
        }
+
        function hasSharedCache() {
                return $this->hasSharedCache;
        }

Modified: branches/FileBackend/phase3/includes/filerepo/backend/FileBackend.php
===================================================================
--- branches/FileBackend/phase3/includes/filerepo/backend/FileBackend.php       
2011-11-20 22:03:13 UTC (rev 103789)
+++ branches/FileBackend/phase3/includes/filerepo/backend/FileBackend.php       
2011-11-20 22:03:58 UTC (rev 103790)
@@ -258,7 +258,7 @@
                                // Get params for this operation
                                $params = $operation;
                                unset( $params['operation'] ); // don't need 
this
-                               // Add operation on to the list of things to do
+                               // Append the FileOp class
                                $performOps[] = new $class( $params );
                        } else {
                                throw new MWException( "Operation `$opName` is 
not supported." );
@@ -270,31 +270,49 @@
 
        final public function doOperations( array $ops ) {
                $status = Status::newGood();
+
                // Build up a list of FileOps...
                $performOps = $this->getOperations( $ops );
+
+               // Build up a list of files to lock...
+               $filesToLock = array();
+               foreach ( $performOps as $index => $fileOp ) {
+                       $filesToLock = array_merge( $filesToLock, 
$fileOp->storagePathsToLock() );
+               }
+               $filesToLock = array_unique( $filesToLock ); // avoid warnings
+
+               // Try to lock those files...
+               $status->merge( $this->lockFiles( $filesToLock ) );
+               if ( !$status->isOK() ) {
+                       return $status; // abort
+               }
+
                // Attempt each operation; abort on failure...
-               foreach ( $performOps as $index => $transaction ) {
-                       $subStatus = $transaction->attempt();
-                       $status->merge( $subStatus );
-                       if ( !$subStatus->isOK() ) { // operation failed?
+               foreach ( $performOps as $index => $fileOp ) {
+                       $status->merge( $fileOp->attempt() );
+                       if ( !$status->isOK() ) { // operation failed?
                                // Revert everything done so far and abort.
                                // Do this by reverting each previous operation 
in reverse order.
                                $pos = $index - 1; // last one failed; no need 
to revert()
                                while ( $pos >= 0 ) {
-                                       $subStatus = 
$performOps[$pos]->revert();
-                                       $status->merge( $subStatus );
+                                       $status->merge( 
$performOps[$pos]->revert() );
                                        $pos--;
                                }
                                return $status;
                        }
                }
+
                // Finish each operation...
-               foreach ( $performOps as $index => $transaction ) {
-                       $subStatus = $transaction->finish();
-                       $status->merge( $subStatus );
+               foreach ( $performOps as $index => $fileOp ) {
+                       $status->merge( $fileOp->finish() );
                }
-               // Make sure status is OK, despite any finish() fatals
+
+               // Unlock all the files
+               $status->merge( $this->unlockFiles( $filesToLock ) );
+
+               // Make sure status is OK, despite any finish() or 
unlockFiles() fatals
                $status->setResult( true );
+
                return $status;
        }
 
@@ -354,10 +372,8 @@
                        throw new MWException( "Cannot attempt operation called 
twice." );
                }
                $this->state = self::STATE_ATTEMPTED;
-               $status = $this->setLocks();
-               if ( $status->isOK() ) {
-                       $status = $this->doAttempt();
-               } else {
+               $status = $this->doAttempt();
+               if ( !$status->isOK() ) {
                        $this->failedAttempt = true;
                }
                return $status;
@@ -373,12 +389,11 @@
                        throw new MWException( "Cannot rollback an unstarted or 
finished operation." );
                }
                $this->state = self::STATE_DONE;
-               if ( !$this->failedAttempt ) {
+               if ( $this->failedAttempt ) {
+                       $status = Status::newGood(); // nothing to revert
+               } else {
                        $status = $this->doRevert();
-               } else {
-                       $status = Status::newGood(); // nothing to revert
                }
-               $this->unsetLocks();
                return $status;
        }
 
@@ -392,39 +407,20 @@
                        throw new MWException( "Cannot cleanup an unstarted or 
finished operation." );
                }
                $this->state = self::STATE_DONE;
-               if ( !$this->failedAttempt ) {
+               if ( $this->failedAttempt ) {
+                       $status = Status::newGood(); // nothing to revert
+               } else {
                        $status = $this->doFinish();
-               } else {
-                       $status = Status::newGood(); // nothing to revert
                }
-               $this->unsetLocks();
                return $status;
        }
 
        /**
-        * Try to lock any files before changing them
-        *
-        * @return Status
-        */
-       private function setLocks() {
-               return $this->backend->lockFiles( $this->storagePathsToLock() );
-       }
-
-       /**
-        * Try to unlock any files that this locked
-        *
-        * @return Status
-        */
-       private function unsetLocks() {
-               return $this->backend->unlockFiles( $this->storagePathsToLock() 
);
-       }
-
-       /**
         * Get a list of storage paths to lock for this operation
         *
         * @return Array
         */
-       protected function storagePathsToLock() {
+       public function storagePathsToLock() {
                return array();
        }
 
@@ -632,6 +628,10 @@
                }
                return $status;
        }
+
+       function storagePathsToLock() {
+               return array( $this->params['source'], $this->params['dest'] );
+       }
 }
 
 /**

Modified: 
branches/FileBackend/phase3/includes/filerepo/backend/FileBackendMultiWrite.php
===================================================================
--- 
branches/FileBackend/phase3/includes/filerepo/backend/FileBackendMultiWrite.php 
    2011-11-20 22:03:13 UTC (rev 103789)
+++ 
branches/FileBackend/phase3/includes/filerepo/backend/FileBackendMultiWrite.php 
    2011-11-20 22:03:58 UTC (rev 103790)
@@ -1,7 +1,5 @@
 <?php
 /**
- * Class for a "backend" consisting of a orioritized list of backend
- *
  * @file
  * @ingroup FileRepo
  */
@@ -41,35 +39,56 @@
 
        final public function doOperations( array $ops ) {
                $status = Status::newGood();
+
                // Build up a list of FileOps. The list will have all the ops
                // for one backend, then all the ops for the next, and so on.
+               // Also build up a list of files to lock...
                $performOps = array();
-               foreach ( $this->fileBackends as $backend ) {
+               $filesToLock = array();
+               foreach ( $this->fileBackends as $index => $backend ) {
                        $performOps = array_merge( $performOps, 
$backend->getOperations( $ops ) );
+                       // Set $filesToLock from the first backend so we don't 
try to set all
+                       // locks two or three times (depending on the number of 
backends).
+                       if ( $index == 0 ) {
+                               foreach ( $performOps as $index => $fileOp ) {
+                                       $filesToLock = array_merge( 
$filesToLock, $fileOp->storagePathsToLock() );
+                               }
+                               $filesToLock = array_unique( $filesToLock ); // 
avoid warnings
+                       }
                }
+
+               // Try to lock those files...
+               $status->merge( $this->lockFiles( $filesToLock ) );
+               if ( !$status->isOK() ) {
+                       return $status; // abort
+               }
+
                // Attempt each operation; abort on failure...
-               foreach ( $performOps as $index => $transaction ) {
-                       $subStatus = $transaction->attempt();
-                       $status->merge( $subStatus );
-                       if ( !$subStatus->isOK() ) { // operation failed?
+               foreach ( $performOps as $index => $fileOp ) {
+                       $status->merge( $fileOp->attempt() );
+                       if ( !$status->isOK() ) { // operation failed?
                                // Revert everything done so far and abort.
                                // Do this by reverting each previous operation 
in reverse order.
                                $pos = $index - 1; // last one failed; no need 
to revert()
                                while ( $pos >= 0 ) {
-                                       $subStatus = 
$performOps[$pos]->revert();
-                                       $status->merge( $subStatus );
+                                       $status->merge( 
$performOps[$pos]->revert() );
                                        $pos--;
                                }
                                return $status;
                        }
                }
+
                // Finish each operation...
-               foreach ( $performOps as $index => $transaction ) {
-                       $subStatus = $transaction->finish();
-                       $status->merge( $subStatus );
+               foreach ( $performOps as $index => $fileOp ) {
+                       $status->merge( $fileOp->finish() );
                }
-               // Make sure status is OK, despite any finish() fatals
+
+               // Unlock all the files
+               $status->merge( $this->unlockFiles( $filesToLock ) );
+
+               // Make sure status is OK, despite any finish() or 
unlockFiles() fatals
                $status->setResult( true );
+
                return $status;
        }
 
@@ -145,13 +164,11 @@
        function lockFiles( array $paths ) {
                $status = Status::newGood();
                foreach ( $this->backends as $index => $backend ) {
-                       $subStatus = $backend->lockFiles( $paths );
-                       $status->merge( $subStatus );
-                       if ( !$subStatus->isOk() ) {
+                       $status->merge( $backend->lockFiles( $paths ) );
+                       if ( !$status->isOk() ) {
                                // Lock failed: release the locks done so far 
each backend
                                for ( $i=0; $i < $index; $i++ ) { // don't do 
backend $index since it failed
-                                       $subStatus = $backend->unlockFiles( 
$paths );
-                                       $status->merge( $subStatus );
+                                       $status->merge( $backend->unlockFiles( 
$paths ) );
                                }
                                return $status;
                        }
@@ -162,8 +179,7 @@
        function unlockFiles( array $paths ) {
                $status = Status::newGood();
                foreach ( $this->backends as $backend ) {
-                       $subStatus = $backend->unlockFile( $paths );
-                       $status->merge( $subStatus );
+                       $status->merge( $backend->unlockFile( $paths ) );
                }
                return $status;
        }

Modified: 
branches/FileBackend/phase3/includes/filerepo/backend/FileLockManager.php
===================================================================
--- branches/FileBackend/phase3/includes/filerepo/backend/FileLockManager.php   
2011-11-20 22:03:13 UTC (rev 103789)
+++ branches/FileBackend/phase3/includes/filerepo/backend/FileLockManager.php   
2011-11-20 22:03:58 UTC (rev 103790)
@@ -97,8 +97,7 @@
                                $lockedKeys[] = $key;
                        } else {
                                // Abort and unlock everything
-                               $subStatus = $this->doUnlock( $lockedKeys );
-                               $status->merge( $subStatus );
+                               $status->merge( $this->doUnlock( $lockedKeys ) 
);
                                return $status;
                        }
                }
@@ -110,8 +109,7 @@
                $status = Status::newGood();
 
                foreach ( $keys as $key ) {
-                       $subStatus = $this->doSingleUnlock( $key );
-                       $status->merge( $subStatus );
+                       $status->merge( $this->doSingleUnlock( $key ) );
                }
 
                return $status;
@@ -181,21 +179,19 @@
  * One or several lock database servers are set up having a `file_locking`
  * table with one field, fl_key, the PRIMARY KEY. The table engine should
  * have row-level locking. For performance, deadlock detection should be
- * disabled and a low lock-wait timeout should be put in the server config.
+ * disabled and a low lock-wait timeout should be set via server config.
  *
  * All lock requests for an item (identified by an abstract key string) will
- * map to one bucket. Each bucket maps to a single server, and each server can
- * have several or no fallback servers. Fallback servers recieve the same lock
- * statements as the servers they are set as fallbacks for. This propagation is
- * only best-effort; lock requests will not be blocked just because a fallback
- * server cannot be contacted to recieve a copy of the lock request.
+ * map to one bucket. Each bucket maps to a single server, though each server
+ * can have several fallback servers.
+ *
+ * Fallback servers recieve the same lock statements as the servers they 
standby for.
+ * This propagation is only best-effort; lock requests will not be blocked just
+ * because a fallback server cannot recieve a copy of the lock request.
  */
 class DBFileLockManager extends FileLockManager {
        /** @var Array Map of bucket indexes to server names */
-       protected $serverMap = array(); // (index => server name)
-       /** @var Array Map of servers to fallback server names */
-       protected $serverFallbackMap = array(); // (server => 
(server1,server2,...))
-
+       protected $serverMap = array(); // (index => (server1,server2,...))
        /** @var Array List of active lock key names */
        protected $locksHeld = array(); // (key => 1)
        /** $var Array Map Lock-active database connections (name => Database) 
*/
@@ -203,54 +199,64 @@
 
        /**
         * Construct a new instance from configuration.
-        * The 'serverMap' param of $config has is an array of consecutive
-        * integer keys, starting from 0, with server name strings as values.
-        * It should have no more than 16 items in the array.
-        * 
-        * The `file_locking` table should have row-level locking (e.g. innoDB).
-        * 
-        * @param array $config 
+        * $config paramaters include:
+        * 'serverMap' : Array of no more than 16 consecutive integer keys,
+        *               starting from 0, with a list of servers as values.
+        *               The first server in each list is the main server and
+        *               the others are fallback servers.
+        *
+        * @param Array $config 
         */
        function __construct( array $config ) {
-               $this->serverMap = $config['serverMap'];
-               $this->serverFallbackMap = $config['serverFallbackMap'];
+               $this->serverMap = (array)$config['serverMap'];
+               // Sanitize serverMap against bad config to prevent PHP errors
+               for ( $i=0; $i < count( $this->serverMap ); $i++ ) {
+                       if (
+                               !isset( $this->serverMap[$i] ) || // not 
consecutive
+                               !is_array( $this->serverMap[$i] ) || // bad type
+                               !count( $this->serverMap[$i] ) // empty list
+                       ) {
+                               $this->serverMap[$i] = null; // see 
getBucketFromKey()
+                               wfWarn( "No key for bucket $i in serverMap or 
server list is empty." );
+                       }
+               }
        }
 
        function doLock( array $keys ) {
                $status = Status::newGood();
 
                $keysToLock = array();
-               // Get locks that need to be acquired and which server they map 
to...
+               // Get locks that need to be acquired (buckets => locks)...
                foreach ( $keys as $key ) {
                        if ( isset( $this->locksHeld[$key] ) ) {
                                $status->warning( 'File already locked.' );
                        } else {
-                               $server = $this->getDBServerFromKey( $key );
-                               if ( $server === null ) { // config error?
-                                       $status->fatal( "Lock server for $key 
is not set." );
+                               $bucket = $this->getBucketFromKey( $key );
+                               if ( $bucket === null ) { // config error?
+                                       $status->fatal( "Lock servers for key 
$key is not set." );
                                        return $status;
                                }
-                               $keysToLock[$server][] = $key;
+                               $keysToLock[$bucket][] = $key;
                        }
                }
 
                $lockedKeys = array(); // files locked in this attempt
                // Attempt to acquire these locks...
-               foreach ( $keysToLock as $server => $keys ) {
+               foreach ( $keysToLock as $bucket => $keys ) {
+                       $server = $this->serverMap[$bucket][0]; // primary lock 
server
+                       $propagateToFallbacks = true; // give lock statements 
to fallback servers
                        // Acquire the locks for this server. Three main cases 
can happen:
                        // (a) Server is up; common case
                        // (b) Server is down but a fallback is up
                        // (c) Server is down and no fallbacks are up (or none 
defined)
-                       $propagateToFallbacks = true;
                        try {
                                $this->lockingSelect( $server, $keys ); // 
SELECT FOR UPDATE
                        } catch ( DBError $e ) {
                                // Can we manage to lock on any of the fallback 
servers?
-                               if ( !$this->lockingSelectFallbacks( $server, 
$keys ) ) {
-                                       // Abort and unlock everything
+                               if ( !$this->lockingSelectFallbacks( $bucket, 
$keys ) ) {
+                                       // Abort and unlock everything we just 
locked
                                        $status->fatal( "Could not contact the 
lock server." );
-                                       $subStatus = $this->doUnlock( 
$lockedKeys );
-                                       $status->merge( $subStatus );
+                                       $status->merge( $this->doUnlock( 
$lockedKeys ) );
                                        return $status;
                                } else { // recovered using fallbacks
                                        $propagateToFallbacks = false; // done 
already
@@ -258,7 +264,7 @@
                        }
                        // Propagate any locks to the fallback servers (best 
effort)
                        if ( $propagateToFallbacks ) {
-                               $this->lockingSelectFallbacks( $server, $keys );
+                               $this->lockingSelectFallbacks( $bucket, $keys );
                        }
                        // Record locks as active
                        foreach ( $keys as $key ) {
@@ -290,7 +296,7 @@
        }
 
        /**
-        * Get a database connection for $server and lock the rows for $keys
+        * Get a DB connection to a lock server and acquire locks on $keys.
         *
         * @param $server string
         * @param $keys Array
@@ -311,24 +317,23 @@
        }
 
        /**
-        * Propagate any locks to the fallback servers for $server.
+        * Propagate any locks to the fallback servers for a bucket.
         * This should avoid throwing any exceptions.
         *
-        * @param $server string
+        * @param $bucket integer
         * @param $keys Array
         * @return bool Locks made on at least one fallback server
         */
-       protected function lockingSelectFallbacks( $server, array $keys ) {
+       protected function lockingSelectFallbacks( $bucket, array $keys ) {
                $locksMade = false;
-               if ( isset( $this->serverFallbackMap[$server] ) ) {
-                       // Propagate the $server locks to each fallback for 
$server...
-                       foreach ( $this->serverFallbackMap[$server] as 
$fallbackServer ) {
-                               try {
-                                       $this->doLockingSelect( 
$fallbackServer, $keys ); // SELECT FOR UPDATE
-                                       $locksMade = true;
-                               } catch ( DBError $e ) {
-                                       // oh well; best effort
-                               }
+               $count = count( $this->serverMap[$bucket] );
+               for ( $i=1; $i < $count; $i++ ) { // start at 1 to only include 
fallbacks
+                       $server = $this->serverMap[$bucket][$i];
+                       try {
+                               $this->doLockingSelect( $server, $keys ); // 
SELECT FOR UPDATE
+                               $locksMade = true; // success for this fallback
+                       } catch ( DBError $e ) {
+                               // oh well; best effort
                        }
                }
                return $locksMade;
@@ -341,14 +346,14 @@
         * @return void
         */
        protected function commitLockTransactions() {
-               try {
-                       foreach ( $this->activeConns as $server => $db ) {
+               foreach ( $this->activeConns as $server => $db ) {
+                       try {
                                $db->commit(); // finish transaction
-                               unset( $this->activeConns[$server] );
+                       } catch ( DBError $e ) {
+                               // oh well
                        }
-               } catch ( DBError $e ) {
-                       // oh well
                }
+               $this->activeConns = array();
        }
 
        /**
@@ -356,29 +361,17 @@
         * This should avoid throwing any exceptions.
         *
         * @param $key string
-        * @return int
+        * @return integer
         */
        protected function getBucketFromKey( $key ) {
                $hash = str_pad( md5( $key ), 32, '0', STR_PAD_LEFT ); // 32 
char hash
                $prefix = substr( $hash, 0, 2 ); // first 2 hex chars (8 bits)
-               return ( intval( base_convert( $prefix, 16, 10 ) ) % count( 
$this->serverMap ) );
-       }
-
-       /**
-        * Get the server name for lock key.
-        * This should avoid throwing any exceptions.
-        *
-        * @param $key string
-        * @return string|null
-        */
-       protected function getDBServerFromKey( $key ) {
-               $bucket = $this->getBucketFromKey( $key );
-               if ( isset( $this->serverMap[$bucket] ) ) {
-                       return $this->serverMap[$bucket];
-               } else {
-                       wfWarn( "No key for bucket $bucket in serverMap." );
-                       return null; // disabled? bad config?
+               $bucket = intval( base_convert( $prefix, 16, 10 ) ) % count( 
$this->serverMap );
+               // Sanity check that at least one server is handling this bucket
+               if ( !isset( $this->serverMap[$bucket] ) ) {
+                       return null; // bad config?
                }
+               return $bucket;
        }
 }
 


_______________________________________________
MediaWiki-CVS mailing list
MediaWiki-CVS@lists.wikimedia.org
https://lists.wikimedia.org/mailman/listinfo/mediawiki-cvs

Reply via email to