Skip to content

Commit 2de307d

Browse files
committed
[Semaphore] Enabled usage of EVALSHA and LOAD SCRIPT over regular EVAL
1 parent 3f35b88 commit 2de307d

File tree

3 files changed

+116
-3
lines changed

3 files changed

+116
-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: 90 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,78 @@ 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+
try {
230+
$this->handlePredisError(fn () => $this->redis->script('LOAD', $script));
231+
232+
return $this->handlePredisError(fn () => $this->redis->evalSha($scriptSha, 1, $resource, ...$args));
233+
} catch (\Predis\NotSupportedException) {
234+
// If 'script LOAD' isn't supported, fallback to eval
235+
return $this->redis->eval(...array_merge([$script, 1, $resource], $args));
236+
}
172237
}
173238

174239
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 +248,26 @@ private function getUniqueToken(Key $key): string
183248

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

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