Skip to content
Open
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
146 changes: 146 additions & 0 deletions .github/workflows/redis-cluster.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
name: redis-cluster

on:
push:
branches:
- master
- '*.x'
pull_request:
paths:
- 'src/Illuminate/Cache/**'
- 'src/Illuminate/Redis/**'
- 'tests/Integration/Cache/Redis*'
- '.github/workflows/redis-cluster.yml'

jobs:
redis-single-endpoint-cluster:
runs-on: ubuntu-24.04

strategy:
fail-fast: false
matrix:
client: ['phpredis', 'predis']

name: Redis Single Endpoint Cluster (${{ matrix.client }})

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: 8.3
extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, redis, :php-psr
tools: composer:v2
coverage: none

- name: Set Framework version
run: composer config version "12.x-dev"

- name: Install dependencies
uses: nick-fields/retry@v3
with:
timeout_minutes: 5
max_attempts: 5
command: composer update --prefer-stable --prefer-dist --no-interaction --no-progress

- name: Create Redis Cluster
run: |
sudo apt update
sudo apt-get install -y --fix-missing redis-server git build-essential
sudo service redis-server stop
redis-server --daemonize yes --port 7000 --cluster-enabled yes --cluster-config-file nodes-7000.conf --cluster-node-timeout 5000
redis-server --daemonize yes --port 7001 --cluster-enabled yes --cluster-config-file nodes-7001.conf --cluster-node-timeout 5000
redis-server --daemonize yes --port 7002 --cluster-enabled yes --cluster-config-file nodes-7002.conf --cluster-node-timeout 5000
sleep 2
redis-cli --cluster create 127.0.0.1:7000 127.0.0.1:7001 127.0.0.1:7002 --cluster-replicas 0 --cluster-yes

- name: Build redis-cluster-proxy
run: |
cd /tmp
git clone https://github.com/RedisLabs/redis-cluster-proxy.git
cd redis-cluster-proxy
sed -i '/^const char \*SDS_NOINIT;$/d' src/sds.h
cat > /tmp/enable-info-command.patch << 'EOF'
diff --git a/src/commands.c b/src/commands.c
index 7b051c9..cb46649 100644
--- a/src/commands.c
+++ b/src/commands.c
@@ -133,7 +133,7 @@ struct redisCommandDef redisCommandTable[203] = {
CMDFLAG_DUPLICATE,
0, NULL, authCommand, getFirstMultipleReply},
{"incrbyfloat", 3, 1, 1, 1, 0, 0, NULL, NULL, NULL},
- {"info", -1, 0, 0, 0, 0, 1, NULL, NULL, NULL},
+ {"info", -1, 0, 0, 0, 0, 0, NULL, NULL, NULL},
{"lpush", -3, 1, 1, 1, 0, 0, NULL, NULL, NULL},
EOF
patch -p1 < /tmp/enable-info-command.patch
make

- name: Start redis-cluster-proxy
run: |
/tmp/redis-cluster-proxy/src/redis-cluster-proxy --port 6380 127.0.0.1:7000 &
sleep 2

- name: Verify cluster setup
run: |
redis-cli -p 7000 cluster info
redis-cli -p 6380 info cluster

- name: Execute tests
run: vendor/bin/phpunit --testdox tests/Integration/Cache/RedisSingleEndpointClusterTest.php
env:
REDIS_CLIENT: ${{ matrix.client }}
REDIS_HOST: 127.0.0.1
REDIS_PORT: 6380

redis-predis-cluster:
runs-on: ubuntu-24.04

strategy:
fail-fast: false

name: Redis Cluster (predis)

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: 8.3
extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, :php-psr
tools: composer:v2
coverage: none

- name: Set Framework version
run: composer config version "12.x-dev"

- name: Install dependencies
uses: nick-fields/retry@v3
with:
timeout_minutes: 5
max_attempts: 5
command: composer update --prefer-stable --prefer-dist --no-interaction --no-progress

