1616use Symfony \Component \Semaphore \Exception \InvalidArgumentException ;
1717use Symfony \Component \Semaphore \Exception \SemaphoreAcquiringException ;
1818use Symfony \Component \Semaphore \Exception \SemaphoreExpiredException ;
19+ use Symfony \Component \Semaphore \Exception \SemaphoreStorageException ;
1920use Symfony \Component \Semaphore \Key ;
2021use Symfony \Component \Semaphore \PersistingStoreInterface ;
2122
2728 */
2829class 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