Thanks to visit codestin.com
Credit goes to github.com

Skip to content

[Cache] Add support for valkey: / valkeys: schemes #59869

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Mar 5, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,7 @@ protected function loadCacheDriver(string $cacheName, string $objectManagerName,
$cacheDef->addMethodCall('setMemcached', [new Reference($this->getObjectManagerElementName(\sprintf('%s_memcached_instance', $objectManagerName)))]);
break;
case 'redis':
case 'valkey':
$redisClass = !empty($cacheDriver['class']) ? $cacheDriver['class'] : '%'.$this->getObjectManagerElementName('cache.redis.class').'%';
$redisInstanceClass = !empty($cacheDriver['instance_class']) ? $cacheDriver['instance_class'] : '%'.$this->getObjectManagerElementName('cache.redis_instance.class').'%';
$redisHost = !empty($cacheDriver['host']) ? $cacheDriver['host'] : '%'.$this->getObjectManagerElementName('cache.redis_host').'%';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1285,6 +1285,7 @@ private function addCacheSection(ArrayNodeDefinition $rootNode, callable $willBe
->scalarNode('directory')->defaultValue('%kernel.cache_dir%/pools/app')->end()
->scalarNode('default_psr6_provider')->end()
->scalarNode('default_redis_provider')->defaultValue('redis://localhost')->end()
->scalarNode('default_valkey_provider')->defaultValue('valkey://localhost')->end()
->scalarNode('default_memcached_provider')->defaultValue('memcached://localhost')->end()
->scalarNode('default_doctrine_dbal_provider')->defaultValue('database_connection')->end()
->scalarNode('default_pdo_provider')->defaultValue($willBeAvailable('doctrine/dbal', Connection::class) && class_exists(DoctrineAdapter::class) ? 'database_connection' : null)->end()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2501,7 +2501,7 @@ private function registerCacheConfiguration(array $config, ContainerBuilder $con
// Inline any env vars referenced in the parameter
$container->setParameter('cache.prefix.seed', $container->resolveEnvPlaceholders($container->getParameter('cache.prefix.seed'), true));
}
foreach (['psr6', 'redis', 'memcached', 'doctrine_dbal', 'pdo'] as $name) {
foreach (['psr6', 'redis', 'valkey', 'memcached', 'doctrine_dbal', 'pdo'] as $name) {
if (isset($config[$name = 'default_'.$name.'_provider'])) {
$container->setAlias('cache.'.$name, new Alias(CachePoolPass::getServiceProvider($container, $config[$name]), false));
}
Expand All @@ -2513,12 +2513,13 @@ private function registerCacheConfiguration(array $config, ContainerBuilder $con
'tags' => false,
];
}
$redisTagAwareAdapters = [['cache.adapter.redis_tag_aware'], ['cache.adapter.valkey_tag_aware']];
foreach ($config['pools'] as $name => $pool) {
$pool['adapters'] = $pool['adapters'] ?: ['cache.app'];

$isRedisTagAware = ['cache.adapter.redis_tag_aware'] === $pool['adapters'];
$isRedisTagAware = \in_array($pool['adapters'], $redisTagAwareAdapters, true);
foreach ($pool['adapters'] as $provider => $adapter) {
if (($config['pools'][$adapter]['adapters'] ?? null) === ['cache.adapter.redis_tag_aware']) {
if (\in_array($config['pools'][$adapter]['adapters'] ?? null, $redisTagAwareAdapters, true)) {
$isRedisTagAware = true;
} elseif ($config['pools'][$adapter]['tags'] ?? false) {
$pool['adapters'][$provider] = $adapter = '.'.$adapter.'.inner';
Expand Down
2 changes: 2 additions & 0 deletions src/Symfony/Bundle/FrameworkBundle/Resources/config/cache.php
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@
'reset' => 'reset',
])
->tag('monolog.logger', ['channel' => 'cache'])
->alias('cache.adapter.valkey', 'cache.adapter.redis')

->set('cache.adapter.redis_tag_aware', RedisTagAwareAdapter::class)
->abstract()
Expand All @@ -156,6 +157,7 @@
'reset' => 'reset',
])
->tag('monolog.logger', ['channel' => 'cache'])
->alias('cache.adapter.valkey_tag_aware', 'cache.adapter.redis_tag_aware')

->set('cache.adapter.memcached', MemcachedAdapter::class)
->abstract()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -883,6 +883,7 @@ protected static function getBundleDefaultConfig()
'system' => 'cache.adapter.system',
'directory' => '%kernel.cache_dir%/pools/app',
'default_redis_provider' => 'redis://localhost',
'default_valkey_provider' => 'valkey://localhost',
'default_memcached_provider' => 'memcached://localhost',
'default_doctrine_dbal_provider' => 'database_connection',
'default_pdo_provider' => ContainerBuilder::willBeAvailable('doctrine/dbal', Connection::class, ['symfony/framework-bundle']) && class_exists(DoctrineAdapter::class) ? 'database_connection' : null,
Expand Down
4 changes: 2 additions & 2 deletions src/Symfony/Component/Cache/Adapter/AbstractAdapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ public static function createSystemCache(string $namespace, int $defaultLifetime

public static function createConnection(#[\SensitiveParameter] string $dsn, array $options = []): mixed
{
if (str_starts_with($dsn, 'redis:') || str_starts_with($dsn, 'rediss:')) {
if (str_starts_with($dsn, 'redis:') || str_starts_with($dsn, 'rediss:') || str_starts_with($dsn, 'valkey:') || str_starts_with($dsn, 'valkeys:')) {
return RedisAdapter::createConnection($dsn, $options);
}
if (str_starts_with($dsn, 'memcached:')) {
Expand All @@ -128,7 +128,7 @@ public static function createConnection(#[\SensitiveParameter] string $dsn, arra
return PdoAdapter::createConnection($dsn, $options);
}

throw new InvalidArgumentException('Unsupported DSN: it does not start with "redis[s]:", "memcached:", "couchbase:", "mysql:", "oci:", "pgsql:", "sqlsrv:" nor "sqlite:".');
throw new InvalidArgumentException('Unsupported DSN: it does not start with "redis[s]:", "valkey[s]:", "memcached:", "couchbase:", "mysql:", "oci:", "pgsql:", "sqlsrv:" nor "sqlite:".');
}

public function commit(): bool
Expand Down
2 changes: 2 additions & 0 deletions src/Symfony/Component/Cache/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ CHANGELOG
---

* Add support for `\Relay\Cluster` in `RedisAdapter`
* Add support for `valkey:` / `valkeys:` schemes
* Rename options "redis_cluster" and "redis_sentinel" to "cluster" and "sentinel" respectively

7.2
---
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,15 @@ public static function setUpBeforeClass(): void
self::markTestSkipped('REDIS_SENTINEL_SERVICE env var is not defined.');
}

self::$redis = AbstractAdapter::createConnection('redis:?host['.str_replace(' ', ']&host[', $hosts).']&timeout=0&retry_interval=0&read_timeout=0', ['redis_sentinel' => $service, 'prefix' => 'prefix_']);
self::$redis = AbstractAdapter::createConnection('redis:?host['.str_replace(' ', ']&host[', $hosts).']&timeout=0&retry_interval=0&read_timeout=0', ['sentinel' => $service, 'prefix' => 'prefix_']);
}

public function testInvalidDSNHasBothClusterAndSentinel()
{
$dsn = 'redis:?host[redis1]&host[redis2]&host[redis3]&redis_cluster=1&redis_sentinel=mymaster';
$dsn = 'redis:?host[redis1]&host[redis2]&host[redis3]&cluster=1&sentinel=mymaster';

$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Cannot use both "redis_cluster" and "redis_sentinel" at the same time.');
$this->expectExceptionMessage('Cannot use both "cluster" and "sentinel" at the same time.');

RedisAdapter::createConnection($dsn);
}
Expand All @@ -51,6 +51,6 @@ public function testExceptionMessageWhenFailingToRetrieveMasterInformation()
$dsn = 'redis:?host['.str_replace(' ', ']&host[', $hosts).']';
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Failed to retrieve master information from sentinel "invalid-masterset-name".');
AbstractAdapter::createConnection($dsn, ['redis_sentinel' => 'invalid-masterset-name']);
AbstractAdapter::createConnection($dsn, ['sentinel' => 'invalid-masterset-name']);
}
}
75 changes: 44 additions & 31 deletions src/Symfony/Component/Cache/Traits/RedisTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,10 @@ trait RedisTrait
'retry_interval' => 0,
'tcp_keepalive' => 0,
'lazy' => null,
'redis_cluster' => false,
'relay_cluster_context' => [],
'redis_sentinel' => null,
'cluster' => false,
'sentinel' => null,
'relay_cluster_context' => [],
'dbindex' => 0,
'failover' => 'none',
'ssl' => null, // see https://php.net/context.ssl
Expand Down Expand Up @@ -90,13 +91,13 @@ private function init(\Redis|Relay|RelayCluster|\RedisArray|\RedisCluster|\Predi
*/
public static function createConnection(#[\SensitiveParameter] string $dsn, array $options = []): \Redis|\RedisArray|\RedisCluster|\Predis\ClientInterface|Relay|RelayCluster
{
if (str_starts_with($dsn, 'redis:')) {
$scheme = 'redis';
} elseif (str_starts_with($dsn, 'rediss:')) {
$scheme = 'rediss';
} else {
throw new InvalidArgumentException('Invalid Redis DSN: it does not start with "redis[s]:".');
}
$scheme = match (true) {
str_starts_with($dsn, 'redis:') => 'redis',
str_starts_with($dsn, 'rediss:') => 'rediss',
str_starts_with($dsn, 'valkey:') => 'valkey',
str_starts_with($dsn, 'valkeys:') => 'valkeys',
default => throw new InvalidArgumentException('Invalid Redis DSN: it does not start with "redis[s]:" nor "valkey[s]:".'),
};

if (!\extension_loaded('redis') && !class_exists(\Predis\Client::class)) {
throw new CacheException('Cannot find the "redis" extension nor the "predis/predis" package.');
Expand Down Expand Up @@ -124,7 +125,7 @@ public static function createConnection(#[\SensitiveParameter] string $dsn, arra

$query = $hosts = [];

$tls = 'rediss' === $scheme;
$tls = 'rediss' === $scheme || 'valkeys' === $scheme;
$tcpScheme = $tls ? 'tls' : 'tcp';

if (isset($params['query'])) {
Expand Down Expand Up @@ -177,32 +178,41 @@ public static function createConnection(#[\SensitiveParameter] string $dsn, arra

$params += $query + $options + self::$defaultConnectionOptions;

if (isset($params['redis_sentinel']) && isset($params['sentinel_master'])) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This removes the fact that you cannot use those options together. You know silently ignore redis_sentinel when sentinel_master (or the new sentinel) is set. Doesn't this introduce a potential WTF debugging moment if $query and $options both configure this using different names for instance ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think the exception is useful so I still prefer removing it.
I improved the merging logic to account for this case.

throw new InvalidArgumentException('Cannot use both "redis_sentinel" and "sentinel_master" at the same time.');
$aliases = [
'sentinel_master' => 'sentinel',
'redis_sentinel' => 'sentinel',
'redis_cluster' => 'cluster',
];
foreach ($aliases as $alias => $key) {
$params[$key] = match (true) {
\array_key_exists($key, $query) => $query[$key],
\array_key_exists($alias, $query) => $query[$alias],
\array_key_exists($key, $options) => $options[$key],
\array_key_exists($alias, $options) => $options[$alias],
default => $params[$key],
};
}

$params['redis_sentinel'] ??= $params['sentinel_master'] ?? null;

if (isset($params['redis_sentinel']) && !class_exists(\Predis\Client::class) && !class_exists(\RedisSentinel::class) && !class_exists(Sentinel::class)) {
if (isset($params['sentinel']) && !class_exists(\Predis\Client::class) && !class_exists(\RedisSentinel::class) && !class_exists(Sentinel::class)) {
throw new CacheException('Redis Sentinel support requires one of: "predis/predis", "ext-redis >= 5.2", "ext-relay".');
}

if (isset($params['lazy'])) {
$params['lazy'] = filter_var($params['lazy'], \FILTER_VALIDATE_BOOLEAN);
}
$params['cluster'] = filter_var($params['cluster'], \FILTER_VALIDATE_BOOLEAN);

$params['redis_cluster'] = filter_var($params['redis_cluster'], \FILTER_VALIDATE_BOOLEAN);
if ($params['redis_cluster'] && isset($params['redis_sentinel'])) {
throw new InvalidArgumentException('Cannot use both "redis_cluster" and "redis_sentinel" at the same time.');
if ($params['cluster'] && isset($params['sentinel'])) {
throw new InvalidArgumentException('Cannot use both "cluster" and "sentinel" at the same time.');
}

$class = $params['class'] ?? match (true) {
$params['redis_cluster'] => match (true) {
$params['cluster'] => match (true) {
\extension_loaded('redis') => \RedisCluster::class,
\extension_loaded('relay') => RelayCluster::class,
default => \Predis\Client::class,
},
isset($params['redis_sentinel']) => match (true) {
isset($params['sentinel']) => match (true) {
\extension_loaded('redis') => \Redis::class,
\extension_loaded('relay') => Relay::class,
default => \Predis\Client::class,
Expand All @@ -213,7 +223,7 @@ public static function createConnection(#[\SensitiveParameter] string $dsn, arra
default => \Predis\Client::class,
};

if (isset($params['redis_sentinel']) && !is_a($class, \Predis\Client::class, true) && !class_exists(\RedisSentinel::class) && !class_exists(Sentinel::class)) {
if (isset($params['sentinel']) && !is_a($class, \Predis\Client::class, true) && !class_exists(\RedisSentinel::class) && !class_exists(Sentinel::class)) {
throw new CacheException(\sprintf('Cannot use Redis Sentinel: class "%s" does not extend "Predis\Client" and neither ext-redis >= 5.2 nor ext-relay have been found.', $class));
}

Expand All @@ -237,7 +247,7 @@ public static function createConnection(#[\SensitiveParameter] string $dsn, arra
$host = 'tls://'.$host;
}

if (!isset($params['redis_sentinel'])) {
if (!isset($params['sentinel'])) {
break;
}

Expand All @@ -263,15 +273,15 @@ public static function createConnection(#[\SensitiveParameter] string $dsn, arra
$sentinel = @new $sentinelClass($host, $port, $params['timeout'], (string) $params['persistent_id'], $params['retry_interval'], $params['read_timeout'], ...$extra);
}

if ($address = @$sentinel->getMasterAddrByName($params['redis_sentinel'])) {
if ($address = @$sentinel->getMasterAddrByName($params['sentinel'])) {
[$host, $port] = $address;
}
} catch (\RedisException|\Relay\Exception $redisException) {
}
} while (++$hostIndex < \count($hosts) && !$address);

if (isset($params['redis_sentinel']) && !$address) {
throw new InvalidArgumentException(\sprintf('Failed to retrieve master information from sentinel "%s".', $params['redis_sentinel']), previous: $redisException ?? null);
if (isset($params['sentinel']) && !$address) {
throw new InvalidArgumentException(\sprintf('Failed to retrieve master information from sentinel "%s".', $params['sentinel']), previous: $redisException ?? null);
}

try {
Expand Down Expand Up @@ -446,11 +456,14 @@ public static function createConnection(#[\SensitiveParameter] string $dsn, arra

$redis = $params['lazy'] ? RedisClusterProxy::createLazyProxy($initializer) : $initializer();
} elseif (is_a($class, \Predis\ClientInterface::class, true)) {
if ($params['redis_cluster']) {
if ($params['cluster']) {
$params['cluster'] = 'redis';
} elseif (isset($params['redis_sentinel'])) {
} else {
unset($params['cluster']);
}
if (isset($params['sentinel'])) {
$params['replication'] = 'sentinel';
$params['service'] = $params['redis_sentinel'];
$params['service'] = $params['sentinel'];
}
$params += ['parameters' => []];
$params['parameters'] += [
Expand Down Expand Up @@ -478,16 +491,16 @@ public static function createConnection(#[\SensitiveParameter] string $dsn, arra
}
}

if (1 === \count($hosts) && !($params['redis_cluster'] || $params['redis_sentinel'])) {
if (1 === \count($hosts) && !isset($params['cluster']) & !isset($params['sentinel'])) {
$hosts = $hosts[0];
} elseif (\in_array($params['failover'], ['slaves', 'distribute'], true) && !isset($params['replication'])) {
$params['replication'] = true;
$hosts[0] += ['alias' => 'master'];
}
$params['exceptions'] = false;

$redis = new $class($hosts, array_diff_key($params, self::$defaultConnectionOptions));
if (isset($params['redis_sentinel'])) {
$redis = new $class($hosts, array_diff_key($params, array_diff_key(self::$defaultConnectionOptions, ['cluster' => null])));
if (isset($params['sentinel'])) {
$redis->getConnection()->setSentinelTimeout($params['timeout']);
}
} elseif (class_exists($class, false)) {
Expand Down
1 change: 1 addition & 0 deletions src/Symfony/Component/HttpFoundation/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ CHANGELOG

* Add support for iterable of string in `StreamedResponse`
* Add `EventStreamResponse` and `ServerEvent` classes to streamline server event streaming
* Add support for `valkey:` / `valkeys:` schemes for sessions

7.2
---
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ public static function createHandler(object|string $connection, array $options =

case str_starts_with($connection, 'redis:'):
case str_starts_with($connection, 'rediss:'):
case str_starts_with($connection, 'valkey:'):
case str_starts_with($connection, 'valkeys:'):
case str_starts_with($connection, 'memcached:'):
if (!class_exists(AbstractAdapter::class)) {
throw new \InvalidArgumentException('Unsupported Redis or Memcached DSN. Try running "composer require symfony/cache".');
Expand Down
5 changes: 5 additions & 0 deletions src/Symfony/Component/Lock/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
CHANGELOG
=========

7.3
---

* Add support for `valkey:` / `valkeys:` schemes

7.2
---

Expand Down
2 changes: 2 additions & 0 deletions src/Symfony/Component/Lock/Store/StoreFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ public static function createStore(#[\SensitiveParameter] object|string $connect

case str_starts_with($connection, 'redis:'):
case str_starts_with($connection, 'rediss:'):
case str_starts_with($connection, 'valkey:'):
case str_starts_with($connection, 'valkeys:'):
case str_starts_with($connection, 'memcached:'):
if (!class_exists(AbstractAdapter::class)) {
throw new InvalidArgumentException('Unsupported Redis or Memcached DSN. Try running "composer require symfony/cache".');
Expand Down
1 change: 1 addition & 0 deletions src/Symfony/Component/Messenger/Bridge/Redis/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ CHANGELOG
---

* Implement the `KeepaliveReceiverInterface` to enable asynchronously notifying Redis that the job is still being processed, in order to avoid timeouts
* Add support for `valkey:` / `valkeys:` schemes

6.3
---
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -423,7 +423,7 @@ public function testInvalidSentinelMasterName()
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage(\sprintf('Failed to retrieve master information from master name "%s" and address "%s".', $uid, $exp));

Connection::fromDsn(\sprintf('%s/messenger-clearlasterror', $master), ['delete_after_ack' => true, 'sentinel_master' => $uid], null);
Connection::fromDsn(\sprintf('%s/messenger-clearlasterror', $master), ['delete_after_ack' => true, 'sentinel' => $uid], null);
}

public function testFromDsnOnUnixSocketWithUserAndPassword()
Expand Down
Loading
Loading