File: /dom877180/wp-content/mu-plugins/object-cache-pro/src/ObjectCaches/Concerns/KeepsMetadata.php
<?php
/**
* Copyright © 2019-2026 Rhubarb Tech Inc. All Rights Reserved.
*
* The Object Cache Pro Software and its related materials are property and confidential
* information of Rhubarb Tech Inc. Any reproduction, use, distribution, or exploitation
* of the Object Cache Pro Software and its related materials, in whole or in part,
* is strictly forbidden unless prior permission is obtained from Rhubarb Tech Inc.
*
* In addition, any reproduction, use, distribution, or exploitation of the Object Cache Pro
* Software and its related materials, in whole or in part, is subject to the End-User License
* Agreement accessible in the included `LICENSE` file, or at: https://objectcache.pro/eula
*/
declare(strict_types=1);
namespace RedisCachePro\ObjectCaches\Concerns;
use Throwable;
use RedisCachePro\Connections\RelayConnection;
use RedisCachePro\Exceptions\MetadataException;
use RedisCachePro\Connectors\Concerns\HandlesBackoff;
use function RedisCachePro\log;
/**
* Keeps track of object cache metadata, such as the used configuration.
*
* If risky configuration options have changed and the `strict` mode is
* enabled, the cache will be automatically flushed to avoid collisions.
*/
trait KeepsMetadata
{
use HandlesBackoff;
/**
* The stored object cache metadata.
*
* @var ?array<string, array<string, mixed>>
*/
private $metadata;
/**
* Boots the metadata component.
*
* @return void
*/
protected function bootMetadata(): void
{
try {
$this->loadMetadata();
$this->throwIfRiskyConfigurationChanged();
} catch (MetadataException $exception) {
$this->integrityProtectionFlush($exception);
} catch (Throwable $exception) {
throw $exception;
}
$this->maybeFlushRelayMemory();
$this->maybeResetRelayRatiosMemory();
$this->maybeUpdateMetadata();
}
/**
* Loads the metadata from the cache.
*
* @return void
*/
private function loadMetadata()
{
$json = $this->retrieveMetadataWithRetries();
if (! is_string($json)) {
throw new MetadataException(
'Cache metadata not found',
MetadataException::NOT_FOUND
);
}
$metadata = json_decode($json, true);
if (! is_array($metadata)) {
throw new MetadataException(sprintf(
'Unable to decode cache metadata (%s)',
(json_last_error() !== JSON_ERROR_NONE)
? json_last_error_msg()
: gettype($metadata) . ' found'
), MetadataException::DECODE_FAILED);
}
$this->metadata = $metadata;
}
/**
* Returns the stored metadata from the cache.
*
* @return string
*/
private function retrieveMetadataWithRetries()
{
$retries = 3;
$attempt = $delay = 0;
while (true) {
$delay = self::nextDelay($this->config(), $attempt, $delay);
try {
return $this->withoutMutations([$this, 'getMetadata']);
} catch (Throwable $exception) {
if (++$attempt >= $retries) {
throw $exception;
}
\usleep($delay * 1000);
}
}
}
/**
* Saves the current metadata to the cache.
*
* @return void
*/
public function writeMetadata()
{
$this->metadata = $this->buildMetadata();
$this->withoutMutations([$this, 'setMetadata']);
}
/**
* Build the metadata based on the current configuration.
*
* @return array<string, array<string, mixed>>
*/
private function buildMetadata(): array
{
global $wp_version;
return [
'config' => [
'client' => strtolower($this->clientName()),
'database' => $this->config->database,
'prefix' => $this->config->prefix,
'serializer' => $this->config->serializer,
'compression' => $this->config->compression,
'prefetch' => $this->config->prefetch,
'split_alloptions' => $this->config->split_alloptions,
],
'versions' => [
'wordpress' => $wp_version,
],
'relay' => [
'flushed_at' => $this->metadata('relay.flushed_at'),
'ratios_reset_at' => $this->metadata('relay.ratios_reset_at'),
],
];
}
/**
* Flushes the Relay memory for the current FPM pool if needed.
*
* @return void
*/
private function maybeFlushRelayMemory()
{
if (! ($this->connection instanceof RelayConnection)) {
return;
}
$flushRequestedAt = $this->metadata('relay.flushed_at');
if (! $flushRequestedAt) {
return;
}
$this->connection->maybeFlushRelayMemory($flushRequestedAt, $this->config->database);
}
/**
* Resets the Relay ratios memory for the current FPM pool if needed.
*
* @return void
*/
private function maybeResetRelayRatiosMemory()
{
if (! ($this->connection instanceof RelayConnection)) {
return;
}
$resetRequestedAt = $this->metadata('relay.ratios_reset_at');
if (! $resetRequestedAt) {
return;
}
$resetAt = $this->connection->adaptiveCache()->lastFlush();
if ($resetAt < $resetRequestedAt) {
$this->connection->adaptiveCache()->flush();
}
}
/**
* Resets the Relay ratios memory for the current FPM pool if needed.
*
* @return void
*/
private function resetRelayRatios()
{
if (! ($this->connection instanceof RelayConnection)) {
return;
}
$this->connection->adaptiveCache()->flush();
}
/**
* Throws an exception if a risky configuration option has changed.
*
* @return void
*/
private function throwIfRiskyConfigurationChanged()
{
global $wp_version;
$storedConfig = $this->metadata['config'] ?? [];
$currentConfig = $this->buildMetadata()['config'];
$riskyOptions = [
'database', // avoid loading foreign dataset
'prefix', // avoid loading foreign dataset
'split_alloptions', // avoid loading stale `alloptions` data
'serializer', // mixing serializers will cause fatal errors
'compression', // mixing data compressions will cause fatal errors
];
foreach ($riskyOptions as $option) {
if (! array_key_exists($option, $storedConfig) || $storedConfig[$option] !== $currentConfig[$option]) {
throw MetadataException::for($option);
}
}
// Relay dataset could be stale
if (! array_key_exists('client', $storedConfig) || strcasecmp(
(string) ($storedConfig['client'] ?? ''),
(string) ($currentConfig['client'] ?? '')
) !== 0) {
throw MetadataException::for('client');
}
$storedVersion = $this->metadata('versions.wordpress') ?? '0';
if (version_compare((string) $wp_version, $storedVersion, '<>')) {
throw new MetadataException(
'WordPress version has changed',
MetadataException::VERSION_WORDPRESS
);
}
}
/**
* Updates the object cache metadata, if it has changed.
*
* @return void
*/
private function maybeUpdateMetadata()
{
$metadata = $this->buildMetadata();
$configChanges = array_diff_assoc(
$metadata['config'],
$this->metadata['config'] ?? []
);
$versionChanges = array_diff_assoc(
$metadata['versions'],
$this->metadata['versions'] ?? []
);
if (! empty($configChanges) || ! empty($versionChanges)) {
$this->writeMetadata();
}
}
/**
* Flushes the object cache for integrity protection, if `strict` mode is enabled.
*
* @param \RedisCachePro\Exceptions\MetadataException $exception
* @return bool
*/
private function integrityProtectionFlush(MetadataException $exception)
{
global $wp_object_cache_flushlog;
$this->metadata = null;
$message = $exception->getMessage();
if (! $this->config->strict) {
log('notice', "{$message}, skipping integrity protection flush because `strict` mode is disabled");
return false;
}
log('notice', "{$message}, flushing cache for integrity protection...");
$wp_object_cache_flushlog[] = [
'type' => 'flush',
'reason' => $message,
'backtrace' => \debug_backtrace(\DEBUG_BACKTRACE_IGNORE_ARGS, 5),
];
try {
$this->flush_runtime();
return $this->connection->flushdb();
} catch (Throwable $exception) {
$this->error($exception);
return false;
} finally {
$this->metrics->flush();
if ($exception->getCode() === MetadataException::PREFIX_CHANGED) {
$this->resetRelayRatios();
}
}
}
/**
* Returns or set metadata paths.
*
* @param string $path
* @param mixed $value
* @return mixed
*/
public function metadata($path, $value = null)
{
if (strpos($path, '.') === false) {
if (! is_null($value)) {
$this->metadata[$path] = $value;
$this->writeMetadata();
}
return $this->metadata[$path] ?? null;
}
[$group, $key] = explode('.', $path);
if (! is_null($value)) {
$this->metadata[$group][$key] = $value;
$this->writeMetadata();
}
return $this->metadata[$group][$key] ?? null;
}
/**
* Internal callback for `loadMetadata()`, improves Query Monitor readability.
*
* @internal
* @return string|false
*/
public function getMetadata()
{
return $this->get('meta', 'objectcache');
}
/**
* Internal callback for `writeMetadata()`, improves Query Monitor readability.
*
* Ignores `maxttl` configuration option.
*
* @internal
* @return void
*/
public function setMetadata()
{
$this->connection->set(
(string) $this->id('meta', 'objectcache'),
json_encode($this->metadata)
);
}
}