Skip to content

Commit b065b9a

Browse files
minor #61162 [Semaphore] Enabled usage of EVALSHA and LOAD SCRIPT over regular EVAL (santysisi)
This PR was merged into the 7.4 branch. Discussion ---------- [Semaphore] Enabled usage of `EVALSHA` and `LOAD SCRIPT` over regular `EVAL` | Q | A | ------------- | --- | Branch? | 7.4 | Bug fix? | no | New feature? | yes | Deprecations? | no | Issues | no | License | MIT This PR updates the Semaphore component to use `EVALSHA` instead of `EVAL` when evaluating Lua scripts, following the same approach introduced in a previous [PR](#58533) for the Lock component. Reusing the cached `SHA` improves performance and avoids sending the full script each time. Commits ------- 4afc680 [Semaphore] Enabled usage of `EVALSHA` and `LOAD SCRIPT` over regular `EVAL`
2 parents 5db13d5 + 4afc680 commit b065b9a

File tree

3 files changed

+117
-3
lines changed

3 files changed

+117
-3
lines changed

src/Symfony/Component/Semaphore/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
CHANGELOG
22
=========
33

4+
7.4
5+
---
6+
7+
* RedisStore uses `EVALSHA` over `EVAL` when evaluating LUA scripts
8+
49
7.3
510
---
611

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Semaphore\Exception;
13+
14+
/**
15+
* SemaphoreStorageException is thrown when an issue happens during the manipulation of a semaphore in a store.
16+
*
17+
* @author Santiago San Martin <sanmartindev@gmail.com>
18+
*/
19+
class SemaphoreStorageException extends \RuntimeException implements ExceptionInterface
20+
{
21+
}

src/Symfony/Component/Semaphore/Store/RedisStore.php

Lines changed: 91 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
use Symfony\Component\Semaphore\Exception\InvalidArgumentException;
1717
use Symfony\Component\Semaphore\Exception\SemaphoreAcquiringException;
1818
use Symfony\Component\Semaphore\Exception\SemaphoreExpiredException;
19+
use Symfony\Component\Semaphore\Exception\SemaphoreStorageException;
1920
use Symfony\Component\Semaphore\Key;
2021
use Symfony\Component\Semaphore\PersistingStoreInterface;
2122

@@ -27,6 +28,8 @@
2728
*/
2829
class RedisStore implements PersistingStoreInterface
2930
{
31+
private const NO_SCRIPT_ERROR_MESSAGE_PREFIX = 'NOSCRIPT';
32+
3033
public function __construct(
3134
private \Redis|Relay|RelayCluster|\RedisArray|\RedisCluster|\Predis\ClientInterface $redis,
3235
) {
@@ -159,16 +162,79 @@ public function exists(Key $key): bool
159162

160163
private function evaluate(string $script, string $resource, array $args): mixed
161164
{
165+
$scriptSha = sha1($script);
166+
162167
if ($this->redis instanceof \Redis || $this->redis instanceof Relay || $this->redis instanceof RelayCluster || $this->redis instanceof \RedisCluster) {
163-
return $this->redis->eval($script, array_merge([$resource], $args), 1);
168+
$this->redis->clearLastError();
169+
170+
$result = $this->redis->evalSha($scriptSha, array_merge([$resource], $args), 1);
171+
if (null !== ($err = $this->redis->getLastError()) && str_starts_with($err, self::NO_SCRIPT_ERROR_MESSAGE_PREFIX)) {
172+
$this->redis->clearLastError();
173+
174+
if ($this->redis instanceof \RedisCluster || $this->redis instanceof RelayCluster) {
175+
foreach ($this->redis->_masters() as $master) {
176+
$this->redis->script($master, 'LOAD', $script);
177+
}
178+
} else {
179+
$this->redis->script('LOAD', $script);
180+
}
181+
182+
if (null !== $err = $this->redis->getLastError()) {
183+
throw new SemaphoreStorageException($err);
184+
}
185+
186+
$result = $this->redis->evalSha($scriptSha, array_merge([$resource], $args), 1);
187+
}
188+
189+
if (null !== $err = $this->redis->getLastError()) {
190+
throw new SemaphoreStorageException($err);
191+
}
192+
193+
return $result;
164194
}
165195

166196
if ($this->redis instanceof \RedisArray) {
167-
return $this->redis->_instance($this->redis->_target($resource))->eval($script, array_merge([$resource], $args), 1);
197+
$client = $this->redis->_instance($this->redis->_target($resource));
198+
$client->clearLastError();
199+
$result = $client->evalSha($scriptSha, array_merge([$resource], $args), 1);
200+
if (null !== ($err = $client->getLastError()) && str_starts_with($err, self::NO_SCRIPT_ERROR_MESSAGE_PREFIX)) {
201+
$client->clearLastError();
202+
203+
$client->script('LOAD', $script);
204+
205+
if (null !== $err = $client->getLastError()) {
206+
throw new SemaphoreStorageException($err);
207+
}
208+
209+
$result = $client->evalSha($scriptSha, array_merge([$resource], $args), 1);
210+
}
211+
212+
if (null !== $err = $client->getLastError()) {
213+
throw new SemaphoreStorageException($err);
214+
}
215+
216+
return $result;
168217
}
169218

170219
if ($this->redis instanceof \Predis\ClientInterface) {
171-
return $this->redis->eval(...array_merge([$script, 1, $resource], $args));
220+
try {
221+
return $this->handlePredisError(fn () => $this->redis->evalSha($scriptSha, 1, $resource, ...$args));
222+
} catch (SemaphoreStorageException $e) {
223+
// Fallthrough only if we need to load the script
224+
if (!str_starts_with($e->getMessage(), self::NO_SCRIPT_ERROR_MESSAGE_PREFIX)) {
225+
throw $e;
226+
}
227+
}
228+
229+
if ($this->redis->getConnection() instanceof \Predis\Connection\Cluster\ClusterInterface) {
230+
foreach ($this->redis as $connection) {
231+
$this->handlePredisError(fn () => $connection->script('LOAD', $script));
232+
}
233+
} else {
234+
$this->handlePredisError(fn () => $this->redis->script('LOAD', $script));
235+
}
236+
237+
return $this->handlePredisError(fn () => $this->redis->evalSha($scriptSha, 1, $resource, ...$args));
172238
}
173239

174240
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
183249

184250
return $key->getState(__CLASS__);
185251
}
252+
253+
/**
254+
* @template T
255+
*
256+
* @param callable(): T $callback
257+
*
258+
* @return T
259+
*/
260+
private function handlePredisError(callable $callback): mixed
261+
{
262+
try {
263+
$result = $callback();
264+
} catch (\Predis\Response\ServerException $e) {
265+
throw new SemaphoreStorageException($e->getMessage(), $e->getCode(), $e);
266+
}
267+
268+
if ($result instanceof \Predis\Response\Error) {
269+
throw new SemaphoreStorageException($result->getMessage());
270+
}
271+
272+
return $result;
273+
}
186274
}

0 commit comments

Comments
 (0)
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