From 4afc680889508ec91f8246539c0816530976f662 Mon Sep 17 00:00:00 2001 From: Santiago San Martin Date: Sat, 19 Jul 2025 18:40:36 -0300 Subject: [PATCH] [Semaphore] Enabled usage of `EVALSHA` and `LOAD SCRIPT` over regular `EVAL` --- src/Symfony/Component/Semaphore/CHANGELOG.md | 5 + .../Exception/SemaphoreStorageException.php | 21 +++++ .../Component/Semaphore/Store/RedisStore.php | 94 ++++++++++++++++++- 3 files changed, 117 insertions(+), 3 deletions(-) create mode 100644 src/Symfony/Component/Semaphore/Exception/SemaphoreStorageException.php diff --git a/src/Symfony/Component/Semaphore/CHANGELOG.md b/src/Symfony/Component/Semaphore/CHANGELOG.md index 8ae9706e21544..42f52c23c578a 100644 --- a/src/Symfony/Component/Semaphore/CHANGELOG.md +++ b/src/Symfony/Component/Semaphore/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +7.4 +--- + + * RedisStore uses `EVALSHA` over `EVAL` when evaluating LUA scripts + 7.3 --- diff --git a/src/Symfony/Component/Semaphore/Exception/SemaphoreStorageException.php b/src/Symfony/Component/Semaphore/Exception/SemaphoreStorageException.php new file mode 100644 index 0000000000000..a86e7a606ca69 --- /dev/null +++ b/src/Symfony/Component/Semaphore/Exception/SemaphoreStorageException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Semaphore\Exception; + +/** + * SemaphoreStorageException is thrown when an issue happens during the manipulation of a semaphore in a store. + * + * @author Santiago San Martin + */ +class SemaphoreStorageException extends \RuntimeException implements ExceptionInterface +{ +} diff --git a/src/Symfony/Component/Semaphore/Store/RedisStore.php b/src/Symfony/Component/Semaphore/Store/RedisStore.php index 5a078891b764d..970d5d9b867f2 100644 --- a/src/Symfony/Component/Semaphore/Store/RedisStore.php +++ b/src/Symfony/Component/Semaphore/Store/RedisStore.php @@ -16,6 +16,7 @@ use Symfony\Component\Semaphore\Exception\InvalidArgumentException; use Symfony\Component\Semaphore\Exception\SemaphoreAcquiringException; use Symfony\Component\Semaphore\Exception\SemaphoreExpiredException; +use Symfony\Component\Semaphore\Exception\SemaphoreStorageException; use Symfony\Component\Semaphore\Key; use Symfony\Component\Semaphore\PersistingStoreInterface; @@ -27,6 +28,8 @@ */ class RedisStore implements PersistingStoreInterface { + private const NO_SCRIPT_ERROR_MESSAGE_PREFIX = 'NOSCRIPT'; + public function __construct( private \Redis|Relay|RelayCluster|\RedisArray|\RedisCluster|\Predis\ClientInterface $redis, ) { @@ -159,16 +162,79 @@ public function exists(Key $key): bool private function evaluate(string $script, string $resource, array $args): mixed { + $scriptSha = sha1($script); + if ($this->redis instanceof \Redis || $this->redis instanceof Relay || $this->redis instanceof RelayCluster || $this->redis instanceof \RedisCluster) { - return $this->redis->eval($script, array_merge([$resource], $args), 1); + $this->redis->clearLastError(); + + $result = $this->redis->evalSha($scriptSha, array_merge([$resource], $args), 1); + if (null !== ($err = $this->redis->getLastError()) && str_starts_with($err, self::NO_SCRIPT_ERROR_MESSAGE_PREFIX)) { + $this->redis->clearLastError(); + + if ($this->redis instanceof \RedisCluster || $this->redis instanceof RelayCluster) { + foreach ($this->redis->_masters() as $master) { + $this->redis->script($master, 'LOAD', $script); + } + } else { + $this->redis->script('LOAD', $script); + } + + if (null !== $err = $this->redis->getLastError()) { + throw new SemaphoreStorageException($err); + } + + $result = $this->redis->evalSha($scriptSha, array_merge([$resource], $args), 1); + } + + if (null !== $err = $this->redis->getLastError()) { + throw new SemaphoreStorageException($err); + } + + return $result; } if ($this->redis instanceof \RedisArray) { - return $this->redis->_instance($this->redis->_target($resource))->eval($script, array_merge([$resource], $args), 1); + $client = $this->redis->_instance($this->redis->_target($resource)); + $client->clearLastError(); + $result = $client->evalSha($scriptSha, array_merge([$resource], $args), 1); + if (null !== ($err = $client->getLastError()) && str_starts_with($err, self::NO_SCRIPT_ERROR_MESSAGE_PREFIX)) { + $client->clearLastError(); + + $client->script('LOAD', $script); + + if (null !== $err = $client->getLastError()) { + throw new SemaphoreStorageException($err); + } + + $result = $client->evalSha($scriptSha, array_merge([$resource], $args), 1); + } + + if (null !== $err = $client->getLastError()) { + throw new SemaphoreStorageException($err); + } + + return $result; } if ($this->redis instanceof \Predis\ClientInterface) { - return $this->redis->eval(...array_merge([$script, 1, $resource], $args)); + try { + return $this->handlePredisError(fn () => $this->redis->evalSha($scriptSha, 1, $resource, ...$args)); + } catch (SemaphoreStorageException $e) { + // Fallthrough only if we need to load the script + if (!str_starts_with($e->getMessage(), self::NO_SCRIPT_ERROR_MESSAGE_PREFIX)) { + throw $e; + } + } + + if ($this->redis->getConnection() instanceof \Predis\Connection\Cluster\ClusterInterface) { + foreach ($this->redis as $connection) { + $this->handlePredisError(fn () => $connection->script('LOAD', $script)); + } + } else { + $this->handlePredisError(fn () => $this->redis->script('LOAD', $script)); + } + + return $this->handlePredisError(fn () => $this->redis->evalSha($scriptSha, 1, $resource, ...$args)); } throw new InvalidArgumentException(\sprintf('"%s()" expects being initialized with a Redis, RedisArray, RedisCluster or Predis\ClientInterface, "%s" given.', __METHOD__, get_debug_type($this->redis))); @@ -183,4 +249,26 @@ private function getUniqueToken(Key $key): string return $key->getState(__CLASS__); } + + /** + * @template T + * + * @param callable(): T $callback + * + * @return T + */ + private function handlePredisError(callable $callback): mixed + { + try { + $result = $callback(); + } catch (\Predis\Response\ServerException $e) { + throw new SemaphoreStorageException($e->getMessage(), $e->getCode(), $e); + } + + if ($result instanceof \Predis\Response\Error) { + throw new SemaphoreStorageException($result->getMessage()); + } + + return $result; + } } pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy