ExternalStore tweaks:
authorAaron Schulz <aaron@users.mediawiki.org>
Wed, 10 Sep 2008 12:07:56 +0000 (12:07 +0000)
committerAaron Schulz <aaron@users.mediawiki.org>
Wed, 10 Sep 2008 12:07:56 +0000 (12:07 +0000)
* On read, spend less time checking on dead slaves
* Add randomInsert() to ES. This does the cluster picking for us
* Make revision text use randomInsert(). On write, fails-over to other clusters as needed instead of throwing db errors

includes/ExternalStore.php
includes/ExternalStoreDB.php
includes/Revision.php
includes/db/Database.php
includes/db/LoadBalancer.php

index e2b7856..e73b9a1 100644 (file)
@@ -14,7 +14,7 @@
  */
 class ExternalStore {
        /* Fetch data from given URL */
-       static function fetchFromURL($url) {
+       static function fetchFromURL( $url ) {
                global $wgExternalStores;
 
                if( !$wgExternalStores )
@@ -44,7 +44,7 @@ class ExternalStore {
 
                $class = 'ExternalStore' . ucfirst( $proto );
                /* Any custom modules should be added to $wgAutoLoadClasses for on-demand loading */
-               if( !class_exists( $class ) ){
+               if( !class_exists( $class ) ) {
                        return false;
                }
 
@@ -66,4 +66,41 @@ class ExternalStore {
                        return $store->store( $params, $data );
                }
        }
+       
+       /**
+        * Like insert() above, but does more of the work for us.
+        * This function does not need a url param, it builds it by
+        * itself. It also fails-over to the next possible clusters.
+        *
+        * @param string $data
+        * Returns the URL of the stored data item, or false on error
+        */
+       public static function randomInsert( $data ) {
+               global $wgDefaultExternalStore;
+               $tryStorages = (array)$wgDefaultExternalStore;
+               // Do not wait and do second retry per master if we
+               // have other active cluster masters to try instead.
+               $retry = count($tryStorages) > 1 ? false : true;
+               while ( count($tryStorages) > 0 ) {
+                       $index = mt_rand(0, count( $tryStorages ) - 1);
+                       $storeUrl = $tryStorages[$index];
+                       list( $proto, $params ) = explode( '://', $storeUrl, 2 );
+                       $store = self::getStoreObject( $proto );
+                       if ( $store === false ) {
+                               throw new MWException( "Invalid external storage protocol - $storeUrl" );
+                               return false; 
+                       } else {
+                               $url = $store->store( $params, $data, $retry ); // Try to save the object
+                               if ( $url ) {
+                                       return $url; // Done!
+                               } else {
+                                       unset( $tryStorages[$index] ); // Don't try this one again!
+                                       sort( $tryStorages ); // Must have consecutive keys
+                                       wfDebugLog( 'ExternalStorage', "Unable to store text to external storage $storeUrl" );
+                               }
+                       }
+               }
+               throw new MWException( "Unable to store text to external storage" );
+               return false; // All cluster masters dead :(
+       }
 }
index 549412d..84d44a4 100644 (file)
@@ -34,13 +34,16 @@ class ExternalStoreDB {
        /** @todo Document.*/
        function &getSlave( $cluster ) {
                $lb =& $this->getLoadBalancer( $cluster );
-               return $lb->getConnection( DB_SLAVE );
+               // Make only two connection attempts, since we still have the master to try
+               return $lb->getConnection( DB_SLAVE, array(), false, 2 );
        }
 
        /** @todo Document.*/
-       function &getMaster( $cluster ) {
+       function &getMaster( $cluster, $retry = true ) {
                $lb =& $this->getLoadBalancer( $cluster );
-               return $lb->getConnection( DB_MASTER );
+               // Make only two connection attempts if there are other clusters to try
+               $attempts = $retry ? false : 2;
+               return $lb->getConnection( DB_MASTER, array(), false, $attempts, LoadBalancer::GRACEFUL );
        }
 
        /** @todo Document.*/
@@ -56,7 +59,7 @@ class ExternalStoreDB {
         * Fetch data from given URL
         * @param string $url An url of the form DB://cluster/id or DB://cluster/id/itemid for concatened storage.
         */
-       function fetchFromURL($url) {
+       function fetchFromURL( $url ) {
                $path = explode( '/', $url );
                $cluster  = $path[2];
                $id       = $path[3];
@@ -119,15 +122,17 @@ class ExternalStoreDB {
         *
         * @param $cluster String: the cluster name
         * @param $data String: the data item
+        * @param $retry bool: allows an extra DB connection retry after 1 second
         * @return string URL
         */
-       function store( $cluster, $data ) {
-               $fname = 'ExternalStoreDB::store';
-
-               $dbw =& $this->getMaster( $cluster );
-
+       function store( $cluster, $data, $retry = true ) {
+               if( !$dbw = $this->getMaster( $cluster, $retry ) ) {
+                       return false; // failed, maybe another cluster is up...
+               }
                $id = $dbw->nextSequenceValue( 'blob_blob_id_seq' );
-               $dbw->insert( $this->getTable( $dbw ), array( 'blob_id' => $id, 'blob_text' => $data ), $fname );
+               $dbw->insert( $this->getTable( $dbw ), 
+                       array( 'blob_id' => $id, 'blob_text' => $data ), 
+                       __METHOD__ );
                $id = $dbw->insertId();
                if ( $dbw->getFlag( DBO_TRX ) ) {
                        $dbw->immediateCommit();
index eba7be4..c6c8be8 100644 (file)
@@ -719,20 +719,13 @@ class Revision {
                $flags = Revision::compressRevisionText( $data );
 
                # Write to external storage if required
-               if ( $wgDefaultExternalStore ) {
-                       if ( is_array( $wgDefaultExternalStore ) ) {
-                               // Distribute storage across multiple clusters
-                               $store = $wgDefaultExternalStore[mt_rand(0, count( $wgDefaultExternalStore ) - 1)];
-                       } else {
-                               $store = $wgDefaultExternalStore;
-                       }
+               if( $wgDefaultExternalStore ) {
                        // Store and get the URL
-                       $data = ExternalStore::insert( $store, $data );
-                       if ( !$data ) {
-                               # This should only happen in the case of a configuration error, where the external store is not valid
-                               throw new MWException( "Unable to store text to external storage $store" );
+                       $data = ExternalStore::randomInsert( $data );
+                       if( !$data ) {
+                               throw new MWException( "Unable to store text to external storage" );
                        }
-                       if ( $flags ) {
+                       if( $flags ) {
                                $flags .= ',';
                        }
                        $flags .= 'external';
index 0ec3974..3c37f70 100644 (file)
@@ -257,9 +257,11 @@ class Database {
         * @param failFunction
         * @param $flags
         * @param $tablePrefix String: database table prefixes. By default use the prefix gave in LocalSettings.php
+        * @param int $max, max connection attempts 
+        ** After the first retry (second attempt), each retry waits 1 second
         */
        function __construct( $server = false, $user = false, $password = false, $dbName = false,
-               $failFunction = false, $flags = 0, $tablePrefix = 'get from global' ) {
+               $failFunction = false, $flags = 0, $tablePrefix = 'get from global', $max = false ) {
 
                global $wgOut, $wgDBprefix, $wgCommandLineMode;
                # Can't get a reference if it hasn't been set yet
@@ -293,7 +295,7 @@ class Database {
                }
 
                if ( $server ) {
-                       $this->open( $server, $user, $password, $dbName );
+                       $this->open( $server, $user, $password, $dbName, $max );
                }
        }
 
@@ -311,7 +313,7 @@ class Database {
         * Usually aborts on failure
         * If the failFunction is set to a non-zero integer, returns success
         */
-       function open( $server, $user, $password, $dbName ) {
+       function open( $server, $user, $password, $dbName, $max = false ) {
                global $wguname, $wgAllDBsAreLocalhost;
                wfProfileIn( __METHOD__ );
 
@@ -343,11 +345,11 @@ class Database {
 
                wfProfileIn("dbconnect-$server");
 
-               # Try to connect up to three times
+               if( !$max ) { $max = 3; }
+               # Try to connect up to three times (by default)
                # The kernel's default SYN retransmission period is far too slow for us,
                # so we use a short timeout plus a manual retry.
                $this->mConn = false;
-               $max = 3;
                $this->installErrorHandler();
                for ( $i = 0; $i < $max && !$this->mConn; $i++ ) {
                        if ( $i > 1 ) {
index 42c4044..dd0c66b 100644 (file)
@@ -11,6 +11,8 @@
  * @ingroup Database
  */
 class LoadBalancer {
+       const GRACEFUL = 1;
+
        /* private */ var $mServers, $mConns, $mLoads, $mGroupLoads;
        /* private */ var $mFailFunction, $mErrorConnection;
        /* private */ var $mReadIndex, $mLastIndex, $mAllowLagged;
@@ -171,7 +173,7 @@ class LoadBalancer {
         *
         * Side effect: opens connections to databases
         */
-       function getReaderIndex( $group = false, $wiki = false ) {
+       function getReaderIndex( $group = false, $wiki = false, $attempts = false ) {
                global $wgReadOnly, $wgDBClusterTimeout, $wgDBAvgStatusPoll, $wgDBtype;
 
                # FIXME: For now, only go through all this for mysql databases
@@ -248,7 +250,7 @@ class LoadBalancer {
                                }
 
                                wfDebugLog( 'connect', __METHOD__.": Using reader #$i: {$this->mServers[$i]['host']}...\n" );
-                               $conn = $this->openConnection( $i, $wiki );
+                               $conn = $this->openConnection( $i, $wiki, $attempts );
 
                                if ( !$conn ) {
                                        wfDebugLog( 'connect', __METHOD__.": Failed connecting to $i/$wiki\n" );
@@ -399,8 +401,13 @@ class LoadBalancer {
        /**
         * Get a connection by index
         * This is the main entry point for this class.
+        * @param int $i Database
+        * @param array $groups Query groups
+        * @param string $wiki Wiki ID
+        * @param int $attempts Max DB connect attempts
+        * @param int $flags
         */
-       public function &getConnection( $i, $groups = array(), $wiki = false ) {
+       public function &getConnection( $i, $groups = array(), $wiki = false, $attempts = false, $flags = 0 ) {
                global $wgDBtype;
                wfProfileIn( __METHOD__ );
 
@@ -432,7 +439,7 @@ class LoadBalancer {
 
                # Operation-based index
                if ( $i == DB_SLAVE ) {
-                       $i = $this->getReaderIndex( false, $wiki );
+                       $i = $this->getReaderIndex( false, $wiki, $attempts );
                } elseif ( $i == DB_LAST ) {
                        # Just use $this->mLastIndex, which should already be set
                        $i = $this->mLastIndex;
@@ -444,12 +451,15 @@ class LoadBalancer {
                }
                # Couldn't find a working server in getReaderIndex()?
                if ( $i === false ) {
+                       if( $flags && self::GRACEFUL ) {
+                               return false;
+                       }
                        $this->reportConnectionError( $this->mErrorConnection );
                }
 
                # Now we have an explicit index into the servers array
-               $conn = $this->openConnection( $i, $wiki );
-               if ( !$conn ) {
+               $conn = $this->openConnection( $i, $wiki, $attempts );
+               if ( !$conn && !($flags & self::GRACEFUL) ) {
                        $this->reportConnectionError( $this->mErrorConnection );
                }
 
@@ -509,11 +519,12 @@ class LoadBalancer {
         *
         * @param integer $i Server index
         * @param string $wiki Wiki ID to open
+        * @param int $attempts Maximum connection attempts
         * @return Database
         *
         * @access private
         */
-       function openConnection( $i, $wiki = false ) {
+       function openConnection( $i, $wiki = false, $attempts = false ) {
                wfProfileIn( __METHOD__ );
                if ( $wiki !== false ) {
                        $conn = $this->openForeignConnection( $i, $wiki );
@@ -525,7 +536,7 @@ class LoadBalancer {
                } else {
                        $server = $this->mServers[$i];
                        $server['serverIndex'] = $i;
-                       $conn = $this->reallyOpenConnection( $server );
+                       $conn = $this->reallyOpenConnection( $server, false, $attempts );
                        if ( $conn->isOpen() ) {
                                $this->mConns['local'][$i][0] = $conn;
                        } else {
@@ -628,7 +639,7 @@ class LoadBalancer {
         * Returns a Database object whether or not the connection was successful.
         * @access private
         */
-       function reallyOpenConnection( $server, $dbNameOverride = false ) {
+       function reallyOpenConnection( $server, $dbNameOverride = false, $attempts = false ) {
                if( !is_array( $server ) ) {
                        throw new MWException( 'You must update your load-balancing configuration. See DefaultSettings.php entry for $wgDBservers.' );
                }
@@ -643,7 +654,7 @@ class LoadBalancer {
 
                # Create object
                wfDebug( "Connecting to $host $dbname...\n" );
-               $db = new $class( $host, $user, $password, $dbname, 1, $flags );
+               $db = new $class( $host, $user, $password, $dbname, 1, $flags, 'get from global', $attempts );
                if ( $db->isOpen() ) {
                        wfDebug( "Connected\n" );
                } else {