27use Psr\Log\LoggerAwareInterface;
28use Psr\Log\LoggerInterface;
29use Psr\Log\NullLogger;
30use Wikimedia\WaitConditionLoop;
195 if ( isset( $client[
'clientId'] ) ) {
196 $this->clientId = $client[
'clientId'];
198 $this->clientId = ( $secret !=
'' )
199 ? hash_hmac(
'md5', $client[
'ip'] .
"\n" . $client[
'agent'], $secret )
200 : md5( $client[
'ip'] .
"\n" . $client[
'agent'] );
203 $this->waitForPosIndex = $posIndex;
205 $this->clientLogInfo = [
206 'clientIP' => $client[
'ip'],
207 'clientAgent' => $client[
'agent'],
208 'clientId' => $client[
'clientId'] ?? null
211 $this->logger =
new NullLogger();
239 $this->positionWaitsEnabled =
$enabled;
256 if ( !$this->enabled || !$this->positionWaitsEnabled ) {
265 $this->logger->debug( __METHOD__ .
": $cluster ($masterName) position is '$pos'" );
268 $this->logger->debug( __METHOD__ .
": $cluster ($masterName) has no position" );
294 $this->logger->debug( __METHOD__ .
": $cluster ($masterName) position now '$pos'" );
295 $this->shutdownPositionsByMaster[$masterName] = $pos;
296 $this->shutdownTimestampsByCluster[$cluster] = $pos->asOfTime();
298 $this->logger->debug( __METHOD__ .
": $cluster ($masterName) position unknown" );
299 $this->shutdownTimestampsByCluster[$cluster] = $this->
getCurrentTime();
302 $this->logger->debug( __METHOD__ .
": $cluster ($masterName) has no replication" );
303 $this->shutdownTimestampsByCluster[$cluster] = $this->
getCurrentTime();
316 if ( !$this->enabled ) {
320 if ( !$this->shutdownTimestampsByCluster ) {
321 $this->logger->debug( __METHOD__ .
": no master positions/timestamps to save" );
326 $scopeLock = $this->store->getScopedLock( $this->key, self::LOCK_TIMEOUT, self::LOCK_TTL );
328 $ok = $this->store->set(
331 $this->store->get( $this->key ),
332 $this->shutdownPositionsByMaster,
333 $this->shutdownTimestampsByCluster,
336 self::POSITION_STORE_TTL
343 $clusterList = implode(
', ', array_keys( $this->shutdownTimestampsByCluster ) );
346 $bouncedPositions = [];
347 $this->logger->debug(
348 __METHOD__ .
": saved master positions/timestamp for DB cluster(s) $clusterList"
355 $this->logger->warning(
356 __METHOD__ .
": failed to save master positions for DB cluster(s) $clusterList"
360 return $bouncedPositions;
373 if ( !$this->enabled ) {
380 $timestamp = $timestampsByCluster[$cluster] ??
null;
381 if ( $timestamp ===
null ) {
382 $recentTouchTimestamp =
false;
383 } elseif ( ( $this->startupTimestamp - $timestamp ) > self::POSITION_COOKIE_TTL ) {
388 $recentTouchTimestamp =
false;
389 $this->logger->debug( __METHOD__ .
": old timestamp ($timestamp) for $cluster" );
391 $recentTouchTimestamp = $timestamp;
392 $this->logger->debug( __METHOD__ .
": recent timestamp ($timestamp) for $cluster" );
395 return $recentTouchTimestamp;
422 if ( $this->startupTimestamp !==
null ) {
427 $this->logger->debug(
429 ": client ID is {$this->clientId}; key is {$this->key}"
437 if ( $this->positionWaitsEnabled && $this->waitForPosIndex > 0 ) {
439 $indexReached =
null;
440 $loop =
new WaitConditionLoop(
441 function () use ( &$data, &$indexReached ) {
442 $data = $this->store->get( $this->key );
443 if ( !is_array( $data ) ) {
444 return WaitConditionLoop::CONDITION_CONTINUE;
445 } elseif ( !isset( $data[self::FLD_WRITE_INDEX] ) ) {
446 return WaitConditionLoop::CONDITION_REACHED;
448 $indexReached = max( $data[self::FLD_WRITE_INDEX], $indexReached );
450 return ( $data[self::FLD_WRITE_INDEX] >= $this->waitForPosIndex )
451 ? WaitConditionLoop::CONDITION_REACHED
452 : WaitConditionLoop::CONDITION_CONTINUE;
456 $result = $loop->invoke();
457 $waitedMs = $loop->getLastWaitTime() * 1e3;
459 if ( $result == $loop::CONDITION_REACHED ) {
460 $this->logger->debug(
461 __METHOD__ .
": expected and found position index {cpPosIndex}.",
463 'cpPosIndex' => $this->waitForPosIndex,
464 'waitTimeMs' => $waitedMs
465 ] + $this->clientLogInfo
468 $this->logger->warning(
469 __METHOD__ .
": expected but failed to find position index {cpPosIndex}.",
471 'cpPosIndex' => $this->waitForPosIndex,
472 'indexReached' => $indexReached,
473 'waitTimeMs' => $waitedMs
474 ] + $this->clientLogInfo
478 $data = $this->store->get( $this->key );
480 if ( $indexReached ) {
481 $this->logger->debug(
482 __METHOD__ .
": found position/timestamp data with index {indexReached}.",
483 [
'indexReached' => $indexReached ] + $this->clientLogInfo
503 array $shutdownPositions,
504 array $shutdownTimestamps,
510 foreach ( $shutdownPositions as $masterName => $pos ) {
512 !isset( $mergedPositions[$masterName] ) ||
513 !( $mergedPositions[$masterName] instanceof
DBMasterPos ) ||
514 $pos->asOfTime() > $mergedPositions[$masterName]->asOfTime()
516 $mergedPositions[$masterName] = $pos;
523 foreach ( $shutdownTimestamps as $cluster => $timestamp ) {
525 !isset( $mergedTimestamps[$cluster] ) ||
526 $timestamp > $mergedTimestamps[$cluster]
528 $mergedTimestamps[$cluster] = $timestamp;
535 self::FLD_POSITIONS => $mergedPositions,
536 self::FLD_TIMESTAMPS => $mergedTimestamps,
537 self::FLD_WRITE_INDEX => ++$cpIndex
547 if ( $this->wallClockOverride ) {
551 $clockTime = (float)time();
554 return max( microtime(
true ), $clockTime );
563 $this->wallClockOverride =& $time;
Class representing a cache/ephemeral data store.
makeGlobalKey( $collection,... $components)
Make a cache key for the default keyspace and given components.