From 3964104c94c01170e514976b942f491fd86d91d1 Mon Sep 17 00:00:00 2001 From: hayase_yasuhiro Date: Mon, 3 Nov 2025 22:24:24 +0900 Subject: [PATCH 1/4] Fix Cache::flexible() to work with Redis Cluster The flexible() method was generating keys that could hash to different slots in Redis Cluster, causing CROSSSLOT errors. This fix adds Redis hash tags to ensure all related keys (value, created timestamp, and lock) are stored in the same slot. Changes: - Add a 4-character MD5 hash prefix as a hash tag for slot allocation - Wrap hash in {} to ensure consistent slot placement in Redis Cluster - Maintain original key structure for non-cluster Redis instances - Works with both phpredis and predis drivers This resolves issues with AWS ElastiCache Serverless (Valkey) and other Redis Cluster environments while remaining backward compatible with standalone Redis instances. --- src/Illuminate/Cache/Repository.php | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/src/Illuminate/Cache/Repository.php b/src/Illuminate/Cache/Repository.php index 880ed23f776c..ac4232d2c865 100755 --- a/src/Illuminate/Cache/Repository.php +++ b/src/Illuminate/Cache/Repository.php @@ -488,15 +488,20 @@ public function rememberForever($key, Closure $callback) */ public function flexible($key, $ttl, $callback, $lock = null, $alwaysDefer = false) { + $hashKey = substr(md5($key), 0, 4); + $valueKey = "{{$hashKey}}:{$key}"; + $createdKey = "{{$hashKey}}:illuminate:cache:flexible:created:{$key}"; + $lockKey = "{{$hashKey}}:illuminate:cache:flexible:lock:{$key}"; + [ - $key => $value, - "illuminate:cache:flexible:created:{$key}" => $created, - ] = $this->many([$key, "illuminate:cache:flexible:created:{$key}"]); + $valueKey => $value, + $createdKey => $created, + ] = $this->many([$valueKey, $createdKey]); if (in_array(null, [$value, $created], true)) { return tap(value($callback), fn ($value) => $this->putMany([ - $key => $value, - "illuminate:cache:flexible:created:{$key}" => Carbon::now()->getTimestamp(), + $valueKey => $value, + $createdKey => Carbon::now()->getTimestamp(), ], $ttl[1])); } @@ -504,19 +509,19 @@ public function flexible($key, $ttl, $callback, $lock = null, $alwaysDefer = fal return $value; } - $refresh = function () use ($key, $ttl, $callback, $lock, $created) { + $refresh = function () use ($valueKey, $createdKey, $lockKey, $ttl, $callback, $created) { $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 ($valueKey, $createdKey, $callback, $created, $ttl) { + if ($created !== $this->get($createdKey)) { return; } $this->putMany([ - $key => value($callback), - "illuminate:cache:flexible:created:{$key}" => Carbon::now()->getTimestamp(), + $valueKey => value($callback), + $createdKey => Carbon::now()->getTimestamp(), ], $ttl[1]); }); }; From 6a820380aa46aa706696d78c0faffea988db0d23 Mon Sep 17 00:00:00 2001 From: hayase_yasuhiro Date: Mon, 3 Nov 2025 22:45:30 +0900 Subject: [PATCH 2/4] Fix PHPStan error: add $lock to closure use clause --- src/Illuminate/Cache/Repository.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Cache/Repository.php b/src/Illuminate/Cache/Repository.php index ac4232d2c865..5dab373d32e1 100755 --- a/src/Illuminate/Cache/Repository.php +++ b/src/Illuminate/Cache/Repository.php @@ -509,7 +509,7 @@ public function flexible($key, $ttl, $callback, $lock = null, $alwaysDefer = fal return $value; } - $refresh = function () use ($valueKey, $createdKey, $lockKey, $ttl, $callback, $created) { + $refresh = function () use ($valueKey, $createdKey, $lockKey, $ttl, $callback, $lock, $created) { $this->store->lock( $lockKey, $lock['seconds'] ?? 0, From 1eadeb14911f5a800aef849e6650024c8a7c17e0 Mon Sep 17 00:00:00 2001 From: hayase_yasuhiro Date: Wed, 5 Nov 2025 11:44:17 +0900 Subject: [PATCH 3/4] Add flexible_cluster_mode config for Redis Cluster compatibility MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This adds a new configuration option 'flexible_cluster_mode' that enables sequential operations in Cache::flexible() for Redis Cluster compatibility. Implementation: - When 'flexible_cluster_mode' is enabled, flexible() uses sequential get/put operations instead of bulk many()/putMany() operations - This avoids CROSSSLOT errors in Redis Cluster environments where keys hash to different slots - Default is false to maintain existing behavior Configuration (config/cache.php): ```php 'stores' => [ 'redis' => [ 'driver' => 'redis', 'flexible_cluster_mode' => env('CACHE_FLEXIBLE_CLUSTER_MODE', false), ], ], ``` Trade-offs: - Sequential mode: 2 network round-trips instead of 1 - Performance impact is minimal since flexible() callback execution dominates - This approach already exists for putMany() in cluster connections All tests pass (75 tests, 302 assertions). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/Illuminate/Cache/Repository.php | 60 ++++++++++++++++++++--------- 1 file changed, 42 insertions(+), 18 deletions(-) diff --git a/src/Illuminate/Cache/Repository.php b/src/Illuminate/Cache/Repository.php index 5dab373d32e1..1bb9cde0972d 100755 --- a/src/Illuminate/Cache/Repository.php +++ b/src/Illuminate/Cache/Repository.php @@ -488,41 +488,65 @@ public function rememberForever($key, Closure $callback) */ public function flexible($key, $ttl, $callback, $lock = null, $alwaysDefer = false) { - $hashKey = substr(md5($key), 0, 4); - $valueKey = "{{$hashKey}}:{$key}"; - $createdKey = "{{$hashKey}}:illuminate:cache:flexible:created:{$key}"; - $lockKey = "{{$hashKey}}:illuminate:cache:flexible:lock:{$key}"; + $createdKey = "illuminate:cache:flexible:created:{$key}"; + $lockKey = "illuminate:cache:flexible:lock:{$key}"; - [ - $valueKey => $value, - $createdKey => $created, - ] = $this->many([$valueKey, $createdKey]); + // Check if sequential mode is enabled (for Redis Cluster compatibility) + $useSequential = $this->config['flexible_cluster_mode'] ?? false; + + if ($useSequential) { + // Sequential operations for Redis Cluster compatibility + $value = $this->get($key); + $created = $this->get($createdKey); + } else { + // Bulk operations (default behavior) + [ + $key => $value, + $createdKey => $created, + ] = $this->many([$key, $createdKey]); + } if (in_array(null, [$value, $created], true)) { - return tap(value($callback), fn ($value) => $this->putMany([ - $valueKey => $value, - $createdKey => 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]); + } + + return $newValue; } if (($created + $this->getSeconds($ttl[0])) > Carbon::now()->getTimestamp()) { return $value; } - $refresh = function () use ($valueKey, $createdKey, $lockKey, $ttl, $callback, $lock, $created) { + $refresh = function () use ($key, $createdKey, $lockKey, $ttl, $callback, $lock, $created, $useSequential) { $this->store->lock( $lockKey, $lock['seconds'] ?? 0, $lock['owner'] ?? null, - )->get(function () use ($valueKey, $createdKey, $callback, $created, $ttl) { + )->get(function () use ($key, $createdKey, $callback, $created, $ttl, $useSequential) { if ($created !== $this->get($createdKey)) { return; } - $this->putMany([ - $valueKey => value($callback), - $createdKey => 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]); + } }); }; From c798cdeda28c69c31f29f396e493366000954a7c Mon Sep 17 00:00:00 2001 From: hayase_yasuhiro Date: Wed, 5 Nov 2025 12:53:43 +0900 Subject: [PATCH 4/4] Pass flexible_cluster_mode config to Repository in CacheManager MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Repository::flexible() references the flexible_cluster_mode config, but CacheManager::repository() was using Arr::only($config, ['store']), which prevented flexible_cluster_mode from being passed to Repository. Changes: - Modified Arr::only($config, ['store', 'flexible_cluster_mode']) - This ensures flexible_cluster_mode config is properly passed to Repository Test results: - Local: 75 tests, 302 assertions all passed - Valkey Serverless: 3/3 patterns all succeeded - phpredis + default: SUCCESS - predis + default: SUCCESS - predis + clusters: SUCCESS 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/Illuminate/Cache/CacheManager.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Cache/CacheManager.php b/src/Illuminate/Cache/CacheManager.php index 2df3fa42ff6f..c34a92c5c853 100755 --- a/src/Illuminate/Cache/CacheManager.php +++ b/src/Illuminate/Cache/CacheManager.php @@ -369,7 +369,7 @@ protected function getSession() */ public function repository(Store $store, array $config = []) { - return tap(new Repository($store, Arr::only($config, ['store'])), function ($repository) use ($config) { + return tap(new Repository($store, Arr::only($config, ['store', 'flexible_cluster_mode'])), function ($repository) use ($config) { if ($config['events'] ?? true) { $this->setEventDispatcher($repository); }