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'" );
288 $this->logger->debug( __METHOD__ .
": $cluster ($primaryName) has no position" );
316 $this->logger->debug( __METHOD__ .
": $cluster ($masterName) position now '$pos'" );
317 $this->shutdownPositionsByPrimary[$masterName] = $pos;
318 $this->shutdownTimestampsByCluster[$cluster] = $pos->asOfTime();
320 $this->logger->debug( __METHOD__ .
": $cluster ($masterName) position unknown" );
321 $this->shutdownTimestampsByCluster[$cluster] = $this->
getCurrentTime();
324 $this->logger->debug( __METHOD__ .
": $cluster ($masterName) has no replication" );
325 $this->shutdownTimestampsByCluster[$cluster] = $this->
getCurrentTime();
338 if ( !$this->enabled ) {
342 if ( !$this->shutdownTimestampsByCluster ) {
343 $this->logger->debug( __METHOD__ .
": no primary positions/timestamps to save" );
348 $scopeLock = $this->store->getScopedLock( $this->key, self::LOCK_TIMEOUT, self::LOCK_TTL );
351 $this->unmarshalPositions( $this->store->get( $this->key ) ),
352 $this->shutdownPositionsByPrimary,
353 $this->shutdownTimestampsByCluster,
357 $ok = $this->store->set(
359 $this->marshalPositions( $positions ),
360 self::POSITION_STORE_TTL
367 $clusterList = implode(
', ', array_keys( $this->shutdownTimestampsByCluster ) );
370 $bouncedPositions = [];
371 $this->logger->debug(
372 __METHOD__ .
": saved primary positions/timestamp for DB cluster(s) $clusterList"
376 $clientPosIndex =
null;
379 $this->logger->warning(
380 __METHOD__ .
": failed to save primary positions for DB cluster(s) $clusterList"
384 return $bouncedPositions;
397 if ( !$this->enabled ) {
404 $timestamp = $timestampsByCluster[$cluster] ??
null;
405 if ( $timestamp ===
null ) {
406 $recentTouchTimestamp =
false;
407 } elseif ( ( $this->startupTimestamp - $timestamp ) > self::POSITION_COOKIE_TTL ) {
412 $recentTouchTimestamp =
false;
413 $this->logger->debug( __METHOD__ .
": old timestamp ($timestamp) for $cluster" );
415 $recentTouchTimestamp = $timestamp;
416 $this->logger->debug( __METHOD__ .
": recent timestamp ($timestamp) for $cluster" );
419 return $recentTouchTimestamp;
446 if ( $this->startupTimestamp !==
null ) {
451 $this->logger->debug(
'ChronologyProtector using store ' . get_class( $this->store ) );
452 $this->logger->debug(
"ChronologyProtector fetching positions for {$this->clientId}" );
454 $data = $this->unmarshalPositions( $this->store->get( $this->key ) );
456 $this->startupPositionsByPrimary = $data ? $data[self::FLD_POSITIONS] : [];
457 $this->startupTimestampsByCluster = $data[self::FLD_TIMESTAMPS] ?? [];
467 $indexReached = is_array( $data ) ? $data[self::FLD_WRITE_INDEX] :
null;
468 if ( $this->positionWaitsEnabled && $this->waitForPosIndex > 0 ) {
469 if ( $indexReached >= $this->waitForPosIndex ) {
470 $this->logger->debug(
'expected and found position index {cpPosIndex}', [
471 'cpPosIndex' => $this->waitForPosIndex,
472 ] + $this->clientLogInfo );
474 $this->logger->warning(
'expected but failed to find position index {cpPosIndex}', [
475 'cpPosIndex' => $this->waitForPosIndex,
476 'indexReached' => $indexReached,
477 'exception' =>
new \RuntimeException(),
478 ] + $this->clientLogInfo );
481 if ( $indexReached ) {
482 $this->logger->debug(
'found position data with index {indexReached}', [
483 'indexReached' => $indexReached
484 ] + $this->clientLogInfo );
488 if ( $indexReached && $this->hasImplicitClientId ) {
489 $isWithinPossibleCookieTTL =
false;
490 foreach ( $this->startupTimestampsByCluster as $timestamp ) {
491 if ( ( $this->startupTimestamp - $timestamp ) < self::POSITION_COOKIE_TTL ) {
492 $isWithinPossibleCookieTTL =
true;
496 if ( $isWithinPossibleCookieTTL ) {
497 $this->logger->warning(
'found position data under a presumed clientId (T314434)', [
498 'indexReached' => $indexReached
499 ] + $this->clientLogInfo );
515 array $shutdownPositions,
516 array $shutdownTimestamps,
517 ?
int &$clientPosIndex =
null
520 $mergedPositions = $storedValue[self::FLD_POSITIONS] ?? [];
522 foreach ( $shutdownPositions as $masterName => $pos ) {
524 !isset( $mergedPositions[$masterName] ) ||
525 !( $mergedPositions[$masterName] instanceof
DBPrimaryPos ) ||
526 $pos->asOfTime() > $mergedPositions[$masterName]->asOfTime()
528 $mergedPositions[$masterName] = $pos;
533 $mergedTimestamps = $storedValue[self::FLD_TIMESTAMPS] ?? [];
535 foreach ( $shutdownTimestamps as $cluster => $timestamp ) {
537 !isset( $mergedTimestamps[$cluster] ) ||
538 $timestamp > $mergedTimestamps[$cluster]
540 $mergedTimestamps[$cluster] = $timestamp;
544 $clientPosIndex = ( $storedValue[self::FLD_WRITE_INDEX] ?? 0 ) + 1;
547 self::FLD_POSITIONS => $mergedPositions,
548 self::FLD_TIMESTAMPS => $mergedTimestamps,
549 self::FLD_WRITE_INDEX => $clientPosIndex
559 if ( $this->wallClockOverride ) {
560 return $this->wallClockOverride;
563 $clockTime = (float)time();
566 return max( microtime(
true ), $clockTime );
575 $this->wallClockOverride =& $time;
578 private function marshalPositions( array $positions ) {
579 foreach ( $positions[ self::FLD_POSITIONS ] as
$key => $pos ) {
580 $positions[ self::FLD_POSITIONS ][
$key ] = $pos->toArray();
590 private function unmarshalPositions( $positions ) {
595 foreach ( $positions[ self::FLD_POSITIONS ] as
$key => $pos ) {
596 $class = $pos[
'_type_' ];
597 $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.