use BagOStuff;
/**
- * Class for ensuring a consistent ordering of events as seen by the user, despite replication.
+ * Helper class for mitigating DB replication lag in order to provide "session consistency"
+ *
+ * This helps to ensure a consistent ordering of events as seen by an client
+ *
* Kind of like Hawking's [[Chronology Protection Agency]].
*/
class ChronologyProtector implements LoggerAwareInterface {
/**
* @param BagOStuff $store
- * @param array[] $client Map of (ip: <IP>, agent: <user-agent> [, clientId: <hash>] )
- * @param int|null $posIndex Write counter index [optional]
+ * @param array $client Map of (ip: <IP>, agent: <user-agent> [, clientId: <hash>] )
+ * @param int|null $posIndex Write counter index
+ * @param string $secret Secret string for HMAC hashing [optional]
* @since 1.27
*/
- public function __construct( BagOStuff $store, array $client, $posIndex = null ) {
+ public function __construct( BagOStuff $store, array $client, $posIndex, $secret = '' ) {
$this->store = $store;
- $this->clientId = $client['clientId'] ??
- md5( $client['ip'] . "\n" . $client['agent'] );
+ if ( isset( $client['clientId'] ) ) {
+ $this->clientId = $client['clientId'];
+ } else {
+ $this->clientId = ( $secret != '' )
+ ? hash_hmac( 'md5', $client['ip'] . "\n" . $client['agent'], $secret )
+ : md5( $client['ip'] . "\n" . $client['agent'] );
+ }
$this->key = $store->makeGlobalKey( __CLASS__, $this->clientId, 'v2' );
$this->waitForPosIndex = $posIndex;
}
/**
- * Initialise a ILoadBalancer to give it appropriate chronology protection.
+ * Apply the "session consistency" DB replication position to a new ILoadBalancer
*
- * If the stash has a previous master position recorded, this will try to
- * make sure that the next query to a replica DB of that master will see changes up
+ * If the stash has a previous master position recorded, this will try to make
+ * sure that the next query to a replica DB of that master will see changes up
* to that position by delaying execution. The delay may timeout and allow stale
* data if no non-lagged replica DBs are available.
*
+ * This method should only be called from LBFactory.
+ *
* @param ILoadBalancer $lb
* @return void
*/
- public function initLB( ILoadBalancer $lb ) {
- if ( !$this->enabled || $lb->getServerCount() <= 1 ) {
- return; // non-replicated setup or disabled
+ public function applySessionReplicationPosition( ILoadBalancer $lb ) {
+ if ( !$this->enabled ) {
+ return; // disabled
}
- $this->initPositions();
-
$masterName = $lb->getServerName( $lb->getWriterIndex() );
- if (
- isset( $this->startupPositions[$masterName] ) &&
- $this->startupPositions[$masterName] instanceof DBMasterPos
- ) {
- $pos = $this->startupPositions[$masterName];
- $this->logger->debug( __METHOD__ . ": LB for '$masterName' set to pos $pos\n" );
+ $startupPositions = $this->getStartupMasterPositions();
+
+ $pos = $startupPositions[$masterName] ?? null;
+ if ( $pos instanceof DBMasterPos ) {
+ $this->logger->debug( __METHOD__ . ": pos for DB '$masterName' set to '$pos'\n" );
$lb->waitFor( $pos );
}
}
/**
- * Notify the ChronologyProtector that the ILoadBalancer is about to shut
- * down. Saves replication positions.
+ * Save the "session consistency" DB replication position for an end-of-life ILoadBalancer
+ *
+ * This saves the replication position of the master DB if this request made writes to it.
+ *
+ * This method should only be called from LBFactory.
*
* @param ILoadBalancer $lb
* @return void
*/
- public function shutdownLB( ILoadBalancer $lb ) {
+ public function storeSessionReplicationPosition( ILoadBalancer $lb ) {
if ( !$this->enabled ) {
- return; // not enabled
+ return; // disabled
} elseif ( !$lb->hasOrMadeRecentMasterChanges( INF ) ) {
// Only save the position if writes have been done on the connection
return;
}
if ( $this->shutdownPositions === [] ) {
+ $this->logger->debug( __METHOD__ . ": no master positions to save\n" );
+
return []; // nothing to save
}
- $this->logger->debug( __METHOD__ . ": saving master pos for " .
+ $this->logger->debug(
+ __METHOD__ . ": saving master pos for " .
implode( ', ', array_keys( $this->shutdownPositions ) ) . "\n"
);
/**
* Load in previous master positions for the client
*/
- protected function initPositions() {
+ protected function getStartupMasterPositions() {
if ( $this->initialized ) {
- return;
+ return $this->startupPositions;
}
$this->initialized = true;
+ $this->logger->debug( __METHOD__ . ": client ID is {$this->clientId} (read)\n" );
+
if ( $this->wait ) {
// If there is an expectation to see master positions from a certain write
// index or higher, then block until it appears, or until a timeout is reached.
$this->startupPositions = [];
$this->logger->debug( __METHOD__ . ": key is {$this->key} (unread)\n" );
}
+
+ return $this->startupPositions;
}
/**