- name: Create Redis Cluster
run: |
sudo apt update
sudo apt-get install -y --fix-missing redis-server
sudo service redis-server stop
redis-server --daemonize yes --port 7000 --cluster-enabled yes --cluster-config-file nodes-7000.conf --cluster-node-timeout 5000
redis-server --daemonize yes --port 7001 --cluster-enabled yes --cluster-config-file nodes-7001.conf --cluster-node-timeout 5000
redis-server --daemonize yes --port 7002 --cluster-enabled yes --cluster-config-file nodes-7002.conf --cluster-node-timeout 5000
sleep 2
redis-cli --cluster create 127.0.0.1:7000 127.0.0.1:7001 127.0.0.1:7002 --cluster-replicas 0 --cluster-yes

- name: Verify cluster setup
run: redis-cli -p 7000 cluster info

- name: Execute tests
run: vendor/bin/phpunit --testdox tests/Integration/Cache/RedisSingleEndpointClusterTest.php
env:
REDIS_CLIENT: predis
REDIS_CLUSTER_HOSTS_AND_PORTS: 127.0.0.1:7000,127.0.0.1:7001,127.0.0.1:7002
64 changes: 38 additions & 26 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,29 +51,41 @@ services:
ports:
- "6379:6379"
restart: always
# redis-cluster-0:
# image: redis:7.0-alpine
# ports:
# - "7000:7000"
# restart: always
# command: redis-server --port 7000 --appendonly yes --cluster-enabled yes
# redis-cluster-1:
# image: redis:7.0-alpine
# ports:
# - "7001:7001"
# restart: always
# command: redis-server --port 7001 --appendonly yes --cluster-enabled yes
# redis-cluster-2:
# image: redis:7.0-alpine
# ports:
# - "7002:7002"
# restart: always
# command: redis-server --port 7002 --appendonly yes --cluster-enabled yes
# redis-cluster-creator:
# image: redis:7.0-alpine
# depends_on:
# - redis-cluster-0
# - redis-cluster-1
# - redis-cluster-2
# command: sh -c 'redis-cli --cluster create redis-cluster-0:7000 redis-cluster-1:7001 redis-cluster-2:7002 --cluster-replicas 0 --cluster-yes || true'
# restart: no
redis-cluster-0:
image: redis:7.0-alpine
command: redis-server --port 7000 --cluster-enabled yes --cluster-config-file nodes.conf --cluster-node-timeout 5000
restart: always
redis-cluster-1:
image: redis:7.0-alpine
command: redis-server --port 7001 --cluster-enabled yes --cluster-config-file nodes.conf --cluster-node-timeout 5000
restart: always
redis-cluster-2:
image: redis:7.0-alpine
command: redis-server --port 7002 --cluster-enabled yes --cluster-config-file nodes.conf --cluster-node-timeout 5000
restart: always
redis-cluster-init:
image: redis:7.0-alpine
depends_on:
- redis-cluster-0
- redis-cluster-1
- redis-cluster-2
command: >
sh -c '
sleep 5 &&
redis-cli --cluster create
redis-cluster-0:7000
redis-cluster-1:7001
redis-cluster-2:7002
--cluster-replicas 0 --cluster-yes
'
restart: "no"
redis-cluster-proxy:
build:
context: ./tests/docker
dockerfile: redis-cluster-proxy.Dockerfile
ports:
- "6380:7777"
command: ["--port", "7777", "redis-cluster-0:7000"]
depends_on:
- redis-cluster-init
restart: always
69 changes: 67 additions & 2 deletions src/Illuminate/Cache/RedisStore.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,13 @@ class RedisStore extends TaggableStore implements LockProvider
*/
protected $lockConnection;

/**
* Cache for cluster mode detection per connection instance.
*
* @var \WeakMap
*/
protected $clusterModeCache;

