*/
class ExternalStore {
/* Fetch data from given URL */
- static function fetchFromURL($url) {
+ static function fetchFromURL( $url ) {
global $wgExternalStores;
if( !$wgExternalStores )
$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;
}
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 :(
+ }
}
/** @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.*/
* 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];
*
* @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();
$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';
* @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
}
if ( $server ) {
- $this->open( $server, $user, $password, $dbName );
+ $this->open( $server, $user, $password, $dbName, $max );
}
}
* 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__ );
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 ) {
* @ingroup Database
*/
class LoadBalancer {
+ const GRACEFUL = 1;
+
/* private */ var $mServers, $mConns, $mLoads, $mGroupLoads;
/* private */ var $mFailFunction, $mErrorConnection;
/* private */ var $mReadIndex, $mLastIndex, $mAllowLagged;
*
* 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
}
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" );
/**
* 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__ );
# 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;
}
# 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 );
}
*
* @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 );
} 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 {
* 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.' );
}
# 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 {