*/
abstract class LBFactory implements ILBFactory {
/** @var ChronologyProtector */
- protected $chronProt;
+ private $chronProt;
/** @var object|string Class name or object With profileIn/profileOut methods */
- protected $profiler;
+ private $profiler;
/** @var TransactionProfiler */
- protected $trxProfiler;
+ private $trxProfiler;
/** @var LoggerInterface */
- protected $replLogger;
+ private $replLogger;
/** @var LoggerInterface */
- protected $connLogger;
+ private $connLogger;
/** @var LoggerInterface */
- protected $queryLogger;
+ private $queryLogger;
/** @var LoggerInterface */
- protected $perfLogger;
+ private $perfLogger;
/** @var callable Error logger */
- protected $errorLogger;
+ private $errorLogger;
/** @var callable Deprecation logger */
- protected $deprecationLogger;
+ private $deprecationLogger;
+
/** @var BagOStuff */
protected $srvCache;
/** @var BagOStuff */
/** @var DatabaseDomain Local domain */
protected $localDomain;
+
/** @var string Local hostname of the app server */
- protected $hostname;
+ private $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 */
- protected $trxRoundId = false;
- /** @var string|bool Reason all LBs are read-only or false if not */
- protected $readOnlyReason = false;
- /** @var callable[] */
- protected $replicationWaitCallbacks = [];
+ private $requestInfo;
+ /** @var bool Whether this PHP instance is for a CLI script */
+ private $cliMode;
+ /** @var string Agent name for query profiling */
+ private $agent;
/** @var array[] $aliases Map of (table => (dbname, schema, prefix) map) */
- protected $tableAliases = [];
+ private $tableAliases = [];
/** @var string[] Map of (index alias => index) */
- protected $indexAliases = [];
-
- /** @var bool Whether this PHP instance is for a CLI script */
- protected $cliMode;
- /** @var string Agent name for query profiling */
- protected $agent;
+ private $indexAliases = [];
+ /** @var callable[] */
+ private $replicationWaitCallbacks = [];
+ /** @var mixed */
+ private $ticket;
+ /** @var string|bool String if a requested DBO_TRX transaction round is active */
+ private $trxRoundId = false;
/** @var string One of the ROUND_* class constants */
private $trxRoundStage = self::ROUND_CURSORY;
+ /** @var string|bool Reason all LBs are read-only or false if not */
+ protected $readOnlyReason = false;
+
+ /** @var string|null */
+ private $defaultGroup = null;
+
const ROUND_CURSORY = 'cursory';
const ROUND_BEGINNING = 'within-begin';
const ROUND_COMMITTING = 'within-commit';
$this->requestInfo = [
'IPAddress' => $_SERVER[ 'REMOTE_ADDR' ] ?? '',
'UserAgent' => $_SERVER['HTTP_USER_AGENT'] ?? '',
- 'ChronologyProtection' => 'true',
- // phpcs:ignore MediaWiki.Usage.SuperGlobalsUsage.SuperGlobals -- library can't use $wgRequest
- 'ChronologyPositionIndex' => $_GET['cpPosIndex'] ?? null
+ // Headers application can inject via LBFactory::setRequestInfo()
+ 'ChronologyClientId' => null, // prior $cpClientId value from LBFactory::shutdown()
+ 'ChronologyPositionIndex' => null // prior $cpIndex value from LBFactory::shutdown()
];
$this->cliMode = $conf['cliMode'] ?? ( PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg' );
$this->hostname = $conf['hostname'] ?? gethostname();
$this->agent = $conf['agent'] ?? '';
+ $this->defaultGroup = $conf['defaultGroup'] ?? null;
$this->ticket = mt_rand();
}
$this->forEachLBCallMethod( 'disable' );
}
+ public function getLocalDomainID() {
+ return $this->localDomain->getId();
+ }
+
+ public function resolveDomainID( $domain ) {
+ return ( $domain !== false ) ? (string)$domain : $this->getLocalDomainID();
+ }
+
public function shutdown(
- $mode = self::SHUTDOWN_CHRONPROT_SYNC, callable $workCallback = null, &$cpIndex = null
+ $mode = self::SHUTDOWN_CHRONPROT_SYNC,
+ callable $workCallback = null,
+ &$cpIndex = null,
+ &$cpClientId = null
) {
$chronProt = $this->getChronologyProtector();
if ( $mode === self::SHUTDOWN_CHRONPROT_SYNC ) {
$this->shutdownChronologyProtector( $chronProt, null, 'async', $cpIndex );
}
+ $cpClientId = $chronProt->getClientId();
+
$this->commitMasterChanges( __METHOD__ ); // sanity
}
protected function forEachLBCallMethod( $methodName, array $args = [] ) {
$this->forEachLB(
function ( ILoadBalancer $loadBalancer, $methodName, array $args ) {
- call_user_func_array( [ $loadBalancer, $methodName ], $args );
+ $loadBalancer->$methodName( ...$args );
},
[ $methodName, $args ]
);
[
'ip' => $this->requestInfo['IPAddress'],
'agent' => $this->requestInfo['UserAgent'],
+ 'clientId' => $this->requestInfo['ChronologyClientId']
],
$this->requestInfo['ChronologyPositionIndex']
);
// 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 );
+ } elseif ( $this->memStash instanceof EmptyBagOStuff ) {
+ // No where to store any DB positions and wait for them to appear
+ $this->chronProt->setEnabled( false );
+ $this->replLogger->info( 'Cannot use ChronologyProtector with EmptyBagOStuff.' );
}
$this->replLogger->debug( __METHOD__ . ': using request info ' .
'hostname' => $this->hostname,
'cliMode' => $this->cliMode,
'agent' => $this->agent,
+ 'defaultGroup' => $this->defaultGroup,
'chronologyCallback' => function ( ILoadBalancer $lb ) {
// Defer ChronologyProtector construction in case setRequestInfo() ends up
// being called later (but before the first connection attempt) (T192611)
/**
* @param int $index Write index
- * @param int $time UNIX timestamp
- * @return string Timestamp-qualified write index of the form "<index>.<timestamp>"
+ * @param int $time UNIX timestamp; can be used to detect stale cookies (T190082)
+ * @param string $clientId Agent ID hash from ILBFactory::shutdown()
+ * @return string Timestamp-qualified write index of the form "<index>@<timestamp>#<hash>"
* @since 1.32
*/
- public static function makeCookieValueFromCPIndex( $index, $time ) {
- return $index . '@' . $time;
+ public static function makeCookieValueFromCPIndex( $index, $time, $clientId ) {
+ return "$index@$time#$clientId";
}
/**
- * @param string $value String possibly of the form "<index>" or "<index>@<timestamp>"
- * @param int $minTimestamp Lowest UNIX timestamp of non-expired values (if present)
- * @return int|null Write index or null if $value is empty or expired
+ * @param string $value Possible result of LBFactory::makeCookieValueFromCPIndex()
+ * @param int $minTimestamp Lowest UNIX timestamp that a non-expired value can have
+ * @return array (index: int or null, clientId: string or null)
* @since 1.32
*/
- public static function getCPIndexFromCookieValue( $value, $minTimestamp ) {
- if ( !preg_match( '/^(\d+)(?:@(\d+))?$/', $value, $m ) ) {
- return null;
+ public static function getCPInfoFromCookieValue( $value, $minTimestamp ) {
+ static $placeholder = [ 'index' => null, 'clientId' => null ];
+
+ if ( !preg_match( '/^(\d+)@(\d+)#([0-9a-f]{32})$/', $value, $m ) ) {
+ return $placeholder; // invalid
}
$index = (int)$m[1];
-
- if ( isset( $m[2] ) && $m[2] !== '' && (int)$m[2] < $minTimestamp ) {
- return null; // expired
+ if ( $index <= 0 ) {
+ return $placeholder; // invalid
+ } elseif ( isset( $m[2] ) && $m[2] !== '' && (int)$m[2] < $minTimestamp ) {
+ return $placeholder; // expired
}
- return ( $index > 0 ) ? $index : null;
+ $clientId = ( isset( $m[3] ) && $m[3] !== '' ) ? $m[3] : null;
+
+ return [ 'index' => $index, 'clientId' => $clientId ];
}
public function setRequestInfo( array $info ) {