From a1789ddde42033f1b05cc4929491214ee6e79383 Mon Sep 17 00:00:00 2001 From: Pierre Schmitz Date: Thu, 17 Dec 2015 09:15:42 +0100 Subject: Update to MediaWiki 1.26.0 --- includes/db/DBConnRef.php | 524 +++++++++++++ includes/db/Database.php | 310 +++----- includes/db/DatabaseError.php | 19 +- includes/db/DatabaseMssql.php | 8 +- includes/db/DatabaseMysql.php | 40 +- includes/db/DatabaseMysqlBase.php | 82 +- includes/db/DatabaseMysqli.php | 43 +- includes/db/DatabaseOracle.php | 38 +- includes/db/DatabasePostgres.php | 25 +- includes/db/DatabaseSqlite.php | 49 +- includes/db/IDatabase.php | 1513 +++++++++++++++++++++++++++++++++++++ includes/db/LBFactory.php | 11 +- includes/db/LBFactoryMulti.php | 2 +- includes/db/LoadBalancer.php | 147 ++-- includes/db/LoadMonitor.php | 87 --- includes/db/LoadMonitorMySQL.php | 124 +++ 16 files changed, 2548 insertions(+), 474 deletions(-) create mode 100644 includes/db/DBConnRef.php create mode 100644 includes/db/IDatabase.php create mode 100644 includes/db/LoadMonitorMySQL.php (limited to 'includes/db') diff --git a/includes/db/DBConnRef.php b/includes/db/DBConnRef.php new file mode 100644 index 00000000..b4f3f792 --- /dev/null +++ b/includes/db/DBConnRef.php @@ -0,0 +1,524 @@ +lb = $lb; + if ( $conn instanceof DatabaseBase ) { + $this->conn = $conn; + } else { + $this->params = $conn; + } + } + + function __call( $name, array $arguments ) { + if ( $this->conn === null ) { + list( $db, $groups, $wiki ) = $this->params; + $this->conn = $this->lb->getConnection( $db, $groups, $wiki ); + } + + return call_user_func_array( array( $this->conn, $name ), $arguments ); + } + + public function getServerInfo() { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function bufferResults( $buffer = null ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function trxLevel() { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function trxTimestamp() { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function tablePrefix( $prefix = null ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function dbSchema( $schema = null ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function getLBInfo( $name = null ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function setLBInfo( $name, $value = null ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function implicitGroupby() { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function implicitOrderby() { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function lastQuery() { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function doneWrites() { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function lastDoneWrites() { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function writesOrCallbacksPending() { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function pendingWriteQueryDuration() { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function isOpen() { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function setFlag( $flag ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function clearFlag( $flag ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function getFlag( $flag ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function getProperty( $name ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function getWikiID() { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function getType() { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function open( $server, $user, $password, $dbName ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function fetchObject( $res ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function fetchRow( $res ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function numRows( $res ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function numFields( $res ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function fieldName( $res, $n ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function insertId() { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function dataSeek( $res, $row ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function lastErrno() { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function lastError() { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function fieldInfo( $table, $field ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function affectedRows() { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function getSoftwareLink() { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function getServerVersion() { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function close() { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function reportConnectionError( $error = 'Unknown error' ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function query( $sql, $fname = __METHOD__, $tempIgnore = false ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function reportQueryError( $error, $errno, $sql, $fname, $tempIgnore = false ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function freeResult( $res ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function selectField( + $table, $var, $cond = '', $fname = __METHOD__, $options = array() + ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function selectFieldValues( + $table, $var, $cond = '', $fname = __METHOD__, $options = array() + ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function select( + $table, $vars, $conds = '', $fname = __METHOD__, + $options = array(), $join_conds = array() + ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function selectSQLText( + $table, $vars, $conds = '', $fname = __METHOD__, + $options = array(), $join_conds = array() + ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function selectRow( + $table, $vars, $conds, $fname = __METHOD__, + $options = array(), $join_conds = array() + ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function estimateRowCount( + $table, $vars = '*', $conds = '', $fname = __METHOD__, $options = array() + ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function selectRowCount( + $table, $vars = '*', $conds = '', $fname = __METHOD__, $options = array() + ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function fieldExists( $table, $field, $fname = __METHOD__ ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function indexExists( $table, $index, $fname = __METHOD__ ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function tableExists( $table, $fname = __METHOD__ ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function indexUnique( $table, $index ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function insert( $table, $a, $fname = __METHOD__, $options = array() ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function update( $table, $values, $conds, $fname = __METHOD__, $options = array() ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function makeList( $a, $mode = LIST_COMMA ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function makeWhereFrom2d( $data, $baseKey, $subKey ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function bitNot( $field ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function bitAnd( $fieldLeft, $fieldRight ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function bitOr( $fieldLeft, $fieldRight ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function buildConcat( $stringList ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function buildGroupConcatField( + $delim, $table, $field, $conds = '', $join_conds = array() + ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function selectDB( $db ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function getDBname() { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function getServer() { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function addQuotes( $s ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function buildLike() { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function anyChar() { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function anyString() { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function nextSequenceValue( $seqName ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function replace( $table, $uniqueIndexes, $rows, $fname = __METHOD__ ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function upsert( + $table, array $rows, array $uniqueIndexes, array $set, $fname = __METHOD__ + ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function deleteJoin( + $delTable, $joinTable, $delVar, $joinVar, $conds, $fname = __METHOD__ + ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function delete( $table, $conds, $fname = __METHOD__ ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function insertSelect( + $destTable, $srcTable, $varMap, $conds, + $fname = __METHOD__, $insertOptions = array(), $selectOptions = array() + ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function unionSupportsOrderAndLimit() { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function unionQueries( $sqls, $all ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function conditional( $cond, $trueVal, $falseVal ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function strreplace( $orig, $old, $new ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function getServerUptime() { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function wasDeadlock() { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function wasLockTimeout() { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function wasErrorReissuable() { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function wasReadOnlyError() { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function masterPosWait( DBMasterPos $pos, $timeout ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function getSlavePos() { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function getMasterPos() { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function onTransactionIdle( $callback ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function onTransactionPreCommitOrIdle( $callback ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function startAtomic( $fname = __METHOD__ ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function endAtomic( $fname = __METHOD__ ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function begin( $fname = __METHOD__ ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function commit( $fname = __METHOD__, $flush = '' ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function rollback( $fname = __METHOD__, $flush = '' ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function listTables( $prefix = null, $fname = __METHOD__ ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function timestamp( $ts = 0 ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function timestampOrNull( $ts = null ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function resultObject( $result ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function ping() { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function getLag() { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function maxListLen() { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function encodeBlob( $b ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function decodeBlob( $b ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function setSessionOptions( array $options ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function setSchemaVars( $vars ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function lockIsFree( $lockName, $method ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function lock( $lockName, $method, $timeout = 5 ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function unlock( $lockName, $method ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function namedLocksEnqueue() { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function getInfinity() { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function encodeExpiry( $expiry ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function decodeExpiry( $expiry, $format = TS_MW ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function setBigSelects( $value = true ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + /** + * Clean up the connection when out of scope + */ + function __destruct() { + if ( $this->conn !== null ) { + $this->lb->reuseConnection( $this->conn ); + } + } +} diff --git a/includes/db/Database.php b/includes/db/Database.php index 0c0248da..1e54f554 100644 --- a/includes/db/Database.php +++ b/includes/db/Database.php @@ -25,13 +25,6 @@ * @ingroup Database */ -/** - * Interface for classes that implement or wrap DatabaseBase - * @ingroup Database - */ -interface IDatabase { -} - /** * Database abstraction object * @ingroup Database @@ -65,10 +58,11 @@ abstract class DatabaseBase implements IDatabase { protected $mSchema; protected $mFlags; protected $mForeign; - protected $mErrorCount = 0; protected $mLBInfo = array(); protected $mDefaultBigSelects = null; protected $mSchemaVars = false; + /** @var array */ + protected $mSessionVars = array(); protected $preparedArgs; @@ -141,6 +135,13 @@ abstract class DatabaseBase implements IDatabase { */ private $mTrxAutomaticAtomic = false; + /** + * Track the seconds spent in write queries for the current transaction + * + * @var float + */ + private $mTrxWriteDuration = 0.0; + /** * @since 1.21 * @var resource File handle for upgrade @@ -228,7 +229,7 @@ abstract class DatabaseBase implements IDatabase { * @param null|bool $ignoreErrors * @return bool The previous value of the flag. */ - public function ignoreErrors( $ignoreErrors = null ) { + protected function ignoreErrors( $ignoreErrors = null ) { return wfSetBit( $this->mFlags, DBO_IGNORE, $ignoreErrors ); } @@ -257,15 +258,6 @@ abstract class DatabaseBase implements IDatabase { 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. - * @return int The error count - */ - public function errorCount( $count = null ) { - return wfSetVar( $this->mErrorCount, $count ); - } - /** * Get/set the table prefix. * @param string $prefix The table prefix to set, or omitted to leave it unchanged. @@ -473,6 +465,18 @@ abstract class DatabaseBase implements IDatabase { ); } + /** + * Get the time spend running write queries for this + * + * High times could be due to scanning, updates, locking, and such + * + * @return float|bool Returns false if not transaction is active + * @since 1.26 + */ + public function pendingWriteQueryDuration() { + return $this->mTrxLevel ? $this->mTrxWriteDuration : false; + } + /** * Is a connection to the database open? * @return bool @@ -591,125 +595,6 @@ abstract class DatabaseBase implements IDatabase { return $this->getSqlFilePath( 'update-keys.sql' ); } - /** - * 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 @@ -719,14 +604,6 @@ abstract class DatabaseBase implements IDatabase { */ 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() * @@ -735,24 +612,6 @@ abstract class DatabaseBase implements IDatabase { */ 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. * @@ -794,15 +653,17 @@ abstract class DatabaseBase implements IDatabase { } } + $this->mSessionVars = $params['variables']; + /** Get the default table prefix*/ - if ( $tablePrefix == 'get from global' ) { + if ( $tablePrefix === 'get from global' ) { $this->mTablePrefix = $wgDBprefix; } else { $this->mTablePrefix = $tablePrefix; } /** Get the database schema*/ - if ( $schema == 'get from global' ) { + if ( $schema === 'get from global' ) { $this->mSchema = $wgDBmwschema; } else { $this->mSchema = $schema; @@ -892,10 +753,6 @@ abstract class DatabaseBase implements IDatabase { // Although postgres and oracle support schemas, we don't use them (yet) // to maintain backwards compatibility $defaultSchemas = array( - 'mysql' => null, - 'postgres' => null, - 'sqlite' => null, - 'oracle' => null, 'mssql' => 'get from global', ); @@ -907,8 +764,11 @@ abstract class DatabaseBase implements IDatabase { $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['variables'] = isset( $p['variables'] ) ? $p['variables'] : array(); $p['tablePrefix'] = isset( $p['tablePrefix'] ) ? $p['tablePrefix'] : 'get from global'; - $p['schema'] = isset( $p['schema'] ) ? $p['schema'] : $defaultSchemas[$dbType]; + if ( !isset( $p['schema'] ) ) { + $p['schema'] = isset( $defaultSchemas[$dbType] ) ? $defaultSchemas[$dbType] : null; + } $p['foreign'] = isset( $p['foreign'] ) ? $p['foreign'] : false; return new $class( $p ); @@ -997,6 +857,17 @@ abstract class DatabaseBase implements IDatabase { return $closed; } + /** + * Make sure isOpen() returns true as a sanity check + * + * @throws DBUnexpectedError + */ + protected function assertOpen() { + if ( !$this->isOpen() ) { + throw new DBUnexpectedError( $this, "DB connection was already closed." ); + } + } + /** * Closes underlying database connection * @since 1.20 @@ -1034,7 +905,7 @@ abstract class DatabaseBase implements IDatabase { * @param string $sql * @return bool */ - public function isWriteQuery( $sql ) { + protected function isWriteQuery( $sql ) { return !preg_match( '/^(?:SELECT|BEGIN|ROLLBACK|COMMIT|SET|SHOW|EXPLAIN|\(SELECT)\b/i', $sql ); } @@ -1047,7 +918,7 @@ abstract class DatabaseBase implements IDatabase { * @param string $sql * @return bool */ - public function isTransactableQuery( $sql ) { + protected function isTransactableQuery( $sql ) { $verb = substr( $sql, 0, strcspn( $sql, " \t\r\n" ) ); return !in_array( $verb, array( 'BEGIN', 'COMMIT', 'ROLLBACK', 'SHOW', 'SET' ) ); } @@ -1153,13 +1024,12 @@ abstract class DatabaseBase implements IDatabase { $queryId = MWDebug::query( $sql, $fname, $isMaster ); # Avoid fatals if close() was called - if ( !$this->isOpen() ) { - throw new DBUnexpectedError( $this, "DB connection was already closed." ); - } + $this->assertOpen(); # Do the query and handle errors $startTime = microtime( true ); $ret = $this->doQuery( $commentedSql ); + $queryRuntime = microtime( true ) - $startTime; # Log the query time and feed it into the DB trx profiler $this->getTransactionProfiler()->recordQueryCompletion( $queryProf, $startTime, $isWriteQuery, $this->affectedRows() ); @@ -1191,6 +1061,7 @@ abstract class DatabaseBase implements IDatabase { # Should be safe to silently retry (no trx and thus no callbacks) $startTime = microtime( true ); $ret = $this->doQuery( $commentedSql ); + $queryRuntime = microtime( true ) - $startTime; # Log the query time and feed it into the DB trx profiler $this->getTransactionProfiler()->recordQueryCompletion( $queryProf, $startTime, $isWriteQuery, $this->affectedRows() ); @@ -1211,6 +1082,10 @@ abstract class DatabaseBase implements IDatabase { $queryProfSection = false; $totalProfSection = false; + if ( $isWriteQuery && $this->mTrxLevel ) { + $this->mTrxWriteDuration += $queryRuntime; + } + return $res; } @@ -1226,8 +1101,6 @@ abstract class DatabaseBase implements IDatabase { * @throws DBQueryError */ public function reportQueryError( $error, $errno, $sql, $fname, $tempIgnore = false ) { - ++$this->mErrorCount; - if ( $this->ignoreErrors() || $tempIgnore ) { wfDebug( "SQL ERROR (ignored): $error\n" ); } else { @@ -1421,6 +1294,7 @@ abstract class DatabaseBase implements IDatabase { * @param string|array $options The query options. See DatabaseBase::select() for details. * * @return bool|array The values from the field, or false on failure + * @throws DBUnexpectedError * @since 1.25 */ public function selectFieldValues( @@ -1881,7 +1755,7 @@ abstract class DatabaseBase implements IDatabase { ) { $rows = 0; $sql = $this->selectSQLText( $table, '1', $conds, $fname, $options ); - $res = $this->query( "SELECT COUNT(*) AS rowcount FROM ($sql) tmp_count" ); + $res = $this->query( "SELECT COUNT(*) AS rowcount FROM ($sql) tmp_count", $fname ); if ( $res ) { $row = $this->fetchRow( $res ); @@ -2015,9 +1889,6 @@ abstract class DatabaseBase implements IDatabase { * This causes a multi-row INSERT on DBMSs that support it. The keys in * each subarray must be identical to each other, and in the same order. * - * Usually throws a DBQueryError on failure. If errors are explicitly ignored, - * returns success. - * * $options is an array of options, with boolean options encoded as values * with numeric keys, in the same style as $options in * DatabaseBase::select(). Supported options are: @@ -2033,6 +1904,9 @@ abstract class DatabaseBase implements IDatabase { * @param string $fname Calling function name (use __METHOD__) for logs/profiling * @param array $options Array of options * + * @throws DBQueryError Usually throws a DBQueryError on failure. If errors are explicitly ignored, + * returns success. + * * @return bool */ public function insert( $table, $a, $fname = __METHOD__, $options = array() ) { @@ -2455,7 +2329,7 @@ abstract class DatabaseBase implements IDatabase { } # Quote $schema and merge it with the table name if needed - if ( $schema !== null ) { + if ( strlen( $schema ) ) { if ( $format == 'quoted' && !$this->isQuotedIdentifier( $schema ) ) { $schema = $this->addIdentifierQuotes( $schema ); } @@ -2842,6 +2716,7 @@ abstract class DatabaseBase implements IDatabase { $rows = array( $rows ); } + // @FXIME: this is not atomic, but a trx would break affectedRows() foreach ( $rows as $row ) { # Delete rows which collide if ( $uniqueIndexes ) { @@ -3302,6 +3177,17 @@ abstract class DatabaseBase implements IDatabase { return false; } + /** + * Determines if the given query error was a connection drop + * STUB + * + * @param integer|string $errno + * @return bool + */ + public function wasConnectionError( $errno ) { + return false; + } + /** * Perform a deadlock-prone transaction. * @@ -3318,7 +3204,8 @@ abstract class DatabaseBase implements IDatabase { * iteration, or false on error, for example if the retry limit was * reached. * - * @return bool + * @return mixed + * @throws DBQueryError */ public function deadlockLoop() { $args = func_get_args(); @@ -3332,15 +3219,13 @@ abstract class DatabaseBase implements IDatabase { $this->begin( __METHOD__ ); + $retVal = null; $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 after a randomized delay usleep( mt_rand( self::DEADLOCK_DELAY_MIN, self::DEADLOCK_DELAY_MAX ) ); @@ -3525,7 +3410,11 @@ abstract class DatabaseBase implements IDatabase { if ( !$this->mTrxLevel ) { $this->begin( $fname ); $this->mTrxAutomatic = true; - $this->mTrxAutomaticAtomic = true; + // If DBO_TRX is set, a series of startAtomic/endAtomic pairs will result + // in all changes being in one transaction to keep requests transactional. + if ( !$this->getFlag( DBO_TRX ) ) { + $this->mTrxAutomaticAtomic = true; + } } $this->mTrxAtomicLevels->push( $fname ); @@ -3605,19 +3494,18 @@ abstract class DatabaseBase implements IDatabase { } $this->runOnTransactionPreCommitCallbacks(); + $writeTime = $this->pendingWriteQueryDuration(); $this->doCommit( $fname ); if ( $this->mTrxDoneWrites ) { $this->mDoneWrites = microtime( true ); $this->getTransactionProfiler()->transactionWritingOut( - $this->mServer, $this->mDBname, $this->mTrxShortId ); + $this->mServer, $this->mDBname, $this->mTrxShortId, $writeTime ); } $this->runOnTransactionIdleCallbacks(); } # Avoid fatals if close() was called - if ( !$this->isOpen() ) { - throw new DBUnexpectedError( $this, "DB connection was already closed." ); - } + $this->assertOpen(); $this->doBegin( $fname ); $this->mTrxTimestamp = microtime( true ); @@ -3629,6 +3517,7 @@ abstract class DatabaseBase implements IDatabase { $this->mTrxIdleCallbacks = array(); $this->mTrxPreCommitCallbacks = array(); $this->mTrxShortId = wfRandomString( 12 ); + $this->mTrxWriteDuration = 0.0; } /** @@ -3681,16 +3570,15 @@ abstract class DatabaseBase implements IDatabase { } # Avoid fatals if close() was called - if ( !$this->isOpen() ) { - throw new DBUnexpectedError( $this, "DB connection was already closed." ); - } + $this->assertOpen(); $this->runOnTransactionPreCommitCallbacks(); + $writeTime = $this->pendingWriteQueryDuration(); $this->doCommit( $fname ); if ( $this->mTrxDoneWrites ) { $this->mDoneWrites = microtime( true ); $this->getTransactionProfiler()->transactionWritingOut( - $this->mServer, $this->mDBname, $this->mTrxShortId ); + $this->mServer, $this->mDBname, $this->mTrxShortId, $writeTime ); } $this->runOnTransactionIdleCallbacks(); } @@ -3739,9 +3627,7 @@ abstract class DatabaseBase implements IDatabase { } # Avoid fatals if close() was called - if ( !$this->isOpen() ) { - throw new DBUnexpectedError( $this, "DB connection was already closed." ); - } + $this->assertOpen(); $this->doRollback( $fname ); $this->mTrxIdleCallbacks = array(); // cancel @@ -3794,6 +3680,7 @@ abstract class DatabaseBase implements IDatabase { * @param string $prefix Only show tables with this prefix, e.g. mw_ * @param string $fname Calling function name * @throws MWException + * @return array */ function listTables( $prefix = null, $fname = __METHOD__ ) { throw new MWException( 'DatabaseBase::listTables is not implemented in descendant class' ); @@ -3816,6 +3703,7 @@ abstract class DatabaseBase implements IDatabase { * @param string $prefix Only show VIEWs with this prefix, eg. unit_test_ * @param string $fname Name of calling function * @throws MWException + * @return array * @since 1.22 */ public function listViews( $prefix = null, $fname = __METHOD__ ) { @@ -3827,6 +3715,7 @@ abstract class DatabaseBase implements IDatabase { * * @param string $name Name of the database-structure to test. * @throws MWException + * @return bool * @since 1.22 */ public function isView( $name ) { @@ -3988,9 +3877,9 @@ abstract class DatabaseBase implements IDatabase { public function sourceFile( $filename, $lineCallback = false, $resultCallback = false, $fname = false, $inputCallback = false ) { - wfSuppressWarnings(); + MediaWiki\suppressWarnings(); $fp = fopen( $filename, 'r' ); - wfRestoreWarnings(); + MediaWiki\restoreWarnings(); if ( false === $fp ) { throw new MWException( "Could not open \"{$filename}\".\n" ); @@ -4205,7 +4094,7 @@ abstract class DatabaseBase implements IDatabase { } /** - * Check to see if a named lock is available. This is non-blocking. + * Check to see if a named lock is available (non-blocking) * * @param string $lockName Name of lock to poll * @param string $method Name of method calling us @@ -4219,8 +4108,7 @@ abstract class DatabaseBase implements IDatabase { /** * Acquire a named lock * - * Abstracted from Filestore::lock() so child classes can implement for - * their own needs. + * Named locks are not related to transactions * * @param string $lockName Name of lock to aquire * @param string $method Name of method calling us @@ -4232,7 +4120,9 @@ abstract class DatabaseBase implements IDatabase { } /** - * Release a lock. + * Release a lock + * + * Named locks are not related to transactions * * @param string $lockName Name of lock to release * @param string $method Name of method calling us @@ -4245,6 +4135,16 @@ abstract class DatabaseBase implements IDatabase { return true; } + /** + * Check to see if a named lock used by lock() use blocking queues + * + * @return bool + * @since 1.26 + */ + public function namedLocksEnqueue() { + return false; + } + /** * Lock specific tables * @@ -4328,7 +4228,7 @@ abstract class DatabaseBase implements IDatabase { * @return string */ public function decodeExpiry( $expiry, $format = TS_MW ) { - return ( $expiry == '' || $expiry == $this->getInfinity() ) + return ( $expiry == '' || $expiry == 'infinity' || $expiry == $this->getInfinity() ) ? 'infinity' : wfTimestamp( $format, $expiry ); } diff --git a/includes/db/DatabaseError.php b/includes/db/DatabaseError.php index 86950a89..928de616 100644 --- a/includes/db/DatabaseError.php +++ b/includes/db/DatabaseError.php @@ -329,12 +329,19 @@ class DBQueryError extends DBExpectedError { * @param string $fname */ function __construct( DatabaseBase $db, $error, $errno, $sql, $fname ) { - $message = "A database error has occurred. Did you forget to run " . - "maintenance/update.php after upgrading? See: " . - "https://www.mediawiki.org/wiki/Manual:Upgrading#Run_the_update_script\n" . - "Query: $sql\n" . - "Function: $fname\n" . - "Error: $errno $error\n"; + if ( $db->wasConnectionError( $errno ) ) { + $message = "A connection error occured. \n" . + "Query: $sql\n" . + "Function: $fname\n" . + "Error: $errno $error\n"; + } else { + $message = "A database error has occurred. Did you forget to run " . + "maintenance/update.php after upgrading? See: " . + "https://www.mediawiki.org/wiki/Manual:Upgrading#Run_the_update_script\n" . + "Query: $sql\n" . + "Function: $fname\n" . + "Error: $errno $error\n"; + } parent::__construct( $db, $message ); $this->error = $error; diff --git a/includes/db/DatabaseMssql.php b/includes/db/DatabaseMssql.php index 2b8f395f..85f1b96d 100644 --- a/includes/db/DatabaseMssql.php +++ b/includes/db/DatabaseMssql.php @@ -119,9 +119,9 @@ class DatabaseMssql extends DatabaseBase { $connectionInfo['PWD'] = $password; } - wfSuppressWarnings(); + MediaWiki\suppressWarnings(); $this->mConn = sqlsrv_connect( $server, $connectionInfo ); - wfRestoreWarnings(); + MediaWiki\restoreWarnings(); if ( $this->mConn === false ) { throw new DBConnectionError( $this, $this->lastError() ); @@ -1089,7 +1089,9 @@ class DatabaseMssql extends DatabaseBase { * @param string $s * @return string */ - public function strencode( $s ) { # Should not be called by us + public function strencode( $s ) { + // Should not be called by us + return str_replace( "'", "''", $s ); } diff --git a/includes/db/DatabaseMysql.php b/includes/db/DatabaseMysql.php index 823d9b67..5b151477 100644 --- a/includes/db/DatabaseMysql.php +++ b/includes/db/DatabaseMysql.php @@ -33,10 +33,12 @@ class DatabaseMysql extends DatabaseMysqlBase { * @return resource False on error */ protected function doQuery( $sql ) { + $conn = $this->getBindingHandle(); + if ( $this->bufferResults() ) { - $ret = mysql_query( $sql, $this->mConn ); + $ret = mysql_query( $sql, $conn ); } else { - $ret = mysql_unbuffered_query( $sql, $this->mConn ); + $ret = mysql_unbuffered_query( $sql, $conn ); } return $ret; @@ -48,8 +50,7 @@ class DatabaseMysql extends DatabaseMysqlBase { * @throws DBConnectionError */ protected function mysqlConnect( $realServer ) { - # Fail now - # Otherwise we get a suppressed fatal error, which is very hard to track down + # Avoid a suppressed fatal error, which is very hard to track down if ( !extension_loaded( 'mysql' ) ) { throw new DBConnectionError( $this, @@ -73,6 +74,9 @@ class DatabaseMysql extends DatabaseMysqlBase { $conn = false; + # 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. for ( $i = 0; $i < $numAttempts && !$conn; $i++ ) { if ( $i > 1 ) { usleep( 1000 ); @@ -93,8 +97,10 @@ class DatabaseMysql extends DatabaseMysqlBase { * @return bool */ protected function mysqlSetCharset( $charset ) { + $conn = $this->getBindingHandle(); + if ( function_exists( 'mysql_set_charset' ) ) { - return mysql_set_charset( $charset, $this->mConn ); + return mysql_set_charset( $charset, $conn ); } else { return $this->query( 'SET NAMES ' . $charset, __METHOD__ ); } @@ -104,14 +110,18 @@ class DatabaseMysql extends DatabaseMysqlBase { * @return bool */ protected function closeConnection() { - return mysql_close( $this->mConn ); + $conn = $this->getBindingHandle(); + + return mysql_close( $conn ); } /** * @return int */ function insertId() { - return mysql_insert_id( $this->mConn ); + $conn = $this->getBindingHandle(); + + return mysql_insert_id( $conn ); } /** @@ -129,7 +139,9 @@ class DatabaseMysql extends DatabaseMysqlBase { * @return int */ function affectedRows() { - return mysql_affected_rows( $this->mConn ); + $conn = $this->getBindingHandle(); + + return mysql_affected_rows( $conn ); } /** @@ -137,9 +149,11 @@ class DatabaseMysql extends DatabaseMysqlBase { * @return bool */ function selectDB( $db ) { + $conn = $this->getBindingHandle(); + $this->mDBname = $db; - return mysql_select_db( $db, $this->mConn ); + return mysql_select_db( $db, $conn ); } protected function mysqlFreeResult( $res ) { @@ -183,10 +197,14 @@ class DatabaseMysql extends DatabaseMysqlBase { } protected function mysqlRealEscapeString( $s ) { - return mysql_real_escape_string( $s, $this->mConn ); + $conn = $this->getBindingHandle(); + + return mysql_real_escape_string( $s, $conn ); } protected function mysqlPing() { - return mysql_ping( $this->mConn ); + $conn = $this->getBindingHandle(); + + return mysql_ping( $conn ); } } diff --git a/includes/db/DatabaseMysqlBase.php b/includes/db/DatabaseMysqlBase.php index aac95a8c..be34242b 100644 --- a/includes/db/DatabaseMysqlBase.php +++ b/includes/db/DatabaseMysqlBase.php @@ -59,22 +59,16 @@ abstract class DatabaseMysqlBase extends DatabaseBase { function open( $server, $user, $password, $dbName ) { global $wgAllDBsAreLocalhost, $wgSQLMode; - # Debugging hack -- fake cluster - if ( $wgAllDBsAreLocalhost ) { - $realServer = 'localhost'; - } else { - $realServer = $server; - } + # Close/unset connection handle $this->close(); + + # Debugging hack -- fake cluster + $realServer = $wgAllDBsAreLocalhost ? 'localhost' : $server; $this->mServer = $server; $this->mUser = $user; $this->mPassword = $password; $this->mDBname = $dbName; - # 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. - $this->mConn = false; $this->installErrorHandler(); try { $this->mConn = $this->mysqlConnect( $realServer ); @@ -104,9 +98,9 @@ abstract class DatabaseMysqlBase extends DatabaseBase { } if ( $dbName != '' ) { - wfSuppressWarnings(); + MediaWiki\suppressWarnings(); $success = $this->selectDB( $dbName ); - wfRestoreWarnings(); + MediaWiki\restoreWarnings(); if ( !$success ) { wfLogDBError( "Error selecting database {db_name} on server {db_server}", @@ -132,6 +126,15 @@ abstract class DatabaseMysqlBase extends DatabaseBase { if ( is_string( $wgSQLMode ) ) { $set[] = 'sql_mode = ' . $this->addQuotes( $wgSQLMode ); } + // Set any custom settings defined by site config + // (e.g. https://dev.mysql.com/doc/refman/4.1/en/innodb-parameters.html) + foreach ( $this->mSessionVars as $var => $val ) { + // Escape strings but not numbers to avoid MySQL complaining + if ( !is_int( $val ) && !is_float( $val ) ) { + $val = $this->addQuotes( $val ); + } + $set[] = $this->addIdentifierQuotes( $var ) . ' = ' . $val; + } if ( $set ) { // Use doQuery() to avoid opening implicit transactions (DBO_TRX) @@ -194,9 +197,9 @@ abstract class DatabaseMysqlBase extends DatabaseBase { if ( $res instanceof ResultWrapper ) { $res = $res->result; } - wfSuppressWarnings(); + MediaWiki\suppressWarnings(); $ok = $this->mysqlFreeResult( $res ); - wfRestoreWarnings(); + MediaWiki\restoreWarnings(); if ( !$ok ) { throw new DBUnexpectedError( $this, "Unable to free MySQL result" ); } @@ -219,9 +222,9 @@ abstract class DatabaseMysqlBase extends DatabaseBase { if ( $res instanceof ResultWrapper ) { $res = $res->result; } - wfSuppressWarnings(); + MediaWiki\suppressWarnings(); $row = $this->mysqlFetchObject( $res ); - wfRestoreWarnings(); + MediaWiki\restoreWarnings(); $errno = $this->lastErrno(); // Unfortunately, mysql_fetch_object does not reset the last errno. @@ -255,9 +258,9 @@ abstract class DatabaseMysqlBase extends DatabaseBase { if ( $res instanceof ResultWrapper ) { $res = $res->result; } - wfSuppressWarnings(); + MediaWiki\suppressWarnings(); $row = $this->mysqlFetchArray( $res ); - wfRestoreWarnings(); + MediaWiki\restoreWarnings(); $errno = $this->lastErrno(); // Unfortunately, mysql_fetch_array does not reset the last errno. @@ -291,9 +294,9 @@ abstract class DatabaseMysqlBase extends DatabaseBase { if ( $res instanceof ResultWrapper ) { $res = $res->result; } - wfSuppressWarnings(); + MediaWiki\suppressWarnings(); $n = $this->mysqlNumRows( $res ); - wfRestoreWarnings(); + MediaWiki\restoreWarnings(); // Unfortunately, mysql_num_rows does not reset the last errno. // We are not checking for any errors here, since @@ -404,12 +407,12 @@ abstract class DatabaseMysqlBase extends DatabaseBase { function lastError() { if ( $this->mConn ) { # Even if it's non-zero, it can still be invalid - wfSuppressWarnings(); + MediaWiki\suppressWarnings(); $error = $this->mysqlError( $this->mConn ); if ( !$error ) { $error = $this->mysqlError(); } - wfRestoreWarnings(); + MediaWiki\restoreWarnings(); } else { $error = $this->mysqlError(); } @@ -577,9 +580,12 @@ abstract class DatabaseMysqlBase extends DatabaseBase { function ping() { $ping = $this->mysqlPing(); if ( $ping ) { + // Connection was good or lost but reconnected... + // @note: mysqlnd (php 5.6+) does not support this (PHP bug 52561) return true; } + // Try a full disconnect/reconnect cycle if ping() failed $this->closeConnection(); $this->mOpened = false; $this->mConn = false; @@ -873,6 +879,10 @@ abstract class DatabaseMysqlBase extends DatabaseBase { return ( $row->lockstatus == 1 ); } + public function namedLocksEnqueue() { + return true; + } + /** * @param array $read * @param array $write @@ -930,7 +940,7 @@ abstract class DatabaseMysqlBase extends DatabaseBase { $value = $this->mDefaultBigSelects; } } elseif ( $this->mDefaultBigSelects === null ) { - $this->mDefaultBigSelects = (bool)$this->selectField( false, '@@sql_big_selects' ); + $this->mDefaultBigSelects = (bool)$this->selectField( false, '@@sql_big_selects', '', __METHOD__ ); } $encValue = $value ? '1' : '0'; $this->query( "SET sql_big_selects=$encValue", __METHOD__ ); @@ -1045,6 +1055,32 @@ abstract class DatabaseMysqlBase extends DatabaseBase { ( $this->lastErrno() == 1290 && strpos( $this->lastError(), '--read-only' ) !== false ); } + function wasConnectionError( $errno ) { + return $errno == 2013 || $errno == 2006; + } + + /** + * Get the underlying binding handle, mConn + * + * Makes sure that mConn is set (disconnects and ping() failure can unset it). + * This catches broken callers than catch and ignore disconnection exceptions. + * Unlike checking isOpen(), this is safe to call inside of open(). + * + * @return resource|object + * @throws DBUnexpectedError + * @since 1.26 + */ + protected function getBindingHandle() { + if ( !$this->mConn ) { + throw new DBUnexpectedError( + $this, + 'DB connection was already closed or the connection dropped.' + ); + } + + return $this->mConn; + } + /** * @param string $oldName * @param string $newName diff --git a/includes/db/DatabaseMysqli.php b/includes/db/DatabaseMysqli.php index ad12e196..8ca23627 100644 --- a/includes/db/DatabaseMysqli.php +++ b/includes/db/DatabaseMysqli.php @@ -29,15 +29,20 @@ * @see Database */ class DatabaseMysqli extends DatabaseMysqlBase { + /** @var mysqli */ + protected $mConn; + /** * @param string $sql * @return resource */ protected function doQuery( $sql ) { + $conn = $this->getBindingHandle(); + if ( $this->bufferResults() ) { - $ret = $this->mConn->query( $sql ); + $ret = $conn->query( $sql ); } else { - $ret = $this->mConn->query( $sql, MYSQLI_USE_RESULT ); + $ret = $conn->query( $sql, MYSQLI_USE_RESULT ); } return $ret; @@ -50,8 +55,8 @@ class DatabaseMysqli extends DatabaseMysqlBase { */ protected function mysqlConnect( $realServer ) { global $wgDBmysql5; - # Fail now - # Otherwise we get a suppressed fatal error, which is very hard to track down + + # Avoid suppressed fatal error, which is very hard to track down if ( !function_exists( 'mysqli_init' ) ) { throw new DBConnectionError( $this, "MySQLi functions missing," . " have you compiled PHP with the --with-mysqli option?\n" ); @@ -116,8 +121,10 @@ class DatabaseMysqli extends DatabaseMysqlBase { * @return bool */ protected function mysqlSetCharset( $charset ) { - if ( method_exists( $this->mConn, 'set_charset' ) ) { - return $this->mConn->set_charset( $charset ); + $conn = $this->getBindingHandle(); + + if ( method_exists( $conn, 'set_charset' ) ) { + return $conn->set_charset( $charset ); } else { return $this->query( 'SET NAMES ' . $charset, __METHOD__ ); } @@ -127,14 +134,18 @@ class DatabaseMysqli extends DatabaseMysqlBase { * @return bool */ protected function closeConnection() { - return $this->mConn->close(); + $conn = $this->getBindingHandle(); + + return $conn->close(); } /** * @return int */ function insertId() { - return $this->mConn->insert_id; + $conn = $this->getBindingHandle(); + + return (int)$conn->insert_id; } /** @@ -152,7 +163,9 @@ class DatabaseMysqli extends DatabaseMysqlBase { * @return int */ function affectedRows() { - return $this->mConn->affected_rows; + $conn = $this->getBindingHandle(); + + return $conn->affected_rows; } /** @@ -160,9 +173,11 @@ class DatabaseMysqli extends DatabaseMysqlBase { * @return bool */ function selectDB( $db ) { + $conn = $this->getBindingHandle(); + $this->mDBname = $db; - return $this->mConn->select_db( $db ); + return $conn->select_db( $db ); } /** @@ -289,11 +304,15 @@ class DatabaseMysqli extends DatabaseMysqlBase { * @return string */ protected function mysqlRealEscapeString( $s ) { - return $this->mConn->real_escape_string( $s ); + $conn = $this->getBindingHandle(); + + return $conn->real_escape_string( $s ); } protected function mysqlPing() { - return $this->mConn->ping(); + $conn = $this->getBindingHandle(); + + return $conn->ping(); } /** diff --git a/includes/db/DatabaseOracle.php b/includes/db/DatabaseOracle.php index 9b00fbd1..87c31646 100644 --- a/includes/db/DatabaseOracle.php +++ b/includes/db/DatabaseOracle.php @@ -220,9 +220,9 @@ class DatabaseOracle extends DatabaseBase { function __destruct() { if ( $this->mOpened ) { - wfSuppressWarnings(); + MediaWiki\suppressWarnings(); $this->close(); - wfRestoreWarnings(); + MediaWiki\restoreWarnings(); } } @@ -306,7 +306,7 @@ class DatabaseOracle extends DatabaseBase { $session_mode = $this->mFlags & DBO_SYSDBA ? OCI_SYSDBA : OCI_DEFAULT; - wfSuppressWarnings(); + MediaWiki\suppressWarnings(); if ( $this->mFlags & DBO_PERSISTENT ) { $this->mConn = oci_pconnect( $this->mUser, @@ -332,7 +332,7 @@ class DatabaseOracle extends DatabaseBase { $session_mode ); } - wfRestoreWarnings(); + MediaWiki\restoreWarnings(); if ( $this->mUser != $this->mDBname ) { //change current schema in session @@ -393,7 +393,7 @@ class DatabaseOracle extends DatabaseBase { $explain_count ); - wfSuppressWarnings(); + MediaWiki\suppressWarnings(); if ( ( $this->mLastResult = $stmt = oci_parse( $this->mConn, $sql ) ) === false ) { $e = oci_error( $this->mConn ); @@ -411,7 +411,7 @@ class DatabaseOracle extends DatabaseBase { } } - wfRestoreWarnings(); + MediaWiki\restoreWarnings(); if ( $explain_count > 0 ) { return $this->doQuery( 'SELECT id, cardinality "ROWS" FROM plan_table ' . @@ -687,7 +687,7 @@ class DatabaseOracle extends DatabaseBase { } } - wfSuppressWarnings(); + MediaWiki\suppressWarnings(); if ( oci_execute( $stmt, $this->execFlags() ) === false ) { $e = oci_error( $stmt ); @@ -702,7 +702,7 @@ class DatabaseOracle extends DatabaseBase { $this->mAffectedRows = oci_num_rows( $stmt ); } - wfRestoreWarnings(); + MediaWiki\restoreWarnings(); if ( isset( $lob ) ) { foreach ( $lob as $lob_v ) { @@ -971,20 +971,6 @@ class DatabaseOracle extends DatabaseBase { return $valuedata; } - 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 ) { - wfDebug( "SQL ERROR (ignored): $error\n" ); - $this->ignoreErrors( $ignore ); - } else { - throw new DBQueryError( $this, $error, $errno, $sql, $fname ); - } - } - /** * @return string Wikitext of a link to the server software's web site */ @@ -1250,9 +1236,9 @@ class DatabaseOracle extends DatabaseBase { } $sql = 'ALTER SESSION SET CURRENT_SCHEMA=' . strtoupper( $db ); $stmt = oci_parse( $this->mConn, $sql ); - wfSuppressWarnings(); + MediaWiki\suppressWarnings(); $success = oci_execute( $stmt ); - wfRestoreWarnings(); + MediaWiki\restoreWarnings(); if ( !$success ) { $e = oci_error( $stmt ); if ( $e['code'] != '1435' ) { @@ -1491,7 +1477,7 @@ class DatabaseOracle extends DatabaseBase { } } - wfSuppressWarnings(); + MediaWiki\suppressWarnings(); if ( oci_execute( $stmt, $this->execFlags() ) === false ) { $e = oci_error( $stmt ); @@ -1506,7 +1492,7 @@ class DatabaseOracle extends DatabaseBase { $this->mAffectedRows = oci_num_rows( $stmt ); } - wfRestoreWarnings(); + MediaWiki\restoreWarnings(); if ( isset( $lob ) ) { foreach ( $lob as $lob_v ) { diff --git a/includes/db/DatabasePostgres.php b/includes/db/DatabasePostgres.php index 9287f7a6..56a5b2cf 100644 --- a/includes/db/DatabasePostgres.php +++ b/includes/db/DatabasePostgres.php @@ -217,7 +217,7 @@ class PostgresTransactionState { * @since 1.19 */ class SavepointPostgres { - /** @var DatabaseBase Establish a savepoint within a transaction */ + /** @var DatabasePostgres Establish a savepoint within a transaction */ protected $dbw; protected $id; protected $didbegin; @@ -551,9 +551,9 @@ class DatabasePostgres extends DatabaseBase { if ( $res instanceof ResultWrapper ) { $res = $res->result; } - wfSuppressWarnings(); + MediaWiki\suppressWarnings(); $ok = pg_free_result( $res ); - wfRestoreWarnings(); + MediaWiki\restoreWarnings(); if ( !$ok ) { throw new DBUnexpectedError( $this, "Unable to free Postgres result\n" ); } @@ -568,9 +568,9 @@ class DatabasePostgres extends DatabaseBase { if ( $res instanceof ResultWrapper ) { $res = $res->result; } - wfSuppressWarnings(); + MediaWiki\suppressWarnings(); $row = pg_fetch_object( $res ); - wfRestoreWarnings(); + MediaWiki\restoreWarnings(); # @todo FIXME: HACK HACK HACK HACK debug # @todo hashar: not sure if the following test really trigger if the object @@ -589,9 +589,9 @@ class DatabasePostgres extends DatabaseBase { if ( $res instanceof ResultWrapper ) { $res = $res->result; } - wfSuppressWarnings(); + MediaWiki\suppressWarnings(); $row = pg_fetch_array( $res ); - wfRestoreWarnings(); + MediaWiki\restoreWarnings(); if ( pg_last_error( $this->mConn ) ) { throw new DBUnexpectedError( $this, @@ -606,9 +606,9 @@ class DatabasePostgres extends DatabaseBase { if ( $res instanceof ResultWrapper ) { $res = $res->result; } - wfSuppressWarnings(); + MediaWiki\suppressWarnings(); $n = pg_num_rows( $res ); - wfRestoreWarnings(); + MediaWiki\restoreWarnings(); if ( pg_last_error( $this->mConn ) ) { throw new DBUnexpectedError( $this, @@ -1510,7 +1510,9 @@ SQL; return pg_unescape_bytea( $b ); } - function strencode( $s ) { # Should not be called by us + function strencode( $s ) { + // Should not be called by us + return pg_escape_string( $this->mConn, $s ); } @@ -1702,4 +1704,5 @@ SQL; } } // end DatabasePostgres class -class PostgresBlob extends Blob {} +class PostgresBlob extends Blob { +} diff --git a/includes/db/DatabaseSqlite.php b/includes/db/DatabaseSqlite.php index ed86bab1..656547b5 100644 --- a/includes/db/DatabaseSqlite.php +++ b/includes/db/DatabaseSqlite.php @@ -64,16 +64,16 @@ class DatabaseSqlite extends DatabaseBase { $this->dbDir = isset( $p['dbDirectory'] ) ? $p['dbDirectory'] : $wgSQLiteDataDir; if ( isset( $p['dbFilePath'] ) ) { - $this->mFlags = isset( $p['flags'] ) ? $p['flags'] : 0; - // Standalone .sqlite file mode + parent::__construct( $p ); + // Standalone .sqlite file mode. + // Super doesn't open when $user is false, but we can work with $dbName, + // which is derived from the file path in this case. $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 + // 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 + // Super 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 ) { @@ -105,8 +105,10 @@ class DatabaseSqlite extends DatabaseBase { */ public static function newStandaloneInstance( $filename, array $p = array() ) { $p['dbFilePath'] = $filename; + $p['schema'] = false; + $p['tablePrefix'] = ''; - return new self( $p ); + return DatabaseBase::factory( 'sqlite', $p ); } /** @@ -282,7 +284,7 @@ class DatabaseSqlite extends DatabaseBase { * @return bool */ function isWriteQuery( $sql ) { - return parent::isWriteQuery( $sql ) && !preg_match( '/^ATTACH\b/i', $sql ); + return parent::isWriteQuery( $sql ) && !preg_match( '/^(ATTACH|PRAGMA)\b/i', $sql ); } /** @@ -962,7 +964,36 @@ class DatabaseSqlite extends DatabaseBase { } } - return $this->query( $sql, $fname ); + $res = $this->query( $sql, $fname ); + + // Take over indexes + $indexList = $this->query( 'PRAGMA INDEX_LIST(' . $this->addQuotes( $oldName ) . ')' ); + foreach ( $indexList as $index ) { + if ( strpos( $index->name, 'sqlite_autoindex' ) === 0 ) { + continue; + } + + if ( $index->unique ) { + $sql = 'CREATE UNIQUE INDEX'; + } else { + $sql = 'CREATE INDEX'; + } + // Try to come up with a new index name, given indexes have database scope in SQLite + $indexName = $newName . '_' . $index->name; + $sql .= ' ' . $indexName . ' ON ' . $newName; + + $indexInfo = $this->query( 'PRAGMA INDEX_INFO(' . $this->addQuotes( $index->name ) . ')' ); + $fields = array(); + foreach ( $indexInfo as $indexInfoRow ) { + $fields[ $indexInfoRow->seqno ] = $indexInfoRow->name; + } + + $sql .= '(' . implode( ',', $fields ) . ')'; + + $this->query( $sql ); + } + + return $res; } /** diff --git a/includes/db/IDatabase.php b/includes/db/IDatabase.php new file mode 100644 index 00000000..49d0514d --- /dev/null +++ b/includes/db/IDatabase.php @@ -0,0 +1,1513 @@ +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 + */ + public 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 + */ + public function fetchRow( $res ); + + /** + * Get the number of rows in a result object + * + * @param mixed $res A SQL result + * @return int + */ + public 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 + */ + public 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 + */ + public 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 + */ + public 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 + */ + public function dataSeek( $res, $row ); + + /** + * Get the last error number + * @see http://www.php.net/mysql_errno + * + * @return int + */ + public function lastErrno(); + + /** + * Get a description of the last error + * @see http://www.php.net/mysql_error + * + * @return string + */ + public 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 + */ + public function fieldInfo( $table, $field ); + + /** + * Get the number of rows affected by the last write query + * @see http://www.php.net/mysql_affected_rows + * + * @return int + */ + public function affectedRows(); + + /** + * 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 + */ + public function getSoftwareLink(); + + /** + * A string describing the current software version, like from + * mysql_get_server_info(). + * + * @return string Version information from the database server. + */ + public function getServerVersion(); + + /** + * Closes a database connection. + * if it is open : commits any open transactions + * + * @throws MWException + * @return bool Operation success. true if already closed. + */ + public function close(); + + /** + * @param string $error Fallback error message, used if none is given by DB + * @throws DBConnectionError + */ + public function reportConnectionError( $error = 'Unknown error' ); + + /** + * Run an SQL query and return the result. Normally throws a DBQueryError + * on failure. If errors are ignored, returns false instead. + * + * In new code, the query wrappers select(), insert(), update(), delete(), + * etc. should be used where possible, since they give much better DBMS + * independence and automatically quote or validate user input in a variety + * of contexts. This function is generally only useful for queries which are + * explicitly DBMS-dependent and are unsupported by the query wrappers, such + * as CREATE TABLE. + * + * However, the query wrappers themselves should call this function. + * + * @param string $sql SQL query + * @param string $fname Name of the calling function, for profiling/SHOW PROCESSLIST + * comment (you can use __METHOD__ or add some extra info) + * @param bool $tempIgnore Whether to avoid throwing an exception on errors... + * maybe best to catch the exception instead? + * @throws MWException + * @return bool|ResultWrapper True for a successful write query, ResultWrapper object + * for a successful read query, or false on failure if $tempIgnore set + */ + public function query( $sql, $fname = __METHOD__, $tempIgnore = false ); + + /** + * Report a query error. Log the error, and if neither the object ignore + * flag nor the $tempIgnore flag is set, throw a DBQueryError. + * + * @param string $error + * @param int $errno + * @param string $sql + * @param string $fname + * @param bool $tempIgnore + * @throws DBQueryError + */ + public function reportQueryError( $error, $errno, $sql, $fname, $tempIgnore = false ); + + /** + * Free a result object returned by query() or select(). It's usually not + * necessary to call this, just use unset() or let the variable holding + * the result object go out of scope. + * + * @param mixed $res A SQL result + */ + public function freeResult( $res ); + + /** + * A SELECT wrapper which returns a single field from a single result row. + * + * 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|mixed The value from the field, or false on failure. + */ + public function selectField( + $table, $var, $cond = '', $fname = __METHOD__, $options = array() + ); + + /** + * 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() + ); + + /** + * Execute a SELECT query constructed using the various parameters provided. + * See below for full details of the parameters. + * + * @param string|array $table Table name + * @param string|array $vars Field names + * @param string|array $conds Conditions + * @param string $fname Caller function name + * @param array $options Query options + * @param array $join_conds Join conditions + * + * + * @param string|array $table + * + * May be either an array of table names, or a single string holding a table + * name. If an array is given, table aliases can be specified, for example: + * + * array( 'a' => 'user' ) + * + * This includes the user table in the query, with the alias "a" available + * for use in field names (e.g. a.user_name). + * + * All of the table names given here are automatically run through + * DatabaseBase::tableName(), which causes the table prefix (if any) to be + * added, and various other table name mappings to be performed. + * + * + * @param string|array $vars + * + * May be either a field name or an array of field names. The field names + * can be complete fragments of SQL, for direct inclusion into the SELECT + * query. If an array is given, field aliases can be specified, for example: + * + * array( 'maxrev' => 'MAX(rev_id)' ) + * + * This includes an expression with the alias "maxrev" in the query. + * + * If an expression is given, care must be taken to ensure that it is + * DBMS-independent. + * + * + * @param string|array $conds + * + * May be either a string containing a single condition, or an array of + * conditions. If an array is given, the conditions constructed from each + * element are combined with AND. + * + * Array elements may take one of two forms: + * + * - Elements with a numeric key are interpreted as raw SQL fragments. + * - Elements with a string key are interpreted as equality conditions, + * where the key is the field name. + * - 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 + * 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 + * expression commonly used in condition queries. See: + * - DatabaseBase::buildLike() + * - DatabaseBase::conditional() + * + * + * @param string|array $options + * + * Optional: Array of query options. Boolean options are specified by + * including them in the array as a string value with a numeric key, for + * example: + * + * array( 'FOR UPDATE' ) + * + * The supported options are: + * + * - OFFSET: Skip this many rows at the start of the result set. OFFSET + * with LIMIT can theoretically be used for paging through a result set, + * but this is discouraged in MediaWiki for performance reasons. + * + * - LIMIT: Integer: return at most this many rows. The rows are sorted + * and then the first rows are taken until the limit is reached. LIMIT + * is applied to a result set after OFFSET. + * + * - FOR UPDATE: Boolean: lock the returned rows so that they can't be + * changed until the next COMMIT. + * + * - DISTINCT: Boolean: return only unique result rows. + * + * - GROUP BY: May be either an SQL fragment string naming a field or + * expression to group by, or an array of such SQL fragments. + * + * - HAVING: May be either an string containing a HAVING clause or an array of + * conditions building the HAVING clause. If an array is given, the conditions + * constructed from each element are combined with AND. + * + * - ORDER BY: May be either an SQL fragment giving a field name or + * expression to order by, or an array of such SQL fragments. + * + * - USE INDEX: This may be either a string giving the index name to use + * for the query, or an array. If it is an associative array, each key + * gives the table name (or alias), each value gives the index name to + * use for that table. All strings are SQL fragments and so should be + * validated by the caller. + * + * - EXPLAIN: In MySQL, this causes an EXPLAIN SELECT query to be run, + * instead of SELECT. + * + * And also the following boolean MySQL extensions, see the MySQL manual + * for documentation: + * + * - LOCK IN SHARE MODE + * - STRAIGHT_JOIN + * - HIGH_PRIORITY + * - SQL_BIG_RESULT + * - SQL_BUFFER_RESULT + * - SQL_SMALL_RESULT + * - SQL_CALC_FOUND_ROWS + * - SQL_CACHE + * - SQL_NO_CACHE + * + * + * @param string|array $join_conds + * + * Optional associative array of table-specific join conditions. In the + * most common case, this is unnecessary, since the join condition can be + * in $conds. However, it is useful for doing a LEFT JOIN. + * + * The key of the array contains the table name or alias. The value is an + * array with two elements, numbered 0 and 1. The first gives the type of + * join, the second is an SQL fragment giving the join condition for that + * table. For example: + * + * array( 'page' => array( 'LEFT JOIN', 'page_latest=rev_id' ) ) + * + * @return ResultWrapper|bool If the query returned no rows, a ResultWrapper + * with no rows in it will be returned. If there was a query error, a + * DBQueryError exception will be thrown, except if the "ignore errors" + * option was set, in which case false will be returned. + */ + public function select( + $table, $vars, $conds = '', $fname = __METHOD__, + $options = array(), $join_conds = array() + ); + + /** + * The equivalent of DatabaseBase::select() except that the constructed SQL + * is returned, instead of being immediately executed. This can be useful for + * doing UNION queries, where the SQL text of each query is needed. In general, + * however, callers outside of Database classes should just use select(). + * + * @param string|array $table Table name + * @param string|array $vars Field names + * @param string|array $conds Conditions + * @param string $fname Caller function name + * @param string|array $options Query options + * @param string|array $join_conds Join conditions + * + * @return string SQL query string. + * @see DatabaseBase::select() + */ + public function selectSQLText( + $table, $vars, $conds = '', $fname = __METHOD__, + $options = array(), $join_conds = array() + ); + + /** + * Single row SELECT wrapper. Equivalent to DatabaseBase::select(), except + * that a single row object is returned. If the query returns no rows, + * false is returned. + * + * @param string|array $table Table name + * @param string|array $vars Field names + * @param array $conds Conditions + * @param string $fname Caller function name + * @param string|array $options Query options + * @param array|string $join_conds Join conditions + * + * @return stdClass|bool + */ + public function selectRow( $table, $vars, $conds, $fname = __METHOD__, + $options = array(), $join_conds = array() + ); + + /** + * Estimate the number of rows in dataset + * + * MySQL allows you to estimate the number of rows that would be returned + * by a SELECT query, using EXPLAIN SELECT. The estimate is provided using + * index cardinality statistics, and is notoriously inaccurate, especially + * when large numbers of rows have recently been added or deleted. + * + * For DBMSs that don't support fast result size estimation, this function + * will actually perform the SELECT COUNT(*). + * + * Takes the same arguments as DatabaseBase::select(). + * + * @param string $table Table name + * @param string $vars Unused + * @param array|string $conds Filters on the table + * @param string $fname Function name for profiling + * @param array $options Options for select + * @return int Row count + */ + public function estimateRowCount( + $table, $vars = '*', $conds = '', $fname = __METHOD__, $options = array() + ); + + /** + * Get the number of rows in dataset + * + * This is useful when trying to do COUNT(*) but with a LIMIT for performance. + * + * Takes the same arguments as DatabaseBase::select(). + * + * @param string $table Table name + * @param string $vars Unused + * @param array|string $conds Filters on the table + * @param string $fname Function name for profiling + * @param array $options Options for select + * @return int Row count + * @since 1.24 + */ + public function selectRowCount( + $table, $vars = '*', $conds = '', $fname = __METHOD__, $options = array() + ); + + /** + * Determines whether a field exists in a table + * + * @param string $table Table name + * @param string $field Filed to check on that table + * @param string $fname Calling function name (optional) + * @return bool Whether $table has filed $field + */ + public function fieldExists( $table, $field, $fname = __METHOD__ ); + + /** + * Determines whether an index exists + * Usually throws a DBQueryError on failure + * If errors are explicitly ignored, returns NULL on failure + * + * @param string $table + * @param string $index + * @param string $fname + * @return bool|null + */ + public function indexExists( $table, $index, $fname = __METHOD__ ); + + /** + * Query whether a given table exists + * + * @param string $table + * @param string $fname + * @return bool + */ + public function tableExists( $table, $fname = __METHOD__ ); + + /** + * Determines if a given index is unique + * + * @param string $table + * @param string $index + * + * @return bool + */ + public function indexUnique( $table, $index ); + + /** + * INSERT wrapper, inserts an array into a table. + * + * $a may be either: + * + * - A single associative array. The array keys are the field names, and + * the values are the values to insert. The values are treated as data + * and will be quoted appropriately. If NULL is inserted, this will be + * converted to a database NULL. + * - An array with numeric keys, holding a list of associative arrays. + * This causes a multi-row INSERT on DBMSs that support it. The keys in + * each subarray must be identical to each other, and in the same order. + * + * Usually throws a DBQueryError on failure. If errors are explicitly ignored, + * returns success. + * + * $options is an array of options, with boolean options encoded as values + * with numeric keys, in the same style as $options in + * DatabaseBase::select(). Supported options are: + * + * - IGNORE: Boolean: if present, duplicate key errors are ignored, and + * any rows which cause duplicate key errors are not inserted. It's + * possible to determine how many rows were successfully inserted using + * DatabaseBase::affectedRows(). + * + * @param string $table Table name. This will be passed through + * DatabaseBase::tableName(). + * @param array $a Array of rows to insert + * @param string $fname Calling function name (use __METHOD__) for logs/profiling + * @param array $options Array of options + * + * @return bool + */ + public function insert( $table, $a, $fname = __METHOD__, $options = array() ); + + /** + * UPDATE wrapper. Takes a condition array and a SET array. + * + * @param string $table Name of the table to UPDATE. This will be passed through + * DatabaseBase::tableName(). + * @param array $values An array of values to SET. For each array element, + * the key gives the field name, and the value gives the data to set + * that field to. The data will be quoted by DatabaseBase::addQuotes(). + * @param array $conds An array of conditions (WHERE). See + * DatabaseBase::select() for the details of the format of condition + * arrays. Use '*' to update all rows. + * @param string $fname The function name of the caller (from __METHOD__), + * for logging and profiling. + * @param array $options An array of UPDATE options, can be: + * - IGNORE: Ignore unique key conflicts + * - LOW_PRIORITY: MySQL-specific, see MySQL manual. + * @return bool + */ + public function update( $table, $values, $conds, $fname = __METHOD__, $options = array() ); + + /** + * Makes an encoded list of strings from an array + * + * @param array $a Containing the data + * @param int $mode Constant + * - LIST_COMMA: Comma separated, no field names + * - LIST_AND: ANDed WHERE clause (without the WHERE). See the + * documentation for $conds in DatabaseBase::select(). + * - LIST_OR: ORed WHERE clause (without the WHERE) + * - LIST_SET: Comma separated with field names, like a SET clause + * - LIST_NAMES: Comma separated field names + * @throws MWException|DBUnexpectedError + * @return string + */ + public function makeList( $a, $mode = LIST_COMMA ); + + /** + * Build a partial where clause from a 2-d array such as used for LinkBatch. + * The keys on each level may be either integers or strings. + * + * @param array $data Organized as 2-d + * array(baseKeyVal => array(subKeyVal => [ignored], ...), ...) + * @param string $baseKey Field name to match the base-level keys to (eg 'pl_namespace') + * @param string $subKey Field name to match the sub-level keys to (eg 'pl_title') + * @return string|bool SQL fragment, or false if no items in array + */ + public function makeWhereFrom2d( $data, $baseKey, $subKey ); + + /** + * @param string $field + * @return string + */ + public function bitNot( $field ); + + /** + * @param string $fieldLeft + * @param string $fieldRight + * @return string + */ + public function bitAnd( $fieldLeft, $fieldRight ); + + /** + * @param string $fieldLeft + * @param string $fieldRight + * @return string + */ + public function bitOr( $fieldLeft, $fieldRight ); + + /** + * Build a concatenation list to feed into a SQL query + * @param array $stringList List of raw SQL expressions; caller is + * responsible for any quoting + * @return string + */ + public function buildConcat( $stringList ); + + /** + * Build a GROUP_CONCAT or equivalent statement for a query. + * + * This is useful for combining a field for several rows into a single string. + * NULL values will not appear in the output, duplicated values will appear, + * and the resulting delimiter-separated values have no defined sort order. + * Code using the results may need to use the PHP unique() or sort() methods. + * + * @param string $delim Glue to bind the results together + * @param string|array $table Table name + * @param string $field Field name + * @param string|array $conds Conditions + * @param string|array $join_conds Join conditions + * @return string SQL text + * @since 1.23 + */ + public function buildGroupConcatField( + $delim, $table, $field, $conds = '', $join_conds = array() + ); + + /** + * Change the current database + * + * @param string $db + * @return bool Success or failure + */ + public function selectDB( $db ); + + /** + * Get the current DB name + * @return string + */ + public function getDBname(); + + /** + * Get the server hostname or IP address + * @return string + */ + public function getServer(); + + /** + * Adds quotes and backslashes. + * + * @param string|Blob $s + * @return string + */ + public function addQuotes( $s ); + + /** + * LIKE statement wrapper, receives a variable-length argument list with + * parts of pattern to match containing either string literals that will be + * escaped or tokens returned by anyChar() or anyString(). Alternatively, + * the function could be provided with an array of aforementioned + * parameters. + * + * Example: $dbr->buildLike( 'My_page_title/', $dbr->anyString() ) returns + * a LIKE clause that searches for subpages of 'My page title'. + * Alternatively: + * $pattern = array( 'My_page_title/', $dbr->anyString() ); + * $query .= $dbr->buildLike( $pattern ); + * + * @since 1.16 + * @return string Fully built LIKE statement + */ + public function buildLike(); + + /** + * Returns a token for buildLike() that denotes a '_' to be used in a LIKE query + * + * @return LikeMatch + */ + public function anyChar(); + + /** + * Returns a token for buildLike() that denotes a '%' to be used in a LIKE query + * + * @return LikeMatch + */ + public function anyString(); + + /** + * Returns an appropriately quoted sequence value for inserting a new row. + * MySQL has autoincrement fields, so this is just NULL. But the PostgreSQL + * subclass will return an integer, and save the value for insertId() + * + * Any implementation of this function should *not* involve reusing + * sequence numbers created for rolled-back transactions. + * See http://bugs.mysql.com/bug.php?id=30767 for details. + * @param string $seqName + * @return null|int + */ + public function nextSequenceValue( $seqName ); + + /** + * REPLACE query wrapper. + * + * REPLACE is a very handy MySQL extension, which functions like an INSERT + * except that when there is a duplicate key error, the old row is deleted + * and the new row is inserted in its place. + * + * We simulate this with standard SQL with a DELETE followed by INSERT. To + * perform the delete, we need to know what the unique indexes are so that + * we know how to find the conflicting rows. + * + * It may be more efficient to leave off unique indexes which are unlikely + * to collide. However if you do this, you run the risk of encountering + * errors which wouldn't have occurred in MySQL. + * + * @param string $table The table to replace the row(s) in. + * @param array $uniqueIndexes Is an array of indexes. Each element may be either + * a field name or an array of field names + * @param array $rows Can be either a single row to insert, or multiple rows, + * in the same format as for DatabaseBase::insert() + * @param string $fname Calling function name (use __METHOD__) for logs/profiling + */ + public function replace( $table, $uniqueIndexes, $rows, $fname = __METHOD__ ); + + /** + * INSERT ON DUPLICATE KEY UPDATE wrapper, upserts an array into a table. + * + * This updates any conflicting rows (according to the unique indexes) using + * the provided SET clause and inserts any remaining (non-conflicted) rows. + * + * $rows may be either: + * - A single associative array. The array keys are the field names, and + * the values are the values to insert. The values are treated as data + * and will be quoted appropriately. If NULL is inserted, this will be + * converted to a database NULL. + * - An array with numeric keys, holding a list of associative arrays. + * This causes a multi-row INSERT on DBMSs that support it. The keys in + * each subarray must be identical to each other, and in the same order. + * + * It may be more efficient to leave off unique indexes which are unlikely + * to collide. However if you do this, you run the risk of encountering + * errors which wouldn't have occurred in MySQL. + * + * Usually throws a DBQueryError on failure. If errors are explicitly ignored, + * returns success. + * + * @since 1.22 + * + * @param string $table Table name. This will be passed through DatabaseBase::tableName(). + * @param array $rows A single row or list of rows to insert + * @param array $uniqueIndexes List of single field names or field name tuples + * @param array $set An array of values to SET. For each array element, the + * key gives the field name, and the value gives the data to set that + * field to. The data will be quoted by DatabaseBase::addQuotes(). + * @param string $fname Calling function name (use __METHOD__) for logs/profiling + * @throws Exception + * @return bool + */ + public function upsert( + $table, array $rows, array $uniqueIndexes, array $set, $fname = __METHOD__ + ); + + /** + * DELETE where the condition is a join. + * + * MySQL overrides this to use a multi-table DELETE syntax, in other databases + * we use sub-selects + * + * For safety, an empty $conds will not delete everything. If you want to + * delete all rows where the join condition matches, set $conds='*'. + * + * DO NOT put the join condition in $conds. + * + * @param string $delTable The table to delete from. + * @param string $joinTable The other table. + * @param string $delVar The variable to join on, in the first table. + * @param string $joinVar The variable to join on, in the second table. + * @param array $conds Condition array of field names mapped to variables, + * ANDed together in the WHERE clause + * @param string $fname Calling function name (use __METHOD__) for logs/profiling + * @throws DBUnexpectedError + */ + public function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds, + $fname = __METHOD__ + ); + + /** + * DELETE query wrapper. + * + * @param array $table Table name + * @param string|array $conds Array of conditions. See $conds in DatabaseBase::select() + * for the format. Use $conds == "*" to delete all rows + * @param string $fname Name of the calling function + * @throws DBUnexpectedError + * @return bool|ResultWrapper + */ + public function delete( $table, $conds, $fname = __METHOD__ ); + + /** + * INSERT SELECT wrapper. Takes data from a SELECT query and inserts it + * into another table. + * + * @param string $destTable The table name to insert into + * @param string|array $srcTable May be either a table name, or an array of table names + * to include in a join. + * + * @param array $varMap Must be an associative array of the form + * array( 'dest1' => 'source1', ...). Source items may be literals + * rather than field names, but strings should be quoted with + * DatabaseBase::addQuotes() + * + * @param array $conds Condition array. See $conds in DatabaseBase::select() for + * the details of the format of condition arrays. May be "*" to copy the + * whole table. + * + * @param string $fname The function name of the caller, from __METHOD__ + * + * @param array $insertOptions Options for the INSERT part of the query, see + * DatabaseBase::insert() for details. + * @param array $selectOptions Options for the SELECT part of the query, see + * DatabaseBase::select() for details. + * + * @return ResultWrapper + */ + public function insertSelect( $destTable, $srcTable, $varMap, $conds, + $fname = __METHOD__, + $insertOptions = array(), $selectOptions = array() + ); + + /** + * Returns true if current database backend supports ORDER BY or LIMIT for separate subqueries + * within the UNION construct. + * @return bool + */ + public function unionSupportsOrderAndLimit(); + + /** + * Construct a UNION query + * This is used for providing overload point for other DB abstractions + * not compatible with the MySQL syntax. + * @param array $sqls SQL statements to combine + * @param bool $all Use UNION ALL + * @return string SQL fragment + */ + public function unionQueries( $sqls, $all ); + + /** + * Returns an SQL expression for a simple conditional. This doesn't need + * to be overridden unless CASE isn't supported in your DBMS. + * + * @param string|array $cond SQL expression which will result in a boolean value + * @param string $trueVal SQL expression to return if true + * @param string $falseVal SQL expression to return if false + * @return string SQL fragment + */ + public function conditional( $cond, $trueVal, $falseVal ); + + /** + * Returns a comand for str_replace function in SQL query. + * Uses REPLACE() in MySQL + * + * @param string $orig Column to modify + * @param string $old Column to seek + * @param string $new Column to replace with + * + * @return string + */ + public function strreplace( $orig, $old, $new ); + + /** + * Determines how long the server has been up + * STUB + * + * @return int + */ + public function getServerUptime(); + + /** + * Determines if the last failure was due to a deadlock + * STUB + * + * @return bool + */ + public function wasDeadlock(); + + /** + * Determines if the last failure was due to a lock timeout + * STUB + * + * @return bool + */ + public function wasLockTimeout(); + + /** + * Determines if the last query error was something that should be dealt + * with by pinging the connection and reissuing the query. + * STUB + * + * @return bool + */ + public function wasErrorReissuable(); + + /** + * Determines if the last failure was due to the database being read-only. + * STUB + * + * @return bool + */ + public function wasReadOnlyError(); + + /** + * Wait for the slave to catch up to a given master position. + * + * @param DBMasterPos $pos + * @param int $timeout The maximum number of seconds to wait for + * synchronisation + * @return int Zero if the slave was past that position already, + * greater than zero if we waited for some period of time, less than + * zero if we timed out. + */ + public function masterPosWait( DBMasterPos $pos, $timeout ); + + /** + * Get the replication position of this slave + * + * @return DBMasterPos|bool False if this is not a slave. + */ + public function getSlavePos(); + + /** + * Get the position of this master + * + * @return DBMasterPos|bool False if this is not a master + */ + public function getMasterPos(); + + /** + * Run an anonymous function as soon as there is no transaction pending. + * If there is a transaction and it is rolled back, then the callback is cancelled. + * Queries in the function will run in AUTO-COMMIT mode unless there are begin() calls. + * Callbacks must commit any transactions that they begin. + * + * This is useful for updates to different systems or when separate transactions are needed. + * For example, one might want to enqueue jobs into a system outside the database, but only + * after the database is updated so that the jobs will see the data when they actually run. + * It can also be used for updates that easily cause deadlocks if locks are held too long. + * + * @param callable $callback + * @since 1.20 + */ + public function onTransactionIdle( $callback ); + + /** + * Run an anonymous function before the current transaction commits or now if there is none. + * If there is a transaction and it is rolled back, then the callback is cancelled. + * Callbacks must not start nor commit any transactions. + * + * This is useful for updates that easily cause deadlocks if locks are held too long + * but where atomicity is strongly desired for these updates and some related updates. + * + * @param callable $callback + * @since 1.22 + */ + public function onTransactionPreCommitOrIdle( $callback ); + + /** + * Begin an atomic section of statements + * + * If a transaction has been started already, just keep track of the given + * section name to make sure the transaction is not committed pre-maturely. + * This function can be used in layers (with sub-sections), so use a stack + * to keep track of the different atomic sections. If there is no transaction, + * start one implicitly. + * + * The goal of this function is to create an atomic section of SQL queries + * without having to start a new transaction if it already exists. + * + * Atomic sections are more strict than transactions. With transactions, + * attempting to begin a new transaction when one is already running results + * in MediaWiki issuing a brief warning and doing an implicit commit. All + * atomic levels *must* be explicitly closed using DatabaseBase::endAtomic(), + * and any database transactions cannot be began or committed until all atomic + * levels are closed. There is no such thing as implicitly opening or closing + * an atomic section. + * + * @since 1.23 + * @param string $fname + * @throws DBError + */ + public function startAtomic( $fname = __METHOD__ ); + + /** + * Ends an atomic section of SQL statements + * + * Ends the next section of atomic SQL statements and commits the transaction + * if necessary. + * + * @since 1.23 + * @see DatabaseBase::startAtomic + * @param string $fname + * @throws DBError + */ + public function endAtomic( $fname = __METHOD__ ); + + /** + * Begin a transaction. If a transaction is already in progress, + * that transaction will be committed before the new transaction is started. + * + * Note that when the DBO_TRX flag is set (which is usually the case for web + * requests, but not for maintenance scripts), any previous database query + * will have started a transaction automatically. + * + * Nesting of transactions is not supported. Attempts to nest transactions + * will cause a warning, unless the current transaction was started + * automatically because of the DBO_TRX flag. + * + * @param string $fname + * @throws DBError + */ + public function begin( $fname = __METHOD__ ); + + /** + * Commits a transaction previously started using begin(). + * If no transaction is in progress, a warning is issued. + * + * Nesting of transactions is not supported. + * + * @param string $fname + * @param string $flush Flush flag, set to 'flush' to disable warnings about + * explicitly committing implicit transactions, or calling commit 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 + */ + public function commit( $fname = __METHOD__, $flush = '' ); + + /** + * Rollback a transaction previously started using begin(). + * If no transaction is in progress, a warning is issued. + * + * No-op on non-transactional databases. + * + * @param string $fname + * @param string $flush Flush flag, set to 'flush' to disable warnings about + * 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 + */ + public function rollback( $fname = __METHOD__, $flush = '' ); + + /** + * List all tables on the database + * + * @param string $prefix Only show tables with this prefix, e.g. mw_ + * @param string $fname Calling function name + * @throws MWException + * @return array + */ + public function listTables( $prefix = null, $fname = __METHOD__ ); + + /** + * Convert a timestamp in one of the formats accepted by wfTimestamp() + * to the format used for inserting into timestamp fields in this DBMS. + * + * The result is unquoted, and needs to be passed through addQuotes() + * before it can be included in raw SQL. + * + * @param string|int $ts + * + * @return string + */ + public function timestamp( $ts = 0 ); + + /** + * Convert a timestamp in one of the formats accepted by wfTimestamp() + * to the format used for inserting into timestamp fields in this DBMS. If + * NULL is input, it is passed through, allowing NULL values to be inserted + * into timestamp fields. + * + * The result is unquoted, and needs to be passed through addQuotes() + * before it can be included in raw SQL. + * + * @param string|int $ts + * + * @return string + */ + public function timestampOrNull( $ts = null ); + + /** + * Take the result from a query, and wrap it in a ResultWrapper if + * necessary. Boolean values are passed through as is, to indicate success + * of write queries or failure. + * + * Once upon a time, DatabaseBase::query() returned a bare MySQL result + * resource, and it was necessary to call this function to convert it to + * a wrapper. Nowadays, raw database objects are never exposed to external + * callers, so this is unnecessary in external code. For compatibility with + * old code, ResultWrapper objects are passed through unaltered. + * + * @param bool|ResultWrapper|resource $result + * @return bool|ResultWrapper + */ + public function resultObject( $result ); + + /** + * Ping the server and try to reconnect if it there is no connection + * + * @return bool Success or failure + */ + public function ping(); + + /** + * Get slave lag. Currently supported only by MySQL. + * + * Note that this function will generate a fatal error on many + * installations. Most callers should use LoadBalancer::safeGetLag() + * instead. + * + * @return int Database replication lag in seconds + */ + public function getLag(); + + /** + * Return the maximum number of items allowed in a list, or 0 for unlimited. + * + * @return int + */ + public function maxListLen(); + + /** + * Some DBMSs have a special format for inserting into blob fields, they + * don't allow simple quoted strings to be inserted. To insert into such + * a field, pass the data through this function before passing it to + * DatabaseBase::insert(). + * + * @param string $b + * @return string + */ + public function encodeBlob( $b ); + + /** + * Some DBMSs return a special placeholder object representing blob fields + * in result objects. Pass the object through this function to return the + * original string. + * + * @param string|Blob $b + * @return string + */ + public function decodeBlob( $b ); + + /** + * Override database's default behavior. $options include: + * 'connTimeout' : Set the connection timeout value in seconds. + * May be useful for very long batch queries such as + * full-wiki dumps, where a single query reads out over + * hours or days. + * + * @param array $options + * @return void + */ + public function setSessionOptions( array $options ); + + /** + * Set variables to be used in sourceFile/sourceStream, in preference to the + * ones in $GLOBALS. If an array is set here, $GLOBALS will not be used at + * all. If it's set to false, $GLOBALS will be used. + * + * @param bool|array $vars Mapping variable name to value. + */ + public function setSchemaVars( $vars ); + + /** + * Check to see if a named lock is available (non-blocking) + * + * @param string $lockName Name of lock to poll + * @param string $method Name of method calling us + * @return bool + * @since 1.20 + */ + public function lockIsFree( $lockName, $method ); + + /** + * Acquire a named lock + * + * Named locks are not related to transactions + * + * @param string $lockName Name of lock to aquire + * @param string $method Name of method calling us + * @param int $timeout + * @return bool + */ + public function lock( $lockName, $method, $timeout = 5 ); + + /** + * Release a lock + * + * Named locks are not related to transactions + * + * @param string $lockName Name of lock to release + * @param string $method Name of method calling us + * + * @return int Returns 1 if the lock was released, 0 if the lock was not established + * by this thread (in which case the lock is not released), and NULL if the named + * lock did not exist + */ + public function unlock( $lockName, $method ); + + /** + * Check to see if a named lock used by lock() use blocking queues + * + * @return bool + * @since 1.26 + */ + public function namedLocksEnqueue(); + + /** + * Find out when 'infinity' is. Most DBMSes support this. This is a special + * keyword for timestamps in PostgreSQL, and works with CHAR(14) as well + * because "i" sorts after all numbers. + * + * @return string + */ + public function getInfinity(); + + /** + * Encode an expiry time into the DBMS dependent format + * + * @param string $expiry Timestamp for expiry, or the 'infinity' string + * @return string + */ + public function encodeExpiry( $expiry ); + + /** + * Decode an expiry time into a DBMS independent format + * + * @param string $expiry DB timestamp field value for expiry + * @param int $format TS_* constant, defaults to TS_MW + * @return string + */ + public function decodeExpiry( $expiry, $format = TS_MW ); + + /** + * Allow or deny "big selects" for this session only. This is done by setting + * the sql_big_selects session variable. + * + * This is a MySQL-specific feature. + * + * @param bool|string $value True for allow, false for deny, or "default" to + * restore the initial value + */ + public function setBigSelects( $value = true ); +} diff --git a/includes/db/LBFactory.php b/includes/db/LBFactory.php index 4551e2d7..cf522b20 100644 --- a/includes/db/LBFactory.php +++ b/includes/db/LBFactory.php @@ -177,6 +177,15 @@ abstract class LBFactory { }, array( $methodName, $args ) ); } + /** + * Commit on all connections. Done for two reasons: + * 1. To commit changes to the masters. + * 2. To release the snapshot on all connections, master and slave. + */ + public function commitAll() { + $this->forEachLBCallMethod( 'commitAll' ); + } + /** * Commit changes on all master connections */ @@ -199,7 +208,7 @@ abstract class LBFactory { */ public function hasMasterChanges() { $ret = false; - $this->forEachLB( function ( $lb ) use ( &$ret ) { + $this->forEachLB( function ( LoadBalancer $lb ) use ( &$ret ) { $ret = $ret || $lb->hasMasterChanges(); } ); return $ret; diff --git a/includes/db/LBFactoryMulti.php b/includes/db/LBFactoryMulti.php index aa305ab1..92fbccd6 100644 --- a/includes/db/LBFactoryMulti.php +++ b/includes/db/LBFactoryMulti.php @@ -232,7 +232,7 @@ class LBFactoryMulti extends LBFactory { public function getMainLB( $wiki = false ) { $section = $this->getSectionForWiki( $wiki ); if ( !isset( $this->mainLBs[$section] ) ) { - $lb = $this->newMainLB( $wiki, $section ); + $lb = $this->newMainLB( $wiki ); $lb->parentInfo( array( 'id' => "main-$section" ) ); $this->chronProt->initLB( $lb ); $this->mainLBs[$section] = $lb; diff --git a/includes/db/LoadBalancer.php b/includes/db/LoadBalancer.php index d9584e14..52dca087 100644 --- a/includes/db/LoadBalancer.php +++ b/includes/db/LoadBalancer.php @@ -60,8 +60,6 @@ class LoadBalancer { private $mLastError = 'Unknown error'; /** @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; @@ -113,8 +111,6 @@ class LoadBalancer { } } } - - $this->mProcCache = new ProcessCacheLRU( 30 ); } /** @@ -214,7 +210,7 @@ class LoadBalancer { * @return bool|int|string */ public function getReaderIndex( $group = false, $wiki = false ) { - global $wgReadOnly, $wgDBtype; + global $wgDBtype; # @todo FIXME: For now, only go through all this for mysql databases if ( $wgDBtype != 'mysql' ) { @@ -258,7 +254,7 @@ class LoadBalancer { # meets our criteria $currentLoads = $nonErrorLoads; while ( count( $currentLoads ) ) { - if ( $wgReadOnly || $this->mAllowLagged || $laggedSlaveMode ) { + if ( $this->mAllowLagged || $laggedSlaveMode ) { $i = ArrayUtils::pickRandom( $currentLoads ); } else { $i = false; @@ -277,8 +273,6 @@ class LoadBalancer { if ( $i === false && count( $currentLoads ) != 0 ) { # All slaves lagged. Switch to read-only mode wfDebugLog( 'replication', "All slaves lagged. Switch to read-only mode" ); - $wgReadOnly = 'The database has been automatically locked ' . - 'while the slave database servers catch up to the master'; $i = ArrayUtils::pickRandom( $currentLoads ); $laggedSlaveMode = true; } @@ -293,8 +287,8 @@ class LoadBalancer { return false; } - wfDebugLog( 'connect', __METHOD__ . - ": Using reader #$i: {$this->mServers[$i]['host']}..." ); + $serverName = $this->getServerName( $i ); + wfDebugLog( 'connect', __METHOD__ . ": Using reader #$i: $serverName..." ); $conn = $this->openConnection( $i, $wiki ); if ( !$conn ) { @@ -330,6 +324,10 @@ class LoadBalancer { } if ( $this->mReadIndex <= 0 && $this->mLoads[$i] > 0 && $group === false ) { $this->mReadIndex = $i; + # Record if the generic reader index is in "lagged slave" mode + if ( $laggedSlaveMode ) { + $this->mLaggedSlaveMode = true; + } } $serverName = $this->getServerName( $i ); wfDebug( __METHOD__ . ": using server $serverName for group '$group'\n" ); @@ -356,6 +354,37 @@ class LoadBalancer { } } + /** + * Set the master wait position and wait for a "generic" slave to catch up to it + * + * This can be used a faster proxy for waitForAll() + * + * @param DBMasterPos $pos + * @param int $timeout Max seconds to wait; default is mWaitTimeout + * @return bool Success (able to connect and no timeouts reached) + * @since 1.26 + */ + public function waitForOne( $pos, $timeout = null ) { + $this->mWaitForPos = $pos; + + $i = $this->mReadIndex; + if ( $i <= 0 ) { + // Pick a generic slave if there isn't one yet + $readLoads = $this->mLoads; + unset( $readLoads[$this->getWriterIndex()] ); // slaves only + $readLoads = array_filter( $readLoads ); // with non-zero load + $i = ArrayUtils::pickRandom( $readLoads ); + } + + if ( $i > 0 ) { + $ok = $this->doWait( $i, true, $timeout ); + } else { + $ok = true; // no applicable loads + } + + return $ok; + } + /** * Set the master wait position and wait for ALL slaves to catch up to it * @param DBMasterPos $pos @@ -429,7 +458,7 @@ class LoadBalancer { if ( $result == -1 || is_null( $result ) ) { # Timed out waiting for slave, use master instead - $server = $this->mServers[$index]['host']; + $server = $server = $this->getServerName( $index ); $msg = __METHOD__ . ": Timed out waiting on $server pos {$this->mWaitForPos}"; wfDebug( "$msg\n" ); wfDebugLog( 'DBPerformance', "$msg:\n" . wfBacktrace( true ) ); @@ -505,7 +534,6 @@ class LoadBalancer { # Now we have an explicit index into the servers array $conn = $this->openConnection( $i, $wiki ); if ( !$conn ) { - return $this->reportConnectionError(); } @@ -618,25 +646,32 @@ class LoadBalancer { public function openConnection( $i, $wiki = false ) { if ( $wiki !== false ) { $conn = $this->openForeignConnection( $i, $wiki ); - - return $conn; - } - if ( isset( $this->mConns['local'][$i][0] ) ) { + } elseif ( isset( $this->mConns['local'][$i][0] ) ) { $conn = $this->mConns['local'][$i][0]; } else { $server = $this->mServers[$i]; $server['serverIndex'] = $i; $conn = $this->reallyOpenConnection( $server, false ); + $serverName = $this->getServerName( $i ); if ( $conn->isOpen() ) { - wfDebug( "Connected to database $i at {$this->mServers[$i]['host']}\n" ); + wfDebug( "Connected to database $i at $serverName\n" ); $this->mConns['local'][$i][0] = $conn; } else { - wfDebug( "Failed to connect to database $i at {$this->mServers[$i]['host']}\n" ); + wfDebug( "Failed to connect to database $i at $serverName\n" ); $this->mErrorConnection = $conn; $conn = false; } } + if ( $conn && !$conn->isOpen() ) { + // Connection was made but later unrecoverably lost for some reason. + // Do not return a handle that will just throw exceptions on use, + // but let the calling code (e.g. getReaderIndex) try another server. + // See DatabaseMyslBase::ping() for how this can happen. + $this->mErrorConnection = $conn; + $conn = false; + } + return $conn; } @@ -812,8 +847,9 @@ class LoadBalancer { /** * @return int + * @since 1.26 */ - private function getWriterIndex() { + public function getWriterIndex() { return 0; } @@ -854,12 +890,14 @@ class LoadBalancer { */ public function getServerName( $i ) { if ( isset( $this->mServers[$i]['hostName'] ) ) { - return $this->mServers[$i]['hostName']; + $name = $this->mServers[$i]['hostName']; } elseif ( isset( $this->mServers[$i]['host'] ) ) { - return $this->mServers[$i]['host']; + $name = $this->mServers[$i]['host']; } else { - return ''; + $name = ''; } + + return ( $name != '' ) ? $name : 'localhost'; } /** @@ -1096,9 +1134,12 @@ class LoadBalancer { } /** - * @return bool + * @return bool Whether the generic connection for reads is highly "lagged" */ public function getLaggedSlaveMode() { + # Get a generic reader connection + $this->getConnection( DB_SLAVE ); + return $this->mLaggedSlaveMode; } @@ -1195,16 +1236,8 @@ class LoadBalancer { return array( 0 => 0 ); // no replication = no lag } - if ( $this->mProcCache->has( 'slave_lag', 'times', 1 ) ) { - return $this->mProcCache->get( 'slave_lag', 'times' ); - } - # 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; + return $this->getLoadMonitor()->getLagTimes( array_keys( $this->mServers ), $wiki ); } /** @@ -1231,54 +1264,10 @@ class LoadBalancer { /** * Clear the cache for slag lag delay times + * + * This is only used for testing */ public function clearLagTimeCache() { - $this->mProcCache->clear( 'slave_lag' ); - } -} - -/** - * 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 - * @since 1.22 - */ -class DBConnRef implements IDatabase { - /** @var LoadBalancer */ - private $lb; - - /** @var DatabaseBase|null */ - private $conn; - - /** @var array|null */ - private $params; - - /** - * @param LoadBalancer $lb - * @param DatabaseBase|array $conn Connection or (server index, group, wiki ID) array - */ - public function __construct( LoadBalancer $lb, $conn ) { - $this->lb = $lb; - if ( $conn instanceof DatabaseBase ) { - $this->conn = $conn; - } else { - $this->params = $conn; - } - } - - public function __call( $name, $arguments ) { - if ( $this->conn === null ) { - list( $db, $groups, $wiki ) = $this->params; - $this->conn = $this->lb->getConnection( $db, $groups, $wiki ); - } - - return call_user_func_array( array( $this->conn, $name ), $arguments ); - } - - public function __destruct() { - if ( $this->conn !== null ) { - $this->lb->reuseConnection( $this->conn ); - } + $this->getLoadMonitor()->clearCaches(); } } diff --git a/includes/db/LoadMonitor.php b/includes/db/LoadMonitor.php index 91840dd9..4975ea19 100644 --- a/includes/db/LoadMonitor.php +++ b/includes/db/LoadMonitor.php @@ -64,90 +64,3 @@ class LoadMonitorNull implements LoadMonitor { return array_fill_keys( $serverIndexes, 0 ); } } - -/** - * Basic MySQL load monitor with no external dependencies - * Uses memcached to cache the replication lag for a short time - * - * @ingroup Database - */ -class LoadMonitorMySQL implements LoadMonitor { - /** @var LoadBalancer */ - public $parent; - /** @var BagOStuff */ - protected $cache; - - public function __construct( $parent ) { - global $wgMemc; - - $this->parent = $parent; - $this->cache = $wgMemc ?: wfGetMainCache(); - } - - public function scaleLoads( &$loads, $group = false, $wiki = false ) { - } - - public function getLagTimes( $serverIndexes, $wiki ) { - if ( count( $serverIndexes ) == 1 && reset( $serverIndexes ) == 0 ) { - // Single server only, just return zero without caching - return array( 0 => 0 ); - } - - $expiry = 5; - $requestRate = 10; - - $cache = $this->cache; - $masterName = $this->parent->getServerName( 0 ); - $memcKey = wfMemcKey( 'lag_times', $masterName ); - $times = $cache->get( $memcKey ); - if ( is_array( $times ) ) { - # Randomly recache with probability rising over $expiry - $elapsed = time() - $times['timestamp']; - $chance = max( 0, ( $expiry - $elapsed ) * $requestRate ); - if ( mt_rand( 0, $chance ) != 0 ) { - unset( $times['timestamp'] ); // hide from caller - - return $times; - } - wfIncrStats( 'lag_cache_miss_expired' ); - } else { - wfIncrStats( 'lag_cache_miss_absent' ); - } - - # Cache key missing or expired - if ( $cache->add( "$memcKey:lock", 1, 10 ) ) { - # Let this process alone update the cache value - $unlocker = new ScopedCallback( function () use ( $cache, $memcKey ) { - $cache->delete( $memcKey ); - } ); - } elseif ( is_array( $times ) ) { - # Could not acquire lock but an old cache exists, so use it - unset( $times['timestamp'] ); // hide from caller - - return $times; - } - - $times = array(); - foreach ( $serverIndexes as $i ) { - if ( $i == 0 ) { # Master - $times[$i] = 0; - } elseif ( false !== ( $conn = $this->parent->getAnyOpenConnection( $i ) ) ) { - $times[$i] = $conn->getLag(); - } elseif ( false !== ( $conn = $this->parent->openConnection( $i, $wiki ) ) ) { - $times[$i] = $conn->getLag(); - // Close the connection to avoid sleeper connections piling up. - // Note that the caller will pick one of these DBs and reconnect, - // which is slightly inefficient, but this only matters for the lag - // time cache miss cache, which is far less common that cache hits. - $this->parent->closeConnection( $conn ); - } - } - - # Add a timestamp key so we know when it was cached - $times['timestamp'] = time(); - $cache->set( $memcKey, $times, $expiry + 10 ); - unset( $times['timestamp'] ); // hide from caller - - return $times; - } -} diff --git a/includes/db/LoadMonitorMySQL.php b/includes/db/LoadMonitorMySQL.php new file mode 100644 index 00000000..30084190 --- /dev/null +++ b/includes/db/LoadMonitorMySQL.php @@ -0,0 +1,124 @@ +parent = $parent; + + $this->srvCache = ObjectCache::newAccelerator( 'hash' ); + $this->mainCache = wfGetMainCache(); + } + + public function scaleLoads( &$loads, $group = false, $wiki = false ) { + } + + public function getLagTimes( $serverIndexes, $wiki ) { + if ( count( $serverIndexes ) == 1 && reset( $serverIndexes ) == 0 ) { + # Single server only, just return zero without caching + return array( 0 => 0 ); + } + + $key = $this->getLagTimeCacheKey(); + # Randomize TTLs to reduce stampedes (4.0 - 5.0 sec) + $ttl = mt_rand( 4e6, 5e6 ) / 1e6; + # Keep keys around longer as fallbacks + $staleTTL = 60; + + # (a) Check the local APC cache + $value = $this->srvCache->get( $key ); + if ( $value && $value['timestamp'] > ( microtime( true ) - $ttl ) ) { + wfDebugLog( 'replication', __FUNCTION__ . ": got lag times ($key) from local cache" ); + return $value['lagTimes']; // cache hit + } + $staleValue = $value ?: false; + + # (b) Check the shared cache and backfill APC + $value = $this->mainCache->get( $key ); + if ( $value && $value['timestamp'] > ( microtime( true ) - $ttl ) ) { + $this->srvCache->set( $key, $value, $staleTTL ); + wfDebugLog( 'replication', __FUNCTION__ . ": got lag times ($key) from main cache" ); + + return $value['lagTimes']; // cache hit + } + $staleValue = $value ?: $staleValue; + + # (c) Cache key missing or expired; regenerate and backfill + if ( $this->mainCache->lock( $key, 0, 10 ) ) { + # Let this process alone update the cache value + $cache = $this->mainCache; + /** @noinspection PhpUnusedLocalVariableInspection */ + $unlocker = new ScopedCallback( function () use ( $cache, $key ) { + $cache->unlock( $key ); + } ); + } elseif ( $staleValue ) { + # Could not acquire lock but an old cache exists, so use it + return $staleValue['lagTimes']; + } + + $lagTimes = array(); + foreach ( $serverIndexes as $i ) { + if ( $i == 0 ) { # Master + $lagTimes[$i] = 0; + } elseif ( false !== ( $conn = $this->parent->getAnyOpenConnection( $i ) ) ) { + $lagTimes[$i] = $conn->getLag(); + } elseif ( false !== ( $conn = $this->parent->openConnection( $i, $wiki ) ) ) { + $lagTimes[$i] = $conn->getLag(); + # Close the connection to avoid sleeper connections piling up. + # Note that the caller will pick one of these DBs and reconnect, + # which is slightly inefficient, but this only matters for the lag + # time cache miss cache, which is far less common that cache hits. + $this->parent->closeConnection( $conn ); + } + } + + # Add a timestamp key so we know when it was cached + $value = array( 'lagTimes' => $lagTimes, 'timestamp' => microtime( true ) ); + $this->mainCache->set( $key, $value, $staleTTL ); + $this->srvCache->set( $key, $value, $staleTTL ); + wfDebugLog( 'replication', __FUNCTION__ . ": re-calculated lag times ($key)" ); + + return $value['lagTimes']; + } + + public function clearCaches() { + $key = $this->getLagTimeCacheKey(); + $this->srvCache->delete( $key ); + $this->mainCache->delete( $key ); + } + + private function getLagTimeCacheKey() { + # Lag is per-server, not per-DB, so key on the master DB name + return wfGlobalCacheKey( 'lag-times', $this->parent->getServerName( 0 ) ); + } +} -- cgit v1.2.3-54-g00ecf