23 use Psr\Log\LoggerAwareInterface;
24 use Psr\Log\LoggerInterface;
25 use Psr\Log\NullLogger;
164 private $wallClockOverride;
184 private $hasImplicitClientId =
false;
189 private const POSITION_STORE_TTL = 60;
192 private const LOCK_TIMEOUT = 3;
194 private const LOCK_TTL = 6;
196 private const FLD_POSITIONS =
'positions';
197 private const FLD_TIMESTAMPS =
'timestamps';
198 private const FLD_WRITE_INDEX =
'writeIndex';
210 ?
int $clientPosIndex,
215 if ( isset( $client[
'clientId'] ) ) {
216 $this->clientId = $client[
'clientId'];
218 $this->hasImplicitClientId =
true;
219 $this->clientId = ( $secret !=
'' )
220 ? hash_hmac(
'md5', $client[
'ip'] .
"\n" . $client[
'agent'], $secret )
221 : md5( $client[
'ip'] .
"\n" . $client[
'agent'] );
224 $this->waitForPosIndex = $clientPosIndex;
226 $this->clientLogInfo = [
227 'clientIP' => $client[
'ip'],
228 'clientAgent' => $client[
'agent'],
229 'clientId' => $client[
'clientId'] ?? null
232 $this->logger =
new NullLogger();
260 $this->positionWaitsEnabled =
$enabled;
277 if ( !$this->enabled || !$this->positionWaitsEnabled ) {
286 $this->logger->debug( __METHOD__ .
": $cluster ($primaryName) position is '$pos'" );
289 $this->logger->debug( __METHOD__ .
": $cluster ($primaryName) has no position" );
315 $this->logger->debug( __METHOD__ .
": $cluster ($masterName) position now '$pos'" );
316 $this->shutdownPositionsByPrimary[$masterName] = $pos;
317 $this->shutdownTimestampsByCluster[$cluster] = $pos->asOfTime();
319 $this->logger->debug( __METHOD__ .
": $cluster ($masterName) position unknown" );
320 $this->shutdownTimestampsByCluster[$cluster] = $this->
getCurrentTime();
323 $this->logger->debug( __METHOD__ .
": $cluster ($masterName) has no replication" );
324 $this->shutdownTimestampsByCluster[$cluster] = $this->
getCurrentTime();
337 if ( !$this->enabled ) {
341 if ( !$this->shutdownTimestampsByCluster ) {
342 $this->logger->debug( __METHOD__ .
": no primary positions/timestamps to save" );
347 $scopeLock = $this->store->getScopedLock( $this->key, self::LOCK_TIMEOUT, self::LOCK_TTL );
350 $this->unmarshalPositions( $this->store->get( $this->key ) ),
351 $this->shutdownPositionsByPrimary,
352 $this->shutdownTimestampsByCluster,
356 $ok = $this->store->set(
358 $this->marshalPositions( $positions ),
359 self::POSITION_STORE_TTL
366 $clusterList = implode(
', ', array_keys( $this->shutdownTimestampsByCluster ) );
369 $bouncedPositions = [];
370 $this->logger->debug(
371 __METHOD__ .
": saved primary positions/timestamp for DB cluster(s) $clusterList"
375 $clientPosIndex =
null;
378 $this->logger->warning(
379 __METHOD__ .
": failed to save primary positions for DB cluster(s) $clusterList"
383 return $bouncedPositions;
396 if ( !$this->enabled ) {
403 $timestamp = $timestampsByCluster[$cluster] ??
null;
404 if ( $timestamp ===
null ) {
405 $recentTouchTimestamp =
false;
406 } elseif ( ( $this->startupTimestamp - $timestamp ) > self::POSITION_COOKIE_TTL ) {
411 $recentTouchTimestamp =
false;
412 $this->logger->debug( __METHOD__ .
": old timestamp ($timestamp) for $cluster" );
414 $recentTouchTimestamp = $timestamp;
415 $this->logger->debug( __METHOD__ .
": recent timestamp ($timestamp) for $cluster" );
418 return $recentTouchTimestamp;
445 if ( $this->startupTimestamp !==
null ) {
450 $this->logger->debug(
'ChronologyProtector using store ' . get_class( $this->store ) );
451 $this->logger->debug(
"ChronologyProtector fetching positions for {$this->clientId}" );
453 $data = $this->unmarshalPositions( $this->store->get( $this->key ) );
455 $this->startupPositionsByPrimary = $data ? $data[self::FLD_POSITIONS] : [];
456 $this->startupTimestampsByCluster = $data[self::FLD_TIMESTAMPS] ?? [];
466 $indexReached = is_array( $data ) ? $data[self::FLD_WRITE_INDEX] :
null;
467 if ( $this->positionWaitsEnabled && $this->waitForPosIndex > 0 ) {
468 if ( $indexReached >= $this->waitForPosIndex ) {
469 $this->logger->debug(
'expected and found position index {cpPosIndex}', [
470 'cpPosIndex' => $this->waitForPosIndex,
471 ] + $this->clientLogInfo );
473 $this->logger->warning(
'expected but failed to find position index {cpPosIndex}', [
474 'cpPosIndex' => $this->waitForPosIndex,
475 'indexReached' => $indexReached,
476 'exception' =>
new \RuntimeException(),
477 ] + $this->clientLogInfo );
480 if ( $indexReached ) {
481 $this->logger->debug(
'found position data with index {indexReached}', [
482 'indexReached' => $indexReached
483 ] + $this->clientLogInfo );
487 if ( $indexReached && $this->hasImplicitClientId ) {
488 $isWithinPossibleCookieTTL =
false;
489 foreach ( $this->startupTimestampsByCluster as $timestamp ) {
490 if ( ( $this->startupTimestamp - $timestamp ) < self::POSITION_COOKIE_TTL ) {
491 $isWithinPossibleCookieTTL =
true;
495 if ( $isWithinPossibleCookieTTL ) {
496 $this->logger->warning(
'found position data under a presumed clientId (T314434)', [
497 'indexReached' => $indexReached
498 ] + $this->clientLogInfo );
514 array $shutdownPositions,
515 array $shutdownTimestamps,
516 ?
int &$clientPosIndex =
null
519 $mergedPositions = $storedValue[self::FLD_POSITIONS] ?? [];
521 foreach ( $shutdownPositions as $masterName => $pos ) {
523 !isset( $mergedPositions[$masterName] ) ||
524 !( $mergedPositions[$masterName] instanceof
DBPrimaryPos ) ||
525 $pos->asOfTime() > $mergedPositions[$masterName]->asOfTime()
527 $mergedPositions[$masterName] = $pos;
532 $mergedTimestamps = $storedValue[self::FLD_TIMESTAMPS] ?? [];
534 foreach ( $shutdownTimestamps as $cluster => $timestamp ) {
536 !isset( $mergedTimestamps[$cluster] ) ||
537 $timestamp > $mergedTimestamps[$cluster]
539 $mergedTimestamps[$cluster] = $timestamp;
543 $clientPosIndex = ( $storedValue[self::FLD_WRITE_INDEX] ?? 0 ) + 1;
546 self::FLD_POSITIONS => $mergedPositions,
547 self::FLD_TIMESTAMPS => $mergedTimestamps,
548 self::FLD_WRITE_INDEX => $clientPosIndex
558 if ( $this->wallClockOverride ) {
559 return $this->wallClockOverride;
562 $clockTime = (float)time();
565 return max( microtime(
true ), $clockTime );
574 $this->wallClockOverride =& $time;
577 private function marshalPositions( array $positions ) {
578 foreach ( $positions[ self::FLD_POSITIONS ] as
$key => $pos ) {
579 $positions[ self::FLD_POSITIONS ][
$key ] = $pos->toArray();
589 private function unmarshalPositions( $positions ) {
594 foreach ( $positions[ self::FLD_POSITIONS ] as
$key => $pos ) {
595 $class = $pos[
'_type_' ];
596 $positions[ self::FLD_POSITIONS ][
$key ] = $class::newFromArray( $pos );
Class representing a cache/ephemeral data store.
makeGlobalKey( $collection,... $components)
Make a cache key for the default keyspace and given components.