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

on:
push:
branches:
- master
- '*.x'
pull_request:
workflow_dispatch:

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

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

name: Redis Cluster (${{ matrix.client }}) Cache Tests

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

- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: 8.2
extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, redis, gmp, :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 --appendonly yes --cluster-enabled yes --cluster-config-file nodes-7000.conf
redis-server --daemonize yes --port 7001 --appendonly yes --cluster-enabled yes --cluster-config-file nodes-7001.conf
redis-server --daemonize yes --port 7002 --appendonly yes --cluster-enabled yes --cluster-config-file nodes-7002.conf
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: Execute tests
run: vendor/bin/phpunit tests/Integration/Cache/
env:
REDIS_CLIENT: ${{ matrix.client }}
REDIS_CLUSTER_HOSTS_AND_PORTS: 127.0.0.1:7000,127.0.0.1:7001,127.0.0.1:7002
CACHE_STORE: redis
REDIS_CACHE_CONNECTION: default
CACHE_FLEXIBLE_CLUSTER_MODE: true
132 changes: 116 additions & 16 deletions src/Illuminate/Cache/Repository.php
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,13 @@ class Repository implements ArrayAccess, CacheContract
*/
protected $config = [];

/**
* Whether the Redis server has cluster mode enabled.
*
* @var bool|null
*/
protected $clusterEnabled = null;

/**
* Create a new cache repository instance.
*
Expand Down Expand Up @@ -488,36 +495,65 @@ public function rememberForever($key, Closure $callback)
*/
public function flexible($key, $ttl, $callback, $lock = null, $alwaysDefer = false)
{
[
$key => $value,
"illuminate:cache:flexible:created:{$key}" => $created,
] = $this->many([$key, "illuminate:cache:flexible:created:{$key}"]);
$createdKey = "illuminate:cache:flexible:created:{$key}";
$lockKey = "illuminate:cache:flexible:lock:{$key}";

if (in_array(null, [$value, $created], true)) {
return tap(value($callback), fn ($value) => $this->putMany([
// Check if sequential operations are needed for Redis Cluster compatibility
$useSequential = $this->shouldUseSequentialOperations();

if ($useSequential) {
// Sequential operations for Redis Cluster compatibility
$value = $this->get($key);
$created = $this->get($createdKey);
} else {
// Bulk operations (default behavior)
[
$key => $value,
"illuminate:cache:flexible:created:{$key}" => Carbon::now()->getTimestamp(),
], $ttl[1]));
$createdKey => $created,
] = $this->many([$key, $createdKey]);
}

if (in_array(null, [$value, $created], true)) {
$newValue = value($callback);

if ($useSequential) {
$this->put($key, $newValue, $ttl[1]);
$this->put($createdKey, Carbon::now()->getTimestamp(), $ttl[1]);
} else {
$this->putMany([
$key => $newValue,
$createdKey => Carbon::now()->getTimestamp(),
], $ttl[1]);
}

return $newValue;
}

if (($created + $this->getSeconds($ttl[0])) > Carbon::now()->getTimestamp()) {
return $value;
}

$refresh = function () use ($key, $ttl, $callback, $lock, $created) {
$refresh = function () use ($key, $createdKey, $lockKey, $ttl, $callback, $lock, $created, $useSequential) {
$this->store->lock(
"illuminate:cache:flexible:lock:{$key}",
$lockKey,
$lock['seconds'] ?? 0,
$lock['owner'] ?? null,
)->get(function () use ($key, $callback, $created, $ttl) {
if ($created !== $this->get("illuminate:cache:flexible:created:{$key}")) {
)->get(function () use ($key, $createdKey, $callback, $created, $ttl, $useSequential) {
if ($created !== $this->get($createdKey)) {
return;
}

$this->putMany([
$key => value($callback),
"illuminate:cache:flexible:created:{$key}" => Carbon::now()->getTimestamp(),
], $ttl[1]);
$newValue = value($callback);

if ($useSequential) {
$this->put($key, $newValue, $ttl[1]);
$this->put($createdKey, Carbon::now()->getTimestamp(), $ttl[1]);
} else {
$this->putMany([
$key => $newValue,
$createdKey => Carbon::now()->getTimestamp(),
], $ttl[1]);
}
});
};

Expand Down Expand Up @@ -816,4 +852,68 @@ public function __clone()
{
$this->store = clone $this->store;
}

/**
* Determine if sequential operations should be used for cluster compatibility.
*
* @return bool
*/
protected function shouldUseSequentialOperations(): bool
{
// Only check for Redis stores
if (!$this->store instanceof RedisStore) {
return false;
}

// Check if cluster mode is enabled (cached after first check)
if ($this->clusterEnabled === null) {
$this->clusterEnabled = $this->detectClusterEnabled();
}

return $this->clusterEnabled;
}

/**
* Detect if the Redis server has cluster mode enabled.
*
* @return bool
*/
protected function detectClusterEnabled(): bool
{
try {
$connection = $this->store->connection();

// Cluster connections always need sequential operations
if ($connection instanceof \Illuminate\Redis\Connections\PhpRedisClusterConnection ||
$connection instanceof \Illuminate\Redis\Connections\PredisClusterConnection) {
return true;
}

$client = $connection->client();

// For phpredis single-node connection
if ($client instanceof \Redis) {
$info = $client->info('cluster');
return ($info['cluster_enabled'] ?? 0) == 1;
}

// For predis single-node connection
if ($client instanceof \Predis\Client) {
$info = $client->info('cluster');
// Predis may return array or string depending on version
if (is_array($info)) {
return ($info['Cluster']['cluster_enabled'] ?? 0) == 1;
}
return str_contains($info, 'cluster_enabled:1');
}

// Unknown client type - assume non-cluster to preserve existing behavior
// This maintains backward compatibility but may not work correctly
// if an unsupported client is used with a cluster-enabled server
return false;
} catch (\Exception $e) {
// Detection failed - assume non-cluster to preserve existing behavior
return false;
}
}
}
Loading