From f6d65e533c62f6deb21342d4901ece24497b433e Mon Sep 17 00:00:00 2001 From: Pierre Schmitz Date: Thu, 4 Jun 2015 07:31:04 +0200 Subject: Update to MediaWiki 1.25.1 --- includes/db/Database.php | 856 +++++++++++++++++++++----------------- includes/db/DatabaseError.php | 6 +- includes/db/DatabaseMssql.php | 86 ++-- includes/db/DatabaseMysql.php | 7 - includes/db/DatabaseMysqlBase.php | 125 ++++-- includes/db/DatabaseMysqli.php | 14 +- includes/db/DatabaseOracle.php | 18 +- includes/db/DatabasePostgres.php | 23 +- includes/db/DatabaseSqlite.php | 163 ++++---- includes/db/DatabaseUtility.php | 27 +- includes/db/IORMTable.php | 18 +- includes/db/LBFactory.php | 78 ++-- includes/db/LBFactoryMulti.php | 58 +-- includes/db/LBFactorySingle.php | 20 +- includes/db/LoadBalancer.php | 390 +++++++++-------- includes/db/LoadMonitor.php | 4 +- includes/db/ORMTable.php | 50 ++- 17 files changed, 1058 insertions(+), 885 deletions(-) (limited to 'includes/db') diff --git a/includes/db/Database.php b/includes/db/Database.php index 9b783a99..0c0248da 100644 --- a/includes/db/Database.php +++ b/includes/db/Database.php @@ -25,186 +25,6 @@ * @ingroup Database */ -/** - * Base interface for all DBMS-specific code. At a bare minimum, all of the - * following must be implemented to support MediaWiki - * - * @file - * @ingroup Database - */ -interface DatabaseType { - /** - * Get the type of the DBMS, as it appears in $wgDBtype. - * - * @return string - */ - function getType(); - - /** - * Open a connection to the database. Usually aborts on failure - * - * @param string $server Database server host - * @param string $user Database user name - * @param string $password Database user password - * @param string $dbName Database name - * @return bool - * @throws DBConnectionError - */ - function open( $server, $user, $password, $dbName ); - - /** - * Fetch the next row from the given result object, in object form. - * Fields can be retrieved with $row->fieldname, with fields acting like - * member variables. - * If no more rows are available, false is returned. - * - * @param ResultWrapper|stdClass $res Object as returned from DatabaseBase::query(), etc. - * @return stdClass|bool - * @throws DBUnexpectedError Thrown if the database returns an error - */ - function fetchObject( $res ); - - /** - * Fetch the next row from the given result object, in associative array - * form. Fields are retrieved with $row['fieldname']. - * If no more rows are available, false is returned. - * - * @param ResultWrapper $res Result object as returned from DatabaseBase::query(), etc. - * @return array|bool - * @throws DBUnexpectedError Thrown if the database returns an error - */ - function fetchRow( $res ); - - /** - * Get the number of rows in a result object - * - * @param mixed $res A SQL result - * @return int - */ - function numRows( $res ); - - /** - * Get the number of fields in a result object - * @see http://www.php.net/mysql_num_fields - * - * @param mixed $res A SQL result - * @return int - */ - function numFields( $res ); - - /** - * Get a field name in a result object - * @see http://www.php.net/mysql_field_name - * - * @param mixed $res A SQL result - * @param int $n - * @return string - */ - function fieldName( $res, $n ); - - /** - * Get the inserted value of an auto-increment row - * - * The value inserted should be fetched from nextSequenceValue() - * - * Example: - * $id = $dbw->nextSequenceValue( 'page_page_id_seq' ); - * $dbw->insert( 'page', array( 'page_id' => $id ) ); - * $id = $dbw->insertId(); - * - * @return int - */ - function insertId(); - - /** - * Change the position of the cursor in a result object - * @see http://www.php.net/mysql_data_seek - * - * @param mixed $res A SQL result - * @param int $row - */ - function dataSeek( $res, $row ); - - /** - * Get the last error number - * @see http://www.php.net/mysql_errno - * - * @return int - */ - function lastErrno(); - - /** - * Get a description of the last error - * @see http://www.php.net/mysql_error - * - * @return string - */ - function lastError(); - - /** - * mysql_fetch_field() wrapper - * Returns false if the field doesn't exist - * - * @param string $table Table name - * @param string $field Field name - * - * @return Field - */ - function fieldInfo( $table, $field ); - - /** - * Get information about an index into an object - * @param string $table Table name - * @param string $index Index name - * @param string $fname Calling function name - * @return mixed Database-specific index description class or false if the index does not exist - */ - function indexInfo( $table, $index, $fname = __METHOD__ ); - - /** - * Get the number of rows affected by the last write query - * @see http://www.php.net/mysql_affected_rows - * - * @return int - */ - function affectedRows(); - - /** - * Wrapper for addslashes() - * - * @param string $s String to be slashed. - * @return string Slashed string. - */ - function strencode( $s ); - - /** - * Returns a wikitext link to the DB's website, e.g., - * return "[http://www.mysql.com/ MySQL]"; - * Should at least contain plain text, if for some reason - * your database has no website. - * - * @return string Wikitext of a link to the server software's web site - */ - function getSoftwareLink(); - - /** - * A string describing the current software version, like from - * mysql_get_server_info(). - * - * @return string Version information from the database server. - */ - function getServerVersion(); - - /** - * A string describing the current software version, and possibly - * other details in a user-friendly way. Will be listed on Special:Version, etc. - * Use getServerVersion() to get machine-friendly information. - * - * @return string Version information from the database server - */ - function getServerInfo(); -} - /** * Interface for classes that implement or wrap DatabaseBase * @ingroup Database @@ -216,7 +36,7 @@ interface IDatabase { * Database abstraction object * @ingroup Database */ -abstract class DatabaseBase implements IDatabase, DatabaseType { +abstract class DatabaseBase implements IDatabase { /** Number of times to re-try an operation in case of deadlock */ const DEADLOCK_TRIES = 4; @@ -226,10 +46,6 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { /** Maximum time to wait before retry */ const DEADLOCK_DELAY_MAX = 1500000; -# ------------------------------------------------------------------------------ -# Variables -# ------------------------------------------------------------------------------ - protected $mLastQuery = ''; protected $mDoneWrites = false; protected $mPHPError = false; @@ -272,9 +88,20 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { * Either a short hexidecimal string if a transaction is active or "" * * @var string + * @see DatabaseBase::mTrxLevel */ protected $mTrxShortId = ''; + /** + * The UNIX time that the transaction started. Callers can assume that if + * snapshot isolation is used, then the data is *at least* up to date to that + * point (possibly more up-to-date since the first SELECT defines the snapshot). + * + * @var float|null + * @see DatabaseBase::mTrxLevel + */ + private $mTrxTimestamp = null; + /** * Remembers the function name given for starting the most recent transaction via begin(). * Used to provide additional context for error reporting. @@ -326,10 +153,8 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { */ protected $allViews = null; -# ------------------------------------------------------------------------------ -# Accessors -# ------------------------------------------------------------------------------ - # These optionally set a variable and return the previous state + /** @var TransactionProfiler */ + protected $trxProfiler; /** * A string describing the current software version, and possibly @@ -419,6 +244,19 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { return $this->mTrxLevel; } + /** + * Get the UNIX timestamp of the time that the transaction was established + * + * This can be used to reason about the staleness of SELECT data + * in REPEATABLE-READ transaction isolation level. + * + * @return float|null Returns null if there is not active transaction + * @since 1.25 + */ + public function trxTimestamp() { + return $this->mTrxLevel ? $this->mTrxTimestamp : null; + } + /** * Get/set the number of errors logged. Only useful when errors are ignored * @param int $count The count to set, or omitted to leave it unchanged. @@ -510,6 +348,15 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { public function setFakeMaster( $enabled = true ) { } + /** + * @return TransactionProfiler + */ + protected function getTransactionProfiler() { + return $this->trxProfiler + ? $this->trxProfiler + : Profiler::instance()->getTransactionProfiler(); + } + /** * Returns true if this database supports (and uses) cascading deletes * @@ -744,9 +591,167 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { return $this->getSqlFilePath( 'update-keys.sql' ); } -# ------------------------------------------------------------------------------ -# Other functions -# ------------------------------------------------------------------------------ + /** + * Get the type of the DBMS, as it appears in $wgDBtype. + * + * @return string + */ + abstract function getType(); + + /** + * Open a connection to the database. Usually aborts on failure + * + * @param string $server Database server host + * @param string $user Database user name + * @param string $password Database user password + * @param string $dbName Database name + * @return bool + * @throws DBConnectionError + */ + abstract function open( $server, $user, $password, $dbName ); + + /** + * Fetch the next row from the given result object, in object form. + * Fields can be retrieved with $row->fieldname, with fields acting like + * member variables. + * If no more rows are available, false is returned. + * + * @param ResultWrapper|stdClass $res Object as returned from DatabaseBase::query(), etc. + * @return stdClass|bool + * @throws DBUnexpectedError Thrown if the database returns an error + */ + abstract function fetchObject( $res ); + + /** + * Fetch the next row from the given result object, in associative array + * form. Fields are retrieved with $row['fieldname']. + * If no more rows are available, false is returned. + * + * @param ResultWrapper $res Result object as returned from DatabaseBase::query(), etc. + * @return array|bool + * @throws DBUnexpectedError Thrown if the database returns an error + */ + abstract function fetchRow( $res ); + + /** + * Get the number of rows in a result object + * + * @param mixed $res A SQL result + * @return int + */ + abstract function numRows( $res ); + + /** + * Get the number of fields in a result object + * @see http://www.php.net/mysql_num_fields + * + * @param mixed $res A SQL result + * @return int + */ + abstract function numFields( $res ); + + /** + * Get a field name in a result object + * @see http://www.php.net/mysql_field_name + * + * @param mixed $res A SQL result + * @param int $n + * @return string + */ + abstract function fieldName( $res, $n ); + + /** + * Get the inserted value of an auto-increment row + * + * The value inserted should be fetched from nextSequenceValue() + * + * Example: + * $id = $dbw->nextSequenceValue( 'page_page_id_seq' ); + * $dbw->insert( 'page', array( 'page_id' => $id ) ); + * $id = $dbw->insertId(); + * + * @return int + */ + abstract function insertId(); + + /** + * Change the position of the cursor in a result object + * @see http://www.php.net/mysql_data_seek + * + * @param mixed $res A SQL result + * @param int $row + */ + abstract function dataSeek( $res, $row ); + + /** + * Get the last error number + * @see http://www.php.net/mysql_errno + * + * @return int + */ + abstract function lastErrno(); + + /** + * Get a description of the last error + * @see http://www.php.net/mysql_error + * + * @return string + */ + abstract function lastError(); + + /** + * mysql_fetch_field() wrapper + * Returns false if the field doesn't exist + * + * @param string $table Table name + * @param string $field Field name + * + * @return Field + */ + abstract function fieldInfo( $table, $field ); + + /** + * Get information about an index into an object + * @param string $table Table name + * @param string $index Index name + * @param string $fname Calling function name + * @return mixed Database-specific index description class or false if the index does not exist + */ + abstract function indexInfo( $table, $index, $fname = __METHOD__ ); + + /** + * Get the number of rows affected by the last write query + * @see http://www.php.net/mysql_affected_rows + * + * @return int + */ + abstract function affectedRows(); + + /** + * Wrapper for addslashes() + * + * @param string $s String to be slashed. + * @return string Slashed string. + */ + abstract function strencode( $s ); + + /** + * Returns a wikitext link to the DB's website, e.g., + * return "[http://www.mysql.com/ MySQL]"; + * Should at least contain plain text, if for some reason + * your database has no website. + * + * @return string Wikitext of a link to the server software's web site + */ + abstract function getSoftwareLink(); + + /** + * A string describing the current software version, like from + * mysql_get_server_info(). + * + * @return string Version information from the database server. + */ + abstract function getServerVersion(); /** * Constructor. @@ -760,32 +765,19 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { * * @param array $params Parameters passed from DatabaseBase::factory() */ - function __construct( $params = null ) { + function __construct( array $params ) { global $wgDBprefix, $wgDBmwschema, $wgCommandLineMode, $wgDebugDBTransactions; $this->mTrxAtomicLevels = new SplStack; - if ( is_array( $params ) ) { // MW 1.22 - $server = $params['host']; - $user = $params['user']; - $password = $params['password']; - $dbName = $params['dbname']; - $flags = $params['flags']; - $tablePrefix = $params['tablePrefix']; - $schema = $params['schema']; - $foreign = $params['foreign']; - } else { // legacy calling pattern - wfDeprecated( __METHOD__ . " method called without parameter array.", "1.23" ); - $args = func_get_args(); - $server = isset( $args[0] ) ? $args[0] : false; - $user = isset( $args[1] ) ? $args[1] : false; - $password = isset( $args[2] ) ? $args[2] : false; - $dbName = isset( $args[3] ) ? $args[3] : false; - $flags = isset( $args[4] ) ? $args[4] : 0; - $tablePrefix = isset( $args[5] ) ? $args[5] : 'get from global'; - $schema = 'get from global'; - $foreign = isset( $args[6] ) ? $args[6] : false; - } + $server = $params['host']; + $user = $params['user']; + $password = $params['password']; + $dbName = $params['dbname']; + $flags = $params['flags']; + $tablePrefix = $params['tablePrefix']; + $schema = $params['schema']; + $foreign = $params['foreign']; $this->mFlags = $flags; if ( $this->mFlags & DBO_DEFAULT ) { @@ -818,6 +810,10 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { $this->mForeign = $foreign; + if ( isset( $params['trxProfiler'] ) ) { + $this->trxProfiler = $params['trxProfiler']; // override + } + if ( $user ) { $this->open( $server, $user, $password, $dbName ); } @@ -905,18 +901,17 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { $class = 'Database' . ucfirst( $driver ); if ( class_exists( $class ) && is_subclass_of( $class, 'DatabaseBase' ) ) { - $params = array( - 'host' => isset( $p['host'] ) ? $p['host'] : false, - 'user' => isset( $p['user'] ) ? $p['user'] : false, - 'password' => isset( $p['password'] ) ? $p['password'] : false, - 'dbname' => isset( $p['dbname'] ) ? $p['dbname'] : false, - 'flags' => isset( $p['flags'] ) ? $p['flags'] : 0, - 'tablePrefix' => isset( $p['tablePrefix'] ) ? $p['tablePrefix'] : 'get from global', - 'schema' => isset( $p['schema'] ) ? $p['schema'] : $defaultSchemas[$dbType], - 'foreign' => isset( $p['foreign'] ) ? $p['foreign'] : false - ); - - return new $class( $params ); + // Resolve some defaults for b/c + $p['host'] = isset( $p['host'] ) ? $p['host'] : false; + $p['user'] = isset( $p['user'] ) ? $p['user'] : false; + $p['password'] = isset( $p['password'] ) ? $p['password'] : false; + $p['dbname'] = isset( $p['dbname'] ) ? $p['dbname'] : false; + $p['flags'] = isset( $p['flags'] ) ? $p['flags'] : 0; + $p['tablePrefix'] = isset( $p['tablePrefix'] ) ? $p['tablePrefix'] : 'get from global'; + $p['schema'] = isset( $p['schema'] ) ? $p['schema'] : $defaultSchemas[$dbType]; + $p['foreign'] = isset( $p['foreign'] ) ? $p['foreign'] : false; + + return new $class( $p ); } else { return null; } @@ -954,6 +949,23 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { $this->mPHPError = $errstr; } + /** + * Create a log context to pass to wfLogDBError or other logging functions. + * + * @param array $extras Additional data to add to context + * @return array + */ + protected function getLogContext( array $extras = array() ) { + return array_merge( + array( + 'db_server' => $this->mServer, + 'db_name' => $this->mDBname, + 'db_user' => $this->mUser, + ), + $extras + ); + } + /** * Closes a database connection. * if it is open : commits any open transactions @@ -1026,6 +1038,20 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { return !preg_match( '/^(?:SELECT|BEGIN|ROLLBACK|COMMIT|SET|SHOW|EXPLAIN|\(SELECT)\b/i', $sql ); } + /** + * Determine whether a SQL statement is sensitive to isolation level. + * A SQL statement is considered transactable if its result could vary + * depending on the transaction isolation level. Operational commands + * such as 'SET' and 'SHOW' are not considered to be transactable. + * + * @param string $sql + * @return bool + */ + public function isTransactableQuery( $sql ) { + $verb = substr( $sql, 0, strcspn( $sql, " \t\r\n" ) ); + return !in_array( $verb, array( 'BEGIN', 'COMMIT', 'ROLLBACK', 'SHOW', 'SET' ) ); + } + /** * Run an SQL query and return the result. Normally throws a DBQueryError * on failure. If errors are ignored, returns false instead. @@ -1052,9 +1078,14 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { global $wgUser, $wgDebugDBTransactions, $wgDebugDumpSqlLength; $this->mLastQuery = $sql; - if ( $this->isWriteQuery( $sql ) ) { + + $isWriteQuery = $this->isWriteQuery( $sql ); + if ( $isWriteQuery ) { + if ( !$this->mDoneWrites ) { + wfDebug( __METHOD__ . ': Writes done: ' . + DatabaseBase::generalizeSQL( $sql ) . "\n" ); + } # Set a flag indicating that writes have been done - wfDebug( __METHOD__ . ': Writes done: ' . DatabaseBase::generalizeSQL( $sql ) . "\n" ); $this->mDoneWrites = microtime( true ); } @@ -1073,50 +1104,38 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { // Or, for one-word queries (like "BEGIN" or COMMIT") add it to the end (bug 42598) $commentedSql = preg_replace( '/\s|$/', " /* $fname $userName */ ", $sql, 1 ); - # If DBO_TRX is set, start a transaction - if ( ( $this->mFlags & DBO_TRX ) && !$this->mTrxLevel && - $sql != 'BEGIN' && $sql != 'COMMIT' && $sql != 'ROLLBACK' - ) { - # Avoid establishing transactions for SHOW and SET statements too - - # that would delay transaction initializations to once connection - # is really used by application - $sqlstart = substr( $sql, 0, 10 ); // very much worth it, benchmark certified(tm) - if ( strpos( $sqlstart, "SHOW " ) !== 0 && strpos( $sqlstart, "SET " ) !== 0 ) { - if ( $wgDebugDBTransactions ) { - wfDebug( "Implicit transaction start.\n" ); - } - $this->begin( __METHOD__ . " ($fname)" ); - $this->mTrxAutomatic = true; + if ( !$this->mTrxLevel && $this->getFlag( DBO_TRX ) && $this->isTransactableQuery( $sql ) ) { + if ( $wgDebugDBTransactions ) { + wfDebug( "Implicit transaction start.\n" ); } + $this->begin( __METHOD__ . " ($fname)" ); + $this->mTrxAutomatic = true; } # Keep track of whether the transaction has write queries pending - if ( $this->mTrxLevel && !$this->mTrxDoneWrites && $this->isWriteQuery( $sql ) ) { + if ( $this->mTrxLevel && !$this->mTrxDoneWrites && $isWriteQuery ) { $this->mTrxDoneWrites = true; - Profiler::instance()->transactionWritingIn( + $this->getTransactionProfiler()->transactionWritingIn( $this->mServer, $this->mDBname, $this->mTrxShortId ); } - $queryProf = ''; - $totalProf = ''; $isMaster = !is_null( $this->getLBInfo( 'master' ) ); + # generalizeSQL will probably cut down the query to reasonable + # logging size most of the time. The substr is really just a sanity check. + if ( $isMaster ) { + $queryProf = 'query-m: ' . substr( DatabaseBase::generalizeSQL( $sql ), 0, 255 ); + $totalProf = 'DatabaseBase::query-master'; + } else { + $queryProf = 'query: ' . substr( DatabaseBase::generalizeSQL( $sql ), 0, 255 ); + $totalProf = 'DatabaseBase::query'; + } + # Include query transaction state + $queryProf .= $this->mTrxShortId ? " [TRX#{$this->mTrxShortId}]" : ""; - if ( !Profiler::instance()->isStub() ) { - # generalizeSQL will probably cut down the query to reasonable - # logging size most of the time. The substr is really just a sanity check. - if ( $isMaster ) { - $queryProf = 'query-m: ' . substr( DatabaseBase::generalizeSQL( $sql ), 0, 255 ); - $totalProf = 'DatabaseBase::query-master'; - } else { - $queryProf = 'query: ' . substr( DatabaseBase::generalizeSQL( $sql ), 0, 255 ); - $totalProf = 'DatabaseBase::query'; - } - # Include query transaction state - $queryProf .= $this->mTrxShortId ? " [TRX#{$this->mTrxShortId}]" : ""; - - $trx = $this->mTrxLevel ? 'TRX=yes' : 'TRX=no'; - wfProfileIn( $totalProf ); - wfProfileIn( $queryProf ); + $profiler = Profiler::instance(); + if ( !$profiler instanceof ProfilerStub ) { + $totalProfSection = $profiler->scopedProfileIn( $totalProf ); + $queryProfSection = $profiler->scopedProfileIn( $queryProf ); } if ( $this->debug() ) { @@ -1139,7 +1158,11 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { } # Do the query and handle errors + $startTime = microtime( true ); $ret = $this->doQuery( $commentedSql ); + # Log the query time and feed it into the DB trx profiler + $this->getTransactionProfiler()->recordQueryCompletion( + $queryProf, $startTime, $isWriteQuery, $this->affectedRows() ); MWDebug::queryTime( $queryId ); @@ -1155,23 +1178,22 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { $lastError = $this->lastError(); $lastErrno = $this->lastErrno(); if ( $this->ping() ) { - global $wgRequestTime; wfDebug( "Reconnected\n" ); - $sqlx = $wgDebugDumpSqlLength ? substr( $commentedSql, 0, $wgDebugDumpSqlLength ) - : $commentedSql; - $sqlx = strtr( $sqlx, "\t\n", ' ' ); - $elapsed = round( microtime( true ) - $wgRequestTime, 3 ); - if ( $elapsed < 300 ) { - # Not a database error to lose a transaction after a minute or two - wfLogDBError( "Connection lost and reconnected after {$elapsed}s, query: $sqlx" ); - } + $server = $this->getServer(); + $msg = __METHOD__ . ": lost connection to $server; reconnected"; + wfDebugLog( 'DBPerformance', "$msg:\n" . wfBacktrace( true ) ); + if ( $hadTrx ) { # Leave $ret as false and let an error be reported. # Callers may catch the exception and continue to use the DB. $this->reportQueryError( $lastError, $lastErrno, $sql, $fname, $tempIgnore ); } else { # Should be safe to silently retry (no trx and thus no callbacks) + $startTime = microtime( true ); $ret = $this->doQuery( $commentedSql ); + # Log the query time and feed it into the DB trx profiler + $this->getTransactionProfiler()->recordQueryCompletion( + $queryProf, $startTime, $isWriteQuery, $this->affectedRows() ); } } else { wfDebug( "Failed\n" ); @@ -1179,15 +1201,17 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { } if ( false === $ret ) { - $this->reportQueryError( $this->lastError(), $this->lastErrno(), $sql, $fname, $tempIgnore ); + $this->reportQueryError( + $this->lastError(), $this->lastErrno(), $sql, $fname, $tempIgnore ); } - if ( !Profiler::instance()->isStub() ) { - wfProfileOut( $queryProf ); - wfProfileOut( $totalProf ); - } + $res = $this->resultObject( $ret ); + + // Destroy profile sections in the opposite order to their creation + $queryProfSection = false; + $totalProfSection = false; - return $this->resultObject( $ret ); + return $res; } /** @@ -1202,16 +1226,22 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { * @throws DBQueryError */ public function reportQueryError( $error, $errno, $sql, $fname, $tempIgnore = false ) { - # Ignore errors during error handling to avoid infinite recursion - $ignore = $this->ignoreErrors( true ); ++$this->mErrorCount; - if ( $ignore || $tempIgnore ) { + if ( $this->ignoreErrors() || $tempIgnore ) { wfDebug( "SQL ERROR (ignored): $error\n" ); - $this->ignoreErrors( $ignore ); } else { $sql1line = mb_substr( str_replace( "\n", "\\n", $sql ), 0, 5 * 1024 ); - wfLogDBError( "$fname\t{$this->mServer}\t$errno\t$error\t$sql1line" ); + wfLogDBError( + "{fname}\t{db_server}\t{errno}\t{error}\t{sql1line}", + $this->getLogContext( array( + 'method' => __METHOD__, + 'errno' => $errno, + 'error' => $error, + 'sql1line' => $sql1line, + 'fname' => $fname, + ) ) + ); wfDebug( "SQL ERROR: " . $error . "\n" ); throw new DBQueryError( $this, $error, $errno, $sql, $fname ); } @@ -1348,9 +1378,13 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { * * @return bool|mixed The value from the field, or false on failure. */ - public function selectField( $table, $var, $cond = '', $fname = __METHOD__, - $options = array() + public function selectField( + $table, $var, $cond = '', $fname = __METHOD__, $options = array() ) { + if ( $var === '*' ) { // sanity + throw new DBUnexpectedError( $this, "Cannot use a * field: got '$var'" ); + } + if ( !is_array( $options ) ) { $options = array( $options ); } @@ -1358,7 +1392,6 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { $options['LIMIT'] = 1; $res = $this->select( $table, $var, $cond, $fname, $options ); - if ( $res === false || !$this->numRows( $res ) ) { return false; } @@ -1372,6 +1405,48 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { } } + /** + * A SELECT wrapper which returns a list of single field values from result rows. + * + * Usually throws a DBQueryError on failure. If errors are explicitly + * ignored, returns false on failure. + * + * If no result rows are returned from the query, false is returned. + * + * @param string|array $table Table name. See DatabaseBase::select() for details. + * @param string $var The field name to select. This must be a valid SQL + * fragment: do not use unvalidated user input. + * @param string|array $cond The condition array. See DatabaseBase::select() for details. + * @param string $fname The function name of the caller. + * @param string|array $options The query options. See DatabaseBase::select() for details. + * + * @return bool|array The values from the field, or false on failure + * @since 1.25 + */ + public function selectFieldValues( + $table, $var, $cond = '', $fname = __METHOD__, $options = array() + ) { + if ( $var === '*' ) { // sanity + throw new DBUnexpectedError( $this, "Cannot use a * field: got '$var'" ); + } + + if ( !is_array( $options ) ) { + $options = array( $options ); + } + + $res = $this->select( $table, $var, $cond, $fname, $options ); + if ( $res === false ) { + return false; + } + + $values = array(); + foreach ( $res as $row ) { + $values[] = $row->$var; + } + + return $values; + } + /** * Returns an optional USE INDEX clause to go after the table, and a * string to go at the end of the query. @@ -1558,9 +1633,10 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { * - If the value of such an array element is a scalar (such as a * string), it will be treated as data and thus quoted appropriately. * If it is null, an IS NULL clause will be added. - * - If the value is an array, an IN(...) clause will be constructed, - * such that the field name may match any of the elements in the - * array. The elements of the array will be quoted. + * - If the value is an array, an IN (...) clause will be constructed + * from its non-null elements, and an IS NULL clause will be added + * if null is present, such that the field may match any of the + * elements in the array. The non-null elements will be quoted. * * Note that expressions are often DBMS-dependent in their syntax. * DBMS-independent wrappers are provided for constructing several types of @@ -1779,7 +1855,7 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { if ( $res ) { $row = $this->fetchRow( $res ); - $rows = ( isset( $row['rowcount'] ) ) ? $row['rowcount'] : 0; + $rows = ( isset( $row['rowcount'] ) ) ? (int)$row['rowcount'] : 0; } return $rows; @@ -1809,7 +1885,7 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { if ( $res ) { $row = $this->fetchRow( $res ); - $rows = ( isset( $row['rowcount'] ) ) ? $row['rowcount'] : 0; + $rows = ( isset( $row['rowcount'] ) ) ? (int)$row['rowcount'] : 0; } return $rows; @@ -2117,16 +2193,36 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { } elseif ( ( $mode == LIST_SET ) && is_numeric( $field ) ) { $list .= "$value"; } elseif ( ( $mode == LIST_AND || $mode == LIST_OR ) && is_array( $value ) ) { - if ( count( $value ) == 0 ) { + // Remove null from array to be handled separately if found + $includeNull = false; + foreach ( array_keys( $value, null, true ) as $nullKey ) { + $includeNull = true; + unset( $value[$nullKey] ); + } + if ( count( $value ) == 0 && !$includeNull ) { throw new MWException( __METHOD__ . ": empty input for field $field" ); - } elseif ( count( $value ) == 1 ) { - // Special-case single values, as IN isn't terribly efficient - // Don't necessarily assume the single key is 0; we don't - // enforce linear numeric ordering on other arrays here. - $value = array_values( $value ); - $list .= $field . " = " . $this->addQuotes( $value[0] ); + } elseif ( count( $value ) == 0 ) { + // only check if $field is null + $list .= "$field IS NULL"; } else { - $list .= $field . " IN (" . $this->makeList( $value ) . ") "; + // IN clause contains at least one valid element + if ( $includeNull ) { + // Group subconditions to ensure correct precedence + $list .= '('; + } + if ( count( $value ) == 1 ) { + // Special-case single values, as IN isn't terribly efficient + // Don't necessarily assume the single key is 0; we don't + // enforce linear numeric ordering on other arrays here. + $value = array_values( $value ); + $list .= $field . " = " . $this->addQuotes( $value[0] ); + } else { + $list .= $field . " IN (" . $this->makeList( $value ) . ") "; + } + // if null present in array, append IS NULL + if ( $includeNull ) { + $list .= " OR $field IS NULL)"; + } } } elseif ( $value === null ) { if ( $mode == LIST_AND || $mode == LIST_OR ) { @@ -2322,7 +2418,7 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { # Split database and table into proper variables. # We reverse the explode so that database.table and table both output # the correct table. - $dbDetails = explode( '.', $name, 2 ); + $dbDetails = explode( '.', $name, 3 ); if ( count( $dbDetails ) == 3 ) { list( $database, $schema, $table ) = $dbDetails; # We don't want any prefix added in this case @@ -2552,12 +2648,16 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { } /** - * Get the name of an index in a given table + * Get the name of an index in a given table. * + * @protected Don't use outside of DatabaseBase and childs * @param string $index * @return string */ - protected function indexName( $index ) { + public function indexName( $index ) { + // @FIXME: Make this protected once we move away from PHP 5.3 + // Needs to be public because of usage in closure (in DatabaseBase::replaceVars) + // Backwards-compatibility hack $renamed = array( 'ar_usertext_timestamp' => 'usertext_timestamp', @@ -2575,10 +2675,13 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { /** * Adds quotes and backslashes. * - * @param string $s + * @param string|Blob $s * @return string */ public function addQuotes( $s ) { + if ( $s instanceof Blob ) { + $s = $s->fetch(); + } if ( $s === null ) { return 'NULL'; } else { @@ -3218,41 +3321,40 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { * @return bool */ public function deadlockLoop() { - $this->begin( __METHOD__ ); $args = func_get_args(); $function = array_shift( $args ); - $oldIgnore = $this->ignoreErrors( true ); $tries = self::DEADLOCK_TRIES; - if ( is_array( $function ) ) { $fname = $function[0]; } else { $fname = $function; } - do { - $retVal = call_user_func_array( $function, $args ); - $error = $this->lastError(); - $errno = $this->lastErrno(); - $sql = $this->lastQuery(); + $this->begin( __METHOD__ ); - if ( $errno ) { + $e = null; + do { + try { + $retVal = call_user_func_array( $function, $args ); + break; + } catch ( DBQueryError $e ) { + $error = $this->lastError(); + $errno = $this->lastErrno(); + $sql = $this->lastQuery(); if ( $this->wasDeadlock() ) { - # Retry + // Retry after a randomized delay usleep( mt_rand( self::DEADLOCK_DELAY_MIN, self::DEADLOCK_DELAY_MAX ) ); } else { - $this->reportQueryError( $error, $errno, $sql, $fname ); + // Throw the error back up + throw $e; } } - } while ( $this->wasDeadlock() && --$tries > 0 ); - - $this->ignoreErrors( $oldIgnore ); + } while ( --$tries > 0 ); if ( $tries <= 0 ) { + // Too many deadlocks; give up $this->rollback( __METHOD__ ); - $this->reportQueryError( $error, $errno, $sql, $fname ); - - return false; + throw $e; } else { $this->commit( __METHOD__ ); @@ -3486,7 +3588,12 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { $msg = "$fname: Transaction already in progress (from {$this->mTrxFname}), " . " performing implicit commit!"; wfWarn( $msg ); - wfLogDBError( $msg ); + wfLogDBError( $msg, + $this->getLogContext( array( + 'method' => __METHOD__, + 'fname' => $fname, + ) ) + ); } else { // if the transaction was automatic and has done write operations, // log it if $wgDebugDBTransactions is enabled. @@ -3500,7 +3607,8 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { $this->runOnTransactionPreCommitCallbacks(); $this->doCommit( $fname ); if ( $this->mTrxDoneWrites ) { - Profiler::instance()->transactionWritingOut( + $this->mDoneWrites = microtime( true ); + $this->getTransactionProfiler()->transactionWritingOut( $this->mServer, $this->mDBname, $this->mTrxShortId ); } $this->runOnTransactionIdleCallbacks(); @@ -3512,6 +3620,7 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { } $this->doBegin( $fname ); + $this->mTrxTimestamp = microtime( true ); $this->mTrxFname = $fname; $this->mTrxDoneWrites = false; $this->mTrxAutomatic = false; @@ -3579,7 +3688,8 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { $this->runOnTransactionPreCommitCallbacks(); $this->doCommit( $fname ); if ( $this->mTrxDoneWrites ) { - Profiler::instance()->transactionWritingOut( + $this->mDoneWrites = microtime( true ); + $this->getTransactionProfiler()->transactionWritingOut( $this->mServer, $this->mDBname, $this->mTrxShortId ); } $this->runOnTransactionIdleCallbacks(); @@ -3609,6 +3719,7 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { * calling rollback when no transaction is in progress. This will silently * break any ongoing explicit transaction. Only set the flush flag if you * are sure that it is safe to ignore these warnings in your context. + * @throws DBUnexpectedError * @since 1.23 Added $flush parameter */ final public function rollback( $fname = __METHOD__, $flush = '' ) { @@ -3637,7 +3748,7 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { $this->mTrxPreCommitCallbacks = array(); // cancel $this->mTrxAtomicLevels = new SplStack; if ( $this->mTrxDoneWrites ) { - Profiler::instance()->transactionWritingOut( + $this->getTransactionProfiler()->transactionWritingOut( $this->mServer, $this->mDBname, $this->mTrxShortId ); } } @@ -3835,10 +3946,13 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { * in result objects. Pass the object through this function to return the * original string. * - * @param string $b + * @param string|Blob $b * @return string */ public function decodeBlob( $b ) { + if ( $b instanceof Blob ) { + $b = $b->fetch(); + } return $b; } @@ -3888,7 +4002,7 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { try { $error = $this->sourceStream( $fp, $lineCallback, $resultCallback, $fname, $inputCallback ); - } catch ( MWException $e ) { + } catch ( Exception $e ) { fclose( $fp ); throw $e; } @@ -4019,47 +4133,49 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { * * - '{$var}' should be used for text and is passed through the database's * addQuotes method. - * - `{$var}` should be used for identifiers (eg: table and database names), - * it is passed through the database's addIdentifierQuotes method which + * - `{$var}` should be used for identifiers (e.g. table and database names). + * It is passed through the database's addIdentifierQuotes method which * can be overridden if the database uses something other than backticks. - * - / *$var* / is just encoded, besides traditional table prefix and - * table options its use should be avoided. + * - / *_* / or / *$wgDBprefix* / passes the name that follows through the + * database's tableName method. + * - / *i* / passes the name that follows through the database's indexName method. + * - In all other cases, / *$var* / is left unencoded. Except for table options, + * its use should be avoided. In 1.24 and older, string encoding was applied. * * @param string $ins SQL statement to replace variables in * @return string The new SQL statement with variables replaced */ - protected function replaceSchemaVars( $ins ) { - $vars = $this->getSchemaVars(); - foreach ( $vars as $var => $value ) { - // replace '{$var}' - $ins = str_replace( '\'{$' . $var . '}\'', $this->addQuotes( $value ), $ins ); - // replace `{$var}` - $ins = str_replace( '`{$' . $var . '}`', $this->addIdentifierQuotes( $value ), $ins ); - // replace /*$var*/ - $ins = str_replace( '/*$' . $var . '*/', $this->strencode( $value ), $ins ); - } - - return $ins; - } - - /** - * Replace variables in sourced SQL - * - * @param string $ins - * @return string - */ protected function replaceVars( $ins ) { - $ins = $this->replaceSchemaVars( $ins ); - - // Table prefixes - $ins = preg_replace_callback( '!/\*(?:\$wgDBprefix|_)\*/([a-zA-Z_0-9]*)!', - array( $this, 'tableNameCallback' ), $ins ); - - // Index names - $ins = preg_replace_callback( '!/\*i\*/([a-zA-Z_0-9]*)!', - array( $this, 'indexNameCallback' ), $ins ); - - return $ins; + $that = $this; + $vars = $this->getSchemaVars(); + return preg_replace_callback( + '! + /\* (\$wgDBprefix|[_i]) \*/ (\w*) | # 1-2. tableName, indexName + \'\{\$ (\w+) }\' | # 3. addQuotes + `\{\$ (\w+) }` | # 4. addIdentifierQuotes + /\*\$ (\w+) \*/ # 5. leave unencoded + !x', + function ( $m ) use ( $that, $vars ) { + // Note: Because of , + // check for both nonexistent keys *and* the empty string. + if ( isset( $m[1] ) && $m[1] !== '' ) { + if ( $m[1] === 'i' ) { + return $that->indexName( $m[2] ); + } else { + return $that->tableName( $m[2] ); + } + } elseif ( isset( $m[3] ) && $m[3] !== '' && array_key_exists( $m[3], $vars ) ) { + return $that->addQuotes( $vars[$m[3]] ); + } elseif ( isset( $m[4] ) && $m[4] !== '' && array_key_exists( $m[4], $vars ) ) { + return $that->addIdentifierQuotes( $vars[$m[4]] ); + } elseif ( isset( $m[5] ) && $m[5] !== '' && array_key_exists( $m[5], $vars ) ) { + return $vars[$m[5]]; + } else { + return $m[0]; + } + }, + $ins + ); } /** @@ -4088,26 +4204,6 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { return array(); } - /** - * Table name callback - * - * @param array $matches - * @return string - */ - protected function tableNameCallback( $matches ) { - return $this->tableName( $matches[1] ); - } - - /** - * Index name callback - * - * @param array $matches - * @return string - */ - protected function indexNameCallback( $matches ) { - return $this->indexName( $matches[1] ); - } - /** * Check to see if a named lock is available. This is non-blocking. * diff --git a/includes/db/DatabaseError.php b/includes/db/DatabaseError.php index 2dfec41d..86950a89 100644 --- a/includes/db/DatabaseError.php +++ b/includes/db/DatabaseError.php @@ -168,12 +168,12 @@ class DBConnectionError extends DBExpectedError { if ( $wgShowHostnames || $wgShowSQLErrors ) { $info = str_replace( '$1', Html::element( 'span', array( 'dir' => 'ltr' ), $this->error ), - htmlspecialchars( $this->msg( 'dberr-info', '(Cannot contact the database server: $1)' ) ) + htmlspecialchars( $this->msg( 'dberr-info', '(Cannot access the database: $1)' ) ) ); } else { $info = htmlspecialchars( $this->msg( 'dberr-info-hidden', - '(Cannot contact the database server)' + '(Cannot access the database)' ) ); } @@ -229,7 +229,7 @@ class DBConnectionError extends DBExpectedError { return; } - } catch ( MWException $e ) { + } catch ( Exception $e ) { // Do nothing, just use the default page } } diff --git a/includes/db/DatabaseMssql.php b/includes/db/DatabaseMssql.php index af3cc72d..2b8f395f 100644 --- a/includes/db/DatabaseMssql.php +++ b/includes/db/DatabaseMssql.php @@ -380,6 +380,9 @@ class DatabaseMssql extends DatabaseBase { * (optional) (e.g. array( 'page' => array('LEFT JOIN','page_latest=rev_id') ) * @return mixed Database result resource (feed to Database::fetchObject * or whatever), or false on failure + * @throws DBQueryError + * @throws DBUnexpectedError + * @throws Exception */ public function select( $table, $vars, $conds = '', $fname = __METHOD__, $options = array(), $join_conds = array() @@ -515,7 +518,7 @@ class DatabaseMssql extends DatabaseBase { $row = $this->fetchRow( $res ); if ( isset( $row['EstimateRows'] ) ) { - $rows = $row['EstimateRows']; + $rows = (int)$row['EstimateRows']; } } @@ -574,8 +577,8 @@ class DatabaseMssql extends DatabaseBase { * @param array $arrToInsert * @param string $fname * @param array $options - * @throws DBQueryError * @return bool + * @throws Exception */ public function insert( $table, $arrToInsert, $fname = __METHOD__, $options = array() ) { # No rows to insert, easy just return now @@ -692,7 +695,7 @@ class DatabaseMssql extends DatabaseBase { if ( !is_null( $identity ) ) { // then we want to get the identity column value we were assigned and save it off $row = $ret->fetchObject(); - if( is_object( $row ) ){ + if ( is_object( $row ) ) { $this->mInsertId = $row->$identity; } } @@ -713,8 +716,8 @@ class DatabaseMssql extends DatabaseBase { * @param string $fname * @param array $insertOptions * @param array $selectOptions - * @throws DBQueryError * @return null|ResultWrapper + * @throws Exception */ public function insertSelect( $destTable, $srcTable, $varMap, $conds, $fname = __METHOD__, $insertOptions = array(), $selectOptions = array() @@ -761,6 +764,9 @@ class DatabaseMssql extends DatabaseBase { * - IGNORE: Ignore unique key conflicts * - LOW_PRIORITY: MySQL-specific, see MySQL manual. * @return bool + * @throws DBUnexpectedError + * @throws Exception + * @throws MWException */ function update( $table, $values, $conds, $fname = __METHOD__, $options = array() ) { $table = $this->tableName( $table ); @@ -806,64 +812,27 @@ class DatabaseMssql extends DatabaseBase { 'DatabaseBase::makeList called with incorrect parameters' ); } - $first = true; - $list = ''; + if ( $mode != LIST_NAMES ) { + // In MS SQL, values need to be specially encoded when they are + // inserted into binary fields. Perform this necessary encoding + // for the specified set of columns. + foreach ( array_keys( $a ) as $field ) { + if ( !isset( $binaryColumns[$field] ) ) { + continue; + } - foreach ( $a as $field => $value ) { - if ( $mode != LIST_NAMES && isset( $binaryColumns[$field] ) ) { - if ( is_array( $value ) ) { - foreach ( $value as &$v ) { + if ( is_array( $a[$field] ) ) { + foreach ( $a[$field] as &$v ) { $v = new MssqlBlob( $v ); } + unset( $v ); } else { - $value = new MssqlBlob( $value ); - } - } - - if ( !$first ) { - if ( $mode == LIST_AND ) { - $list .= ' AND '; - } elseif ( $mode == LIST_OR ) { - $list .= ' OR '; - } else { - $list .= ','; + $a[$field] = new MssqlBlob( $a[$field] ); } - } else { - $first = false; - } - - if ( ( $mode == LIST_AND || $mode == LIST_OR ) && is_numeric( $field ) ) { - $list .= "($value)"; - } elseif ( ( $mode == LIST_SET ) && is_numeric( $field ) ) { - $list .= "$value"; - } elseif ( ( $mode == LIST_AND || $mode == LIST_OR ) && is_array( $value ) ) { - if ( count( $value ) == 0 ) { - throw new MWException( __METHOD__ . ": empty input for field $field" ); - } elseif ( count( $value ) == 1 ) { - // Special-case single values, as IN isn't terribly efficient - // Don't necessarily assume the single key is 0; we don't - // enforce linear numeric ordering on other arrays here. - $value = array_values( $value ); - $list .= $field . " = " . $this->addQuotes( $value[0] ); - } else { - $list .= $field . " IN (" . $this->makeList( $value ) . ") "; - } - } elseif ( $value === null ) { - if ( $mode == LIST_AND || $mode == LIST_OR ) { - $list .= "$field IS "; - } elseif ( $mode == LIST_SET ) { - $list .= "$field = "; - } - $list .= 'NULL'; - } else { - if ( $mode == LIST_AND || $mode == LIST_OR || $mode == LIST_SET ) { - $list .= "$field = "; - } - $list .= $mode == LIST_NAMES ? $value : $this->addQuotes( $value ); } } - return $list; + return parent::makeList( $a, $mode ); } /** @@ -893,6 +862,7 @@ class DatabaseMssql extends DatabaseBase { * @param int $limit The SQL limit * @param bool|int $offset The SQL offset (default false) * @return array|string + * @throws DBUnexpectedError */ public function limitResult( $sql, $limit, $offset = false ) { if ( $offset === false || $offset == 0 ) { @@ -953,12 +923,8 @@ class DatabaseMssql extends DatabaseBase { // Matches: LIMIT {[offset,] row_count | row_count OFFSET offset} $pattern = '/\bLIMIT\s+((([0-9]+)\s*,\s*)?([0-9]+)(\s+OFFSET\s+([0-9]+))?)/i'; if ( preg_match( $pattern, $sql, $matches ) ) { - // row_count = $matches[4] $row_count = $matches[4]; - // offset = $matches[3] OR $matches[6] - $offset = $matches[3] or - $offset = $matches[6] or - $offset = false; + $offset = $matches[3] ?: $matches[6] ?: false; // strip the matching LIMIT clause out $sql = str_replace( $matches[0], '', $sql ); @@ -1128,7 +1094,7 @@ class DatabaseMssql extends DatabaseBase { } /** - * @param string $s + * @param string|Blob $s * @return string */ public function addQuotes( $s ) { diff --git a/includes/db/DatabaseMysql.php b/includes/db/DatabaseMysql.php index dc4a67df..823d9b67 100644 --- a/includes/db/DatabaseMysql.php +++ b/includes/db/DatabaseMysql.php @@ -142,13 +142,6 @@ class DatabaseMysql extends DatabaseMysqlBase { return mysql_select_db( $db, $this->mConn ); } - /** - * @return string - */ - function getServerVersion() { - return mysql_get_server_info( $this->mConn ); - } - protected function mysqlFreeResult( $res ) { return mysql_free_result( $res ); } diff --git a/includes/db/DatabaseMysqlBase.php b/includes/db/DatabaseMysqlBase.php index ba0f39ff..aac95a8c 100644 --- a/includes/db/DatabaseMysqlBase.php +++ b/includes/db/DatabaseMysqlBase.php @@ -38,6 +38,9 @@ abstract class DatabaseMysqlBase extends DatabaseBase { protected $mFakeMaster = false; + /** @var string|null */ + private $serverVersion = null; + /** * @return string */ @@ -55,7 +58,6 @@ abstract class DatabaseMysqlBase extends DatabaseBase { */ function open( $server, $user, $password, $dbName ) { global $wgAllDBsAreLocalhost, $wgSQLMode; - wfProfileIn( __METHOD__ ); # Debugging hack -- fake cluster if ( $wgAllDBsAreLocalhost ) { @@ -69,8 +71,6 @@ abstract class DatabaseMysqlBase extends DatabaseBase { $this->mPassword = $password; $this->mDBname = $dbName; - wfProfileIn( "dbconnect-$server" ); - # The kernel's default SYN retransmission period is far too slow for us, # so we use a short timeout plus a manual retry. Retrying means that a small # but finite rate of SYN packet loss won't cause user-visible errors. @@ -79,27 +79,27 @@ abstract class DatabaseMysqlBase extends DatabaseBase { try { $this->mConn = $this->mysqlConnect( $realServer ); } catch ( Exception $ex ) { - wfProfileOut( "dbconnect-$server" ); - wfProfileOut( __METHOD__ ); $this->restoreErrorHandler(); throw $ex; } $error = $this->restoreErrorHandler(); - wfProfileOut( "dbconnect-$server" ); - # Always log connection errors if ( !$this->mConn ) { if ( !$error ) { $error = $this->lastError(); } - wfLogDBError( "Error connecting to {$this->mServer}: $error" ); + wfLogDBError( + "Error connecting to {db_server}: {error}", + $this->getLogContext( array( + 'method' => __METHOD__, + 'error' => $error, + ) ) + ); wfDebug( "DB connection error\n" . "Server: $server, User: $user, Password: " . substr( $password, 0, 3 ) . "..., error: " . $error . "\n" ); - wfProfileOut( __METHOD__ ); - $this->reportConnectionError( $error ); } @@ -108,12 +108,15 @@ abstract class DatabaseMysqlBase extends DatabaseBase { $success = $this->selectDB( $dbName ); wfRestoreWarnings(); if ( !$success ) { - wfLogDBError( "Error selecting database $dbName on server {$this->mServer}" ); + wfLogDBError( + "Error selecting database {db_name} on server {db_server}", + $this->getLogContext( array( + 'method' => __METHOD__, + ) ) + ); wfDebug( "Error selecting database $dbName on server {$this->mServer} " . "from client host " . wfHostname() . "\n" ); - wfProfileOut( __METHOD__ ); - $this->reportConnectionError( "Error selecting database $dbName" ); } } @@ -123,20 +126,29 @@ abstract class DatabaseMysqlBase extends DatabaseBase { $this->reportConnectionError( "Error setting character set" ); } + // Abstract over any insane MySQL defaults + $set = array( 'group_concat_max_len = 262144' ); // Set SQL mode, default is turning them all off, can be overridden or skipped with null if ( is_string( $wgSQLMode ) ) { - $mode = $this->addQuotes( $wgSQLMode ); + $set[] = 'sql_mode = ' . $this->addQuotes( $wgSQLMode ); + } + + if ( $set ) { // Use doQuery() to avoid opening implicit transactions (DBO_TRX) - $success = $this->doQuery( "SET sql_mode = $mode", __METHOD__ ); + $success = $this->doQuery( 'SET ' . implode( ', ', $set ), __METHOD__ ); if ( !$success ) { - wfLogDBError( "Error setting sql_mode to $mode on server {$this->mServer}" ); - wfProfileOut( __METHOD__ ); - $this->reportConnectionError( "Error setting sql_mode to $mode" ); + wfLogDBError( + 'Error setting MySQL variables on server {db_server} (check $wgSQLMode)', + $this->getLogContext( array( + 'method' => __METHOD__, + ) ) + ); + $this->reportConnectionError( + 'Error setting MySQL variables on server {db_server} (check $wgSQLMode)' ); } } $this->mOpened = true; - wfProfileOut( __METHOD__ ); return true; } @@ -456,7 +468,7 @@ abstract class DatabaseMysqlBase extends DatabaseBase { $rows *= $plan->rows > 0 ? $plan->rows : 1; // avoid resetting to zero } - return $rows; + return (int)$rows; } /** @@ -652,7 +664,6 @@ abstract class DatabaseMysqlBase extends DatabaseBase { return '0'; // http://dev.mysql.com/doc/refman/5.0/en/miscellaneous-functions.html } - wfProfileIn( __METHOD__ ); # Commit any open transactions $this->commit( __METHOD__, 'flush' ); @@ -661,18 +672,15 @@ abstract class DatabaseMysqlBase extends DatabaseBase { if ( $wait > $timeout * 1e6 ) { wfDebug( "Fake slave timed out waiting for $pos ($wait us)\n" ); - wfProfileOut( __METHOD__ ); return -1; } elseif ( $wait > 0 ) { wfDebug( "Fake slave waiting $wait us\n" ); usleep( $wait ); - wfProfileOut( __METHOD__ ); return 1; } else { wfDebug( "Fake slave up to date ($wait us)\n" ); - wfProfileOut( __METHOD__ ); return 0; } @@ -692,8 +700,6 @@ abstract class DatabaseMysqlBase extends DatabaseBase { } } - wfProfileOut( __METHOD__ ); - return $status; } @@ -763,9 +769,9 @@ abstract class DatabaseMysqlBase extends DatabaseBase { * @return string */ public function getSoftwareLink() { - // MariaDB includes its name in its version string (sent when the connection is opened), - // and this is how MariaDB's version of the mysql command-line client identifies MariaDB - // servers (see the mariadb_connection() function in libmysql/libmysql.c). + // MariaDB includes its name in its version string; this is how MariaDB's version of + // the mysql command-line client identifies MariaDB servers (see mariadb_connection() + // in libmysql/libmysql.c). $version = $this->getServerVersion(); if ( strpos( $version, 'MariaDB' ) !== false || strpos( $version, '-maria-' ) !== false ) { return '[{{int:version-db-mariadb-url}} MariaDB]'; @@ -777,6 +783,19 @@ abstract class DatabaseMysqlBase extends DatabaseBase { return '[{{int:version-db-mysql-url}} MySQL]'; } + /** + * @return string + */ + public function getServerVersion() { + // Not using mysql_get_server_info() or similar for consistency: in the handshake, + // MariaDB 10 adds the prefix "5.5.5-", and only some newer client libraries strip + // it off (see RPL_VERSION_HACK in include/mysql_com.h). + if ( $this->serverVersion === null ) { + $this->serverVersion = $this->selectField( '', 'VERSION()', '', __METHOD__ ); + } + return $this->serverVersion; + } + /** * @param array $options */ @@ -1169,7 +1188,8 @@ abstract class DatabaseMysqlBase extends DatabaseBase { */ class MySQLField implements Field { private $name, $tablename, $default, $max_length, $nullable, - $is_pk, $is_unique, $is_multiple, $is_key, $type, $binary; + $is_pk, $is_unique, $is_multiple, $is_key, $type, $binary, + $is_numeric, $is_blob, $is_unsigned, $is_zerofill; function __construct( $info ) { $this->name = $info->name; @@ -1183,6 +1203,10 @@ class MySQLField implements Field { $this->is_key = ( $this->is_pk || $this->is_unique || $this->is_multiple ); $this->type = $info->type; $this->binary = isset( $info->binary ) ? $info->binary : false; + $this->is_numeric = isset( $info->numeric ) ? $info->numeric : false; + $this->is_blob = isset( $info->blob ) ? $info->blob : false; + $this->is_unsigned = isset( $info->unsigned ) ? $info->unsigned : false; + $this->is_zerofill = isset( $info->zerofill ) ? $info->zerofill : false; } /** @@ -1231,21 +1255,54 @@ class MySQLField implements Field { return $this->is_multiple; } + /** + * @return bool + */ function isBinary() { return $this->binary; } + + /** + * @return bool + */ + function isNumeric() { + return $this->is_numeric; + } + + /** + * @return bool + */ + function isBlob() { + return $this->is_blob; + } + + /** + * @return bool + */ + function isUnsigned() { + return $this->is_unsigned; + } + + /** + * @return bool + */ + function isZerofill() { + return $this->is_zerofill; + } } class MySQLMasterPos implements DBMasterPos { /** @var string */ public $file; - - /** @var int Timestamp */ + /** @var int Position */ public $pos; + /** @var float UNIX timestamp */ + public $asOfTime = 0.0; function __construct( $file, $pos ) { $this->file = $file; $this->pos = $pos; + $this->asOfTime = microtime( true ); } function __toString() { @@ -1271,4 +1328,8 @@ class MySQLMasterPos implements DBMasterPos { return ( $thisPos && $thatPos && $thisPos >= $thatPos ); } + + function asOfTime() { + return $this->asOfTime; + } } diff --git a/includes/db/DatabaseMysqli.php b/includes/db/DatabaseMysqli.php index a03c9aaf..ad12e196 100644 --- a/includes/db/DatabaseMysqli.php +++ b/includes/db/DatabaseMysqli.php @@ -165,13 +165,6 @@ class DatabaseMysqli extends DatabaseMysqlBase { return $this->mConn->select_db( $db ); } - /** - * @return string - */ - function getServerVersion() { - return $this->mConn->server_info; - } - /** * @param mysqli $res * @return bool @@ -231,11 +224,18 @@ class DatabaseMysqli extends DatabaseMysqlBase { */ protected function mysqlFetchField( $res, $n ) { $field = $res->fetch_field_direct( $n ); + + // Add missing properties to result (using flags property) + // which will be part of function mysql-fetch-field for backward compatibility $field->not_null = $field->flags & MYSQLI_NOT_NULL_FLAG; $field->primary_key = $field->flags & MYSQLI_PRI_KEY_FLAG; $field->unique_key = $field->flags & MYSQLI_UNIQUE_KEY_FLAG; $field->multiple_key = $field->flags & MYSQLI_MULTIPLE_KEY_FLAG; $field->binary = $field->flags & MYSQLI_BINARY_FLAG; + $field->numeric = $field->flags & MYSQLI_NUM_FLAG; + $field->blob = $field->flags & MYSQLI_BLOB_FLAG; + $field->unsigned = $field->flags & MYSQLI_UNSIGNED_FLAG; + $field->zerofill = $field->flags & MYSQLI_ZEROFILL_FLAG; return $field; } diff --git a/includes/db/DatabaseOracle.php b/includes/db/DatabaseOracle.php index f031f780..9b00fbd1 100644 --- a/includes/db/DatabaseOracle.php +++ b/includes/db/DatabaseOracle.php @@ -207,29 +207,15 @@ class DatabaseOracle extends DatabaseBase { /** @var array */ private $mFieldInfoCache = array(); - function __construct( $p = null ) { + function __construct( array $p ) { global $wgDBprefix; - if ( !is_array( $p ) ) { // legacy calling pattern - wfDeprecated( __METHOD__ . " method called without parameter array.", "1.22" ); - $args = func_get_args(); - $p = array( - 'host' => isset( $args[0] ) ? $args[0] : false, - 'user' => isset( $args[1] ) ? $args[1] : false, - 'password' => isset( $args[2] ) ? $args[2] : false, - 'dbname' => isset( $args[3] ) ? $args[3] : false, - 'flags' => isset( $args[4] ) ? $args[4] : 0, - 'tablePrefix' => isset( $args[5] ) ? $args[5] : 'get from global', - 'schema' => 'get from global', - 'foreign' => isset( $args[6] ) ? $args[6] : false - ); - } if ( $p['tablePrefix'] == 'get from global' ) { $p['tablePrefix'] = $wgDBprefix; } $p['tablePrefix'] = strtoupper( $p['tablePrefix'] ); parent::__construct( $p ); - wfRunHooks( 'DatabaseOraclePostInit', array( $this ) ); + Hooks::run( 'DatabaseOraclePostInit', array( $this ) ); } function __destruct() { diff --git a/includes/db/DatabasePostgres.php b/includes/db/DatabasePostgres.php index ce14d7a9..9287f7a6 100644 --- a/includes/db/DatabasePostgres.php +++ b/includes/db/DatabasePostgres.php @@ -530,10 +530,12 @@ class DatabasePostgres extends DatabaseBase { return; } } - /* Transaction stays in the ERROR state until rolledback */ + /* Transaction stays in the ERROR state until rolled back */ if ( $this->mTrxLevel ) { + $ignore = $this->ignoreErrors( true ); $this->rollback( __METHOD__ ); - }; + $this->ignoreErrors( $ignore ); + } parent::reportQueryError( $error, $errno, $sql, $fname, false ); } @@ -712,7 +714,7 @@ class DatabasePostgres extends DatabaseBase { $row = $this->fetchRow( $res ); $count = array(); if ( preg_match( '/rows=(\d+)/', $row[0], $count ) ) { - $rows = $count[1]; + $rows = (int)$count[1]; } } @@ -1495,12 +1497,14 @@ SQL; * @return Blob */ function encodeBlob( $b ) { - return new Blob( pg_escape_bytea( $this->mConn, $b ) ); + return new PostgresBlob( pg_escape_bytea( $b ) ); } function decodeBlob( $b ) { - if ( $b instanceof Blob ) { + if ( $b instanceof PostgresBlob ) { $b = $b->fetch(); + } elseif ( $b instanceof Blob ) { + return $b->fetch(); } return pg_unescape_bytea( $b ); @@ -1520,7 +1524,12 @@ SQL; } elseif ( is_bool( $s ) ) { return intval( $s ); } elseif ( $s instanceof Blob ) { - return "'" . $s->fetch( $s ) . "'"; + if ( $s instanceof PostgresBlob ) { + $s = $s->fetch(); + } else { + $s = pg_escape_bytea( $this->mConn, $s->fetch() ); + } + return "'$s'"; } return "'" . pg_escape_string( $this->mConn, $s ) . "'"; @@ -1692,3 +1701,5 @@ SQL; return wfBaseConvert( substr( sha1( $lockName ), 0, 15 ), 16, 10 ); } } // end DatabasePostgres class + +class PostgresBlob extends Blob {} diff --git a/includes/db/DatabaseSqlite.php b/includes/db/DatabaseSqlite.php index dd2e813e..ed86bab1 100644 --- a/includes/db/DatabaseSqlite.php +++ b/includes/db/DatabaseSqlite.php @@ -29,8 +29,14 @@ class DatabaseSqlite extends DatabaseBase { /** @var bool Whether full text is enabled */ private static $fulltextEnabled = null; + /** @var string Directory */ + protected $dbDir; + /** @var string File name for SQLite database file */ - public $mDatabaseFile; + protected $dbPath; + + /** @var string Transaction mode */ + protected $trxMode; /** @var int The number of rows affected as an integer */ protected $mAffectedRows; @@ -44,35 +50,63 @@ class DatabaseSqlite extends DatabaseBase { /** @var FSLockManager (hopefully on the same server as the DB) */ protected $lockMgr; - function __construct( $p = null ) { + /** + * Additional params include: + * - dbDirectory : directory containing the DB and the lock file directory + * [defaults to $wgSQLiteDataDir] + * - dbFilePath : use this to force the path of the DB file + * - trxMode : one of (deferred, immediate, exclusive) + * @param array $p + */ + function __construct( array $p ) { global $wgSharedDB, $wgSQLiteDataDir; - if ( !is_array( $p ) ) { // legacy calling pattern - wfDeprecated( __METHOD__ . " method called without parameter array.", "1.22" ); - $args = func_get_args(); - $p = array( - 'host' => isset( $args[0] ) ? $args[0] : false, - 'user' => isset( $args[1] ) ? $args[1] : false, - 'password' => isset( $args[2] ) ? $args[2] : false, - 'dbname' => isset( $args[3] ) ? $args[3] : false, - 'flags' => isset( $args[4] ) ? $args[4] : 0, - 'tablePrefix' => isset( $args[5] ) ? $args[5] : 'get from global', - 'schema' => 'get from global', - 'foreign' => isset( $args[6] ) ? $args[6] : false - ); - } - $this->mDBname = $p['dbname']; - parent::__construct( $p ); - // parent doesn't open when $user is false, but we can work with $dbName - if ( $p['dbname'] && !$this->isOpen() ) { - if ( $this->open( $p['host'], $p['user'], $p['password'], $p['dbname'] ) ) { - if ( $wgSharedDB ) { - $this->attachDatabase( $wgSharedDB ); + $this->dbDir = isset( $p['dbDirectory'] ) ? $p['dbDirectory'] : $wgSQLiteDataDir; + + if ( isset( $p['dbFilePath'] ) ) { + $this->mFlags = isset( $p['flags'] ) ? $p['flags'] : 0; + // Standalone .sqlite file mode + $this->openFile( $p['dbFilePath'] ); + // @FIXME: clean up base constructor so this can call super instead + $this->mTrxAtomicLevels = new SplStack; + } else { + $this->mDBname = $p['dbname']; + // Stock wiki mode using standard file names per DB + parent::__construct( $p ); + // parent doesn't open when $user is false, but we can work with $dbName + if ( $p['dbname'] && !$this->isOpen() ) { + if ( $this->open( $p['host'], $p['user'], $p['password'], $p['dbname'] ) ) { + if ( $wgSharedDB ) { + $this->attachDatabase( $wgSharedDB ); + } } } } - $this->lockMgr = new FSLockManager( array( 'lockDirectory' => "$wgSQLiteDataDir/locks" ) ); + $this->trxMode = isset( $p['trxMode'] ) ? strtoupper( $p['trxMode'] ) : null; + if ( $this->trxMode && + !in_array( $this->trxMode, array( 'DEFERRED', 'IMMEDIATE', 'EXCLUSIVE' ) ) + ) { + $this->trxMode = null; + wfWarn( "Invalid SQLite transaction mode provided." ); + } + + $this->lockMgr = new FSLockManager( array( 'lockDirectory' => "{$this->dbDir}/locks" ) ); + } + + /** + * @param string $filename + * @param array $p Options map; supports: + * - flags : (same as __construct counterpart) + * - trxMode : (same as __construct counterpart) + * - dbDirectory : (same as __construct counterpart) + * @return DatabaseSqlite + * @since 1.25 + */ + public static function newStandaloneInstance( $filename, array $p = array() ) { + $p['dbFilePath'] = $filename; + + return new self( $p ); } /** @@ -103,10 +137,8 @@ class DatabaseSqlite extends DatabaseBase { * @return PDO */ function open( $server, $user, $pass, $dbName ) { - global $wgSQLiteDataDir; - $this->close(); - $fileName = self::generateFileName( $wgSQLiteDataDir, $dbName ); + $fileName = self::generateFileName( $this->dbDir, $dbName ); if ( !is_readable( $fileName ) ) { $this->mConn = false; throw new DBConnectionError( $this, "SQLite database not accessible" ); @@ -123,10 +155,10 @@ class DatabaseSqlite extends DatabaseBase { * @throws DBConnectionError * @return PDO|bool SQL connection or false if failed */ - function openFile( $fileName ) { + protected function openFile( $fileName ) { $err = false; - $this->mDatabaseFile = $fileName; + $this->dbPath = $fileName; try { if ( $this->mFlags & DBO_PERSISTENT ) { $this->mConn = new PDO( "sqlite:$fileName", '', '', @@ -144,8 +176,8 @@ class DatabaseSqlite extends DatabaseBase { } $this->mOpened = !!$this->mConn; - # set error codes only, don't raise exceptions if ( $this->mOpened ) { + # Set error codes only, don't raise exceptions $this->mConn->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT ); # Enforce LIKE to be case sensitive, just like MySQL $this->query( 'PRAGMA case_sensitive_like = 1' ); @@ -156,6 +188,14 @@ class DatabaseSqlite extends DatabaseBase { return false; } + /** + * @return string SQLite DB file path + * @since 1.25 + */ + public function getDbFilePath() { + return $this->dbPath; + } + /** * Does not actually close the connection, just destroys the reference for GC to do its work * @return bool @@ -206,8 +246,7 @@ class DatabaseSqlite extends DatabaseBase { $cachedResult = false; $table = 'dummy_search_test'; - $db = new DatabaseSqliteStandalone( ':memory:' ); - + $db = self::newStandaloneInstance( ':memory:' ); if ( $db->query( "CREATE VIRTUAL TABLE $table USING FTS3(dummy_field)", __METHOD__, true ) ) { $cachedResult = 'FTS3'; } @@ -223,14 +262,13 @@ class DatabaseSqlite extends DatabaseBase { * @param string $name Database name to be used in queries like * SELECT foo FROM dbname.table * @param bool|string $file Database file name. If omitted, will be generated - * using $name and $wgSQLiteDataDir + * using $name and configured data directory * @param string $fname Calling function name * @return ResultWrapper */ function attachDatabase( $name, $file = false, $fname = __METHOD__ ) { - global $wgSQLiteDataDir; if ( !$file ) { - $file = self::generateFileName( $wgSQLiteDataDir, $name ); + $file = self::generateFileName( $this->dbDir, $name ); } $file = $this->addQuotes( $file ); @@ -712,37 +750,14 @@ class DatabaseSqlite extends DatabaseBase { } protected function doBegin( $fname = '' ) { - if ( $this->mTrxLevel == 1 ) { - $this->commit( __METHOD__ ); - } - try { - $this->mConn->beginTransaction(); - } catch ( PDOException $e ) { - throw new DBUnexpectedError( $this, 'Error in BEGIN query: ' . $e->getMessage() ); + if ( $this->trxMode ) { + $this->query( "BEGIN {$this->trxMode}", $fname ); + } else { + $this->query( 'BEGIN', $fname ); } $this->mTrxLevel = 1; } - protected function doCommit( $fname = '' ) { - if ( $this->mTrxLevel == 0 ) { - return; - } - try { - $this->mConn->commit(); - } catch ( PDOException $e ) { - throw new DBUnexpectedError( $this, 'Error in COMMIT query: ' . $e->getMessage() ); - } - $this->mTrxLevel = 0; - } - - protected function doRollback( $fname = '' ) { - if ( $this->mTrxLevel == 0 ) { - return; - } - $this->mConn->rollBack(); - $this->mTrxLevel = 0; - } - /** * @param string $s * @return string @@ -787,6 +802,10 @@ class DatabaseSqlite extends DatabaseBase { // https://bugs.php.net/bug.php?id=63419 // There was already a similar report for SQLite3::escapeString, bug #62361: // https://bugs.php.net/bug.php?id=62361 + // There is an additional bug regarding sorting this data after insert + // on older versions of sqlite shipped with ubuntu 12.04 + // https://bugzilla.wikimedia.org/show_bug.cgi?id=72367 + wfDebugLog( __CLASS__, __FUNCTION__ . ': Quoting value containing null byte. For consistency all binary data should have been first processed with self::encodeBlob()' ); return "x'" . bin2hex( $s ) . "'"; } else { return $this->mConn->quote( $s ); @@ -882,11 +901,9 @@ class DatabaseSqlite extends DatabaseBase { } public function lock( $lockName, $method, $timeout = 5 ) { - global $wgSQLiteDataDir; - - if ( !is_dir( "$wgSQLiteDataDir/locks" ) ) { // create dir as needed - if ( !is_writable( $wgSQLiteDataDir ) || !mkdir( "$wgSQLiteDataDir/locks" ) ) { - throw new DBError( "Cannot create directory \"$wgSQLiteDataDir/locks\"." ); + if ( !is_dir( "{$this->dbDir}/locks" ) ) { // create dir as needed + if ( !is_writable( $this->dbDir ) || !mkdir( "{$this->dbDir}/locks" ) ) { + throw new DBError( "Cannot create directory \"{$this->dbDir}/locks\"." ); } } @@ -980,18 +997,6 @@ class DatabaseSqlite extends DatabaseBase { } } // end DatabaseSqlite class -/** - * This class allows simple acccess to a SQLite database independently from main database settings - * @ingroup Database - */ -class DatabaseSqliteStandalone extends DatabaseSqlite { - public function __construct( $fileName, $flags = 0 ) { - $this->mFlags = $flags; - $this->tablePrefix( null ); - $this->openFile( $fileName ); - } -} - /** * @ingroup Database */ diff --git a/includes/db/DatabaseUtility.php b/includes/db/DatabaseUtility.php index c1e80d33..9a520ff9 100644 --- a/includes/db/DatabaseUtility.php +++ b/includes/db/DatabaseUtility.php @@ -21,29 +21,6 @@ * @ingroup Database */ -/** - * Utility class. - * @ingroup Database - */ -class DBObject { - public $mData; - - function __construct( $data ) { - $this->mData = $data; - } - - /** - * @return bool - */ - function isLOB() { - return false; - } - - function data() { - return $this->mData; - } -} - /** * Utility class * @ingroup Database @@ -339,4 +316,8 @@ class LikeMatch { * The implementation details of this opaque type are up to the database subclass. */ interface DBMasterPos { + /** + * @return float UNIX timestamp + */ + public function asOfTime(); } diff --git a/includes/db/IORMTable.php b/includes/db/IORMTable.php index 4dc693ac..b2527f95 100644 --- a/includes/db/IORMTable.php +++ b/includes/db/IORMTable.php @@ -94,7 +94,7 @@ interface IORMTable { public function getSummaryFields(); /** - * Selects the the specified fields of the records matching the provided + * Selects the specified fields of the records matching the provided * conditions and returns them as DBDataObject. Field names get prefixed. * * @see DatabaseBase::select() @@ -113,7 +113,7 @@ interface IORMTable { array $options = array(), $functionName = null ); /** - * Selects the the specified fields of the records matching the provided + * Selects the specified fields of the records matching the provided * conditions and returns them as DBDataObject. Field names get prefixed. * * @since 1.20 @@ -145,7 +145,7 @@ interface IORMTable { array $options = array(), $functionName = null ); /** - * Selects the the specified fields of the records matching the provided + * Selects the specified fields of the records matching the provided * conditions and returns them as associative arrays. * Provided field names get prefixed. * Returned field names will not have a prefix. @@ -170,7 +170,7 @@ interface IORMTable { array $options = array(), $collapse = true, $functionName = null ); /** - * Selects the the specified fields of the first matching record. + * Selects the specified fields of the first matching record. * Field names get prefixed. * * @since 1.20 @@ -186,7 +186,7 @@ interface IORMTable { array $options = array(), $functionName = null ); /** - * Selects the the specified fields of the records matching the provided + * Selects the specified fields of the records matching the provided * conditions. Field names do NOT get prefixed. * * @since 1.20 @@ -202,7 +202,7 @@ interface IORMTable { array $options = array(), $functionName = null ); /** - * Selects the the specified fields of the first record matching the provided + * Selects the specified fields of the first record matching the provided * conditions and returns it as an associative array, or false when nothing matches. * This method makes use of selectFields and expects the same parameters and * returns the same results (if there are any, if there are none, this method returns false). @@ -442,10 +442,11 @@ interface IORMTable { * Takes an array of field names with prefix and returns the unprefixed equivalent. * * @since 1.20 + * @deprecated since 1.25, will be removed * - * @param array $fieldNames + * @param string[] $fieldNames * - * @return array + * @return string[] */ public function unprefixFieldNames( array $fieldNames ); @@ -453,6 +454,7 @@ interface IORMTable { * Takes a field name with prefix and returns the unprefixed equivalent. * * @since 1.20 + * @deprecated since 1.25, will be removed * * @param string $fieldName * diff --git a/includes/db/LBFactory.php b/includes/db/LBFactory.php index 73456e23..4551e2d7 100644 --- a/includes/db/LBFactory.php +++ b/includes/db/LBFactory.php @@ -27,7 +27,7 @@ */ abstract class LBFactory { /** @var LBFactory */ - protected static $instance; + private static $instance; /** * Disables all access to the load balancer, will cause all database access @@ -43,7 +43,7 @@ abstract class LBFactory { * * @return LBFactory */ - static function &singleton() { + public static function singleton() { global $wgLBFactoryConf; if ( is_null( self::$instance ) ) { @@ -87,7 +87,7 @@ abstract class LBFactory { /** * Shut down, close connections and destroy the cached instance. */ - static function destroyInstance() { + public static function destroyInstance() { if ( self::$instance ) { self::$instance->shutdown(); self::$instance->forEachLBCallMethod( 'closeAll' ); @@ -100,7 +100,7 @@ abstract class LBFactory { * * @param LBFactory $instance */ - static function setInstance( $instance ) { + public static function setInstance( $instance ) { self::destroyInstance(); self::$instance = $instance; } @@ -109,7 +109,7 @@ abstract class LBFactory { * Construct a factory based on a configuration array (typically from $wgLBFactoryConf) * @param array $conf */ - abstract function __construct( $conf ); + abstract public function __construct( array $conf ); /** * Create a new load balancer object. The resulting object will be untracked, @@ -118,7 +118,7 @@ abstract class LBFactory { * @param bool|string $wiki Wiki ID, or false for the current wiki * @return LoadBalancer */ - abstract function newMainLB( $wiki = false ); + abstract public function newMainLB( $wiki = false ); /** * Get a cached (tracked) load balancer object. @@ -126,7 +126,7 @@ abstract class LBFactory { * @param bool|string $wiki Wiki ID, or false for the current wiki * @return LoadBalancer */ - abstract function getMainLB( $wiki = false ); + abstract public function getMainLB( $wiki = false ); /** * Create a new load balancer for external storage. The resulting object will be @@ -137,7 +137,7 @@ abstract class LBFactory { * @param bool|string $wiki Wiki ID, or false for the current wiki * @return LoadBalancer */ - abstract function newExternalLB( $cluster, $wiki = false ); + abstract protected function newExternalLB( $cluster, $wiki = false ); /** * Get a cached (tracked) load balancer for external storage @@ -146,7 +146,7 @@ abstract class LBFactory { * @param bool|string $wiki Wiki ID, or false for the current wiki * @return LoadBalancer */ - abstract function &getExternalLB( $cluster, $wiki = false ); + abstract public function &getExternalLB( $cluster, $wiki = false ); /** * Execute a function for each tracked load balancer @@ -156,13 +156,13 @@ abstract class LBFactory { * @param callable $callback * @param array $params */ - abstract function forEachLB( $callback, $params = array() ); + abstract public function forEachLB( $callback, array $params = array() ); /** * Prepare all tracked load balancers for shutdown * STUB */ - function shutdown() { + public function shutdown() { } /** @@ -171,24 +171,16 @@ abstract class LBFactory { * @param string $methodName * @param array $args */ - function forEachLBCallMethod( $methodName, $args = array() ) { - $this->forEachLB( array( $this, 'callMethod' ), array( $methodName, $args ) ); - } - - /** - * Private helper for forEachLBCallMethod - * @param LoadBalancer $loadBalancer - * @param string $methodName - * @param array $args - */ - function callMethod( $loadBalancer, $methodName, $args ) { - call_user_func_array( array( $loadBalancer, $methodName ), $args ); + private function forEachLBCallMethod( $methodName, array $args = array() ) { + $this->forEachLB( function ( LoadBalancer $loadBalancer, $methodName, array $args ) { + call_user_func_array( array( $loadBalancer, $methodName ), $args ); + }, array( $methodName, $args ) ); } /** * Commit changes on all master connections */ - function commitMasterChanges() { + public function commitMasterChanges() { $this->forEachLBCallMethod( 'commitMasterChanges' ); } @@ -196,7 +188,7 @@ abstract class LBFactory { * Rollback changes on all master connections * @since 1.23 */ - function rollbackMasterChanges() { + public function rollbackMasterChanges() { $this->forEachLBCallMethod( 'rollbackMasterChanges' ); } @@ -205,7 +197,7 @@ abstract class LBFactory { * @since 1.23 * @return bool */ - function hasMasterChanges() { + public function hasMasterChanges() { $ret = false; $this->forEachLB( function ( $lb ) use ( &$ret ) { $ret = $ret || $lb->hasMasterChanges(); @@ -219,15 +211,15 @@ abstract class LBFactory { */ class LBFactorySimple extends LBFactory { /** @var LoadBalancer */ - protected $mainLB; + private $mainLB; /** @var LoadBalancer[] */ - protected $extLBs = array(); + private $extLBs = array(); /** @var ChronologyProtector */ - protected $chronProt; + private $chronProt; - function __construct( $conf ) { + public function __construct( array $conf ) { $this->chronProt = new ChronologyProtector; } @@ -235,7 +227,7 @@ class LBFactorySimple extends LBFactory { * @param bool|string $wiki * @return LoadBalancer */ - function newMainLB( $wiki = false ) { + public function newMainLB( $wiki = false ) { global $wgDBservers; if ( $wgDBservers ) { $servers = $wgDBservers; @@ -274,7 +266,7 @@ class LBFactorySimple extends LBFactory { * @param bool|string $wiki * @return LoadBalancer */ - function getMainLB( $wiki = false ) { + public function getMainLB( $wiki = false ) { if ( !isset( $this->mainLB ) ) { $this->mainLB = $this->newMainLB( $wiki ); $this->mainLB->parentInfo( array( 'id' => 'main' ) ); @@ -290,7 +282,7 @@ class LBFactorySimple extends LBFactory { * @param bool|string $wiki * @return LoadBalancer */ - function newExternalLB( $cluster, $wiki = false ) { + protected function newExternalLB( $cluster, $wiki = false ) { global $wgExternalServers; if ( !isset( $wgExternalServers[$cluster] ) ) { throw new MWException( __METHOD__ . ": Unknown cluster \"$cluster\"" ); @@ -306,7 +298,7 @@ class LBFactorySimple extends LBFactory { * @param bool|string $wiki * @return array */ - function &getExternalLB( $cluster, $wiki = false ) { + public function &getExternalLB( $cluster, $wiki = false ) { if ( !isset( $this->extLBs[$cluster] ) ) { $this->extLBs[$cluster] = $this->newExternalLB( $cluster, $wiki ); $this->extLBs[$cluster]->parentInfo( array( 'id' => "ext-$cluster" ) ); @@ -324,7 +316,7 @@ class LBFactorySimple extends LBFactory { * @param callable $callback * @param array $params */ - function forEachLB( $callback, $params = array() ) { + public function forEachLB( $callback, array $params = array() ) { if ( isset( $this->mainLB ) ) { call_user_func_array( $callback, array_merge( array( $this->mainLB ), $params ) ); } @@ -333,7 +325,7 @@ class LBFactorySimple extends LBFactory { } } - function shutdown() { + public function shutdown() { if ( $this->mainLB ) { $this->chronProt->shutdownLB( $this->mainLB ); } @@ -352,26 +344,26 @@ class LBFactorySimple extends LBFactory { * LBFactory::enableBackend() to return to normal behavior */ class LBFactoryFake extends LBFactory { - function __construct( $conf ) { + public function __construct( array $conf ) { } - function newMainLB( $wiki = false ) { + public function newMainLB( $wiki = false ) { throw new DBAccessError; } - function getMainLB( $wiki = false ) { + public function getMainLB( $wiki = false ) { throw new DBAccessError; } - function newExternalLB( $cluster, $wiki = false ) { + protected function newExternalLB( $cluster, $wiki = false ) { throw new DBAccessError; } - function &getExternalLB( $cluster, $wiki = false ) { + public function &getExternalLB( $cluster, $wiki = false ) { throw new DBAccessError; } - function forEachLB( $callback, $params = array() ) { + public function forEachLB( $callback, array $params = array() ) { } } @@ -379,7 +371,7 @@ class LBFactoryFake extends LBFactory { * Exception class for attempted DB access */ class DBAccessError extends MWException { - function __construct() { + public function __construct() { parent::__construct( "Mediawiki tried to access the database via wfGetDB(). " . "This is not allowed." ); } diff --git a/includes/db/LBFactoryMulti.php b/includes/db/LBFactoryMulti.php index bac96523..aa305ab1 100644 --- a/includes/db/LBFactoryMulti.php +++ b/includes/db/LBFactoryMulti.php @@ -77,82 +77,82 @@ class LBFactoryMulti extends LBFactory { // Required settings /** @var array A map of database names to section names */ - protected $sectionsByDB; + private $sectionsByDB; /** * @var array A 2-d map. For each section, gives a map of server names to * load ratios */ - protected $sectionLoads; + private $sectionLoads; /** * @var array A server info associative array as documented for * $wgDBservers. The host, hostName and load entries will be * overridden */ - protected $serverTemplate; + private $serverTemplate; // Optional settings /** @var array A 3-d map giving server load ratios for each section and group */ - protected $groupLoadsBySection = array(); + private $groupLoadsBySection = array(); /** @var array A 3-d map giving server load ratios by DB name */ - protected $groupLoadsByDB = array(); + private $groupLoadsByDB = array(); /** @var array A map of hostname to IP address */ - protected $hostsByName = array(); + private $hostsByName = array(); /** @var array A map of external storage cluster name to server load map */ - protected $externalLoads = array(); + private $externalLoads = array(); /** * @var array A set of server info keys overriding serverTemplate for * external storage */ - protected $externalTemplateOverrides; + private $externalTemplateOverrides; /** * @var array A 2-d map overriding serverTemplate and * externalTemplateOverrides on a server-by-server basis. Applies to both * core and external storage */ - protected $templateOverridesByServer; + private $templateOverridesByServer; /** @var array A 2-d map overriding the server info by external storage cluster */ - protected $templateOverridesByCluster; + private $templateOverridesByCluster; /** @var array An override array for all master servers */ - protected $masterTemplateOverrides; + private $masterTemplateOverrides; /** * @var array|bool A map of section name to read-only message. Missing or * false for read/write */ - protected $readOnlyBySection = array(); + private $readOnlyBySection = array(); // Other stuff /** @var array Load balancer factory configuration */ - protected $conf; + private $conf; /** @var LoadBalancer[] */ - protected $mainLBs = array(); + private $mainLBs = array(); /** @var LoadBalancer[] */ - protected $extLBs = array(); + private $extLBs = array(); /** @var string */ - protected $lastWiki; + private $lastWiki; /** @var string */ - protected $lastSection; + private $lastSection; /** * @param array $conf * @throws MWException */ - function __construct( $conf ) { + public function __construct( array $conf ) { $this->chronProt = new ChronologyProtector; $this->conf = $conf; $required = array( 'sectionsByDB', 'sectionLoads', 'serverTemplate' ); @@ -186,7 +186,7 @@ class LBFactoryMulti extends LBFactory { * @param bool|string $wiki * @return string */ - function getSectionForWiki( $wiki = false ) { + private function getSectionForWiki( $wiki = false ) { if ( $this->lastWiki === $wiki ) { return $this->lastSection; } @@ -206,7 +206,7 @@ class LBFactoryMulti extends LBFactory { * @param bool|string $wiki * @return LoadBalancer */ - function newMainLB( $wiki = false ) { + public function newMainLB( $wiki = false ) { list( $dbName, ) = $this->getDBNameAndPrefix( $wiki ); $section = $this->getSectionForWiki( $wiki ); $groupLoads = array(); @@ -229,7 +229,7 @@ class LBFactoryMulti extends LBFactory { * @param bool|string $wiki * @return LoadBalancer */ - function getMainLB( $wiki = false ) { + public function getMainLB( $wiki = false ) { $section = $this->getSectionForWiki( $wiki ); if ( !isset( $this->mainLBs[$section] ) ) { $lb = $this->newMainLB( $wiki, $section ); @@ -247,7 +247,7 @@ class LBFactoryMulti extends LBFactory { * @throws MWException * @return LoadBalancer */ - function newExternalLB( $cluster, $wiki = false ) { + protected function newExternalLB( $cluster, $wiki = false ) { if ( !isset( $this->externalLoads[$cluster] ) ) { throw new MWException( __METHOD__ . ": Unknown cluster \"$cluster\"" ); } @@ -267,7 +267,7 @@ class LBFactoryMulti extends LBFactory { * @param bool|string $wiki Wiki ID, or false for the current wiki * @return LoadBalancer */ - function &getExternalLB( $cluster, $wiki = false ) { + public function &getExternalLB( $cluster, $wiki = false ) { if ( !isset( $this->extLBs[$cluster] ) ) { $this->extLBs[$cluster] = $this->newExternalLB( $cluster, $wiki ); $this->extLBs[$cluster]->parentInfo( array( 'id' => "ext-$cluster" ) ); @@ -285,7 +285,7 @@ class LBFactoryMulti extends LBFactory { * @param array $groupLoads * @return LoadBalancer */ - function newLoadBalancer( $template, $loads, $groupLoads ) { + private function newLoadBalancer( $template, $loads, $groupLoads ) { $servers = $this->makeServerArray( $template, $loads, $groupLoads ); $lb = new LoadBalancer( array( 'servers' => $servers, @@ -302,7 +302,7 @@ class LBFactoryMulti extends LBFactory { * @param array $groupLoads * @return array */ - function makeServerArray( $template, $loads, $groupLoads ) { + private function makeServerArray( $template, $loads, $groupLoads ) { $servers = array(); $master = true; $groupLoadsByServer = $this->reindexGroupLoads( $groupLoads ); @@ -344,7 +344,7 @@ class LBFactoryMulti extends LBFactory { * @param array $groupLoads * @return array */ - function reindexGroupLoads( $groupLoads ) { + private function reindexGroupLoads( $groupLoads ) { $reindexed = array(); foreach ( $groupLoads as $group => $loads ) { foreach ( $loads as $server => $load ) { @@ -360,7 +360,7 @@ class LBFactoryMulti extends LBFactory { * @param bool|string $wiki * @return array */ - function getDBNameAndPrefix( $wiki = false ) { + private function getDBNameAndPrefix( $wiki = false ) { if ( $wiki === false ) { global $wgDBname, $wgDBprefix; @@ -377,7 +377,7 @@ class LBFactoryMulti extends LBFactory { * @param callable $callback * @param array $params */ - function forEachLB( $callback, $params = array() ) { + public function forEachLB( $callback, array $params = array() ) { foreach ( $this->mainLBs as $lb ) { call_user_func_array( $callback, array_merge( array( $lb ), $params ) ); } @@ -386,7 +386,7 @@ class LBFactoryMulti extends LBFactory { } } - function shutdown() { + public function shutdown() { foreach ( $this->mainLBs as $lb ) { $this->chronProt->shutdownLB( $lb ); } diff --git a/includes/db/LBFactorySingle.php b/includes/db/LBFactorySingle.php index 3a4d829b..a41dadfa 100644 --- a/includes/db/LBFactorySingle.php +++ b/includes/db/LBFactorySingle.php @@ -26,13 +26,13 @@ */ class LBFactorySingle extends LBFactory { /** @var LoadBalancerSingle */ - protected $lb; + private $lb; /** * @param array $conf An associative array with one member: * - connection: The DatabaseBase connection object */ - function __construct( $conf ) { + public function __construct( array $conf ) { $this->lb = new LoadBalancerSingle( $conf ); } @@ -40,7 +40,7 @@ class LBFactorySingle extends LBFactory { * @param bool|string $wiki * @return LoadBalancerSingle */ - function newMainLB( $wiki = false ) { + public function newMainLB( $wiki = false ) { return $this->lb; } @@ -48,7 +48,7 @@ class LBFactorySingle extends LBFactory { * @param bool|string $wiki * @return LoadBalancerSingle */ - function getMainLB( $wiki = false ) { + public function getMainLB( $wiki = false ) { return $this->lb; } @@ -57,7 +57,7 @@ class LBFactorySingle extends LBFactory { * @param bool|string $wiki Wiki ID, or false for the current wiki * @return LoadBalancerSingle */ - function newExternalLB( $cluster, $wiki = false ) { + protected function newExternalLB( $cluster, $wiki = false ) { return $this->lb; } @@ -66,7 +66,7 @@ class LBFactorySingle extends LBFactory { * @param bool|string $wiki Wiki ID, or false for the current wiki * @return LoadBalancerSingle */ - function &getExternalLB( $cluster, $wiki = false ) { + public function &getExternalLB( $cluster, $wiki = false ) { return $this->lb; } @@ -74,7 +74,7 @@ class LBFactorySingle extends LBFactory { * @param string|callable $callback * @param array $params */ - function forEachLB( $callback, $params = array() ) { + public function forEachLB( $callback, array $params = array() ) { call_user_func_array( $callback, array_merge( array( $this->lb ), $params ) ); } } @@ -84,12 +84,12 @@ class LBFactorySingle extends LBFactory { */ class LoadBalancerSingle extends LoadBalancer { /** @var DatabaseBase */ - protected $db; + private $db; /** * @param array $params */ - function __construct( $params ) { + public function __construct( array $params ) { $this->db = $params['connection']; parent::__construct( array( 'servers' => array( array( 'type' => $this->db->getType(), @@ -106,7 +106,7 @@ class LoadBalancerSingle extends LoadBalancer { * * @return DatabaseBase */ - function reallyOpenConnection( $server, $dbNameOverride = false ) { + protected function reallyOpenConnection( $server, $dbNameOverride = false ) { return $this->db; } } diff --git a/includes/db/LoadBalancer.php b/includes/db/LoadBalancer.php index e517a025..d9584e14 100644 --- a/includes/db/LoadBalancer.php +++ b/includes/db/LoadBalancer.php @@ -28,13 +28,13 @@ * @ingroup Database */ class LoadBalancer { - /** @var array Map of (server index => server config array) */ + /** @var array[] Map of (server index => server config array) */ private $mServers; - /** @var array Map of (local/foreignUsed/foreignFree => server index => DatabaseBase array) */ + /** @var array[] Map of (local/foreignUsed/foreignFree => server index => DatabaseBase array) */ private $mConns; /** @var array Map of (server index => weight) */ private $mLoads; - /** @var array Map of (group => server index => weight) */ + /** @var array[] Map of (group => server index => weight) */ private $mGroupLoads; /** @var bool Whether to disregard slave lag as a factor in slave selection */ private $mAllowLagged; @@ -58,8 +58,13 @@ class LoadBalancer { private $mLaggedSlaveMode; /** @var string The last DB selection or connection error */ private $mLastError = 'Unknown error'; - /** @var array Process cache of LoadMonitor::getLagTimes() */ - private $mLagTimes; + /** @var integer Total connections opened */ + private $connsOpened = 0; + /** @var ProcessCacheLRU */ + private $mProcCache; + + /** @var integer Warn when this many connection are held */ + const CONN_HELD_WARN_THRESHOLD = 10; /** * @param array $params Array with keys: @@ -67,7 +72,7 @@ class LoadBalancer { * loadMonitor Name of a class used to fetch server lag and load. * @throws MWException */ - function __construct( $params ) { + public function __construct( array $params ) { if ( !isset( $params['servers'] ) ) { throw new MWException( __CLASS__ . ': missing servers parameter' ); } @@ -108,6 +113,8 @@ class LoadBalancer { } } } + + $this->mProcCache = new ProcessCacheLRU( 30 ); } /** @@ -115,7 +122,7 @@ class LoadBalancer { * * @return LoadMonitor */ - function getLoadMonitor() { + private function getLoadMonitor() { if ( !isset( $this->mLoadMonitor ) ) { $class = $this->mLoadMonitorClass; $this->mLoadMonitor = new $class( $this ); @@ -129,7 +136,7 @@ class LoadBalancer { * @param mixed $x * @return mixed */ - function parentInfo( $x = null ) { + public function parentInfo( $x = null ) { return wfSetVar( $this->mParentInfo, $x ); } @@ -142,24 +149,30 @@ class LoadBalancer { * @param array $weights * @return bool|int|string */ - function pickRandom( $weights ) { + public function pickRandom( array $weights ) { return ArrayUtils::pickRandom( $weights ); } /** * @param array $loads * @param bool|string $wiki Wiki to get non-lagged for + * @param float $maxLag Restrict the maximum allowed lag to this many seconds * @return bool|int|string */ - function getRandomNonLagged( $loads, $wiki = false ) { - # Unset excessively lagged servers + private function getRandomNonLagged( array $loads, $wiki = false, $maxLag = INF ) { $lags = $this->getLagTimes( $wiki ); + + # Unset excessively lagged servers foreach ( $lags as $i => $lag ) { if ( $i != 0 ) { + $maxServerLag = $maxLag; + if ( isset( $this->mServers[$i]['max lag'] ) ) { + $maxServerLag = min( $maxServerLag, $this->mServers[$i]['max lag'] ); + } if ( $lag === false ) { wfDebugLog( 'replication', "Server #$i is not replicating" ); unset( $loads[$i] ); - } elseif ( isset( $this->mServers[$i]['max lag'] ) && $lag > $this->mServers[$i]['max lag'] ) { + } elseif ( $lag > $maxServerLag ) { wfDebugLog( 'replication', "Server #$i is excessively lagged ($lag seconds)" ); unset( $loads[$i] ); } @@ -195,12 +208,12 @@ class LoadBalancer { * always return a consistent index during a given invocation * * Side effect: opens connections to databases - * @param bool|string $group - * @param bool|string $wiki + * @param string|bool $group Query group, or false for the generic reader + * @param string|bool $wiki Wiki ID, or false for the current wiki * @throws MWException * @return bool|int|string */ - function getReaderIndex( $group = false, $wiki = false ) { + public function getReaderIndex( $group = false, $wiki = false ) { global $wgReadOnly, $wgDBtype; # @todo FIXME: For now, only go through all this for mysql databases @@ -216,8 +229,6 @@ class LoadBalancer { return $this->mReadIndex; } - $section = new ProfileSection( __METHOD__ ); - # Find the relevant load array if ( $group !== false ) { if ( isset( $this->mGroupLoads[$group] ) ) { @@ -250,7 +261,19 @@ class LoadBalancer { if ( $wgReadOnly || $this->mAllowLagged || $laggedSlaveMode ) { $i = ArrayUtils::pickRandom( $currentLoads ); } else { - $i = $this->getRandomNonLagged( $currentLoads, $wiki ); + $i = false; + if ( $this->mWaitForPos && $this->mWaitForPos->asOfTime() ) { + # ChronologyProtecter causes mWaitForPos to be set via sessions. + # This triggers doWait() after connect, so it's especially good to + # avoid lagged servers so as to avoid just blocking in that method. + $ago = microtime( true ) - $this->mWaitForPos->asOfTime(); + # Aim for <= 1 second of waiting (being too picky can backfire) + $i = $this->getRandomNonLagged( $currentLoads, $wiki, $ago + 1 ); + } + if ( $i === false ) { + # Any server with less lag than it's 'max lag' param is preferable + $i = $this->getRandomNonLagged( $currentLoads, $wiki ); + } if ( $i === false && count( $currentLoads ) != 0 ) { # All slaves lagged. Switch to read-only mode wfDebugLog( 'replication', "All slaves lagged. Switch to read-only mode" ); @@ -305,28 +328,16 @@ class LoadBalancer { $this->mServers[$i]['slave pos'] = $conn->getSlavePos(); } } - if ( $this->mReadIndex <= 0 && $this->mLoads[$i] > 0 && $group !== false ) { + if ( $this->mReadIndex <= 0 && $this->mLoads[$i] > 0 && $group === false ) { $this->mReadIndex = $i; } + $serverName = $this->getServerName( $i ); + wfDebug( __METHOD__ . ": using server $serverName for group '$group'\n" ); } return $i; } - /** - * Wait for a specified number of microseconds, and return the period waited - * @param int $t - * @return int - */ - function sleep( $t ) { - wfProfileIn( __METHOD__ ); - wfDebug( __METHOD__ . ": waiting $t us\n" ); - usleep( $t ); - wfProfileOut( __METHOD__ ); - - return $t; - } - /** * Set the master wait position * If a DB_SLAVE connection has been opened already, waits @@ -334,7 +345,6 @@ class LoadBalancer { * @param DBMasterPos $pos */ public function waitFor( $pos ) { - wfProfileIn( __METHOD__ ); $this->mWaitForPos = $pos; $i = $this->mReadIndex; @@ -344,7 +354,6 @@ class LoadBalancer { $this->mLaggedSlaveMode = true; } } - wfProfileOut( __METHOD__ ); } /** @@ -354,7 +363,6 @@ class LoadBalancer { * @return bool Success (able to connect and no timeouts reached) */ public function waitForAll( $pos, $timeout = null ) { - wfProfileIn( __METHOD__ ); $this->mWaitForPos = $pos; $serverCount = count( $this->mServers ); @@ -364,7 +372,6 @@ class LoadBalancer { $ok = $this->doWait( $i, true, $timeout ) && $ok; } } - wfProfileOut( __METHOD__ ); return $ok; } @@ -376,7 +383,7 @@ class LoadBalancer { * @param int $i * @return DatabaseBase|bool False on failure */ - function getAnyOpenConnection( $i ) { + public function getAnyOpenConnection( $i ) { foreach ( $this->mConns as $conns ) { if ( !empty( $conns[$i] ) ) { return reset( $conns[$i] ); @@ -394,7 +401,9 @@ class LoadBalancer { * @return bool */ protected function doWait( $index, $open = false, $timeout = null ) { - # Find a connection to wait on + $close = false; // close the connection afterwards + + # Find a connection to wait on, creating one if needed and allowed $conn = $this->getAnyOpenConnection( $index ); if ( !$conn ) { if ( !$open ) { @@ -408,6 +417,9 @@ class LoadBalancer { return false; } + // Avoid connection spam in waitForAll() when connections + // are made just for the sake of doing this lag check. + $close = true; } } @@ -417,14 +429,21 @@ class LoadBalancer { if ( $result == -1 || is_null( $result ) ) { # Timed out waiting for slave, use master instead - wfDebug( __METHOD__ . ": Timed out waiting for slave #$index pos {$this->mWaitForPos}\n" ); - - return false; + $server = $this->mServers[$index]['host']; + $msg = __METHOD__ . ": Timed out waiting on $server pos {$this->mWaitForPos}"; + wfDebug( "$msg\n" ); + wfDebugLog( 'DBPerformance', "$msg:\n" . wfBacktrace( true ) ); + $ok = false; } else { wfDebug( __METHOD__ . ": Done\n" ); + $ok = true; + } - return true; + if ( $close ) { + $this->closeConnection( $conn ); } + + return $ok; } /** @@ -432,17 +451,14 @@ class LoadBalancer { * This is the main entry point for this class. * * @param int $i Server index - * @param array $groups Query groups - * @param bool|string $wiki Wiki ID + * @param array|string|bool $groups Query group(s), or false for the generic reader + * @param string|bool $wiki Wiki ID, or false for the current wiki * * @throws MWException * @return DatabaseBase */ - public function &getConnection( $i, $groups = array(), $wiki = false ) { - wfProfileIn( __METHOD__ ); - + public function getConnection( $i, $groups = array(), $wiki = false ) { if ( $i === null || $i === false ) { - wfProfileOut( __METHOD__ ); throw new MWException( 'Attempt to call ' . __METHOD__ . ' with invalid server index' ); } @@ -451,22 +467,20 @@ class LoadBalancer { $wiki = false; } - # Query groups + $groups = ( $groups === false || $groups === array() ) + ? array( false ) // check one "group": the generic pool + : (array)$groups; + + $masterOnly = ( $i == DB_MASTER || $i == $this->getWriterIndex() ); + $oldConnsOpened = $this->connsOpened; // connections open now + if ( $i == DB_MASTER ) { $i = $this->getWriterIndex(); - } elseif ( !is_array( $groups ) ) { - $groupIndex = $this->getReaderIndex( $groups, $wiki ); - if ( $groupIndex !== false ) { - $serverName = $this->getServerName( $groupIndex ); - wfDebug( __METHOD__ . ": using server $serverName for group $groups\n" ); - $i = $groupIndex; - } } else { + # Try to find an available server in any the query groups (in order) foreach ( $groups as $group ) { $groupIndex = $this->getReaderIndex( $group, $wiki ); if ( $groupIndex !== false ) { - $serverName = $this->getServerName( $groupIndex ); - wfDebug( __METHOD__ . ": using server $serverName for group $group\n" ); $i = $groupIndex; break; } @@ -476,11 +490,13 @@ class LoadBalancer { # Operation-based index if ( $i == DB_SLAVE ) { $this->mLastError = 'Unknown error'; // reset error string - $i = $this->getReaderIndex( false, $wiki ); + # Try the general server pool if $groups are unavailable. + $i = in_array( false, $groups, true ) + ? false // don't bother with this if that is what was tried above + : $this->getReaderIndex( false, $wiki ); # Couldn't find a working server in getReaderIndex()? if ( $i === false ) { $this->mLastError = 'No working slave server: ' . $this->mLastError; - wfProfileOut( __METHOD__ ); return $this->reportConnectionError(); } @@ -489,12 +505,17 @@ class LoadBalancer { # Now we have an explicit index into the servers array $conn = $this->openConnection( $i, $wiki ); if ( !$conn ) { - wfProfileOut( __METHOD__ ); return $this->reportConnectionError(); } - wfProfileOut( __METHOD__ ); + # Profile any new connections that happen + if ( $this->connsOpened > $oldConnsOpened ) { + $host = $conn->getServer(); + $dbname = $conn->getDBname(); + $trxProf = Profiler::instance()->getTransactionProfiler(); + $trxProf->recordConnection( $host, $dbname, $masterOnly ); + } return $conn; } @@ -556,8 +577,8 @@ class LoadBalancer { * @see LoadBalancer::getConnection() for parameter information * * @param int $db - * @param mixed $groups - * @param bool|string $wiki + * @param array|string|bool $groups Query group(s), or false for the generic reader + * @param string|bool $wiki Wiki ID, or false for the current wiki * @return DBConnRef */ public function getConnectionRef( $db, $groups = array(), $wiki = false ) { @@ -572,8 +593,8 @@ class LoadBalancer { * @see LoadBalancer::getConnection() for parameter information * * @param int $db - * @param mixed $groups - * @param bool|string $wiki + * @param array|string|bool $groups Query group(s), or false for the generic reader + * @param string|bool $wiki Wiki ID, or false for the current wiki * @return DBConnRef */ public function getLazyConnectionRef( $db, $groups = array(), $wiki = false ) { @@ -589,16 +610,14 @@ class LoadBalancer { * error will be available via $this->mErrorConnection. * * @param int $i Server index - * @param bool|string $wiki Wiki ID to open + * @param string|bool $wiki Wiki ID, or false for the current wiki * @return DatabaseBase * * @access private */ - function openConnection( $i, $wiki = false ) { - wfProfileIn( __METHOD__ ); + public function openConnection( $i, $wiki = false ) { if ( $wiki !== false ) { $conn = $this->openForeignConnection( $i, $wiki ); - wfProfileOut( __METHOD__ ); return $conn; } @@ -617,7 +636,6 @@ class LoadBalancer { $conn = false; } } - wfProfileOut( __METHOD__ ); return $conn; } @@ -640,8 +658,7 @@ class LoadBalancer { * @param string $wiki Wiki ID to open * @return DatabaseBase */ - function openForeignConnection( $i, $wiki ) { - wfProfileIn( __METHOD__ ); + private function openForeignConnection( $i, $wiki ) { list( $dbName, $prefix ) = wfSplitWikiID( $wiki ); if ( isset( $this->mConns['foreignUsed'][$i][$wiki] ) ) { // Reuse an already-used connection @@ -658,7 +675,9 @@ class LoadBalancer { $conn = reset( $this->mConns['foreignFree'][$i] ); $oldWiki = key( $this->mConns['foreignFree'][$i] ); - if ( !$conn->selectDB( $dbName ) ) { + // The empty string as a DB name means "don't care". + // DatabaseMysqlBase::open() already handle this on connection. + if ( $dbName !== '' && !$conn->selectDB( $dbName ) ) { $this->mLastError = "Error selecting database $dbName on server " . $conn->getServer() . " from client host " . wfHostname() . "\n"; $this->mErrorConnection = $conn; @@ -692,7 +711,6 @@ class LoadBalancer { $refCount = $conn->getLBInfo( 'foreignPoolRefCount' ); $conn->setLBInfo( 'foreignPoolRefCount', $refCount + 1 ); } - wfProfileOut( __METHOD__ ); return $conn; } @@ -704,7 +722,7 @@ class LoadBalancer { * @access private * @return bool */ - function isOpen( $index ) { + private function isOpen( $index ) { if ( !is_integer( $index ) ) { return false; } @@ -722,7 +740,7 @@ class LoadBalancer { * @throws MWException * @return DatabaseBase */ - function reallyOpenConnection( $server, $dbNameOverride = false ) { + protected function reallyOpenConnection( $server, $dbNameOverride = false ) { if ( !is_array( $server ) ) { throw new MWException( 'You must update your load-balancing configuration. ' . 'See DefaultSettings.php entry for $wgDBservers.' ); @@ -732,6 +750,14 @@ class LoadBalancer { $server['dbname'] = $dbNameOverride; } + // Log when many connection are made on requests + if ( ++$this->connsOpened >= self::CONN_HELD_WARN_THRESHOLD ) { + $masterAddr = $this->getServerName( 0 ); + wfDebugLog( 'DBPerformance', __METHOD__ . ": " . + "{$this->connsOpened}+ connections made (master=$masterAddr)\n" . + wfBacktrace( true ) ); + } + # Create object try { $db = DatabaseBase::factory( $server['type'], $server ); @@ -758,17 +784,27 @@ class LoadBalancer { */ private function reportConnectionError() { $conn = $this->mErrorConnection; // The connection which caused the error + $context = array( + 'method' => __METHOD__, + 'last_error' => $this->mLastError, + ); if ( !is_object( $conn ) ) { // No last connection, probably due to all servers being too busy - wfLogDBError( "LB failure with no last connection. Connection error: {$this->mLastError}" ); + wfLogDBError( + "LB failure with no last connection. Connection error: {last_error}", + $context + ); // If all servers were busy, mLastError will contain something sensible throw new DBConnectionError( null, $this->mLastError ); } else { - $server = $conn->getProperty( 'mServer' ); - wfLogDBError( "Connection error: {$this->mLastError} ({$server})" ); - $conn->reportConnectionError( "{$this->mLastError} ({$server})" ); // throws DBConnectionError + $context['db_server'] = $conn->getProperty( 'mServer' ); + wfLogDBError( + "Connection error: {last_error} ({db_server})", + $context + ); + $conn->reportConnectionError( "{$this->mLastError} ({$context['db_server']})" ); // throws DBConnectionError } return false; /* not reached */ @@ -777,7 +813,7 @@ class LoadBalancer { /** * @return int */ - function getWriterIndex() { + private function getWriterIndex() { return 0; } @@ -787,7 +823,7 @@ class LoadBalancer { * @param string $i * @return bool */ - function haveIndex( $i ) { + public function haveIndex( $i ) { return array_key_exists( $i, $this->mServers ); } @@ -797,7 +833,7 @@ class LoadBalancer { * @param string $i * @return bool */ - function isNonZeroLoad( $i ) { + public function isNonZeroLoad( $i ) { return array_key_exists( $i, $this->mServers ) && $this->mLoads[$i] != 0; } @@ -806,7 +842,7 @@ class LoadBalancer { * * @return int */ - function getServerCount() { + public function getServerCount() { return count( $this->mServers ); } @@ -816,7 +852,7 @@ class LoadBalancer { * @param string $i * @return string */ - function getServerName( $i ) { + public function getServerName( $i ) { if ( isset( $this->mServers[$i]['hostName'] ) ) { return $this->mServers[$i]['hostName']; } elseif ( isset( $this->mServers[$i]['host'] ) ) { @@ -831,7 +867,7 @@ class LoadBalancer { * @param int $i * @return array|bool */ - function getServerInfo( $i ) { + public function getServerInfo( $i ) { if ( isset( $this->mServers[$i] ) ) { return $this->mServers[$i]; } else { @@ -845,7 +881,7 @@ class LoadBalancer { * @param int $i * @param array $serverInfo */ - function setServerInfo( $i, $serverInfo ) { + public function setServerInfo( $i, array $serverInfo ) { $this->mServers[$i] = $serverInfo; } @@ -853,7 +889,7 @@ class LoadBalancer { * Get the current master position for chronology control purposes * @return mixed */ - function getMasterPos() { + public function getMasterPos() { # If this entire request was served from a slave without opening a connection to the # master (however unlikely that may be), then we can fetch the position from the slave. $masterConn = $this->getAnyOpenConnection( 0 ); @@ -879,7 +915,7 @@ class LoadBalancer { /** * Close all open connections */ - function closeAll() { + public function closeAll() { foreach ( $this->mConns as $conns2 ) { foreach ( $conns2 as $conns3 ) { /** @var DatabaseBase $conn */ @@ -893,6 +929,7 @@ class LoadBalancer { 'foreignFree' => array(), 'foreignUsed' => array(), ); + $this->connsOpened = 0; } /** @@ -901,7 +938,7 @@ class LoadBalancer { * If you use $conn->close() directly, the load balancer won't update its state. * @param DatabaseBase $conn */ - function closeConnection( $conn ) { + public function closeConnection( $conn ) { $done = false; foreach ( $this->mConns as $i1 => $conns2 ) { foreach ( $conns2 as $i2 => $conns3 ) { @@ -909,6 +946,7 @@ class LoadBalancer { if ( $conn === $candidateConn ) { $conn->close(); unset( $this->mConns[$i1][$i2][$i3] ); + --$this->connsOpened; $done = true; break; } @@ -923,7 +961,7 @@ class LoadBalancer { /** * Commit transactions on all open connections */ - function commitAll() { + public function commitAll() { foreach ( $this->mConns as $conns2 ) { foreach ( $conns2 as $conns3 ) { /** @var DatabaseBase[] $conns3 */ @@ -939,8 +977,7 @@ class LoadBalancer { /** * Issue COMMIT only on master, only if queries were done on connection */ - function commitMasterChanges() { - // Always 0, but who knows.. :) + public function commitMasterChanges() { $masterIndex = $this->getWriterIndex(); foreach ( $this->mConns as $conns2 ) { if ( empty( $conns2[$masterIndex] ) ) { @@ -959,8 +996,9 @@ class LoadBalancer { * Issue ROLLBACK only on master, only if queries were done on connection * @since 1.23 */ - function rollbackMasterChanges() { - // Always 0, but who knows.. :) + public function rollbackMasterChanges() { + $failedServers = array(); + $masterIndex = $this->getWriterIndex(); foreach ( $this->mConns as $conns2 ) { if ( empty( $conns2[$masterIndex] ) ) { @@ -969,28 +1007,36 @@ class LoadBalancer { /** @var DatabaseBase $conn */ foreach ( $conns2[$masterIndex] as $conn ) { if ( $conn->trxLevel() && $conn->writesOrCallbacksPending() ) { - $conn->rollback( __METHOD__, 'flush' ); + try { + $conn->rollback( __METHOD__, 'flush' ); + } catch ( DBError $e ) { + MWExceptionHandler::logException( $e ); + $failedServers[] = $conn->getServer(); + } } } } + + if ( $failedServers ) { + throw new DBExpectedError( null, "Rollback failed on server(s) " . + implode( ', ', array_unique( $failedServers ) ) ); + } } /** * @return bool Whether a master connection is already open * @since 1.24 */ - function hasMasterConnection() { + public function hasMasterConnection() { return $this->isOpen( $this->getWriterIndex() ); } /** - * Determine if there are any pending changes that need to be rolled back - * or committed. + * Determine if there are pending changes in a transaction by this thread * @since 1.23 * @return bool */ - function hasMasterChanges() { - // Always 0, but who knows.. :) + public function hasMasterChanges() { $masterIndex = $this->getWriterIndex(); foreach ( $this->mConns as $conns2 ) { if ( empty( $conns2[$masterIndex] ) ) { @@ -1006,18 +1052,53 @@ class LoadBalancer { return false; } + /** + * Get the timestamp of the latest write query done by this thread + * @since 1.25 + * @return float|bool UNIX timestamp or false + */ + public function lastMasterChangeTimestamp() { + $lastTime = false; + $masterIndex = $this->getWriterIndex(); + foreach ( $this->mConns as $conns2 ) { + if ( empty( $conns2[$masterIndex] ) ) { + continue; + } + /** @var DatabaseBase $conn */ + foreach ( $conns2[$masterIndex] as $conn ) { + $lastTime = max( $lastTime, $conn->lastDoneWrites() ); + } + } + return $lastTime; + } + + /** + * Check if this load balancer object had any recent or still + * pending writes issued against it by this PHP thread + * + * @param float $age How many seconds ago is "recent" [defaults to mWaitTimeout] + * @return bool + * @since 1.25 + */ + public function hasOrMadeRecentMasterChanges( $age = null ) { + $age = ( $age === null ) ? $this->mWaitTimeout : $age; + + return ( $this->hasMasterChanges() + || $this->lastMasterChangeTimestamp() > microtime( true ) - $age ); + } + /** * @param mixed $value * @return mixed */ - function waitTimeout( $value = null ) { + public function waitTimeout( $value = null ) { return wfSetVar( $this->mWaitTimeout, $value ); } /** * @return bool */ - function getLaggedSlaveMode() { + public function getLaggedSlaveMode() { return $this->mLaggedSlaveMode; } @@ -1026,7 +1107,7 @@ class LoadBalancer { * @param null|bool $mode * @return bool */ - function allowLagged( $mode = null ) { + public function allowLagged( $mode = null ) { if ( $mode === null ) { return $this->mAllowLagged; } @@ -1038,7 +1119,7 @@ class LoadBalancer { /** * @return bool */ - function pingAll() { + public function pingAll() { $success = true; foreach ( $this->mConns as $conns2 ) { foreach ( $conns2 as $conns3 ) { @@ -1059,7 +1140,7 @@ class LoadBalancer { * @param callable $callback * @param array $params */ - function forEachOpenConnection( $callback, $params = array() ) { + public function forEachOpenConnection( $callback, array $params = array() ) { foreach ( $this->mConns as $conns2 ) { foreach ( $conns2 as $conns3 ) { foreach ( $conns3 as $conn ) { @@ -1071,7 +1152,8 @@ class LoadBalancer { } /** - * Get the hostname and lag time of the most-lagged slave. + * Get the hostname and lag time of the most-lagged slave + * * This is useful for maintenance scripts that need to throttle their updates. * May attempt to open connections to slaves on the default DB. If there is * no lag, the maximum lag will be reported as -1. @@ -1079,72 +1161,50 @@ class LoadBalancer { * @param bool|string $wiki Wiki ID, or false for the default database * @return array ( host, max lag, index of max lagged host ) */ - function getMaxLag( $wiki = false ) { + public function getMaxLag( $wiki = false ) { $maxLag = -1; $host = ''; $maxIndex = 0; - if ( $this->getServerCount() <= 1 ) { // no replication = no lag - return array( $host, $maxLag, $maxIndex ); + if ( $this->getServerCount() <= 1 ) { + return array( $host, $maxLag, $maxIndex ); // no replication = no lag } - // Try to get the max lag info from the server cache - $key = 'loadbalancer:maxlag:cluster:' . $this->mServers[0]['host']; - $cache = ObjectCache::newAccelerator( array(), 'hash' ); - $maxLagInfo = $cache->get( $key ); // (host, lag, index) - - // Fallback to connecting to each slave and getting the lag - if ( !$maxLagInfo ) { - foreach ( $this->mServers as $i => $conn ) { - if ( $i == $this->getWriterIndex() ) { - continue; // nothing to check - } - $conn = false; - if ( $wiki === false ) { - $conn = $this->getAnyOpenConnection( $i ); - } - if ( !$conn ) { - $conn = $this->openConnection( $i, $wiki ); - } - if ( !$conn ) { - continue; - } - $lag = $conn->getLag(); - if ( $lag > $maxLag ) { - $maxLag = $lag; - $host = $this->mServers[$i]['host']; - $maxIndex = $i; - } + $lagTimes = $this->getLagTimes( $wiki ); + foreach ( $lagTimes as $i => $lag ) { + if ( $lag > $maxLag ) { + $maxLag = $lag; + $host = $this->mServers[$i]['host']; + $maxIndex = $i; } - $maxLagInfo = array( $host, $maxLag, $maxIndex ); - $cache->set( $key, $maxLagInfo, 5 ); } - return $maxLagInfo; + return array( $host, $maxLag, $maxIndex ); } /** * Get lag time for each server - * Results are cached for a short time in memcached, and indefinitely in the process cache + * + * Results are cached for a short time in memcached/process cache * * @param string|bool $wiki - * @return array + * @return int[] Map of (server index => seconds) */ - function getLagTimes( $wiki = false ) { - # Try process cache - if ( isset( $this->mLagTimes ) ) { - return $this->mLagTimes; + public function getLagTimes( $wiki = false ) { + if ( $this->getServerCount() <= 1 ) { + return array( 0 => 0 ); // no replication = no lag } - if ( $this->getServerCount() == 1 ) { - # No replication - $this->mLagTimes = array( 0 => 0 ); - } else { - # Send the request to the load monitor - $this->mLagTimes = $this->getLoadMonitor()->getLagTimes( - array_keys( $this->mServers ), $wiki ); + + if ( $this->mProcCache->has( 'slave_lag', 'times', 1 ) ) { + return $this->mProcCache->get( 'slave_lag', 'times' ); } - return $this->mLagTimes; + # Send the request to the load monitor + $times = $this->getLoadMonitor()->getLagTimes( array_keys( $this->mServers ), $wiki ); + + $this->mProcCache->set( 'slave_lag', 'times', $times ); + + return $times; } /** @@ -1161,7 +1221,7 @@ class LoadBalancer { * @param DatabaseBase $conn * @return int */ - function safeGetLag( $conn ) { + public function safeGetLag( $conn ) { if ( $this->getServerCount() == 1 ) { return 0; } else { @@ -1170,15 +1230,15 @@ class LoadBalancer { } /** - * Clear the cache for getLagTimes + * Clear the cache for slag lag delay times */ - function clearLagTimeCache() { - $this->mLagTimes = null; + public function clearLagTimeCache() { + $this->mProcCache->clear( 'slave_lag' ); } } /** - * Helper class to handle automatically marking connectons as reusable (via RAII pattern) + * Helper class to handle automatically marking connections as reusable (via RAII pattern) * as well handling deferring the actual network connection until the handle is used * * @ingroup Database @@ -1186,13 +1246,13 @@ class LoadBalancer { */ class DBConnRef implements IDatabase { /** @var LoadBalancer */ - protected $lb; + private $lb; /** @var DatabaseBase|null */ - protected $conn; + private $conn; /** @var array|null */ - protected $params; + private $params; /** * @param LoadBalancer $lb @@ -1216,7 +1276,7 @@ class DBConnRef implements IDatabase { return call_user_func_array( array( $this->conn, $name ), $arguments ); } - function __destruct() { + public function __destruct() { if ( $this->conn !== null ) { $this->lb->reuseConnection( $this->conn ); } diff --git a/includes/db/LoadMonitor.php b/includes/db/LoadMonitor.php index 7281485b..91840dd9 100644 --- a/includes/db/LoadMonitor.php +++ b/includes/db/LoadMonitor.php @@ -48,7 +48,7 @@ interface LoadMonitor { * @param array $serverIndexes * @param string $wiki * - * @return array + * @return array Map of (server index => seconds) */ public function getLagTimes( $serverIndexes, $wiki ); } @@ -93,8 +93,6 @@ class LoadMonitorMySQL implements LoadMonitor { return array( 0 => 0 ); } - $section = new ProfileSection( __METHOD__ ); - $expiry = 5; $requestRate = 10; diff --git a/includes/db/ORMTable.php b/includes/db/ORMTable.php index 2f898b75..562a8106 100644 --- a/includes/db/ORMTable.php +++ b/includes/db/ORMTable.php @@ -129,6 +129,7 @@ class ORMTable extends DBAccessBase implements IORMTable { * Gets the db field prefix. * * @since 1.20 + * @deprecated since 1.25, use the $this->fieldPrefix property instead * * @return string */ @@ -189,7 +190,7 @@ class ORMTable extends DBAccessBase implements IORMTable { } /** - * Selects the the specified fields of the records matching the provided + * Selects the specified fields of the records matching the provided * conditions and returns them as DBDataObject. Field names get prefixed. * * @since 1.20 @@ -210,7 +211,7 @@ class ORMTable extends DBAccessBase implements IORMTable { } /** - * Selects the the specified fields of the records matching the provided + * Selects the specified fields of the records matching the provided * conditions and returns them as DBDataObject. Field names get prefixed. * * @since 1.20 @@ -247,8 +248,8 @@ class ORMTable extends DBAccessBase implements IORMTable { * @param array $options * @param null|string $functionName * @return ResultWrapper - * @throws DBQueryError If the query failed (even if the database was in - * ignoreErrors mode). + * @throws Exception + * @throws MWException */ public function rawSelect( $fields = null, array $conditions = array(), array $options = array(), $functionName = null @@ -295,7 +296,7 @@ class ORMTable extends DBAccessBase implements IORMTable { } /** - * Selects the the specified fields of the records matching the provided + * Selects the specified fields of the records matching the provided * conditions and returns them as associative arrays. * Provided field names get prefixed. * Returned field names will not have a prefix. @@ -345,7 +346,7 @@ class ORMTable extends DBAccessBase implements IORMTable { } /** - * Selects the the specified fields of the first matching record. + * Selects the specified fields of the first matching record. * Field names get prefixed. * * @since 1.20 @@ -368,7 +369,7 @@ class ORMTable extends DBAccessBase implements IORMTable { } /** - * Selects the the specified fields of the records matching the provided + * Selects the specified fields of the records matching the provided * conditions. Field names do NOT get prefixed. * * @since 1.20 @@ -399,7 +400,7 @@ class ORMTable extends DBAccessBase implements IORMTable { } /** - * Selects the the specified fields of the first record matching the provided + * Selects the specified fields of the first record matching the provided * conditions and returns it as an associative array, or false when nothing matches. * This method makes use of selectFields and expects the same parameters and * returns the same results (if there are any, if there are none, this method returns false). @@ -770,33 +771,54 @@ class ORMTable extends DBAccessBase implements IORMTable { * @return string */ public function getPrefixedField( $field ) { - return $this->getFieldPrefix() . $field; + return $this->fieldPrefix . $field; } /** * Takes an array of field names with prefix and returns the unprefixed equivalent. * * @since 1.20 + * @deprecated since 1.25, will be removed * - * @param array $fieldNames + * @param string[] $fieldNames * - * @return array + * @return string[] */ public function unprefixFieldNames( array $fieldNames ) { - return array_map( array( $this, 'unprefixFieldName' ), $fieldNames ); + wfDeprecated( __METHOD__, '1.25' ); + + return $this->stripFieldPrefix( $fieldNames ); + } + + /** + * Takes an array of field names with prefix and returns the unprefixed equivalent. + * + * @param string[] $fieldNames + * + * @return string[] + */ + private function stripFieldPrefix( array $fieldNames ) { + $start = strlen( $this->fieldPrefix ); + + return array_map( function ( $fieldName ) use ( $start ) { + return substr( $fieldName, $start ); + }, $fieldNames ); } /** * Takes a field name with prefix and returns the unprefixed equivalent. * * @since 1.20 + * @deprecated since 1.25, will be removed * * @param string $fieldName * * @return string */ public function unprefixFieldName( $fieldName ) { - return substr( $fieldName, strlen( $this->getFieldPrefix() ) ); + wfDeprecated( __METHOD__, '1.25' ); + + return substr( $fieldName, strlen( $this->fieldPrefix ) ); } /** @@ -832,7 +854,7 @@ class ORMTable extends DBAccessBase implements IORMTable { $result = (array)$result; $rawFields = array_combine( - $this->unprefixFieldNames( array_keys( $result ) ), + $this->stripFieldPrefix( array_keys( $result ) ), array_values( $result ) ); -- cgit v1.2.3-54-g00ecf