/**
* Create a new Redis store.
*
Expand All @@ -57,6 +64,7 @@ public function __construct(Redis $redis, $prefix = '', $connection = 'default')
$this->redis = $redis;
$this->setPrefix($prefix);
$this->setConnection($connection);
$this->clusterModeCache = new \WeakMap();
}

/**
Expand Down Expand Up @@ -92,6 +100,16 @@ public function many(array $keys)

$connection = $this->connection();

// In cluster mode, mget may not be supported for cross-slot keys
// Fall back to individual get operations
if ($this->needsClusterFallback($connection)) {
foreach ($keys as $key) {
$results[$key] = $this->get($key);
}

return $results;
}

$values = $connection->mget(array_map(function ($key) {
return $this->prefix.$key;
}, $keys));
Expand Down Expand Up @@ -132,8 +150,7 @@ public function putMany(array $values, $seconds)
$connection = $this->connection();

// Cluster connections do not support writing multiple values if the keys hash differently...
if ($connection instanceof PhpRedisClusterConnection ||
$connection instanceof PredisClusterConnection) {
if ($this->needsClusterFallback($connection)) {
return $this->putManyAlias($values, $seconds);
}

Expand Down Expand Up @@ -346,6 +363,54 @@ protected function currentTags($chunkSize = 1000)
}))->map(fn (string $tagKey) => Str::match('/^'.preg_quote($prefix, '/').'tag:(.*):entries$/', $tagKey));
}

/**
* Determine if cluster fallback is needed for the given connection.
*
* @param \Illuminate\Redis\Connections\Connection $connection
* @return bool
*/
protected function needsClusterFallback($connection)
{
// Explicit cluster connections always need fallback
if ($connection instanceof PhpRedisClusterConnection ||
$connection instanceof PredisClusterConnection) {
return true;
}

// Check if server is in cluster mode (e.g., single-endpoint clusters)
return $this->isServerInClusterMode($connection);
}

/**
* Determine if the Redis server is running in cluster mode.
*
* @param \Illuminate\Redis\Connections\Connection $connection
* @return bool
*/
protected function isServerInClusterMode($connection)
{
// Use WeakMap to cache cluster mode detection per connection instance
if (! isset($this->clusterModeCache[$connection])) {
try {
$info = $connection->client()->info('cluster');

// Handle different formats from phpredis vs predis
// phpredis: ['cluster_enabled' => 1]
// predis: ['Cluster' => ['cluster_enabled' => '1']]
$clusterEnabled = $info['cluster_enabled']
?? $info['Cluster']['cluster_enabled']
?? null;

$this->clusterModeCache[$connection] = $clusterEnabled == 1;
} catch (\Exception $e) {
// If INFO command fails, assume not in cluster mode
$this->clusterModeCache[$connection] = false;
}
}

return $this->clusterModeCache[$connection];
}

/**
* Get the Redis connection instance.
*
Expand Down
53 changes: 49 additions & 4 deletions src/Illuminate/Cache/RedisTaggedCache.php
Original file line number Diff line number Diff line change
Expand Up @@ -114,8 +114,8 @@ public function flush()
{
$connection = $this->store->connection();

if ($connection instanceof PredisClusterConnection ||
$connection instanceof PhpRedisClusterConnection) {
// Use cluster-aware flush for cluster connections and cluster-mode servers
if ($this->needsClusterFallback($connection)) {
return $this->flushClusteredConnection();
}

Expand Down Expand Up @@ -193,8 +193,53 @@ protected function flushValues()
->map(fn (string $key) => $this->store->getPrefix().$key)
->chunk(1000);

foreach ($entries as $cacheKeys) {
$this->store->connection()->del(...$cacheKeys);
$connection = $this->store->connection();

// In cluster mode, multi-key DEL is not supported
// Fall back to individual deletions
if ($this->needsClusterFallback($connection)) {
foreach ($entries as $cacheKeys) {
foreach ($cacheKeys as $key) {
$connection->del($key);
}
}
} else {
foreach ($entries as $cacheKeys) {
$connection->del(...$cacheKeys);
}
}
}

/**
* Determine if cluster fallback is needed for the given connection.
*
* @param \Illuminate\Redis\Connections\Connection $connection
* @return bool
*/
protected function needsClusterFallback($connection)
{
// Explicit cluster connections always need fallback
if ($connection instanceof PhpRedisClusterConnection ||
$connection instanceof PredisClusterConnection) {
return true;
}

// For non-cluster connections, check if server is in cluster mode
// This handles single-endpoint clusters (e.g., AWS Elasticache Valkey Serverless)
try {
$info = $connection->client()->info('cluster');

// Handle different formats from phpredis vs predis
// phpredis: ['cluster_enabled' => 1]
// predis: ['Cluster' => ['cluster_enabled' => '1']]
$clusterEnabled = $info['cluster_enabled']
?? $info['Cluster']['cluster_enabled']
?? null;

return $clusterEnabled == 1;
} catch (\Exception $e) {
// If INFO command fails, assume not in cluster mode
return false;
}
}

Expand Down
Loading