diff --git a/vendor/magento/framework/Cache/Backend/RemoteSynchronizedCache.php b/vendor/magento/framework/Cache/Backend/RemoteSynchronizedCache.php index 85218242057e4..7e0bb0897c062 100644 --- a/vendor/magento/framework/Cache/Backend/RemoteSynchronizedCache.php +++ b/vendor/magento/framework/Cache/Backend/RemoteSynchronizedCache.php @@ -1,7 +1,7 @@ _options); + $this->initializeLocalBackend($universalOptions); + $this->initializeRemoteBackend($universalOptions); + $this->lockSign = $this->generateLockSign(); + } + + /** + * Check if remote backend is available. + * + * @return bool + */ + private function isRemoteAvailable(): bool + { + return $this->remote !== null; + } + + /** + * Initialize local cache backend. + * + * @param array $universalOptions + * @return void + * @throws \Zend_Cache_Exception + */ + private function initializeLocalBackend(array $universalOptions): void + { + if ($this->_options['local_backend'] === null) { + \Zend_Cache::throwException('local_backend option must be set'); + } + + if ($this->_options['local_backend'] instanceof \Zend_Cache_Backend_ExtendedInterface) { + $this->local = $this->_options['local_backend']; + return; + } + $this->local = \Zend_Cache::_makeBackend( + $this->_options['local_backend'], + array_merge($universalOptions, $this->_options['local_backend_options']), + $this->_options['local_backend_custom_naming'], + $this->_options['local_backend_autoload'] + ); + + if (!($this->local instanceof \Zend_Cache_Backend_ExtendedInterface)) { + \Zend_Cache::throwException( + 'local_backend must implement the Zend_Cache_Backend_ExtendedInterface interface' + ); + } + } + + /** + * Initialize remote cache backend with graceful degradation. + * + * @param array $universalOptions + * @return void + * @throws \Zend_Cache_Exception + */ + private function initializeRemoteBackend(array $universalOptions): void + { if ($this->_options['remote_backend'] === null) { - \Zend_Cache::throwException('remote_backend option must be set'); - } elseif ($this->_options['remote_backend'] instanceof \Zend_Cache_Backend_ExtendedInterface) { + $this->handleMissingRemoteBackend(); + return; + } + + if ($this->_options['remote_backend'] instanceof \Zend_Cache_Backend_ExtendedInterface) { $this->remote = $this->_options['remote_backend']; - } else { + return; + } + + $this->createRemoteBackend($universalOptions); + } + + /** + * Handle missing remote backend configuration. + * + * @return void + * @throws \Zend_Cache_Exception + */ + private function handleMissingRemoteBackend(): void + { + if (!$this->_options['use_stale_cache']) { + \Zend_Cache::throwException('remote_backend option must be set'); + } + $this->remote = null; + } + + /** + * Create remote backend instance with error handling. + * + * @param array $universalOptions + * @return void + * @throws \Zend_Cache_Exception|\Throwable + */ + private function createRemoteBackend(array $universalOptions): void + { + try { $this->remote = \Zend_Cache::_makeBackend( $this->_options['remote_backend'], array_merge($universalOptions, $this->_options['remote_backend_options']), $this->_options['remote_backend_custom_naming'], $this->_options['remote_backend_autoload'] ); + if (!($this->remote instanceof \Zend_Cache_Backend_ExtendedInterface)) { - \Zend_Cache::throwException( + $this->handleRemoteBackendFailure( 'remote_backend must implement the Zend_Cache_Backend_ExtendedInterface interface' ); } - } - - if ($this->_options['local_backend'] === null) { - \Zend_Cache::throwException('local_backend option must be set'); - } elseif ($this->_options['local_backend'] instanceof \Zend_Cache_Backend_ExtendedInterface) { - $this->local = $this->_options['local_backend']; - } else { - $this->local = \Zend_Cache::_makeBackend( - $this->_options['local_backend'], - array_merge($universalOptions, $this->_options['local_backend_options']), - $this->_options['local_backend_custom_naming'], - $this->_options['local_backend_autoload'] - ); - if (!($this->local instanceof \Zend_Cache_Backend_ExtendedInterface)) { - \Zend_Cache::throwException( - 'local_backend must implement the Zend_Cache_Backend_ExtendedInterface interface' - ); + } catch (\Throwable $e) { + if (!$this->_options['use_stale_cache']) { + throw $e; } + $this->remote = null; } + } - $this->lockSign = $this->generateLockSign(); + /** + * Handle remote backend initialization failure. + * + * @param string $message + * @return void + * @throws \Zend_Cache_Exception + */ + private function handleRemoteBackendFailure(string $message): void + { + if (!$this->_options['use_stale_cache']) { + \Zend_Cache::throwException($message); + } + $this->remote = null; } /** @@ -135,7 +225,9 @@ public function __construct(array $options = []) */ public function setDirectives($directives) { - $this->remote->setDirectives($directives); + if ($this->isRemoteAvailable()) { + $this->remote->setDirectives($directives); + } $this->local->setDirectives($directives); } @@ -158,6 +250,9 @@ private function getDataVersion(string $data) */ private function loadRemoteDataVersion(string $id) { + if ($this->remote === null) { + return false; + } return $this->remote->load( $id . self::HASH_SUFFIX ); @@ -174,6 +269,9 @@ private function loadRemoteDataVersion(string $id) */ private function saveRemoteDataVersion(string $data, string $id, array $tags, $specificLifetime = false) { + if ($this->remote === null) { + return false; + } return $this->remote->save($this->getDataVersion($data), $id . self::HASH_SUFFIX, $tags, $specificLifetime); } @@ -185,37 +283,152 @@ private function saveRemoteDataVersion(string $data, string $id, array $tags, $s */ private function removeRemoteDataVersion($id) { + if ($this->remote === null) { + return false; + } return $this->remote->remove($id . self::HASH_SUFFIX); } + /** + * Synchronize local data to remote cache. + * + * @param string $data + * @param string $id + * @return bool + */ + private function syncLocalToRemote(string $data, string $id): bool + { + if ($this->remote === null) { + return false; + } + try { + $remoteSaveResult = $this->remote->save($data, $id); + $remoteHashSaveResult = $this->saveRemoteDataVersion($data, $id, []); + return $remoteSaveResult && $remoteHashSaveResult; + } catch (\Throwable $e) { + return false; + } + } + /** * @inheritdoc */ public function load($id, $doNotTestCacheValidity = false) { $localData = $this->local->load($id); + $loadContext = [ + 'remoteHash' => false, + 'isRemoteAccessible' => true + ]; - if ($localData !== false) { - if ($this->getDataVersion($localData) === $this->loadRemoteDataVersion($id)) { - return $localData; - } + if ($localData !== false && $this->isLocalDataValid($id, $localData, $loadContext)) { + return $localData; } - $remoteData = $this->remote->load($id); - if ($remoteData !== false) { - $this->local->save($remoteData, $id); + $remoteResult = $this->loadFromRemote($id, $localData, $loadContext); + if ($remoteResult !== null) { + return $remoteResult; + } - return $remoteData; - } elseif ($localData && $this->_options['use_stale_cache']) { - if ($this->lock($id)) { - return false; - } else { - $this->notifyStaleCache(); - return $localData; - } + return $this->handleStaleCacheFallback($id, $localData, $loadContext['isRemoteAccessible']); + } + + /** + * Check if local data is valid by comparing with remote hash. + * + * @param string $id + * @param mixed $localData + * @param array $context + * @return bool + */ + private function isLocalDataValid(string $id, $localData, array &$context): bool + { + try { + $context['remoteHash'] = $this->loadRemoteDataVersion($id); + $localHash = $this->getDataVersion($localData); + + return $context['remoteHash'] !== false && $localHash === $context['remoteHash']; + } catch (\Throwable $e) { + $context['isRemoteAccessible'] = false; + return false; } + } - return false; + /** + * Load data from remote backend with sync logic. + * + * @param string $id + * @param mixed $localData + * @param array $context + * @return mixed|null Returns data if found, null if should continue to fallback + */ + private function loadFromRemote(string $id, $localData, array &$context) + { + if (!$context['isRemoteAccessible'] || !$this->isRemoteAvailable()) { + $context['isRemoteAccessible'] = false; + return null; + } + + try { + $remoteData = $this->remote->load($id); + } catch (\Throwable $e) { + $context['isRemoteAccessible'] = false; + return null; + } + + if ($remoteData === false) { + return null; + } + + if ($this->shouldUseLocalData($localData, $remoteData, $context['remoteHash'])) { + $this->syncLocalToRemote($localData, $id); + return $localData; + } + + $this->local->save($remoteData, $id); + return $remoteData; + } + + /** + * Determine if local data should be used instead of remote. + * + * @param mixed $localData + * @param mixed $remoteData + * @param mixed $remoteHash + * @return bool + */ + private function shouldUseLocalData($localData, $remoteData, $remoteHash): bool + { + if ($localData === false || !$this->_options['use_stale_cache']) { + return false; + } + + $localHash = $this->getDataVersion($localData); + $remoteDataHash = $this->getDataVersion($remoteData); + + return $remoteHash === false || $localHash !== $remoteDataHash; + } + + /** + * Handle stale cache fallback when remote is unavailable. + * + * @param string $id + * @param mixed $localData + * @param bool $isRemoteAccessible + * @return mixed + */ + private function handleStaleCacheFallback(string $id, $localData, bool $isRemoteAccessible) + { + if (!$localData || !$this->_options['use_stale_cache']) { + return false; + } + + if ($isRemoteAccessible && $this->lock($id)) { + return false; + } + + $this->notifyStaleCache(); + return $localData; } /** @@ -223,9 +436,14 @@ public function load($id, $doNotTestCacheValidity = false) */ public function test($id) { - return $this->_options['use_stale_cache'] ? - ($this->local->test($id) ?? $this->remote->test($id)) - : $this->remote->test($id); + if ($this->_options['use_stale_cache']) { + $localTest = $this->local->test($id); + if ($localTest !== false) { + return $localTest; + } + return $this->isRemoteAvailable() ? $this->remote->test($id) : false; + } + return $this->isRemoteAvailable() ? $this->remote->test($id) : false; } /** @@ -233,22 +451,98 @@ public function test($id) */ public function save($data, $id, $tags = [], $specificLifetime = false) { - $dataToSave = $data; - $remHash = $this->loadRemoteDataVersion($id); - $isRemoteUpToDate = false; - if ($remHash !== false && $this->getDataVersion($data) === $remHash) { + $saveContext = $this->checkRemoteCache($data, $id); + $dataToSave = $saveContext['dataToSave']; + $remoteUpdateFailed = $saveContext['remoteFailed']; + + if (!$saveContext['isUpToDate'] && !$remoteUpdateFailed) { + $remoteUpdateFailed = !$this->saveToRemote($data, $id, $tags, $specificLifetime); + } + + $this->handlePostSaveCleanup($id, $remoteUpdateFailed); + + // Local cache doesn't save tags intentionally since it will cause inconsistency after flushing the cache + // in multinode environment + return $this->local->save($dataToSave, $id, [], $specificLifetime); + } + + /** + * Check if remote cache is up to date. + * + * @param mixed $data + * @param string $id + * @return array + */ + private function checkRemoteCache($data, string $id): array + { + $result = [ + 'isUpToDate' => false, + 'dataToSave' => $data, + 'remoteFailed' => false + ]; + + if (!$this->isRemoteAvailable()) { + $result['remoteFailed'] = true; + return $result; + } + + try { + $remHash = $this->loadRemoteDataVersion($id); + if ($remHash === false || $this->getDataVersion($data) !== $remHash) { + return $result; + } + $remoteData = $this->remote->load($id); if ($remoteData !== false && $this->getDataVersion($data) === $this->getDataVersion($remoteData)) { - $isRemoteUpToDate = true; - $dataToSave = $remoteData; + $result['isUpToDate'] = true; + $result['dataToSave'] = $remoteData; } + } catch (\Throwable $e) { + $result['remoteFailed'] = true; + } + + return $result; + } + + /** + * Save data to remote backend. + * + * @param mixed $data + * @param string $id + * @param array $tags + * @param mixed $specificLifetime + * @return bool + */ + private function saveToRemote($data, string $id, array $tags, $specificLifetime): bool + { + if (!$this->isRemoteAvailable()) { + return false; } - if (!$isRemoteUpToDate) { - $this->remote->save($data, $id, $tags, $specificLifetime); - $this->saveRemoteDataVersion($data, $id, $tags, $specificLifetime); + + try { + $remoteSaveResult = $this->remote->save($data, $id, $tags, $specificLifetime); + + if ($remoteSaveResult === false) { + return false; + } + + $remoteHashSaveResult = $this->saveRemoteDataVersion($data, $id, $tags, $specificLifetime); + return $remoteHashSaveResult !== false; + } catch (\Throwable $e) { + return false; } + } - if ($this->_options['use_stale_cache']) { + /** + * Handle post-save cleanup operations. + * + * @param string $id + * @param bool $remoteUpdateFailed + * @return void + */ + private function handlePostSaveCleanup(string $id, bool $remoteUpdateFailed): void + { + if ($this->_options['use_stale_cache'] && !$remoteUpdateFailed) { $this->unlock($id); } @@ -257,10 +551,6 @@ public function save($data, $id, $tags = [], $specificLifetime = false) if (!mt_rand(0, 100) && $this->checkIfLocalCacheSpaceExceeded()) { $this->local->clean(); } - - // Local cache doesn't save tags intentionally since it will cause inconsistency after flushing the cache - // in multinode environment - return $this->local->save($dataToSave, $id, [], $specificLifetime); } /** @@ -278,10 +568,21 @@ private function checkIfLocalCacheSpaceExceeded() */ public function remove($id) { - $result = $this->removeRemoteDataVersion($id) && $this->remote->remove($id); + $result = false; + if ($this->isRemoteAvailable()) { + try { + $result = $this->removeRemoteDataVersion($id) && $this->remote->remove($id); + } catch (\Throwable $e) { + $result = false; + } + } + if ($result && !$this->_options['use_stale_cache']) { $result = $this->local->remove($id); + } elseif (!$result && $this->_options['use_stale_cache']) { + $this->local->remove($id); } + return $result; } @@ -290,8 +591,18 @@ public function remove($id) */ public function clean($mode = \Zend_Cache::CLEANING_MODE_ALL, $tags = []) { - return $this->remote->clean($mode, $tags) && - $this->local->clean($mode); + $result = false; + if ($this->isRemoteAvailable()) { + try { + $result = $this->remote->clean($mode, $tags); + } catch (\Throwable $e) { + $result = false; + } + } + + $localResult = $this->local->clean($mode); + + return $result && $localResult; } /** @@ -299,7 +610,10 @@ public function clean($mode = \Zend_Cache::CLEANING_MODE_ALL, $tags = []) */ public function getIds() { - return $this->remote->getIds(); + if ($this->isRemoteAvailable()) { + return $this->remote->getIds(); + } + return $this->_options['use_stale_cache'] ? $this->local->getIds() : []; } /** @@ -307,7 +621,10 @@ public function getIds() */ public function getTags() { - return $this->remote->getTags(); + if ($this->isRemoteAvailable()) { + return $this->remote->getTags(); + } + return $this->_options['use_stale_cache'] ? $this->local->getTags() : []; } /** @@ -315,7 +632,10 @@ public function getTags() */ public function getIdsMatchingTags($tags = []) { - return $this->remote->getIdsMatchingTags($tags); + if ($this->isRemoteAvailable()) { + return $this->remote->getIdsMatchingTags($tags); + } + return $this->_options['use_stale_cache'] ? $this->local->getIdsMatchingTags($tags) : []; } /** @@ -323,7 +643,10 @@ public function getIdsMatchingTags($tags = []) */ public function getIdsNotMatchingTags($tags = []) { - return $this->remote->getIdsNotMatchingTags($tags); + if ($this->isRemoteAvailable()) { + return $this->remote->getIdsNotMatchingTags($tags); + } + return $this->_options['use_stale_cache'] ? $this->local->getIdsNotMatchingTags($tags) : []; } /** @@ -331,7 +654,10 @@ public function getIdsNotMatchingTags($tags = []) */ public function getIdsMatchingAnyTags($tags = []) { - return $this->remote->getIdsMatchingAnyTags($tags); + if ($this->isRemoteAvailable()) { + return $this->remote->getIdsMatchingAnyTags($tags); + } + return $this->_options['use_stale_cache'] ? $this->local->getIdsMatchingAnyTags($tags) : []; } /** @@ -339,7 +665,10 @@ public function getIdsMatchingAnyTags($tags = []) */ public function getFillingPercentage() { - return $this->remote->getFillingPercentage(); + if ($this->isRemoteAvailable()) { + return $this->remote->getFillingPercentage(); + } + return $this->_options['use_stale_cache'] ? $this->local->getFillingPercentage() : 0; } /** @@ -347,7 +676,10 @@ public function getFillingPercentage() */ public function getMetadatas($id) { - return $this->remote->getMetadatas($id); + if ($this->isRemoteAvailable()) { + return $this->remote->getMetadatas($id); + } + return $this->_options['use_stale_cache'] ? $this->local->getMetadatas($id) : false; } /** @@ -355,7 +687,10 @@ public function getMetadatas($id) */ public function touch($id, $extraLifetime) { - return $this->remote->touch($id, $extraLifetime); + if ($this->isRemoteAvailable()) { + return $this->remote->touch($id, $extraLifetime); + } + return $this->_options['use_stale_cache'] ? $this->local->touch($id, $extraLifetime) : false; } /** @@ -363,7 +698,11 @@ public function touch($id, $extraLifetime) */ public function getCapabilities() { - return $this->remote->getCapabilities(); + if ($this->isRemoteAvailable()) { + return $this->remote->getCapabilities(); + } + + return $this->local->getCapabilities(); } /** @@ -374,6 +713,10 @@ public function getCapabilities() */ private function lock(string $id): bool { + if (!$this->isRemoteAvailable()) { + return false; + } + $this->lockList[$id] = microtime(true); $data = $this->remote->load($this->getLockName($id)); @@ -405,6 +748,10 @@ private function unlock(string $id): bool unset($this->lockList[$id]); } + if (!$this->isRemoteAvailable()) { + return false; + } + $data = $this->remote->load($this->getLockName($id)); if (false === $data) { @@ -480,8 +827,13 @@ private function generateLockSign() */ private function notifyStaleCache(): void { - $this->notifier = $this->notifier ?? - ObjectManager::getInstance()->get(CompositeStaleCacheNotifier::class); + if ($this->notifier === null) { + try { + $this->notifier = ObjectManager::getInstance()->get(CompositeStaleCacheNotifier::class); + } catch (\Throwable $e) { + return; + } + } $this->notifier->cacheLoaderIsUsingStaleCache(); } }