1<?php
2
3/*
4 * This file is part of the Predis package.
5 *
6 * (c) Daniele Alessandri <suppakilla@gmail.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
12namespace Predis\Cluster;
13
14use Predis\Command\CommandInterface;
15use Predis\Command\ScriptCommand;
16
17/**
18 * Common class implementing the logic needed to support clustering strategies.
19 *
20 * @author Daniele Alessandri <suppakilla@gmail.com>
21 */
22abstract class ClusterStrategy implements StrategyInterface
23{
24    protected $commands;
25
26    /**
27     *
28     */
29    public function __construct()
30    {
31        $this->commands = $this->getDefaultCommands();
32    }
33
34    /**
35     * Returns the default map of supported commands with their handlers.
36     *
37     * @return array
38     */
39    protected function getDefaultCommands()
40    {
41        $getKeyFromFirstArgument = array($this, 'getKeyFromFirstArgument');
42        $getKeyFromAllArguments = array($this, 'getKeyFromAllArguments');
43
44        return array(
45            /* commands operating on the key space */
46            'EXISTS' => $getKeyFromAllArguments,
47            'DEL' => $getKeyFromAllArguments,
48            'TYPE' => $getKeyFromFirstArgument,
49            'EXPIRE' => $getKeyFromFirstArgument,
50            'EXPIREAT' => $getKeyFromFirstArgument,
51            'PERSIST' => $getKeyFromFirstArgument,
52            'PEXPIRE' => $getKeyFromFirstArgument,
53            'PEXPIREAT' => $getKeyFromFirstArgument,
54            'TTL' => $getKeyFromFirstArgument,
55            'PTTL' => $getKeyFromFirstArgument,
56            'SORT' => array($this, 'getKeyFromSortCommand'),
57            'DUMP' => $getKeyFromFirstArgument,
58            'RESTORE' => $getKeyFromFirstArgument,
59
60            /* commands operating on string values */
61            'APPEND' => $getKeyFromFirstArgument,
62            'DECR' => $getKeyFromFirstArgument,
63            'DECRBY' => $getKeyFromFirstArgument,
64            'GET' => $getKeyFromFirstArgument,
65            'GETBIT' => $getKeyFromFirstArgument,
66            'MGET' => $getKeyFromAllArguments,
67            'SET' => $getKeyFromFirstArgument,
68            'GETRANGE' => $getKeyFromFirstArgument,
69            'GETSET' => $getKeyFromFirstArgument,
70            'INCR' => $getKeyFromFirstArgument,
71            'INCRBY' => $getKeyFromFirstArgument,
72            'INCRBYFLOAT' => $getKeyFromFirstArgument,
73            'SETBIT' => $getKeyFromFirstArgument,
74            'SETEX' => $getKeyFromFirstArgument,
75            'MSET' => array($this, 'getKeyFromInterleavedArguments'),
76            'MSETNX' => array($this, 'getKeyFromInterleavedArguments'),
77            'SETNX' => $getKeyFromFirstArgument,
78            'SETRANGE' => $getKeyFromFirstArgument,
79            'STRLEN' => $getKeyFromFirstArgument,
80            'SUBSTR' => $getKeyFromFirstArgument,
81            'BITOP' => array($this, 'getKeyFromBitOp'),
82            'BITCOUNT' => $getKeyFromFirstArgument,
83            'BITFIELD' => $getKeyFromFirstArgument,
84
85            /* commands operating on lists */
86            'LINSERT' => $getKeyFromFirstArgument,
87            'LINDEX' => $getKeyFromFirstArgument,
88            'LLEN' => $getKeyFromFirstArgument,
89            'LPOP' => $getKeyFromFirstArgument,
90            'RPOP' => $getKeyFromFirstArgument,
91            'RPOPLPUSH' => $getKeyFromAllArguments,
92            'BLPOP' => array($this, 'getKeyFromBlockingListCommands'),
93            'BRPOP' => array($this, 'getKeyFromBlockingListCommands'),
94            'BRPOPLPUSH' => array($this, 'getKeyFromBlockingListCommands'),
95            'LPUSH' => $getKeyFromFirstArgument,
96            'LPUSHX' => $getKeyFromFirstArgument,
97            'RPUSH' => $getKeyFromFirstArgument,
98            'RPUSHX' => $getKeyFromFirstArgument,
99            'LRANGE' => $getKeyFromFirstArgument,
100            'LREM' => $getKeyFromFirstArgument,
101            'LSET' => $getKeyFromFirstArgument,
102            'LTRIM' => $getKeyFromFirstArgument,
103
104            /* commands operating on sets */
105            'SADD' => $getKeyFromFirstArgument,
106            'SCARD' => $getKeyFromFirstArgument,
107            'SDIFF' => $getKeyFromAllArguments,
108            'SDIFFSTORE' => $getKeyFromAllArguments,
109            'SINTER' => $getKeyFromAllArguments,
110            'SINTERSTORE' => $getKeyFromAllArguments,
111            'SUNION' => $getKeyFromAllArguments,
112            'SUNIONSTORE' => $getKeyFromAllArguments,
113            'SISMEMBER' => $getKeyFromFirstArgument,
114            'SMEMBERS' => $getKeyFromFirstArgument,
115            'SSCAN' => $getKeyFromFirstArgument,
116            'SPOP' => $getKeyFromFirstArgument,
117            'SRANDMEMBER' => $getKeyFromFirstArgument,
118            'SREM' => $getKeyFromFirstArgument,
119
120            /* commands operating on sorted sets */
121            'ZADD' => $getKeyFromFirstArgument,
122            'ZCARD' => $getKeyFromFirstArgument,
123            'ZCOUNT' => $getKeyFromFirstArgument,
124            'ZINCRBY' => $getKeyFromFirstArgument,
125            'ZINTERSTORE' => array($this, 'getKeyFromZsetAggregationCommands'),
126            'ZRANGE' => $getKeyFromFirstArgument,
127            'ZRANGEBYSCORE' => $getKeyFromFirstArgument,
128            'ZRANK' => $getKeyFromFirstArgument,
129            'ZREM' => $getKeyFromFirstArgument,
130            'ZREMRANGEBYRANK' => $getKeyFromFirstArgument,
131            'ZREMRANGEBYSCORE' => $getKeyFromFirstArgument,
132            'ZREVRANGE' => $getKeyFromFirstArgument,
133            'ZREVRANGEBYSCORE' => $getKeyFromFirstArgument,
134            'ZREVRANK' => $getKeyFromFirstArgument,
135            'ZSCORE' => $getKeyFromFirstArgument,
136            'ZUNIONSTORE' => array($this, 'getKeyFromZsetAggregationCommands'),
137            'ZSCAN' => $getKeyFromFirstArgument,
138            'ZLEXCOUNT' => $getKeyFromFirstArgument,
139            'ZRANGEBYLEX' => $getKeyFromFirstArgument,
140            'ZREMRANGEBYLEX' => $getKeyFromFirstArgument,
141            'ZREVRANGEBYLEX' => $getKeyFromFirstArgument,
142
143            /* commands operating on hashes */
144            'HDEL' => $getKeyFromFirstArgument,
145            'HEXISTS' => $getKeyFromFirstArgument,
146            'HGET' => $getKeyFromFirstArgument,
147            'HGETALL' => $getKeyFromFirstArgument,
148            'HMGET' => $getKeyFromFirstArgument,
149            'HMSET' => $getKeyFromFirstArgument,
150            'HINCRBY' => $getKeyFromFirstArgument,
151            'HINCRBYFLOAT' => $getKeyFromFirstArgument,
152            'HKEYS' => $getKeyFromFirstArgument,
153            'HLEN' => $getKeyFromFirstArgument,
154            'HSET' => $getKeyFromFirstArgument,
155            'HSETNX' => $getKeyFromFirstArgument,
156            'HVALS' => $getKeyFromFirstArgument,
157            'HSCAN' => $getKeyFromFirstArgument,
158            'HSTRLEN' => $getKeyFromFirstArgument,
159
160            /* commands operating on HyperLogLog */
161            'PFADD' => $getKeyFromFirstArgument,
162            'PFCOUNT' => $getKeyFromAllArguments,
163            'PFMERGE' => $getKeyFromAllArguments,
164
165            /* scripting */
166            'EVAL' => array($this, 'getKeyFromScriptingCommands'),
167            'EVALSHA' => array($this, 'getKeyFromScriptingCommands'),
168
169            /* commands performing geospatial operations */
170            'GEOADD' => $getKeyFromFirstArgument,
171            'GEOHASH' => $getKeyFromFirstArgument,
172            'GEOPOS' => $getKeyFromFirstArgument,
173            'GEODIST' => $getKeyFromFirstArgument,
174            'GEORADIUS' => array($this, 'getKeyFromGeoradiusCommands'),
175            'GEORADIUSBYMEMBER' => array($this, 'getKeyFromGeoradiusCommands'),
176        );
177    }
178
179    /**
180     * Returns the list of IDs for the supported commands.
181     *
182     * @return array
183     */
184    public function getSupportedCommands()
185    {
186        return array_keys($this->commands);
187    }
188
189    /**
190     * Sets an handler for the specified command ID.
191     *
192     * The signature of the callback must have a single parameter of type
193     * Predis\Command\CommandInterface.
194     *
195     * When the callback argument is omitted or NULL, the previously associated
196     * handler for the specified command ID is removed.
197     *
198     * @param string $commandID Command ID.
199     * @param mixed  $callback  A valid callable object, or NULL to unset the handler.
200     *
201     * @throws \InvalidArgumentException
202     */
203    public function setCommandHandler($commandID, $callback = null)
204    {
205        $commandID = strtoupper($commandID);
206
207        if (!isset($callback)) {
208            unset($this->commands[$commandID]);
209
210            return;
211        }
212
213        if (!is_callable($callback)) {
214            throw new \InvalidArgumentException(
215                'The argument must be a callable object or NULL.'
216            );
217        }
218
219        $this->commands[$commandID] = $callback;
220    }
221
222    /**
223     * Extracts the key from the first argument of a command instance.
224     *
225     * @param CommandInterface $command Command instance.
226     *
227     * @return string
228     */
229    protected function getKeyFromFirstArgument(CommandInterface $command)
230    {
231        return $command->getArgument(0);
232    }
233
234    /**
235     * Extracts the key from a command with multiple keys only when all keys in
236     * the arguments array produce the same hash.
237     *
238     * @param CommandInterface $command Command instance.
239     *
240     * @return string|null
241     */
242    protected function getKeyFromAllArguments(CommandInterface $command)
243    {
244        $arguments = $command->getArguments();
245
246        if ($this->checkSameSlotForKeys($arguments)) {
247            return $arguments[0];
248        }
249    }
250
251    /**
252     * Extracts the key from a command with multiple keys only when all keys in
253     * the arguments array produce the same hash.
254     *
255     * @param CommandInterface $command Command instance.
256     *
257     * @return string|null
258     */
259    protected function getKeyFromInterleavedArguments(CommandInterface $command)
260    {
261        $arguments = $command->getArguments();
262        $keys = array();
263
264        for ($i = 0; $i < count($arguments); $i += 2) {
265            $keys[] = $arguments[$i];
266        }
267
268        if ($this->checkSameSlotForKeys($keys)) {
269            return $arguments[0];
270        }
271    }
272
273    /**
274     * Extracts the key from SORT command.
275     *
276     * @param CommandInterface $command Command instance.
277     *
278     * @return string|null
279     */
280    protected function getKeyFromSortCommand(CommandInterface $command)
281    {
282        $arguments = $command->getArguments();
283        $firstKey = $arguments[0];
284
285        if (1 === $argc = count($arguments)) {
286            return $firstKey;
287        }
288
289        $keys = array($firstKey);
290
291        for ($i = 1; $i < $argc; ++$i) {
292            if (strtoupper($arguments[$i]) === 'STORE') {
293                $keys[] = $arguments[++$i];
294            }
295        }
296
297        if ($this->checkSameSlotForKeys($keys)) {
298            return $firstKey;
299        }
300    }
301
302    /**
303     * Extracts the key from BLPOP and BRPOP commands.
304     *
305     * @param CommandInterface $command Command instance.
306     *
307     * @return string|null
308     */
309    protected function getKeyFromBlockingListCommands(CommandInterface $command)
310    {
311        $arguments = $command->getArguments();
312
313        if ($this->checkSameSlotForKeys(array_slice($arguments, 0, count($arguments) - 1))) {
314            return $arguments[0];
315        }
316    }
317
318    /**
319     * Extracts the key from BITOP command.
320     *
321     * @param CommandInterface $command Command instance.
322     *
323     * @return string|null
324     */
325    protected function getKeyFromBitOp(CommandInterface $command)
326    {
327        $arguments = $command->getArguments();
328
329        if ($this->checkSameSlotForKeys(array_slice($arguments, 1, count($arguments)))) {
330            return $arguments[1];
331        }
332    }
333
334    /**
335     * Extracts the key from GEORADIUS and GEORADIUSBYMEMBER commands.
336     *
337     * @param CommandInterface $command Command instance.
338     *
339     * @return string|null
340     */
341    protected function getKeyFromGeoradiusCommands(CommandInterface $command)
342    {
343        $arguments = $command->getArguments();
344        $argc = count($arguments);
345        $startIndex = $command->getId() === 'GEORADIUS' ? 5 : 4;
346
347        if ($argc > $startIndex) {
348            $keys = array($arguments[0]);
349
350            for ($i = $startIndex; $i < $argc; ++$i) {
351                $argument = strtoupper($arguments[$i]);
352                if ($argument === 'STORE' || $argument === 'STOREDIST') {
353                    $keys[] = $arguments[++$i];
354                }
355            }
356
357            if ($this->checkSameSlotForKeys($keys)) {
358                return $arguments[0];
359            } else {
360                return;
361            }
362        }
363
364        return $arguments[0];
365    }
366
367    /**
368     * Extracts the key from ZINTERSTORE and ZUNIONSTORE commands.
369     *
370     * @param CommandInterface $command Command instance.
371     *
372     * @return string|null
373     */
374    protected function getKeyFromZsetAggregationCommands(CommandInterface $command)
375    {
376        $arguments = $command->getArguments();
377        $keys = array_merge(array($arguments[0]), array_slice($arguments, 2, $arguments[1]));
378
379        if ($this->checkSameSlotForKeys($keys)) {
380            return $arguments[0];
381        }
382    }
383
384    /**
385     * Extracts the key from EVAL and EVALSHA commands.
386     *
387     * @param CommandInterface $command Command instance.
388     *
389     * @return string|null
390     */
391    protected function getKeyFromScriptingCommands(CommandInterface $command)
392    {
393        if ($command instanceof ScriptCommand) {
394            $keys = $command->getKeys();
395        } else {
396            $keys = array_slice($args = $command->getArguments(), 2, $args[1]);
397        }
398
399        if ($keys && $this->checkSameSlotForKeys($keys)) {
400            return $keys[0];
401        }
402    }
403
404    /**
405     * {@inheritdoc}
406     */
407    public function getSlot(CommandInterface $command)
408    {
409        $slot = $command->getSlot();
410
411        if (!isset($slot) && isset($this->commands[$cmdID = $command->getId()])) {
412            $key = call_user_func($this->commands[$cmdID], $command);
413
414            if (isset($key)) {
415                $slot = $this->getSlotByKey($key);
416                $command->setSlot($slot);
417            }
418        }
419
420        return $slot;
421    }
422
423    /**
424     * Checks if the specified array of keys will generate the same hash.
425     *
426     * @param array $keys Array of keys.
427     *
428     * @return bool
429     */
430    protected function checkSameSlotForKeys(array $keys)
431    {
432        if (!$count = count($keys)) {
433            return false;
434        }
435
436        $currentSlot = $this->getSlotByKey($keys[0]);
437
438        for ($i = 1; $i < $count; ++$i) {
439            $nextSlot = $this->getSlotByKey($keys[$i]);
440
441            if ($currentSlot !== $nextSlot) {
442                return false;
443            }
444
445            $currentSlot = $nextSlot;
446        }
447
448        return true;
449    }
450
451    /**
452     * Returns only the hashable part of a key (delimited by "{...}"), or the
453     * whole key if a key tag is not found in the string.
454     *
455     * @param string $key A key.
456     *
457     * @return string
458     */
459    protected function extractKeyTag($key)
460    {
461        if (false !== $start = strpos($key, '{')) {
462            if (false !== ($end = strpos($key, '}', $start)) && $end !== ++$start) {
463                $key = substr($key, $start, $end - $start);
464            }
465        }
466
467        return $key;
468    }
469}
470