From d8177f2600d915a2228d6db6f19bf7be7c98ca98 Mon Sep 17 00:00:00 2001 From: Markus Klein <markus.klein@typo3.org> Date: Tue, 3 Sep 2024 13:52:05 +0200 Subject: [PATCH] [FEATURE] Allow key prefix for Redis keys MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the possibility to configure Redis key prefixes for cache and session backends, which allows to use the same Redis database for multiple caches and/or TYPO3 instances. Resolves: #104451 Releases: main Change-Id: Id6aa2f7ba9a4a0dbfb2335150602aeb836e09b90 Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/85324 Tested-by: Benni Mack <benni@typo3.org> Tested-by: core-ci <typo3@b13.com> Tested-by: Stefan Bürk <stefan@buerk.tech> Reviewed-by: Benni Mack <benni@typo3.org> Reviewed-by: Stefan Bürk <stefan@buerk.tech> --- .../Classes/Cache/Backend/RedisBackend.php | 29 ++++-- .../Session/Backend/RedisSessionBackend.php | 2 +- ...51-RedisBackendsSupportForKeyPrefixing.rst | 74 +++++++++++++++ .../Cache/Backend/RedisBackendTest.php | 90 +++++++++++++++++++ 4 files changed, 188 insertions(+), 7 deletions(-) create mode 100644 typo3/sysext/core/Documentation/Changelog/13.3/Feature-104451-RedisBackendsSupportForKeyPrefixing.rst diff --git a/typo3/sysext/core/Classes/Cache/Backend/RedisBackend.php b/typo3/sysext/core/Classes/Cache/Backend/RedisBackend.php index 038101957393..865e0c16a2c7 100644 --- a/typo3/sysext/core/Classes/Cache/Backend/RedisBackend.php +++ b/typo3/sysext/core/Classes/Cache/Backend/RedisBackend.php @@ -132,6 +132,11 @@ class RedisBackend extends AbstractBackend implements TaggableBackendInterface */ protected $connectionTimeout = 0; + /** + * Used as prefix for all Redis keys/identifiers + */ + protected string $keyPrefix = ''; + /** * Construct this backend * @@ -292,6 +297,11 @@ class RedisBackend extends AbstractBackend implements TaggableBackendInterface $this->connectionTimeout = $connectionTimeout; } + protected function setKeyPrefix(string $keyPrefix): void + { + $this->keyPrefix = $keyPrefix; + } + /** * Save data in the cache * @@ -445,14 +455,21 @@ class RedisBackend extends AbstractBackend implements TaggableBackendInterface /** * Removes all cache entries of this cache. - * - * Scales O(1) with number of cache entries */ public function flush(): void { - if ($this->connected) { + if (!$this->connected) { + return; + } + // unless we have a key prefix all data can be flushed + if ($this->keyPrefix === '') { $this->redis->flushDB(); + return; } + $keys = $this->redis->keys($this->keyPrefix . '*'); + $queue = $this->redis->multi(); + $queue->del($keys); + $queue->exec(); } /** @@ -556,16 +573,16 @@ class RedisBackend extends AbstractBackend implements TaggableBackendInterface protected function getDataIdentifier(string $identifier): string { - return self::IDENTIFIER_DATA_PREFIX . $identifier; + return $this->keyPrefix . self::IDENTIFIER_DATA_PREFIX . $identifier; } protected function getTagsIdentifier(string $identifier): string { - return self::IDENTIFIER_TAGS_PREFIX . $identifier; + return $this->keyPrefix . self::IDENTIFIER_TAGS_PREFIX . $identifier; } protected function getTagIdentifier(string $tag): string { - return self::TAG_IDENTIFIERS_PREFIX . $tag; + return $this->keyPrefix . self::TAG_IDENTIFIERS_PREFIX . $tag; } } diff --git a/typo3/sysext/core/Classes/Session/Backend/RedisSessionBackend.php b/typo3/sysext/core/Classes/Session/Backend/RedisSessionBackend.php index 009fb02910c0..df34d024315d 100644 --- a/typo3/sysext/core/Classes/Session/Backend/RedisSessionBackend.php +++ b/typo3/sysext/core/Classes/Session/Backend/RedisSessionBackend.php @@ -60,7 +60,7 @@ class RedisSessionBackend implements SessionBackendInterface, HashableSessionBac $this->configuration = $configuration; $this->identifier = $identifier; - $this->applicationIdentifier = 'typo3_ses_' + $this->applicationIdentifier = ($configuration['keyPrefix'] ?? '') . 'typo3_ses_' . $identifier . '_' . sha1($GLOBALS['TYPO3_CONF_VARS']['SYS']['encryptionKey']) . '_'; } diff --git a/typo3/sysext/core/Documentation/Changelog/13.3/Feature-104451-RedisBackendsSupportForKeyPrefixing.rst b/typo3/sysext/core/Documentation/Changelog/13.3/Feature-104451-RedisBackendsSupportForKeyPrefixing.rst new file mode 100644 index 000000000000..7de513430fa1 --- /dev/null +++ b/typo3/sysext/core/Documentation/Changelog/13.3/Feature-104451-RedisBackendsSupportForKeyPrefixing.rst @@ -0,0 +1,74 @@ +.. include:: /Includes.rst.txt + +.. _feature-104451-1721646565: + +=========================================================== +Feature: #104451 - Redis backends support for key prefixing +=========================================================== + +See :issue:`104451` + +Description +=========== + +It is now possible to add a dedicated key prefix for all invocations of a Redis +cache or session backend. This allows to use the same Redis database for multiple +caches or even for multiple TYPO3 instances if the provided prefix is unique. + +Possible use cases are: + +* Using Redis caching for multiple caches, if only one Redis database is available +* Pre-fill caches upon deployments using a new prefix (zero downtime deployments) + +.. code-block:: php + :caption: additional.php example for using Redis as session backend + + $GLOBALS['TYPO3_CONF_VARS']['SYS']['session']['BE'] = [ + 'backend' => \TYPO3\CMS\Core\Session\Backend\RedisSessionBackend::class, + 'options' => [ + 'hostname' => 'redis', + 'database' => '11', + 'compression' => true, + 'keyPrefix' => 'be_sessions_', + ], + ]; + $GLOBALS['TYPO3_CONF_VARS']['SYS']['session']['FE'] = [ + 'backend' => \TYPO3\CMS\Core\Session\Backend\RedisSessionBackend::class, + 'options' => [ + 'hostname' => 'redis', + 'database' => '11', + 'compression' => true, + 'keyPrefix' => 'fe_sessions_', + 'has_anonymous' => true, + ], + ]; + +.. code-block:: php + :caption: additional.php example for pages cache + + $GLOBALS['TYPO3_CONF_VARS']['SYS']['caching']['cacheConfigurations']['pages']['backend'] = \TYPO3\CMS\Core\Cache\Backend\RedisBackend::class; + $GLOBALS['TYPO3_CONF_VARS']['SYS']['caching']['cacheConfigurations']['pages']['options']['hostname'] = 'redis'; + $GLOBALS['TYPO3_CONF_VARS']['SYS']['caching']['cacheConfigurations']['pages']['options']['database'] = 11; + $GLOBALS['TYPO3_CONF_VARS']['SYS']['caching']['cacheConfigurations']['pages']['options']['compression'] = true; + $GLOBALS['TYPO3_CONF_VARS']['SYS']['caching']['cacheConfigurations']['pages']['options']['keyPrefix'] = 'pages_'; + + $GLOBALS['TYPO3_CONF_VARS']['SYS']['caching']['cacheConfigurations']['rootline']['backend'] = \TYPO3\CMS\Core\Cache\Backend\RedisBackend::class; + $GLOBALS['TYPO3_CONF_VARS']['SYS']['caching']['cacheConfigurations']['rootline']['options']['hostname'] = 'redis'; + $GLOBALS['TYPO3_CONF_VARS']['SYS']['caching']['cacheConfigurations']['rootline']['options']['database'] = 11; + $GLOBALS['TYPO3_CONF_VARS']['SYS']['caching']['cacheConfigurations']['rootline']['options']['compression'] = true; + $GLOBALS['TYPO3_CONF_VARS']['SYS']['caching']['cacheConfigurations']['rootline']['options']['keyPrefix'] = 'rootline_'; + +Impact +====== + +The new feature allows to use the same Redis database for multiple caches or even +for multiple TYPO3 instances while having no impact on existing configuration. + +.. attention:: + If you start using the same Redis database for multiple caches or + using the same database also for session storage, make sure any involved + cache configuration uses **a unique key prefix**. + If only one of the caches does not use a key prefix, any cache flush + operation will always flush the whole database, hence also all other caches/sessions. + +.. index:: Frontend, LocalConfiguration, ext:core diff --git a/typo3/sysext/core/Tests/Functional/Cache/Backend/RedisBackendTest.php b/typo3/sysext/core/Tests/Functional/Cache/Backend/RedisBackendTest.php index 244473fb9aad..72cf6aee2edf 100644 --- a/typo3/sysext/core/Tests/Functional/Cache/Backend/RedisBackendTest.php +++ b/typo3/sysext/core/Tests/Functional/Cache/Backend/RedisBackendTest.php @@ -217,6 +217,96 @@ final class RedisBackendTest extends FunctionalTestCase self::assertSame($lifetime, $lifetimeRegisteredInBackend); } + #[Test] + public function setSavesEntryWithSpecifiedKeyPrefix(): void + { + $firstBackend = $this->setUpSubject(['keyPrefix' => 'kp1_']); + $secondBackend = $this->setUpSubject(['keyPrefix' => 'kp2_']); + $redis = $this->setUpRedis(); + + $identifier = StringUtility::getUniqueId('identifier'); + $data = 'data'; + + $firstBackend->set($identifier, $data); + self::assertSame($data, $firstBackend->get($identifier)); + self::assertSame($data, $redis->get(sprintf('kp1_identData:%s', $identifier))); + self::assertFalse($redis->get(sprintf('kp2_identData:%s', $identifier))); + self::assertFalse($secondBackend->get($identifier)); + } + + #[Test] + public function flushOnPrefixedBackendDoesNotDeleteKeysOfSecondPrefixedBackend(): void + { + $firstBackend = $this->setUpSubject(['keyPrefix' => 'kp1_']); + $secondBackend = $this->setUpSubject(['keyPrefix' => 'kp2_']); + $redis = $this->setUpRedis(); + $redis->flushAll(); + + $identifier = StringUtility::getUniqueId('identifier'); + $data = 'data'; + + $firstBackend->set($identifier, $data); + $secondBackend->set($identifier, $data); + self::assertSame($data, $firstBackend->get($identifier)); + self::assertSame($data, $secondBackend->get($identifier)); + self::assertSame($data, $redis->get(sprintf('kp1_identData:%s', $identifier))); + self::assertSame($data, $redis->get(sprintf('kp2_identData:%s', $identifier))); + + $firstBackend->flush(); + self::assertFalse($firstBackend->get($identifier)); + self::assertFalse($redis->get(sprintf('kp1_identData:%s', $identifier))); + self::assertSame($data, $secondBackend->get($identifier)); + self::assertSame($data, $redis->get(sprintf('kp2_identData:%s', $identifier))); + } + + #[Test] + public function flushByTagOnPrefixedBackendDoesNotDeleteKeysOfSecondPrefixedBackend(): void + { + $firstBackend = $this->setUpSubject(['keyPrefix' => 'kp1_']); + $secondBackend = $this->setUpSubject(['keyPrefix' => 'kp2_']); + $redis = $this->setUpRedis(); + $redis->flushAll(); + + $identifier = StringUtility::getUniqueId('identifier'); + $tagName = 'some-tag'; + $data = 'data'; + + $firstBackend->set($identifier, $data, [$tagName]); + $secondBackend->set($identifier, $data, [$tagName]); + self::assertSame($data, $firstBackend->get($identifier)); + self::assertSame($data, $secondBackend->get($identifier)); + self::assertSame($data, $redis->get(sprintf('kp1_identData:%s', $identifier))); + self::assertSame($data, $redis->get(sprintf('kp2_identData:%s', $identifier))); + + $firstBackend->flushByTag($tagName); + self::assertFalse($firstBackend->get($identifier)); + self::assertFalse($redis->get(sprintf('kp1_identData:%s', $identifier))); + self::assertSame($data, $secondBackend->get($identifier)); + self::assertSame($data, $redis->get(sprintf('kp2_identData:%s', $identifier))); + } + + #[Test] + public function flushByTagsOnPrefixedBackendDoesNotDeleteKeysOfSecondPrefixedBackend(): void + { + $firstBackend = $this->setUpSubject(['keyPrefix' => 'kp1_']); + $secondBackend = $this->setUpSubject(['keyPrefix' => 'kp2_']); + $redis = $this->setUpRedis(); + $redis->flushAll(); + + $identifier = StringUtility::getUniqueId('identifier'); + $tagName = 'some-tag'; + $data = 'data'; + + $firstBackend->set($identifier, $data, [$tagName]); + $secondBackend->set($identifier, $data, [$tagName]); + self::assertSame($data, $firstBackend->get($identifier)); + self::assertSame($data, $secondBackend->get($identifier)); + + $firstBackend->flushByTags([$tagName]); + self::assertFalse($firstBackend->get($identifier)); + self::assertSame($data, $secondBackend->get($identifier)); + } + #[Test] public function setSavesEntryWithUnlimitedLifeTime(): void { -- GitLab