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 */
+ /** @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 */
* @param array $conf
*/
public function __construct( array $conf ) {
- $this->localDomain = isset( $conf['localDomain'] ) ? $conf['localDomain'] : '';
+ $this->localDomain = isset( $conf['localDomain'] )
+ ? DatabaseDomain::newFromId( $conf['localDomain'] )
+ : DatabaseDomain::newUnspecified();
if ( isset( $conf['readOnlyReason'] ) && is_string( $conf['readOnlyReason'] ) ) {
$this->readOnlyReason = $conf['readOnlyReason'];
: function ( Exception $e ) {
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->chronProt = isset( $conf['chronProt'] ) ? $conf['chronProt'] : null;
+
+ $this->profiler = isset( $params['profiler'] ) ? $params['profiler'] : null;
$this->trxProfiler = isset( $conf['trxProfiler'] )
? $conf['trxProfiler']
: new TransactionProfiler();
- $this->ticket = mt_rand();
+ $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();
}
/**
* 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
+ * @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 );
* cleaning it up.
*
* @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 );
* 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
$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() );
}
* @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 );
+ $this->chronProt->setLogger( $this->replLogger );
+
if ( $this->cliMode ) {
- $chronProt->setEnabled( false );
+ $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;
}
/**
'readOnlyReason' => $this->readOnlyReason,
'srvCache' => $this->srvCache,
'wanCache' => $this->wanCache,
+ 'profiler' => $this->profiler,
'trxProfiler' => $this->trxProfiler,
'queryLogger' => $this->queryLogger,
'connLogger' => $this->connLogger,
* @since 1.28
*/
public function setDomainPrefix( $prefix ) {
- list( $dbName, ) = explode( '-', $this->localDomain, 2 );
- $this->localDomain = "{$dbName}-{$prefix}";
+ $this->localDomain = new DatabaseDomain(
+ $this->localDomain->getDatabase(),
+ null,
+ $prefix
+ );
$this->forEachLB( function( LoadBalancer $lb ) use ( $prefix ) {
$lb->setDomainPrefix( $prefix );
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();
+ }
}