protected $mServer, $mUser, $mPassword, $mDBname;
+ /** @var BagOStuff APC cache */
+ protected $srvCache;
+
/** @var resource Database connection */
protected $mConn = null;
protected $mOpened = false;
*/
private $mTrxTimestamp = null;
+ /** @var float Lag estimate at the time of BEGIN */
+ private $mTrxSlaveLag = null;
+
/**
* Remembers the function name given for starting the most recent transaction via begin().
* Used to provide additional context for error reporting.
/**
* Array of levels of atomicity within transactions
*
- * @var SplStack
+ * @var array
*/
- private $mTrxAtomicLevels;
+ private $mTrxAtomicLevels = array();
/**
* Record if the current transaction was started implicitly by DatabaseBase::startAtomic
}
}
- /**
- * Set lag time in seconds for a fake slave
- *
- * @param mixed $lag Valid values for this parameter are determined by the
- * subclass, but should be a PHP scalar or array that would be sensible
- * as part of $wgLBFactoryConf.
- */
- public function setFakeSlaveLag( $lag ) {
- }
-
- /**
- * Make this connection a fake master
- *
- * @param bool $enabled
- */
- public function setFakeMaster( $enabled = true ) {
- }
-
/**
* @return TransactionProfiler
*/
* - DBO_PERSISTENT: use persistant database connection
*/
public function setFlag( $flag ) {
- global $wgDebugDBTransactions;
$this->mFlags |= $flag;
- if ( ( $flag & DBO_TRX ) && $wgDebugDBTransactions ) {
- wfDebug( "Implicit transactions are now enabled.\n" );
- }
}
/**
* - DBO_PERSISTENT: use persistant database connection
*/
public function clearFlag( $flag ) {
- global $wgDebugDBTransactions;
$this->mFlags &= ~$flag;
- if ( ( $flag & DBO_TRX ) && $wgDebugDBTransactions ) {
- wfDebug( "Implicit transactions are now disabled.\n" );
- }
}
/**
* @param array $params Parameters passed from DatabaseBase::factory()
*/
function __construct( array $params ) {
- global $wgDBprefix, $wgDBmwschema, $wgCommandLineMode, $wgDebugDBTransactions;
+ global $wgDBprefix, $wgDBmwschema, $wgCommandLineMode;
$this->mTrxAtomicLevels = new SplStack;
+ $this->srvCache = ObjectCache::newAccelerator( 'hash' );
$server = $params['host'];
$user = $params['user'];
if ( $this->mFlags & DBO_DEFAULT ) {
if ( $wgCommandLineMode ) {
$this->mFlags &= ~DBO_TRX;
- if ( $wgDebugDBTransactions ) {
- wfDebug( "Implicit transaction open disabled.\n" );
- }
} else {
$this->mFlags |= DBO_TRX;
- if ( $wgDebugDBTransactions ) {
- wfDebug( "Implicit transaction open enabled.\n" );
- }
}
}
* for a successful read query, or false on failure if $tempIgnore set
*/
public function query( $sql, $fname = __METHOD__, $tempIgnore = false ) {
- global $wgUser, $wgDebugDBTransactions, $wgDebugDumpSqlLength;
+ global $wgUser;
$this->mLastQuery = $sql;
$isWriteQuery = $this->isWriteQuery( $sql );
if ( $isWriteQuery ) {
- if ( !$this->mDoneWrites ) {
- wfDebug( __METHOD__ . ': Writes done: ' .
- DatabaseBase::generalizeSQL( $sql ) . "\n" );
+ $reason = $this->getLBInfo( 'readOnlyReason' );
+ if ( is_string( $reason ) ) {
+ throw new DBReadOnlyError( $this, "Database is read-only: $reason" );
}
# Set a flag indicating that writes have been done
$this->mDoneWrites = microtime( true );
$commentedSql = preg_replace( '/\s|$/', " /* $fname $userName */ ", $sql, 1 );
if ( !$this->mTrxLevel && $this->getFlag( DBO_TRX ) && $this->isTransactableQuery( $sql ) ) {
- if ( $wgDebugDBTransactions ) {
- wfDebug( "Implicit transaction start.\n" );
- }
$this->begin( __METHOD__ . " ($fname)" );
$this->mTrxAutomatic = true;
}
}
if ( $this->debug() ) {
- static $cnt = 0;
-
- $cnt++;
- $sqlx = $wgDebugDumpSqlLength ? substr( $commentedSql, 0, $wgDebugDumpSqlLength )
- : $commentedSql;
- $sqlx = strtr( $sqlx, "\t\n", ' ' );
-
- $master = $isMaster ? 'master' : 'slave';
- wfDebug( "Query {$this->mDBname} ($cnt) ($master): $sqlx\n" );
+ wfDebugLog( 'queries', sprintf( "%s: %s", $this->mDBname, $sql ) );
}
$queryId = MWDebug::query( $sql, $fname, $isMaster );
$res = $this->resultObject( $ret );
// Destroy profile sections in the opposite order to their creation
- $queryProfSection = false;
- $totalProfSection = false;
+ ScopedCallback::consume( $queryProfSection );
+ ScopedCallback::consume( $totalProfSection );
if ( $isWriteQuery && $this->mTrxLevel ) {
$this->mTrxWriteDuration += $queryRuntime;
* @param string|array $options The query options. See DatabaseBase::select() for details.
*
* @return bool|mixed The value from the field, or false on failure.
+ * @throws DBUnexpectedError
*/
public function selectField(
$table, $var, $cond = '', $fname = __METHOD__, $options = array()
*
* Takes the same arguments as DatabaseBase::select().
*
- * @param string $table Table name
+ * @since 1.27 Added $join_conds parameter
+ *
+ * @param array|string $tables Table names
* @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
+ * @param array $join_conds Join conditions (since 1.27)
* @return int Row count
- * @since 1.24
*/
public function selectRowCount(
- $table, $vars = '*', $conds = '', $fname = __METHOD__, $options = array()
+ $tables, $vars = '*', $conds = '', $fname = __METHOD__, $options = array(), $join_conds = array()
) {
$rows = 0;
- $sql = $this->selectSQLText( $table, '1', $conds, $fname, $options );
+ $sql = $this->selectSQLText( $tables, '1', $conds, $fname, $options, $join_conds );
$res = $this->query( "SELECT COUNT(*) AS rowcount FROM ($sql) tmp_count", $fname );
if ( $res ) {
*
* @return string
*/
- static function generalizeSQL( $sql ) {
+ protected static function generalizeSQL( $sql ) {
# This does the same as the regexp below would do, but in such a way
# as to avoid crashing php on some large strings.
# $sql = preg_replace( "/'([^\\\\']|\\\\.)*'|\"([^\\\\\"]|\\\\.)*\"/", "'X'", $sql );
$args = func_get_args();
$function = array_shift( $args );
$tries = self::DEADLOCK_TRIES;
- if ( is_array( $function ) ) {
- $fname = $function[0];
- } else {
- $fname = $function;
- }
$this->begin( __METHOD__ );
}
}
- $this->mTrxAtomicLevels->push( $fname );
+ $this->mTrxAtomicLevels[] = $fname;
}
/**
if ( !$this->mTrxLevel ) {
throw new DBUnexpectedError( $this, 'No atomic transaction is open.' );
}
- if ( $this->mTrxAtomicLevels->isEmpty() ||
- $this->mTrxAtomicLevels->pop() !== $fname
+ if ( !$this->mTrxAtomicLevels ||
+ array_pop( $this->mTrxAtomicLevels ) !== $fname
) {
throw new DBUnexpectedError( $this, 'Invalid atomic section ended.' );
}
- if ( $this->mTrxAtomicLevels->isEmpty() && $this->mTrxAutomaticAtomic ) {
+ if ( !$this->mTrxAtomicLevels && $this->mTrxAutomaticAtomic ) {
$this->commit( $fname, 'flush' );
}
}
* @throws DBError
*/
final public function begin( $fname = __METHOD__ ) {
- global $wgDebugDBTransactions;
-
if ( $this->mTrxLevel ) { // implicit commit
- if ( !$this->mTrxAtomicLevels->isEmpty() ) {
+ if ( $this->mTrxAtomicLevels ) {
// If the current transaction was an automatic atomic one, then we definitely have
// a problem. Same if there is any unclosed atomic level.
- throw new DBUnexpectedError( $this,
- "Attempted to start explicit transaction when atomic levels are still open."
+ $levels = implode( ', ', $this->mTrxAtomicLevels );
+ throw new DBUnexpectedError(
+ $this,
+ "Got explicit BEGIN while atomic sections $levels are still open."
);
} elseif ( !$this->mTrxAutomatic ) {
// We want to warn about inadvertently nested begin/commit pairs, but not about
) )
);
} else {
- // if the transaction was automatic and has done write operations,
- // log it if $wgDebugDBTransactions is enabled.
- if ( $this->mTrxDoneWrites && $wgDebugDBTransactions ) {
+ // if the transaction was automatic and has done write operations
+ if ( $this->mTrxDoneWrites ) {
wfDebug( "$fname: Automatic transaction with writes in progress" .
" (from {$this->mTrxFname}), performing implicit commit!\n"
);
$this->mTrxDoneWrites = false;
$this->mTrxAutomatic = false;
$this->mTrxAutomaticAtomic = false;
- $this->mTrxAtomicLevels = new SplStack;
+ $this->mTrxAtomicLevels = array();
$this->mTrxIdleCallbacks = array();
$this->mTrxPreCommitCallbacks = array();
$this->mTrxShortId = wfRandomString( 12 );
$this->mTrxWriteDuration = 0.0;
+ // First SELECT after BEGIN will establish the snapshot in REPEATABLE-READ.
+ // Get an estimate of the slave lag before then, treating estimate staleness
+ // as lag itself just to be safe
+ $status = $this->getApproximateLagStatus();
+ $this->mTrxSlaveLag = $status['lag'] + ( microtime( true ) - $status['since'] );
}
/**
* @throws DBUnexpectedError
*/
final public function commit( $fname = __METHOD__, $flush = '' ) {
- if ( !$this->mTrxAtomicLevels->isEmpty() ) {
+ if ( $this->mTrxLevel && $this->mTrxAtomicLevels ) {
// There are still atomic sections open. This cannot be ignored
+ $levels = implode( ', ', $this->mTrxAtomicLevels );
throw new DBUnexpectedError(
$this,
- "Attempted to commit transaction while atomic sections are still open"
+ "Got COMMIT while atomic sections $levels are still open"
);
}
$this->doRollback( $fname );
$this->mTrxIdleCallbacks = array(); // cancel
$this->mTrxPreCommitCallbacks = array(); // cancel
- $this->mTrxAtomicLevels = new SplStack;
+ $this->mTrxAtomicLevels = array();
if ( $this->mTrxDoneWrites ) {
$this->getTransactionProfiler()->transactionWritingOut(
$this->mServer, $this->mDBname, $this->mTrxShortId );
return true;
}
+ /**
+ * Get the slave lag when the current transaction started
+ * or a general lag estimate if not transaction is active
+ *
+ * This is useful when transactions might use snapshot isolation
+ * (e.g. REPEATABLE-READ in innodb), so the "real" lag of that data
+ * is this lag plus transaction duration. If they don't, it is still
+ * safe to be pessimistic. In AUTO-COMMIT mode, this still gives an
+ * indication of the staleness of subsequent reads.
+ *
+ * @return array ('lag': seconds, 'since': UNIX timestamp of BEGIN)
+ * @since 1.27
+ */
+ public function getSessionLagStatus() {
+ return $this->getTransactionLagStatus() ?: $this->getApproximateLagStatus();
+ }
+
+ /**
+ * Get the slave lag when the current transaction started
+ *
+ * This is useful when transactions might use snapshot isolation
+ * (e.g. REPEATABLE-READ in innodb), so the "real" lag of that data
+ * is this lag plus transaction duration. If they don't, it is still
+ * safe to be pessimistic. This returns null if there is no transaction.
+ *
+ * @return array|null ('lag': seconds, 'since': UNIX timestamp of BEGIN)
+ * @since 1.27
+ */
+ public function getTransactionLagStatus() {
+ return $this->mTrxLevel
+ ? array( 'lag' => $this->mTrxSlaveLag, 'since' => $this->trxTimestamp() )
+ : null;
+ }
+
+ /**
+ * Get a slave lag estimate for this server
+ *
+ * @return array ('lag': seconds, 'since': UNIX timestamp of estimate)
+ * @since 1.27
+ */
+ public function getApproximateLagStatus() {
+ return array(
+ 'lag' => $this->getLBInfo( 'slave' ) ? $this->getLag() : 0,
+ 'since' => microtime( true )
+ );
+ }
+
+ /**
+ * Merge the result of getSessionLagStatus() for several DBs
+ * using the most pessimistic values to estimate the lag of
+ * any data derived from them in combination
+ *
+ * This is information is useful for caching modules
+ *
+ * @see WANObjectCache::set()
+ * @see WANObjectCache::getWithSetCallback()
+ *
+ * @param IDatabase $db1
+ * @param IDatabase ...
+ * @return array ('lag': highest lag, 'since': lowest estimate UNIX timestamp)
+ * @since 1.27
+ */
+ public static function getCacheSetOptions( IDatabase $db1 ) {
+ $res = array( 'lag' => 0, 'since' => INF );
+ foreach ( func_get_args() as $db ) {
+ /** @var IDatabase $db */
+ $status = $db->getSessionLagStatus();
+ $res['lag'] = max( $res['lag'], $status['lag'] );
+ $res['since'] = min( $res['since'], $status['since'] );
+ }
+
+ return $res;
+ }
+
/**
* Get slave lag. Currently supported only by MySQL.
*
}
}
}
+
+/**
+ * @since 1.27
+ */
+abstract class Database extends DatabaseBase {
+ // B/C until nothing type hints for DatabaseBase
+ // @TODO: finish renaming DatabaseBase => Database
+}