Skip to content

Commit b065b9a

Browse files
minor symfony#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](symfony#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 <[email protected]>
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 <[email protected]>
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)