Skip to content

Commit 05bfb14

Browse files
authored
Make connection_persistent parameter accept a connection id (#739)
1 parent 6c37565 commit 05bfb14

File tree

7 files changed

+320
-11
lines changed

7 files changed

+320
-11
lines changed

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
"monolog/monolog": "*",
3838
"phpunit/phpunit": "^9.5.28 || ^10",
3939
"predis/predis": "^2.0 || ^3.0 ",
40-
"seec/phpunit-consecutive-params": "dev-master",
40+
"seec/phpunit-consecutive-params": "^1.1.4",
4141
"symfony/browser-kit": "^5.4.20 ||^6.0 || ^7.0",
4242
"symfony/cache": "^5.4.20 ||^6.0 || ^7.0",
4343
"symfony/config": "^5.4.20 ||^6.0 || ^7.0",

docs/README.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,44 @@ snc_redis:
171171

172172
It also works for the Phpredis Cluster mode.
173173

174+
### Persistent Connections ###
175+
176+
When using persistent connections, you can provide a unique identifier to ensure that different clients don't share the same socket connection. This is especially important when operating multiple clients within the same application that communicate with the same Redis server.
177+
178+
The `connection_persistent` option accepts either a boolean or string value:
179+
- `true`: Enable persistent connections using the client alias as the connection ID
180+
- `false`: Disable persistent connections (default)
181+
- `string`: Enable persistent connections using the provided string as the connection ID
182+
183+
``` yaml
184+
snc_redis:
185+
clients:
186+
app_cache:
187+
type: predis # or phpredis
188+
alias: app_cache
189+
dsn: redis://localhost/0
190+
options:
191+
connection_persistent: "app_cache_connection" # Custom persistent ID
192+
session_store:
193+
type: predis # or phpredis
194+
alias: session_store
195+
dsn: redis://localhost/1
196+
options:
197+
connection_persistent: true # Uses alias as persistent ID
198+
simple_cache:
199+
type: phpredis
200+
alias: simple_cache
201+
dsn: redis://localhost/2
202+
options:
203+
connection_persistent: false # No persistent connection
204+
```
205+
206+
**Why this matters:** Without a unique persistent connection ID, two clients connecting to different databases on the same Redis server would share the same persistent socket, leading to unexpected behavior where commands from one client could affect the database context of another.
207+
208+
**For phpredis clients:** The persistent ID is passed directly to the `pconnect()` function as the persistent ID parameter.
209+
210+
**For predis clients:** The persistent ID is mapped to the `conn_uid` connection option (requires predis/predis version 2.4.0 or higher). This ensures each client creates its own socket so the connection context won't be shared across clients.
211+
174212
### Sessions ###
175213

176214
Use Redis sessions by utilizing Symfony built-in Redis session handler like so:

src/DependencyInjection/Configuration/Configuration.php

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,9 @@
2929
use Symfony\Component\Config\Definition\ConfigurationInterface;
3030

3131
use function class_exists;
32+
use function is_bool;
3233
use function is_iterable;
34+
use function is_string;
3335
use function trigger_deprecation;
3436

3537
class Configuration implements ConfigurationInterface
@@ -124,7 +126,13 @@ private function addClientsSection(ArrayNodeDefinition $rootNode): void
124126
->scalarPrototype()->end()
125127
->end()
126128
->booleanNode('connection_async')->defaultFalse()->end()
127-
->booleanNode('connection_persistent')->defaultFalse()->end()
129+
->variableNode('connection_persistent')
130+
->defaultFalse()
131+
->validate()
132+
->ifTrue(static fn ($v) => !(is_bool($v) || (is_string($v) && $v !== '')))
133+
->thenInvalid('connection_persistent must be a boolean or string')
134+
->end()
135+
->end()
128136
->scalarNode('connection_timeout')->cannotBeEmpty()->defaultValue(5)->end()
129137
->scalarNode('read_write_timeout')->defaultNull()->end()
130138
->booleanNode('iterable_multibulk')->defaultFalse()->end()

src/DependencyInjection/SncRedisExtension.php

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
use InvalidArgumentException;
1717
use LogicException;
18+
use Predis\Client;
1819
use RedisSentinel;
1920
use Relay\Sentinel;
2021
use Snc\RedisBundle\DependencyInjection\Configuration\Configuration;
@@ -35,7 +36,9 @@
3536
use function assert;
3637
use function class_exists;
3738
use function count;
39+
use function is_string;
3840
use function sprintf;
41+
use function version_compare;
3942

4043
class SncRedisExtension extends Extension
4144
{
@@ -135,10 +138,28 @@ private function loadPredisClient(array $client, ContainerBuilder $container): v
135138
// predis connection parameters have been renamed in v0.8
136139
$client['options']['async_connect'] = $client['options']['connection_async'];
137140
$client['options']['timeout'] = $client['options']['connection_timeout'];
138-
$client['options']['persistent'] = $client['options']['connection_persistent'];
139-
$client['options']['exceptions'] = $client['options']['throw_errors'];
141+
// Handle connection_persistent as bool|string
142+
if (is_string($client['options']['connection_persistent'])) {
143+
$client['options']['persistent'] = true;
144+
// For predis, use the string value as conn_uid (requires predis >= 2.4.0)
145+
if (class_exists('Predis\Client')) {
146+
if (!version_compare(Client::VERSION, '2.4.0', '>=')) {
147+
throw new InvalidConfigurationException(
148+
'Using connection_persistent as string for Predis requires predis/predis version 2.4.0 or higher. ' .
149+
sprintf('Current version: %s', Client::VERSION),
150+
);
151+
}
152+
153+
$client['options']['conn_uid'] = $client['options']['connection_persistent'];
154+
}
155+
} else {
156+
$client['options']['persistent'] = $client['options']['connection_persistent'];
157+
}
158+
159+
$client['options']['exceptions'] = $client['options']['throw_errors'];
140160
// fix ssl configuration key name
141161
$client['options']['ssl'] = $client['options']['parameters']['ssl_context'] ?? [];
162+
142163
unset($client['options']['connection_async']);
143164
unset($client['options']['connection_timeout']);
144165
unset($client['options']['connection_persistent']);

src/Factory/PhpredisClientFactory.php

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
use function in_array;
3333
use function is_a;
3434
use function is_array;
35+
use function is_string;
3536
use function phpversion;
3637
use function spl_autoload_register;
3738
use function sprintf;
@@ -111,9 +112,9 @@ public function create(string $class, array $dsns, array $options, string $alias
111112
}
112113

113114
/**
114-
* @param class-string $class
115-
* @param list<RedisDsn> $dsns
116-
* @param array{service: ?string, connection_persistent: ?bool, connection_timeout: ?string, read_write_timeout: ?string, parameters: array{sentinel_username: string, sentinel_password: string}} $options
115+
* @param class-string $class
116+
* @param list<RedisDsn> $dsns
117+
* @param array{service: ?string, connection_persistent: bool|non-empty-string|null, connection_timeout: ?string, read_write_timeout: ?string, parameters: array{sentinel_username: string, sentinel_password: string}} $options
117118
*
118119
* @return Redis|Relay
119120
*/
@@ -123,9 +124,15 @@ private function createClientFromSentinel(string $class, array $dsns, string $al
123124
$sentinelClass = $isRelay ? Sentinel::class : RedisSentinel::class;
124125
$masterName = $options['service'];
125126
$connectionTimeout = $options['connection_timeout'] ?? 0;
126-
$connectionPersistent = $options['connection_persistent'] ? $masterName : null;
127-
$readTimeout = $options['read_write_timeout'] ?? 0;
128-
$parameters = $options['parameters'];
127+
$connectionPersistent = null;
128+
if ($options['connection_persistent']) {
129+
$connectionPersistent = is_string($options['connection_persistent'])
130+
? $options['connection_persistent']
131+
: $masterName;
132+
}
133+
134+
$readTimeout = $options['read_write_timeout'] ?? 0;
135+
$parameters = $options['parameters'];
129136

130137
foreach ($dsns as $dsn) {
131138
$args = [
@@ -254,11 +261,18 @@ private function createClient(RedisDsn $dsn, string $class, string $alias, array
254261
$context['stream'] = $options['parameters']['ssl_context'];
255262
}
256263

264+
$persistentId = null;
265+
if (!empty($options['connection_persistent'])) {
266+
$persistentId = is_string($options['connection_persistent'])
267+
? $options['connection_persistent']
268+
: $alias;
269+
}
270+
257271
$connectParameters = [
258272
$socket ?? ($dsn->getTls() ? 'tls://' : '') . $dsn->getHost(),
259273
$dsn->getPort(),
260274
$options['connection_timeout'],
261-
empty($options['connection_persistent']) ? null : $dsn->getPersistentId(),
275+
$persistentId,
262276
5, // retry interval
263277
5, // read timeout
264278
$context,

tests/DependencyInjection/SncRedisExtensionTest.php

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
use InvalidArgumentException;
1717
use PHPUnit\Framework\TestCase;
18+
use Predis\Client;
1819
use Redis;
1920
use RedisException;
2021
use Relay\Relay;
@@ -30,9 +31,11 @@
3031
use Symfony\Component\Yaml\Parser;
3132

3233
use function array_key_exists;
34+
use function class_exists;
3335
use function count;
3436
use function current;
3537
use function sys_get_temp_dir;
38+
use function version_compare;
3639

3740
/**
3841
* SncRedisExtensionTest
@@ -755,6 +758,174 @@ private function getPhpRedisWithInvalidACLYamlMinimalConfig(): string
755758
username: user
756759
password: password
757760

761+
YAML;
762+
}
763+
764+
public function testPredisWithConnectionPersistentBool(): void
765+
{
766+
if (!class_exists('Predis\Client')) {
767+
$this->markTestSkipped('Predis not available');
768+
}
769+
770+
if (version_compare(Client::VERSION, '2.4.0', '<')) {
771+
$this->markTestSkipped('Predis version 2.4.0 or higher required for connection_persistent');
772+
}
773+
774+
$extension = new SncRedisExtension();
775+
$config = $this->parseYaml($this->getPredisWithConnectionPersistentBoolYamlConfig());
776+
$extension->load([$config], $container = $this->getContainer());
777+
778+
$this->assertTrue($container->hasDefinition('snc_redis.default'));
779+
$definition = $container->getDefinition('snc_redis.connection.default_parameters.default');
780+
$this->assertTrue($definition->getArgument(0)['persistent']);
781+
$this->assertNull($definition->getArgument(0)['conn_uid']);
782+
}
783+
784+
public function testPredisWithConnectionPersistentString(): void
785+
{
786+
if (!class_exists('Predis\Client')) {
787+
$this->markTestSkipped('Predis not available');
788+
}
789+
790+
if (version_compare(Client::VERSION, '2.4.0', '<')) {
791+
$this->markTestSkipped('Predis version 2.4.0 or higher required for connection_persistent');
792+
}
793+
794+
$extension = new SncRedisExtension();
795+
$config = $this->parseYaml($this->getPredisWithConnectionPersistentStringYamlConfig());
796+
$extension->load([$config], $container = $this->getContainer());
797+
798+
$this->assertTrue($container->hasDefinition('snc_redis.default'));
799+
$definition = $container->getDefinition('snc_redis.connection.default_parameters.default');
800+
$this->assertTrue($definition->getArgument(0)['persistent']);
801+
$this->assertSame('my_custom_conn_uid', $definition->getArgument(0)['conn_uid']);
802+
}
803+
804+
public function testPredisWithConnectionPersistentVersionTooOld(): void
805+
{
806+
if (!class_exists('Predis\Client')) {
807+
$this->markTestSkipped('Predis not available');
808+
}
809+
810+
if (version_compare(Client::VERSION, '2.4.0', '>=')) {
811+
$this->markTestSkipped('This test requires Predis version < 2.4.0');
812+
}
813+
814+
$this->expectException(InvalidConfigurationException::class);
815+
$this->expectExceptionMessage('Using connection_persistent as string for Predis requires predis/predis version 2.4.0 or higher');
816+
817+
$extension = new SncRedisExtension();
818+
$config = $this->parseYaml($this->getPredisWithConnectionPersistentStringYamlConfig());
819+
$extension->load([$config], $container = $this->getContainer());
820+
}
821+
822+
private function getPredisWithConnectionPersistentBoolYamlConfig(): string
823+
{
824+
return <<<'YAML'
825+
clients:
826+
default:
827+
type: predis
828+
alias: default
829+
dsn: redis://localhost:6379/0
830+
options:
831+
connection_persistent: true
832+
833+
YAML;
834+
}
835+
836+
private function getPredisWithConnectionPersistentStringYamlConfig(): string
837+
{
838+
return <<<'YAML'
839+
clients:
840+
default:
841+
type: predis
842+
alias: default
843+
dsn: redis://localhost:6379/0
844+
options:
845+
connection_persistent: my_custom_conn_uid
846+
847+
YAML;
848+
}
849+
850+
public function testPredisWithConnectionPersistentFalse(): void
851+
{
852+
if (!class_exists('Predis\Client')) {
853+
$this->markTestSkipped('Predis not available');
854+
}
855+
856+
if (version_compare(Client::VERSION, '2.4.0', '<')) {
857+
$this->markTestSkipped('Predis version 2.4.0 or higher required for connection_persistent');
858+
}
859+
860+
$extension = new SncRedisExtension();
861+
$config = $this->parseYaml($this->getPredisWithConnectionPersistentFalseYamlConfig());
862+
$extension->load([$config], $container = $this->getContainer());
863+
864+
$this->assertTrue($container->hasDefinition('snc_redis.default'));
865+
$definition = $container->getDefinition('snc_redis.connection.default_parameters.default');
866+
$this->assertFalse($definition->getArgument(0)['persistent']);
867+
$this->assertNull($definition->getArgument(0)['conn_uid']);
868+
}
869+
870+
private function getPredisWithConnectionPersistentFalseYamlConfig(): string
871+
{
872+
return <<<'YAML'
873+
clients:
874+
default:
875+
type: predis
876+
alias: default
877+
dsn: redis://localhost:6379/0
878+
options:
879+
connection_persistent: false
880+
881+
YAML;
882+
}
883+
884+
public function testInvalidConnectionPersistentValue(): void
885+
{
886+
$this->expectException(InvalidConfigurationException::class);
887+
$this->expectExceptionMessage('connection_persistent must be a boolean or string');
888+
889+
$extension = new SncRedisExtension();
890+
$config = $this->parseYaml($this->getInvalidConnectionPersistentYamlConfig());
891+
$extension->load([$config], $this->getContainer());
892+
}
893+
894+
private function getInvalidConnectionPersistentYamlConfig(): string
895+
{
896+
return <<<'YAML'
897+
clients:
898+
default:
899+
type: predis
900+
alias: default
901+
dsn: redis://localhost:6379/0
902+
options:
903+
connection_persistent: []
904+
905+
YAML;
906+
}
907+
908+
public function testEmptyStringConnectionPersistentValue(): void
909+
{
910+
$this->expectException(InvalidConfigurationException::class);
911+
$this->expectExceptionMessage('connection_persistent must be a boolean or string');
912+
913+
$extension = new SncRedisExtension();
914+
$config = $this->parseYaml($this->getEmptyStringConnectionPersistentYamlConfig());
915+
$extension->load([$config], $this->getContainer());
916+
}
917+
918+
private function getEmptyStringConnectionPersistentYamlConfig(): string
919+
{
920+
return <<<'YAML'
921+
clients:
922+
default:
923+
type: predis
924+
alias: default
925+
dsn: redis://localhost:6379/0
926+
options:
927+
connection_persistent: ""
928+
758929
YAML;
759930
}
760931

0 commit comments

Comments
 (0)