16
16
use Symfony \Component \Semaphore \Exception \InvalidArgumentException ;
17
17
use Symfony \Component \Semaphore \Exception \SemaphoreAcquiringException ;
18
18
use Symfony \Component \Semaphore \Exception \SemaphoreExpiredException ;
19
+ use Symfony \Component \Semaphore \Exception \SemaphoreStorageException ;
19
20
use Symfony \Component \Semaphore \Key ;
20
21
use Symfony \Component \Semaphore \PersistingStoreInterface ;
21
22
27
28
*/
28
29
class RedisStore implements PersistingStoreInterface
29
30
{
31
+ private const NO_SCRIPT_ERROR_MESSAGE_PREFIX = 'NOSCRIPT ' ;
32
+
30
33
public function __construct (
31
34
private \Redis |Relay |RelayCluster |\RedisArray |\RedisCluster |\Predis \ClientInterface $ redis ,
32
35
) {
@@ -159,16 +162,78 @@ public function exists(Key $key): bool
159
162
160
163
private function evaluate (string $ script , string $ resource , array $ args ): mixed
161
164
{
165
+ $ scriptSha = sha1 ($ script );
166
+
162
167
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 ;
164
194
}
165
195
166
196
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 ;
168
217
}
169
218
170
219
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
+ }
172
237
}
173
238
174
239
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
183
248
184
249
return $ key ->getState (__CLASS__ );
185
250
}
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
+ }
186
273
}
0 commit comments