abstract class LBFactory {
/** @var ChronologyProtector */
protected $chronProt;
+ /** @var object|string Class name or object With profileIn/profileOut methods */
+ protected $profiler;
/** @var TransactionProfiler */
protected $trxProfiler;
/** @var LoggerInterface */
/** @var WANObjectCache */
protected $wanCache;
- /** @var string Local domain */
- protected $domain;
+ /** @var DatabaseDomain Local domain */
+ protected $localDomain;
/** @var string Local hostname of the app server */
protected $hostname;
+ /** @var array Web request information about the client */
+ protected $requestInfo;
+
/** @var mixed */
protected $ticket;
/** @var string|bool String if a requested DBO_TRX transaction round is active */
/** @var callable[] */
protected $replicationWaitCallbacks = [];
+ /** @var bool Whether this PHP instance is for a CLI script */
+ protected $cliMode;
+ /** @var string Agent name for query profiling */
+ protected $agent;
+
const SHUTDOWN_NO_CHRONPROT = 0; // don't save DB positions at all
const SHUTDOWN_CHRONPROT_ASYNC = 1; // save DB positions, but don't wait on remote DCs
const SHUTDOWN_CHRONPROT_SYNC = 2; // save DB positions, waiting on all DCs
[ 'replLogger', 'connLogger', 'queryLogger', 'perfLogger' ];
/**
- * @TODO: document base params here
- * @param array $conf
+ * Construct a manager of ILoadBalancer objects
+ *
+ * Sub-classes will extend the required keys in $conf with additional parameters
+ *
+ * @param $conf $params Array with keys:
+ * - localDomain: A DatabaseDomain or domain ID string.
+ * - readOnlyReason : Reason the master DB is read-only if so [optional]
+ * - srvCache : BagOStuff object for server cache [optional]
+ * - memCache : BagOStuff object for cluster memory cache [optional]
+ * - wanCache : WANObjectCache object [optional]
+ * - hostname : The name of the current server [optional]
+ * - cliMode: Whether the execution context is a CLI script. [optional]
+ * - profiler : Class name or instance with profileIn()/profileOut() methods. [optional]
+ * - trxProfiler: TransactionProfiler instance. [optional]
+ * - replLogger: PSR-3 logger instance. [optional]
+ * - connLogger: PSR-3 logger instance. [optional]
+ * - queryLogger: PSR-3 logger instance. [optional]
+ * - perfLogger: PSR-3 logger instance. [optional]
+ * - errorLogger : Callback that takes an Exception and logs it. [optional]
+ * @throws InvalidArgumentException
*/
public function __construct( array $conf ) {
- $this->domain = isset( $conf['domain'] ) ? $conf['domain'] : '';
+ $this->localDomain = isset( $conf['localDomain'] )
+ ? DatabaseDomain::newFromId( $conf['localDomain'] )
+ : DatabaseDomain::newUnspecified();
+
if ( isset( $conf['readOnlyReason'] ) && is_string( $conf['readOnlyReason'] ) ) {
$this->readOnlyReason = $conf['readOnlyReason'];
}
$this->errorLogger = isset( $conf['errorLogger'] )
? $conf['errorLogger']
: function ( Exception $e ) {
- trigger_error( E_WARNING, $e->getMessage() );
+ trigger_error( E_WARNING, get_class( $e ) . ': ' . $e->getMessage() );
};
- $this->hostname = isset( $conf['hostname'] )
- ? $conf['hostname']
- : gethostname();
- $this->chronProt = isset( $conf['chronProt'] )
- ? $conf['chronProt']
- : $this->newChronologyProtector();
+ $this->profiler = isset( $params['profiler'] ) ? $params['profiler'] : null;
$this->trxProfiler = isset( $conf['trxProfiler'] )
? $conf['trxProfiler']
: new TransactionProfiler();
+ $this->requestInfo = [
+ 'IPAddress' => isset( $_SERVER[ 'REMOTE_ADDR' ] ) ? $_SERVER[ 'REMOTE_ADDR' ] : '',
+ 'UserAgent' => isset( $_SERVER['HTTP_USER_AGENT'] ) ? $_SERVER['HTTP_USER_AGENT'] : '',
+ 'ChronologyProtection' => 'true'
+ ];
+
+ $this->cliMode = isset( $params['cliMode'] ) ? $params['cliMode'] : PHP_SAPI === 'cli';
+ $this->hostname = isset( $conf['hostname'] ) ? $conf['hostname'] : gethostname();
+ $this->agent = isset( $params['agent'] ) ? $params['agent'] : '';
+
$this->ticket = mt_rand();
}
/**
* Disables all load balancers. All connections are closed, and any attempt to
* open a new connection will result in a DBAccessError.
- * @see LoadBalancer::disable()
+ * @see ILoadBalancer::disable()
*/
public function destroy() {
$this->shutdown( self::SHUTDOWN_NO_CHRONPROT );
* Create a new load balancer object. The resulting object will be untracked,
* not chronology-protected, and the caller is responsible for cleaning it up.
*
- * @param bool|string $domain Wiki ID, or false for the current wiki
+ * This method is for only advanced usage and callers should almost always use
+ * getMainLB() instead. This method can be useful when a table is used as a key/value
+ * store. In that cases, one might want to query it in autocommit mode (DBO_TRX off)
+ * but still use DBO_TRX transaction rounds on other tables.
+ *
+ * @param bool|string $domain Domain ID, or false for the current domain
* @return ILoadBalancer
*/
abstract public function newMainLB( $domain = false );
/**
* Get a cached (tracked) load balancer object.
*
- * @param bool|string $domain Wiki ID, or false for the current wiki
+ * @param bool|string $domain Domain ID, or false for the current domain
* @return ILoadBalancer
*/
abstract public function getMainLB( $domain = false );
* untracked, not chronology-protected, and the caller is responsible for
* cleaning it up.
*
+ * This method is for only advanced usage and callers should almost always use
+ * getExternalLB() instead. This method can be useful when a table is used as a
+ * key/value store. In that cases, one might want to query it in autocommit mode
+ * (DBO_TRX off) but still use DBO_TRX transaction rounds on other tables.
+ *
* @param string $cluster External storage cluster, or false for core
- * @param bool|string $domain Wiki ID, or false for the current wiki
+ * @param bool|string $domain Domain ID, or false for the current domain
* @return ILoadBalancer
*/
- abstract protected function newExternalLB( $cluster, $domain = false );
+ abstract public function newExternalLB( $cluster, $domain = false );
/**
* Get a cached (tracked) load balancer for external storage
*
* @param string $cluster External storage cluster, or false for core
- * @param bool|string $domain Wiki ID, or false for the current wiki
+ * @param bool|string $domain Domain ID, or false for the current domain
* @return ILoadBalancer
*/
abstract public function getExternalLB( $cluster, $domain = false );
public function shutdown(
$mode = self::SHUTDOWN_CHRONPROT_SYNC, callable $workCallback = null
) {
+ $chronProt = $this->getChronologyProtector();
if ( $mode === self::SHUTDOWN_CHRONPROT_SYNC ) {
- $this->shutdownChronologyProtector( $this->chronProt, $workCallback, 'sync' );
+ $this->shutdownChronologyProtector( $chronProt, $workCallback, 'sync' );
} elseif ( $mode === self::SHUTDOWN_CHRONPROT_ASYNC ) {
- $this->shutdownChronologyProtector( $this->chronProt, null, 'async' );
+ $this->shutdownChronologyProtector( $chronProt, null, 'async' );
}
$this->commitMasterChanges( __METHOD__ ); // sanity
* This makes sense when lag being waiting on is caused by the code that does this check.
* In that case, setting "ifWritesSince" can avoid the overhead of waiting for clusters
* that were not changed since the last wait check. To forcefully wait on a specific cluster
- * for a given wiki, use the 'wiki' parameter. To forcefully wait on an "external" cluster,
+ * for a given domain, use the 'domain' parameter. To forcefully wait on an "external" cluster,
* use the "cluster" parameter.
*
* Never call this function after a large DB write that is *still* in a transaction.
* It only makes sense to call this after the possible lag inducing changes were committed.
*
* @param array $opts Optional fields that include:
- * - wiki : wait on the load balancer DBs that handles the given wiki
+ * - domain : wait on the load balancer DBs that handles the given domain ID
* - cluster : wait on the given external load balancer DBs
* - timeout : Max wait time. Default: ~60 seconds
* - ifWritesSince: Only wait if writes were done since this UNIX timestamp
*/
public function waitForReplication( array $opts = [] ) {
$opts += [
- 'wiki' => false,
+ 'domain' => false,
'cluster' => false,
'timeout' => 60,
'ifWritesSince' => null
];
+ if ( $opts['domain'] === false && isset( $opts['wiki'] ) ) {
+ $opts['domain'] = $opts['wiki']; // b/c
+ }
+
// Figure out which clusters need to be checked
/** @var ILoadBalancer[] $lbs */
$lbs = [];
if ( $opts['cluster'] !== false ) {
$lbs[] = $this->getExternalLB( $opts['cluster'] );
- } elseif ( $opts['wiki'] !== false ) {
- $lbs[] = $this->getMainLB( $opts['wiki'] );
+ } elseif ( $opts['domain'] !== false ) {
+ $lbs[] = $this->getMainLB( $opts['domain'] );
} else {
$this->forEachLB( function ( ILoadBalancer $lb ) use ( &$lbs ) {
$lbs[] = $lb;
$failed = [];
foreach ( $lbs as $i => $lb ) {
if ( $masterPositions[$i] ) {
- // The DBMS may not support getMasterPos() or the whole
- // load balancer might be fake (e.g. $wgAllDBsAreLocalhost).
+ // The DBMS may not support getMasterPos()
if ( !$lb->waitForAll( $masterPositions[$i], $opts['timeout'] ) ) {
$failed[] = $lb->getServerName( $lb->getWriterIndex() );
}
*/
public function getEmptyTransactionTicket( $fname ) {
if ( $this->hasMasterChanges() ) {
- $this->queryLogger->error( __METHOD__ . ": $fname does not have outer scope." );
+ $this->queryLogger->error( __METHOD__ . ": $fname does not have outer scope.\n" .
+ ( new RuntimeException() )->getTraceAsString() );
+
return null;
}
*/
public function commitAndWaitForReplication( $fname, $ticket, array $opts = [] ) {
if ( $ticket !== $this->ticket ) {
- $this->perfLogger->error( __METHOD__ . ": $fname does not have outer scope." );
+ $this->perfLogger->error( __METHOD__ . ": $fname does not have outer scope.\n" .
+ ( new RuntimeException() )->getTraceAsString() );
+
return;
}
* @since 1.28
*/
public function getChronologyProtectorTouched( $dbName ) {
- return $this->chronProt->getTouched( $dbName );
+ return $this->getChronologyProtector()->getTouched( $dbName );
}
/**
* @since 1.27
*/
public function disableChronologyProtection() {
- $this->chronProt->setEnabled( false );
+ $this->getChronologyProtector()->setEnabled( false );
}
/**
* @return ChronologyProtector
*/
- protected function newChronologyProtector() {
- $chronProt = new ChronologyProtector(
+ protected function getChronologyProtector() {
+ if ( $this->chronProt ) {
+ return $this->chronProt;
+ }
+
+ $this->chronProt = new ChronologyProtector(
$this->memCache,
[
- 'ip' => isset( $_SERVER[ 'REMOTE_ADDR' ] ) ? $_SERVER[ 'REMOTE_ADDR' ] : '',
- 'agent' => isset( $_SERVER['HTTP_USER_AGENT'] ) ? $_SERVER['HTTP_USER_AGENT'] : ''
+ 'ip' => $this->requestInfo['IPAddress'],
+ 'agent' => $this->requestInfo['UserAgent'],
],
isset( $_GET['cpPosTime'] ) ? $_GET['cpPosTime'] : null
);
- $chronProt->setLogger( $this->replLogger );
- if ( PHP_SAPI === 'cli' ) {
- $chronProt->setEnabled( false );
+ $this->chronProt->setLogger( $this->replLogger );
+
+ if ( $this->cliMode ) {
+ $this->chronProt->setEnabled( false );
+ } elseif ( $this->requestInfo['ChronologyProtection'] === 'false' ) {
+ // Request opted out of using position wait logic. This is useful for requests
+ // done by the job queue or background ETL that do not have a meaningful session.
+ $this->chronProt->setWaitEnabled( false );
}
- return $chronProt;
+ $this->replLogger->debug( __METHOD__ . ': using request info ' .
+ json_encode( $this->requestInfo, JSON_PRETTY_PRINT ) );
+
+ return $this->chronProt;
}
/**
*/
final protected function baseLoadBalancerParams() {
return [
- 'localDomain' => $this->domain,
+ 'localDomain' => $this->localDomain,
'readOnlyReason' => $this->readOnlyReason,
'srvCache' => $this->srvCache,
'wanCache' => $this->wanCache,
+ 'profiler' => $this->profiler,
'trxProfiler' => $this->trxProfiler,
'queryLogger' => $this->queryLogger,
'connLogger' => $this->connLogger,
'replLogger' => $this->replLogger,
'errorLogger' => $this->errorLogger,
- 'hostname' => $this->hostname
+ 'hostname' => $this->hostname,
+ 'cliMode' => $this->cliMode,
+ 'agent' => $this->agent
];
}
}
/**
- * Define a new local domain (for testing)
+ * Set a new table prefix for the existing local domain ID for testing
*
- * Caller should make sure no local connection are open to the old local domain
- *
- * @param string $domain
+ * @param string $prefix
* @since 1.28
*/
- public function setDomainPrefix( $domain ) {
- $this->domain = $domain;
+ public function setDomainPrefix( $prefix ) {
+ $this->localDomain = new DatabaseDomain(
+ $this->localDomain->getDatabase(),
+ null,
+ $prefix
+ );
+
+ $this->forEachLB( function( ILoadBalancer $lb ) use ( $prefix ) {
+ $lb->setDomainPrefix( $prefix );
+ } );
}
/**
public function closeAll() {
$this->forEachLBCallMethod( 'closeAll', [] );
}
+
+ /**
+ * @param string $agent Agent name for query profiling
+ * @since 1.28
+ */
+ public function setAgentName( $agent ) {
+ $this->agent = $agent;
+ }
+
+ /**
+ * Append ?cpPosTime parameter to a URL for ChronologyProtector purposes if needed
+ *
+ * Note that unlike cookies, this works accross domains
+ *
+ * @param string $url
+ * @param float $time UNIX timestamp just before shutdown() was called
+ * @return string
+ * @since 1.28
+ */
+ public function appendPreShutdownTimeAsQuery( $url, $time ) {
+ $usedCluster = 0;
+ $this->forEachLB( function ( ILoadBalancer $lb ) use ( &$usedCluster ) {
+ $usedCluster |= ( $lb->getServerCount() > 1 );
+ } );
+
+ if ( !$usedCluster ) {
+ return $url; // no master/replica clusters touched
+ }
+
+ return strpos( $url, '?' ) === false ? "$url?cpPosTime=$time" : "$url&cpPosTime=$time";
+ }
+
+ /**
+ * @param array $info Map of fields, including:
+ * - IPAddress : IP address
+ * - UserAgent : User-Agent HTTP header
+ * - ChronologyProtection : cookie/header value specifying ChronologyProtector usage
+ * @since 1.28
+ */
+ public function setRequestInfo( array $info ) {
+ $this->requestInfo = $info + $this->requestInfo;
+ }
+
+ function __destruct() {
+ $this->destroy();
+ }
}