Browse Source

Redesign the whole internals for clustering.

This change actually had a positive impact on the design of the whole
internals for clustering which is now cleaner and easier to maintain.
It is still far from perfect, but we also have to keep performances in
consideration so we can say that we ended up with a good compromise.

Previously Predis assigned an hash to each command instance which was
computed from its key, now we changed approach and the library caches
the slot assigned to each command. This works for both our client-side
sharding cluster and the upcoming redis-cluster, but the former is the
one that needed most changes.

The PredisCluster aggregate connection now only takes an instance of
StrategyInterface, which in turn wraps the chosen distributor. After
all, in order to be able to calculate the assigned slot for a command
or key, the cluster strategy must have access to the distributor that
manages the distribution of the whole keyspace. Nothing really changes
in terms of configurability as it is still possible to decide which
distributor to use for client-side sharding, it is simply different:

  $distributor = new Predis\Cluster\Distribution\KetamaRing();
  $strategy = new Predis\Cluster\PredisStrategy($distributor);
  $cluster = new Predis\Connection\Aggregate\PredisCluster($strategy);

As for the RedisCluster aggregate connection, the only change is that
the mathematical operation of calculating the assigned slot from a key
has been completely moved inside the cluster strategy instance.

The strategy for redis-cluster does not use external distributors so
trying to StrategyInterface::getDistributor() will throw an exception.
This may change in future releases, but this is not a priority since
redis-cluster relies on a fixed, well-defined distribution mechanism.
Daniele Alessandri 10 years ago
parent
commit
2ff8c37104

+ 35 - 5
examples/custom_cluster_distributor.php

@@ -15,9 +15,10 @@ require __DIR__.'/shared.php';
 // their own distributors used by the client to distribute keys among a cluster
 // of servers.
 
-use Predis\Connection\Aggregate\PredisCluster;
+use Predis\Cluster\PredisStrategy;
 use Predis\Cluster\Distributor\DistributorInterface;
 use Predis\Cluster\Hash\HashGeneratorInterface;
+use Predis\Connection\Aggregate\PredisCluster;
 
 class NaiveDistributor implements DistributorInterface, HashGeneratorInterface
 {
@@ -45,13 +46,36 @@ class NaiveDistributor implements DistributorInterface, HashGeneratorInterface
         $this->nodesCount = count($this->nodes);
     }
 
-    public function get($key)
+    public function getSlot($hash)
+    {
+        return $this->nodesCount > 1 ? abs($hash % $this->nodesCount) : 0;
+    }
+
+    public function getBySlot($slot)
+    {
+        if (isset($this->nodes[$slot])) {
+            return $this->nodes[$slot];
+        }
+    }
+
+    public function getByHash($hash)
     {
-        if (0 === $count = $this->nodesCount) {
+        if (!$this->nodesCount) {
             throw new RuntimeException('No connections.');
         }
 
-        return $this->nodes[$count > 1 ? abs($key % $count) : 0];
+        $slot = $this->getSlot($hash);
+        $node = $this->getBySlot($slot);
+
+        return $node;
+    }
+
+    public function get($value)
+    {
+        $hash = $this->hash($value);
+        $node = $this->getByHash($hash);
+
+        return $node;
     }
 
     public function hash($value)
@@ -68,7 +92,8 @@ class NaiveDistributor implements DistributorInterface, HashGeneratorInterface
 $options = array(
     'cluster' => function () {
         $distributor = new NaiveDistributor();
-        $cluster = new PredisCluster($distributor);
+        $strategy = new PredisStrategy($distributor);
+        $cluster = new PredisCluster($strategy);
 
         return $cluster;
     },
@@ -84,6 +109,11 @@ for ($i = 0; $i < 100; $i++) {
 $server1 = $client->getClientFor('first')->info();
 $server2 = $client->getClientFor('second')->info();
 
+if (isset($server1['Keyspace'], $server2['Keyspace'])) {
+    $server1 = $server1['Keyspace'];
+    $server2 = $server2['Keyspace'];
+}
+
 printf("Server '%s' has %d keys while server '%s' has %d keys.\n",
     'first', $server1['db15']['keys'], 'second', $server2['db15']['keys']
 );

+ 396 - 0
src/Cluster/ClusterStrategy.php

@@ -0,0 +1,396 @@
+<?php
+
+/*
+ * This file is part of the Predis package.
+ *
+ * (c) Daniele Alessandri <suppakilla@gmail.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Predis\Cluster;
+
+use InvalidArgumentException;
+use Predis\Command\CommandInterface;
+use Predis\Command\ScriptCommand;
+
+/**
+ * Common class implementing the logic needed to support clustering strategies.
+ *
+ * @author Daniele Alessandri <suppakilla@gmail.com>
+ */
+abstract class ClusterStrategy implements StrategyInterface
+{
+    protected $commands;
+
+    /**
+     *
+     */
+    public function __construct()
+    {
+        $this->commands = $this->getDefaultCommands();
+    }
+
+    /**
+     * Returns the default map of supported commands with their handlers.
+     *
+     * @return array
+     */
+    protected function getDefaultCommands()
+    {
+        $getKeyFromFirstArgument = array($this, 'getKeyFromFirstArgument');
+        $getKeyFromAllArguments = array($this, 'getKeyFromAllArguments');
+
+        return array(
+            /* commands operating on the key space */
+            'EXISTS'                => $getKeyFromFirstArgument,
+            'DEL'                   => $getKeyFromAllArguments,
+            'TYPE'                  => $getKeyFromFirstArgument,
+            'EXPIRE'                => $getKeyFromFirstArgument,
+            'EXPIREAT'              => $getKeyFromFirstArgument,
+            'PERSIST'               => $getKeyFromFirstArgument,
+            'PEXPIRE'               => $getKeyFromFirstArgument,
+            'PEXPIREAT'             => $getKeyFromFirstArgument,
+            'TTL'                   => $getKeyFromFirstArgument,
+            'PTTL'                  => $getKeyFromFirstArgument,
+            'SORT'                  => $getKeyFromFirstArgument, // TODO
+            'DUMP'                  => $getKeyFromFirstArgument,
+            'RESTORE'               => $getKeyFromFirstArgument,
+
+            /* commands operating on string values */
+            'APPEND'                => $getKeyFromFirstArgument,
+            'DECR'                  => $getKeyFromFirstArgument,
+            'DECRBY'                => $getKeyFromFirstArgument,
+            'GET'                   => $getKeyFromFirstArgument,
+            'GETBIT'                => $getKeyFromFirstArgument,
+            'MGET'                  => $getKeyFromAllArguments,
+            'SET'                   => $getKeyFromFirstArgument,
+            'GETRANGE'              => $getKeyFromFirstArgument,
+            'GETSET'                => $getKeyFromFirstArgument,
+            'INCR'                  => $getKeyFromFirstArgument,
+            'INCRBY'                => $getKeyFromFirstArgument,
+            'INCRBYFLOAT'           => $getKeyFromFirstArgument,
+            'SETBIT'                => $getKeyFromFirstArgument,
+            'SETEX'                 => $getKeyFromFirstArgument,
+            'MSET'                  => array($this, 'getKeyFromInterleavedArguments'),
+            'MSETNX'                => array($this, 'getKeyFromInterleavedArguments'),
+            'SETNX'                 => $getKeyFromFirstArgument,
+            'SETRANGE'              => $getKeyFromFirstArgument,
+            'STRLEN'                => $getKeyFromFirstArgument,
+            'SUBSTR'                => $getKeyFromFirstArgument,
+            'BITOP'                 => array($this, 'getKeyFromBitOp'),
+            'BITCOUNT'              => $getKeyFromFirstArgument,
+
+            /* commands operating on lists */
+            'LINSERT'               => $getKeyFromFirstArgument,
+            'LINDEX'                => $getKeyFromFirstArgument,
+            'LLEN'                  => $getKeyFromFirstArgument,
+            'LPOP'                  => $getKeyFromFirstArgument,
+            'RPOP'                  => $getKeyFromFirstArgument,
+            'RPOPLPUSH'             => $getKeyFromAllArguments,
+            'BLPOP'                 => array($this, 'getKeyFromBlockingListCommands'),
+            'BRPOP'                 => array($this, 'getKeyFromBlockingListCommands'),
+            'BRPOPLPUSH'            => array($this, 'getKeyFromBlockingListCommands'),
+            'LPUSH'                 => $getKeyFromFirstArgument,
+            'LPUSHX'                => $getKeyFromFirstArgument,
+            'RPUSH'                 => $getKeyFromFirstArgument,
+            'RPUSHX'                => $getKeyFromFirstArgument,
+            'LRANGE'                => $getKeyFromFirstArgument,
+            'LREM'                  => $getKeyFromFirstArgument,
+            'LSET'                  => $getKeyFromFirstArgument,
+            'LTRIM'                 => $getKeyFromFirstArgument,
+
+            /* commands operating on sets */
+            'SADD'                  => $getKeyFromFirstArgument,
+            'SCARD'                 => $getKeyFromFirstArgument,
+            'SDIFF'                 => $getKeyFromAllArguments,
+            'SDIFFSTORE'            => $getKeyFromAllArguments,
+            'SINTER'                => $getKeyFromAllArguments,
+            'SINTERSTORE'           => $getKeyFromAllArguments,
+            'SUNION'                => $getKeyFromAllArguments,
+            'SUNIONSTORE'           => $getKeyFromAllArguments,
+            'SISMEMBER'             => $getKeyFromFirstArgument,
+            'SMEMBERS'              => $getKeyFromFirstArgument,
+            'SSCAN'                 => $getKeyFromFirstArgument,
+            'SPOP'                  => $getKeyFromFirstArgument,
+            'SRANDMEMBER'           => $getKeyFromFirstArgument,
+            'SREM'                  => $getKeyFromFirstArgument,
+
+            /* commands operating on sorted sets */
+            'ZADD'                  => $getKeyFromFirstArgument,
+            'ZCARD'                 => $getKeyFromFirstArgument,
+            'ZCOUNT'                => $getKeyFromFirstArgument,
+            'ZINCRBY'               => $getKeyFromFirstArgument,
+            'ZINTERSTORE'           => array($this, 'getKeyFromZsetAggregationCommands'),
+            'ZRANGE'                => $getKeyFromFirstArgument,
+            'ZRANGEBYSCORE'         => $getKeyFromFirstArgument,
+            'ZRANK'                 => $getKeyFromFirstArgument,
+            'ZREM'                  => $getKeyFromFirstArgument,
+            'ZREMRANGEBYRANK'       => $getKeyFromFirstArgument,
+            'ZREMRANGEBYSCORE'      => $getKeyFromFirstArgument,
+            'ZREVRANGE'             => $getKeyFromFirstArgument,
+            'ZREVRANGEBYSCORE'      => $getKeyFromFirstArgument,
+            'ZREVRANK'              => $getKeyFromFirstArgument,
+            'ZSCORE'                => $getKeyFromFirstArgument,
+            'ZUNIONSTORE'           => array($this, 'getKeyFromZsetAggregationCommands'),
+            'ZSCAN'                 => $getKeyFromFirstArgument,
+            'ZLEXCOUNT'             => $getKeyFromFirstArgument,
+            'ZRANGEBYLEX'           => $getKeyFromFirstArgument,
+            'ZREMRANGEBYLEX'        => $getKeyFromFirstArgument,
+
+            /* commands operating on hashes */
+            'HDEL'                  => $getKeyFromFirstArgument,
+            'HEXISTS'               => $getKeyFromFirstArgument,
+            'HGET'                  => $getKeyFromFirstArgument,
+            'HGETALL'               => $getKeyFromFirstArgument,
+            'HMGET'                 => $getKeyFromFirstArgument,
+            'HMSET'                 => $getKeyFromFirstArgument,
+            'HINCRBY'               => $getKeyFromFirstArgument,
+            'HINCRBYFLOAT'          => $getKeyFromFirstArgument,
+            'HKEYS'                 => $getKeyFromFirstArgument,
+            'HLEN'                  => $getKeyFromFirstArgument,
+            'HSET'                  => $getKeyFromFirstArgument,
+            'HSETNX'                => $getKeyFromFirstArgument,
+            'HVALS'                 => $getKeyFromFirstArgument,
+            'HSCAN'                 => $getKeyFromFirstArgument,
+
+            /* commands operating on HyperLogLog */
+            'PFADD'                 => $getKeyFromFirstArgument,
+            'PFCOUNT'               => $getKeyFromAllArguments,
+            'PFMERGE'               => $getKeyFromAllArguments,
+
+            /* scripting */
+            'EVAL'                  => array($this, 'getKeyFromScriptingCommands'),
+            'EVALSHA'               => array($this, 'getKeyFromScriptingCommands'),
+        );
+    }
+
+    /**
+     * Returns the list of IDs for the supported commands.
+     *
+     * @return array
+     */
+    public function getSupportedCommands()
+    {
+        return array_keys($this->commands);
+    }
+
+    /**
+     * Sets an handler for the specified command ID.
+     *
+     * The signature of the callback must have a single parameter of type
+     * Predis\Command\CommandInterface.
+     *
+     * When the callback argument is omitted or NULL, the previously associated
+     * handler for the specified command ID is removed.
+     *
+     * @param string $commandID Command ID.
+     * @param mixed  $callback  A valid callable object, or NULL to unset the handler.
+     */
+    public function setCommandHandler($commandID, $callback = null)
+    {
+        $commandID = strtoupper($commandID);
+
+        if (!isset($callback)) {
+            unset($this->commands[$commandID]);
+
+            return;
+        }
+
+        if (!is_callable($callback)) {
+            throw new InvalidArgumentException(
+                "The argument must be a callable object or NULL."
+            );
+        }
+
+        $this->commands[$commandID] = $callback;
+    }
+
+    /**
+     * Extracts the key from the first argument of a command instance.
+     *
+     * @param  CommandInterface $command Command instance.
+     * @return string
+     */
+    protected function getKeyFromFirstArgument(CommandInterface $command)
+    {
+        return $command->getArgument(0);
+    }
+
+    /**
+     * Extracts the key from a command with multiple keys only when all keys in
+     * the arguments array produce the same hash.
+     *
+     * @param  CommandInterface $command Command instance.
+     * @return string
+     */
+    protected function getKeyFromAllArguments(CommandInterface $command)
+    {
+        $arguments = $command->getArguments();
+
+        if ($this->checkSameSlotForKeys($arguments)) {
+            return $arguments[0];
+        }
+    }
+
+    /**
+     * Extracts the key from a command with multiple keys only when all keys in
+     * the arguments array produce the same hash.
+     *
+     * @param  CommandInterface $command Command instance.
+     * @return string
+     */
+    protected function getKeyFromInterleavedArguments(CommandInterface $command)
+    {
+        $arguments = $command->getArguments();
+        $keys = array();
+
+        for ($i = 0; $i < count($arguments); $i += 2) {
+            $keys[] = $arguments[$i];
+        }
+
+        if ($this->checkSameSlotForKeys($keys)) {
+            return $arguments[0];
+        }
+    }
+
+    /**
+     * Extracts the key from BLPOP and BRPOP commands.
+     *
+     * @param  CommandInterface $command Command instance.
+     * @return string
+     */
+    protected function getKeyFromBlockingListCommands(CommandInterface $command)
+    {
+        $arguments = $command->getArguments();
+
+        if ($this->checkSameSlotForKeys(array_slice($arguments, 0, count($arguments) - 1))) {
+            return $arguments[0];
+        }
+    }
+
+    /**
+     * Extracts the key from BITOP command.
+     *
+     * @param  CommandInterface $command Command instance.
+     * @return string
+     */
+    protected function getKeyFromBitOp(CommandInterface $command)
+    {
+        $arguments = $command->getArguments();
+
+        if ($this->checkSameSlotForKeys(array_slice($arguments, 1, count($arguments)))) {
+            return $arguments[1];
+        }
+    }
+
+    /**
+     * Extracts the key from ZINTERSTORE and ZUNIONSTORE commands.
+     *
+     * @param  CommandInterface $command Command instance.
+     * @return string
+     */
+    protected function getKeyFromZsetAggregationCommands(CommandInterface $command)
+    {
+        $arguments = $command->getArguments();
+        $keys = array_merge(array($arguments[0]), array_slice($arguments, 2, $arguments[1]));
+
+        if ($this->checkSameSlotForKeys($keys)) {
+            return $arguments[0];
+        }
+    }
+
+    /**
+     * Extracts the key from EVAL and EVALSHA commands.
+     *
+     * @param  CommandInterface $command Command instance.
+     * @return string
+     */
+    protected function getKeyFromScriptingCommands(CommandInterface $command)
+    {
+        if ($command instanceof ScriptCommand) {
+            $keys = $command->getKeys();
+        } else {
+            $keys = array_slice($args = $command->getArguments(), 2, $args[1]);
+        }
+
+        if ($keys && $this->checkSameSlotForKeys($keys)) {
+            return $keys[0];
+        }
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getSlot(CommandInterface $command)
+    {
+        $slot = $command->getSlot();
+
+        if (!isset($slot) && isset($this->commands[$cmdID = $command->getId()])) {
+            $key = call_user_func($this->commands[$cmdID], $command);
+
+            if (isset($key)) {
+                $slot = $this->getSlotByKey($key);
+                $command->setSlot($slot);
+            }
+        }
+
+        return $slot;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    abstract public function getSlotByKey($key);
+
+    /**
+     * Checks if the specified array of keys will generate the same hash.
+     *
+     * @param  array $keys Array of keys.
+     * @return bool
+     */
+    protected function checkSameSlotForKeys(array $keys)
+    {
+        if (!$count = count($keys)) {
+            return false;
+        }
+
+        $currentSlot = $this->getSlotByKey($keys[0]);
+
+        for ($i = 1; $i < $count; $i++) {
+            $nextSlot = $this->getSlotByKey($keys[$i]);
+
+            if ($currentSlot !== $nextSlot) {
+                return false;
+            }
+
+            $currentSlot = $nextSlot;
+        }
+
+        return true;
+    }
+
+    /**
+     * Returns only the hashable part of a key (delimited by "{...}"), or the
+     * whole key if a key tag is not found in the string.
+     *
+     * @param  string $key A key.
+     * @return string
+     */
+    protected function extractKeyTag($key)
+    {
+        if (false !== $start = strpos($key, '{')) {
+            if (false !== ($end = strpos($key, '}', $start)) && $end !== ++$start) {
+                $key = substr($key, $start, $end - $start);
+            }
+        }
+
+        return $key;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    abstract public function getDistributor();
+}

+ 27 - 2
src/Cluster/Distributor/DistributorInterface.php

@@ -37,12 +37,37 @@ interface DistributorInterface
     public function remove($node);
 
     /**
-     * Returns a node from the distributor using the computed hash of a key.
+     * Returns the corresponding slot of a node from the distributor using the
+     * computed hash of a key.
      *
      * @param  mixed $key
      * @return mixed
      */
-    public function get($key);
+    public function getSlot($hash);
+
+    /**
+     * Returns a node from the distributor using its assigned slot ID.
+     *
+     * @param  mixed $key
+     * @return mixed
+     */
+    public function getBySlot($slot);
+
+    /**
+     * Returns a node from the distributor using the computed hash of a key.
+     *
+     * @param  mixed $hash
+     * @return mixed
+     */
+    public function getByHash($hash);
+
+    /**
+     * Returns a node from the distributor mapping to the specified value.
+     *
+     * @param  string $value
+     * @return mixed
+     */
+    public function get($value);
 
     /**
      * Returns the underlying hash generator instance.

+ 31 - 13
src/Cluster/Distributor/HashRing.php

@@ -179,10 +179,7 @@ class HashRing implements DistributorInterface, HashGeneratorInterface
     }
 
     /**
-     * Calculates the hash for the specified value.
-     *
-     * @param  string $value Input value.
-     * @return int
+     * {@inheritdoc}
      */
     public function hash($value)
     {
@@ -192,20 +189,30 @@ class HashRing implements DistributorInterface, HashGeneratorInterface
     /**
      * {@inheritdoc}
      */
-    public function get($key)
+    public function getByHash($hash)
     {
-        return $this->ring[$this->getNodeKey($key)];
+        return $this->ring[$this->getSlot($hash)];
     }
 
     /**
-     * Calculates the corrisponding key of a node distributed in the hashring.
-     *
-     * @param  int $key Computed hash of a key.
-     * @return int
+     * {@inheritdoc}
      */
-    private function getNodeKey($key)
+    public function getBySlot($slot)
     {
         $this->initialize();
+
+        if (isset($this->ring[$slot])) {
+            return $this->ring[$slot];
+        }
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getSlot($hash)
+    {
+        $this->initialize();
+
         $ringKeys = $this->ringKeys;
         $upper = $this->ringKeysCount - 1;
         $lower = 0;
@@ -214,9 +221,9 @@ class HashRing implements DistributorInterface, HashGeneratorInterface
             $index = ($lower + $upper) >> 1;
             $item = $ringKeys[$index];
 
-            if ($item > $key) {
+            if ($item > $hash) {
                 $upper = $index - 1;
-            } elseif ($item < $key) {
+            } elseif ($item < $hash) {
                 $lower = $index + 1;
             } else {
                 return $item;
@@ -226,6 +233,17 @@ class HashRing implements DistributorInterface, HashGeneratorInterface
         return $ringKeys[$this->wrapAroundStrategy($upper, $lower, $this->ringKeysCount)];
     }
 
+    /**
+     * {@inheritdoc}
+     */
+    public function get($value)
+    {
+        $hash = $this->hash($value);
+        $node = $this->getByHash($hash);
+
+        return $node;
+    }
+
     /**
      * Implements a strategy to deal with wrap-around errors during binary searches.
      *

+ 18 - 340
src/Cluster/PredisStrategy.php

@@ -11,356 +11,44 @@
 
 namespace Predis\Cluster;
 
-use InvalidArgumentException;
-use Predis\Cluster\Hash\HashGeneratorInterface;
-use Predis\Command\CommandInterface;
-use Predis\Command\ScriptCommand;
+use Predis\Cluster\Distributor\DistributorInterface;
+use Predis\Cluster\Distributor\HashRing;
 
 /**
- * Default class used by Predis for client-side sharding to calculate hashes out
- * of keys for supported commands.
+ * Default cluster strategy used by Predis to handle client-side sharding.
  *
  * @author Daniele Alessandri <suppakilla@gmail.com>
  */
-class PredisStrategy implements StrategyInterface
+class PredisStrategy extends ClusterStrategy
 {
-    protected $commands;
-    protected $hashGenerator;
+    protected $distributor;
 
     /**
-     * @param HashGeneratorInterface $hashGenerator Hash generator instance.
+     * @param DistributorInterface $distributor Optional distributor instance.
      */
-    public function __construct(HashGeneratorInterface $hashGenerator)
+    public function __construct(DistributorInterface $distributor = null)
     {
-        $this->commands = $this->getDefaultCommands();
-        $this->hashGenerator = $hashGenerator;
-    }
-
-    /**
-     * Returns the default map of supported commands with their handlers.
-     *
-     * @return array
-     */
-    protected function getDefaultCommands()
-    {
-        $getKeyFromFirstArgument = array($this, 'getKeyFromFirstArgument');
-        $getKeyFromAllArguments = array($this, 'getKeyFromAllArguments');
-
-        return array(
-            /* commands operating on the key space */
-            'EXISTS'                => $getKeyFromFirstArgument,
-            'DEL'                   => $getKeyFromAllArguments,
-            'TYPE'                  => $getKeyFromFirstArgument,
-            'EXPIRE'                => $getKeyFromFirstArgument,
-            'EXPIREAT'              => $getKeyFromFirstArgument,
-            'PERSIST'               => $getKeyFromFirstArgument,
-            'PEXPIRE'               => $getKeyFromFirstArgument,
-            'PEXPIREAT'             => $getKeyFromFirstArgument,
-            'TTL'                   => $getKeyFromFirstArgument,
-            'PTTL'                  => $getKeyFromFirstArgument,
-            'SORT'                  => $getKeyFromFirstArgument, // TODO
-            'DUMP'                  => $getKeyFromFirstArgument,
-            'RESTORE'               => $getKeyFromFirstArgument,
-
-            /* commands operating on string values */
-            'APPEND'                => $getKeyFromFirstArgument,
-            'DECR'                  => $getKeyFromFirstArgument,
-            'DECRBY'                => $getKeyFromFirstArgument,
-            'GET'                   => $getKeyFromFirstArgument,
-            'GETBIT'                => $getKeyFromFirstArgument,
-            'MGET'                  => $getKeyFromAllArguments,
-            'SET'                   => $getKeyFromFirstArgument,
-            'GETRANGE'              => $getKeyFromFirstArgument,
-            'GETSET'                => $getKeyFromFirstArgument,
-            'INCR'                  => $getKeyFromFirstArgument,
-            'INCRBY'                => $getKeyFromFirstArgument,
-            'INCRBYFLOAT'           => $getKeyFromFirstArgument,
-            'SETBIT'                => $getKeyFromFirstArgument,
-            'SETEX'                 => $getKeyFromFirstArgument,
-            'MSET'                  => array($this, 'getKeyFromInterleavedArguments'),
-            'MSETNX'                => array($this, 'getKeyFromInterleavedArguments'),
-            'SETNX'                 => $getKeyFromFirstArgument,
-            'SETRANGE'              => $getKeyFromFirstArgument,
-            'STRLEN'                => $getKeyFromFirstArgument,
-            'SUBSTR'                => $getKeyFromFirstArgument,
-            'BITOP'                 => array($this, 'getKeyFromBitOp'),
-            'BITCOUNT'              => $getKeyFromFirstArgument,
-
-            /* commands operating on lists */
-            'LINSERT'               => $getKeyFromFirstArgument,
-            'LINDEX'                => $getKeyFromFirstArgument,
-            'LLEN'                  => $getKeyFromFirstArgument,
-            'LPOP'                  => $getKeyFromFirstArgument,
-            'RPOP'                  => $getKeyFromFirstArgument,
-            'RPOPLPUSH'             => $getKeyFromAllArguments,
-            'BLPOP'                 => array($this, 'getKeyFromBlockingListCommands'),
-            'BRPOP'                 => array($this, 'getKeyFromBlockingListCommands'),
-            'BRPOPLPUSH'            => array($this, 'getKeyFromBlockingListCommands'),
-            'LPUSH'                 => $getKeyFromFirstArgument,
-            'LPUSHX'                => $getKeyFromFirstArgument,
-            'RPUSH'                 => $getKeyFromFirstArgument,
-            'RPUSHX'                => $getKeyFromFirstArgument,
-            'LRANGE'                => $getKeyFromFirstArgument,
-            'LREM'                  => $getKeyFromFirstArgument,
-            'LSET'                  => $getKeyFromFirstArgument,
-            'LTRIM'                 => $getKeyFromFirstArgument,
-
-            /* commands operating on sets */
-            'SADD'                  => $getKeyFromFirstArgument,
-            'SCARD'                 => $getKeyFromFirstArgument,
-            'SDIFF'                 => $getKeyFromAllArguments,
-            'SDIFFSTORE'            => $getKeyFromAllArguments,
-            'SINTER'                => $getKeyFromAllArguments,
-            'SINTERSTORE'           => $getKeyFromAllArguments,
-            'SUNION'                => $getKeyFromAllArguments,
-            'SUNIONSTORE'           => $getKeyFromAllArguments,
-            'SISMEMBER'             => $getKeyFromFirstArgument,
-            'SMEMBERS'              => $getKeyFromFirstArgument,
-            'SSCAN'                 => $getKeyFromFirstArgument,
-            'SPOP'                  => $getKeyFromFirstArgument,
-            'SRANDMEMBER'           => $getKeyFromFirstArgument,
-            'SREM'                  => $getKeyFromFirstArgument,
-
-            /* commands operating on sorted sets */
-            'ZADD'                  => $getKeyFromFirstArgument,
-            'ZCARD'                 => $getKeyFromFirstArgument,
-            'ZCOUNT'                => $getKeyFromFirstArgument,
-            'ZINCRBY'               => $getKeyFromFirstArgument,
-            'ZINTERSTORE'           => array($this, 'getKeyFromZsetAggregationCommands'),
-            'ZRANGE'                => $getKeyFromFirstArgument,
-            'ZRANGEBYSCORE'         => $getKeyFromFirstArgument,
-            'ZRANK'                 => $getKeyFromFirstArgument,
-            'ZREM'                  => $getKeyFromFirstArgument,
-            'ZREMRANGEBYRANK'       => $getKeyFromFirstArgument,
-            'ZREMRANGEBYSCORE'      => $getKeyFromFirstArgument,
-            'ZREVRANGE'             => $getKeyFromFirstArgument,
-            'ZREVRANGEBYSCORE'      => $getKeyFromFirstArgument,
-            'ZREVRANK'              => $getKeyFromFirstArgument,
-            'ZSCORE'                => $getKeyFromFirstArgument,
-            'ZUNIONSTORE'           => array($this, 'getKeyFromZsetAggregationCommands'),
-            'ZSCAN'                 => $getKeyFromFirstArgument,
-            'ZLEXCOUNT'             => $getKeyFromFirstArgument,
-            'ZRANGEBYLEX'           => $getKeyFromFirstArgument,
-            'ZREMRANGEBYLEX'        => $getKeyFromFirstArgument,
-
-            /* commands operating on hashes */
-            'HDEL'                  => $getKeyFromFirstArgument,
-            'HEXISTS'               => $getKeyFromFirstArgument,
-            'HGET'                  => $getKeyFromFirstArgument,
-            'HGETALL'               => $getKeyFromFirstArgument,
-            'HMGET'                 => $getKeyFromFirstArgument,
-            'HMSET'                 => $getKeyFromFirstArgument,
-            'HINCRBY'               => $getKeyFromFirstArgument,
-            'HINCRBYFLOAT'          => $getKeyFromFirstArgument,
-            'HKEYS'                 => $getKeyFromFirstArgument,
-            'HLEN'                  => $getKeyFromFirstArgument,
-            'HSET'                  => $getKeyFromFirstArgument,
-            'HSETNX'                => $getKeyFromFirstArgument,
-            'HVALS'                 => $getKeyFromFirstArgument,
-            'HSCAN'                 => $getKeyFromFirstArgument,
-
-            /* commands operating on HyperLogLog */
-            'PFADD'                 => $getKeyFromFirstArgument,
-            'PFCOUNT'               => $getKeyFromAllArguments,
-            'PFMERGE'               => $getKeyFromAllArguments,
-
-            /* scripting */
-            'EVAL'                  => array($this, 'getKeyFromScriptingCommands'),
-            'EVALSHA'               => array($this, 'getKeyFromScriptingCommands'),
-        );
-    }
-
-    /**
-     * Returns the list of IDs for the supported commands.
-     *
-     * @return array
-     */
-    public function getSupportedCommands()
-    {
-        return array_keys($this->commands);
-    }
-
-    /**
-     * Sets an handler for the specified command ID.
-     *
-     * The signature of the callback must have a single parameter of type
-     * Predis\Command\CommandInterface.
-     *
-     * When the callback argument is omitted or NULL, the previously associated
-     * handler for the specified command ID is removed.
-     *
-     * @param string $commandID Command ID.
-     * @param mixed  $callback  A valid callable object, or NULL to unset the handler.
-     */
-    public function setCommandHandler($commandID, $callback = null)
-    {
-        $commandID = strtoupper($commandID);
-
-        if (!isset($callback)) {
-            unset($this->commands[$commandID]);
-
-            return;
-        }
-
-        if (!is_callable($callback)) {
-            throw new InvalidArgumentException(
-                "The argument must be a callable object or NULL."
-            );
-        }
-
-        $this->commands[$commandID] = $callback;
-    }
-
-    /**
-     * Extracts the key from the first argument of a command instance.
-     *
-     * @param  CommandInterface $command Command instance.
-     * @return string
-     */
-    protected function getKeyFromFirstArgument(CommandInterface $command)
-    {
-        return $command->getArgument(0);
-    }
-
-    /**
-     * Extracts the key from a command with multiple keys only when all keys in
-     * the arguments array produce the same hash.
-     *
-     * @param  CommandInterface $command Command instance.
-     * @return string
-     */
-    protected function getKeyFromAllArguments(CommandInterface $command)
-    {
-        $arguments = $command->getArguments();
-
-        if ($this->checkSameHashForKeys($arguments)) {
-            return $arguments[0];
-        }
-    }
-
-    /**
-     * Extracts the key from a command with multiple keys only when all keys in
-     * the arguments array produce the same hash.
-     *
-     * @param  CommandInterface $command Command instance.
-     * @return string
-     */
-    protected function getKeyFromInterleavedArguments(CommandInterface $command)
-    {
-        $arguments = $command->getArguments();
-        $keys = array();
-
-        for ($i = 0; $i < count($arguments); $i += 2) {
-            $keys[] = $arguments[$i];
-        }
-
-        if ($this->checkSameHashForKeys($keys)) {
-            return $arguments[0];
-        }
-    }
-
-    /**
-     * Extracts the key from BLPOP and BRPOP commands.
-     *
-     * @param  CommandInterface $command Command instance.
-     * @return string
-     */
-    protected function getKeyFromBlockingListCommands(CommandInterface $command)
-    {
-        $arguments = $command->getArguments();
-
-        if ($this->checkSameHashForKeys(array_slice($arguments, 0, count($arguments) - 1))) {
-            return $arguments[0];
-        }
-    }
-
-    /**
-     * Extracts the key from BITOP command.
-     *
-     * @param  CommandInterface $command Command instance.
-     * @return string
-     */
-    protected function getKeyFromBitOp(CommandInterface $command)
-    {
-        $arguments = $command->getArguments();
-
-        if ($this->checkSameHashForKeys(array_slice($arguments, 1, count($arguments)))) {
-            return $arguments[1];
-        }
-    }
+        parent::__construct();
 
-    /**
-     * Extracts the key from ZINTERSTORE and ZUNIONSTORE commands.
-     *
-     * @param  CommandInterface $command Command instance.
-     * @return string
-     */
-    protected function getKeyFromZsetAggregationCommands(CommandInterface $command)
-    {
-        $arguments = $command->getArguments();
-        $keys = array_merge(array($arguments[0]), array_slice($arguments, 2, $arguments[1]));
-
-        if ($this->checkSameHashForKeys($keys)) {
-            return $arguments[0];
-        }
-    }
-
-    /**
-     * Extracts the key from EVAL and EVALSHA commands.
-     *
-     * @param  CommandInterface $command Command instance.
-     * @return string
-     */
-    protected function getKeyFromScriptingCommands(CommandInterface $command)
-    {
-        if ($command instanceof ScriptCommand) {
-            $keys = $command->getKeys();
-        } else {
-            $keys = array_slice($args = $command->getArguments(), 2, $args[1]);
-        }
-
-        if ($keys && $this->checkSameHashForKeys($keys)) {
-            return $keys[0];
-        }
+        $this->distributor = $distributor ?: new HashRing();
     }
 
     /**
      * {@inheritdoc}
      */
-    public function getHash(CommandInterface $command)
-    {
-        $hash = $command->getHash();
-
-        if (!isset($hash) && isset($this->commands[$cmdID = $command->getId()])) {
-            $key = call_user_func($this->commands[$cmdID], $command);
-
-            if (isset($key)) {
-                $hash = $this->getKeyHash($key);
-                $command->setHash($hash);
-            }
-        }
-
-        return $hash;
-    }
-
-    /**
-     * {@inheritdoc}
-     */
-    public function getKeyHash($key)
+    public function getSlotByKey($key)
     {
         $key = $this->extractKeyTag($key);
-        $hash = $this->hashGenerator->hash($key);
+        $hash = $this->distributor->hash($key);
+        $slot = $this->distributor->getSlot($hash);
 
-        return $hash;
+        return $slot;
     }
 
     /**
-     * Checks if the specified array of keys will generate the same hash.
-     *
-     * @param  array $keys Array of keys.
-     * @return bool
+     * {@inheritdoc}
      */
-    protected function checkSameHashForKeys(array $keys)
+    protected function checkSameSlotForKeys(array $keys)
     {
         if (!$count = count($keys)) {
             return false;
@@ -382,20 +70,10 @@ class PredisStrategy implements StrategyInterface
     }
 
     /**
-     * Returns only the hashable part of a key (delimited by "{...}"), or the
-     * whole key if a key tag is not found in the string.
-     *
-     * @param  string $key A key.
-     * @return string
+     * {@inheritdoc}
      */
-    protected function extractKeyTag($key)
+    public function getDistributor()
     {
-        if (false !== $start = strpos($key, '{')) {
-            if (false !== ($end = strpos($key, '}', $start)) && $end !== ++$start) {
-                $key = substr($key, $start, $end - $start);
-            }
-        }
-
-        return $key;
+        return $this->distributor;
     }
 }

+ 32 - 3
src/Cluster/RedisStrategy.php

@@ -11,19 +11,48 @@
 
 namespace Predis\Cluster;
 
+use Predis\NotSupportedException;
+use Predis\Cluster\Hash\HashGeneratorInterface;
+use Predis\Cluster\Hash\CRC16;
+
 /**
  * Default class used by Predis to calculate hashes out of keys of
  * commands supported by redis-cluster.
  *
  * @author Daniele Alessandri <suppakilla@gmail.com>
  */
-class RedisStrategy extends PredisStrategy
+class RedisStrategy extends ClusterStrategy
 {
+    protected $hashGenerator;
+
     /**
-     *
+     * @param HashGeneratorInterface $hashGenerator Hash generator instance.
      */
     public function __construct(HashGeneratorInterface $hashGenerator = null)
     {
-        parent::__construct($hashGenerator ?: new Hash\CRC16());
+        parent::__construct();
+
+        $this->hashGenerator = $hashGenerator ?: new CRC16();
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getSlotByKey($key)
+    {
+        $key  = $this->extractKeyTag($key);
+        $slot = $this->hashGenerator->hash($key) & 0x3FFF;
+
+        return $slot;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getDistributor()
+    {
+        throw new NotSupportedException(
+            'This cluster strategy does not provide an external distributor'
+        );
     }
 }

+ 15 - 6
src/Cluster/StrategyInterface.php

@@ -11,6 +11,7 @@
 
 namespace Predis\Cluster;
 
+use Predis\Cluster\Distribution\DistributorInterface;
 use Predis\Command\CommandInterface;
 
 /**
@@ -24,19 +25,27 @@ use Predis\Command\CommandInterface;
 interface StrategyInterface
 {
     /**
-     * Returns the hash for the given command using the specified algorithm, or
-     * null when the command cannot be hashed.
+     * Returns a slot for the given command used for clustering distribution or
+     * NULL when this is not possible.
      *
      * @param  CommandInterface $command Command instance.
      * @return int
      */
-    public function getHash(CommandInterface $command);
+    public function getSlot(CommandInterface $command);
 
     /**
-     * Returns the hash for the given key using the specified algorithm.
+     * Returns a slot for the given key used for clustering distribution or NULL
+     * when this is not possible.
      *
      * @param  string $key Key string.
-     * @return string
+     * @return int
+     */
+    public function getSlotByKey($key);
+
+    /**
+     * Returns a distributor instance to be used by the cluster.
+     *
+     * @return DistributorInterface
      */
-    public function getKeyHash($key);
+    public function getDistributor();
 }

+ 8 - 8
src/Command/Command.php

@@ -18,7 +18,7 @@ namespace Predis\Command;
  */
 abstract class Command implements CommandInterface
 {
-    private $hash;
+    private $slot;
     private $arguments = array();
 
     /**
@@ -38,7 +38,7 @@ abstract class Command implements CommandInterface
     public function setArguments(array $arguments)
     {
         $this->arguments = $this->filterArguments($arguments);
-        unset($this->hash);
+        unset($this->slot);
     }
 
     /**
@@ -47,7 +47,7 @@ abstract class Command implements CommandInterface
     public function setRawArguments(array $arguments)
     {
         $this->arguments = $arguments;
-        unset($this->hash);
+        unset($this->slot);
     }
 
     /**
@@ -71,18 +71,18 @@ abstract class Command implements CommandInterface
     /**
      * {@inheritdoc}
      */
-    public function setHash($hash)
+    public function setSlot($slot)
     {
-        $this->hash = $hash;
+        $this->slot = $slot;
     }
 
     /**
      * {@inheritdoc}
      */
-    public function getHash()
+    public function getSlot()
     {
-        if (isset($this->hash)) {
-            return $this->hash;
+        if (isset($this->slot)) {
+            return $this->slot;
         }
     }
 

+ 5 - 5
src/Command/CommandInterface.php

@@ -27,18 +27,18 @@ interface CommandInterface
     public function getId();
 
     /**
-     * Set the hash for the command.
+     * Assign the specified slot to the command for clustering distribution.
      *
-     * @param int $hash Calculated hash.
+     * @param int $slot Slot ID.
      */
-    public function setHash($hash);
+    public function setSlot($slot);
 
     /**
-     * Returns the hash of the command.
+     * Returns the assigned slot of the command for clustering distribution.
      *
      * @return int
      */
-    public function getHash();
+    public function getSlot();
 
     /**
      * Sets the arguments for the command.

+ 7 - 7
src/Command/RawCommand.php

@@ -25,7 +25,7 @@ use InvalidArgumentException;
  */
 class RawCommand implements CommandInterface
 {
-    private $hash;
+    private $slot;
     private $commandID;
     private $arguments;
 
@@ -70,7 +70,7 @@ class RawCommand implements CommandInterface
     public function setArguments(array $arguments)
     {
         $this->arguments = $arguments;
-        unset($this->hash);
+        unset($this->slot);
     }
 
     /**
@@ -102,18 +102,18 @@ class RawCommand implements CommandInterface
     /**
      * {@inheritdoc}
      */
-    public function setHash($hash)
+    public function setSlot($slot)
     {
-        $this->hash = $hash;
+        $this->slot = $slot;
     }
 
     /**
      * {@inheritdoc}
      */
-    public function getHash()
+    public function getSlot()
     {
-        if (isset($this->hash)) {
-            return $this->hash;
+        if (isset($this->slot)) {
+            return $this->slot;
         }
     }
 

+ 11 - 15
src/Connection/Aggregate/PredisCluster.php

@@ -15,10 +15,8 @@ use ArrayIterator;
 use Countable;
 use IteratorAggregate;
 use Predis\NotSupportedException;
-use Predis\Cluster\PredisStrategy as PredisClusterStrategy;
-use Predis\Cluster\StrategyInterface as ClusterStrategyInterface;
-use Predis\Cluster\Distributor\DistributorInterface;
-use Predis\Cluster\Distributor\HashRing;
+use Predis\Cluster\PredisStrategy;
+use Predis\Cluster\StrategyInterface;
 use Predis\Command\CommandInterface;
 use Predis\Connection\NodeConnectionInterface;
 
@@ -36,15 +34,13 @@ class PredisCluster implements ClusterInterface, IteratorAggregate, Countable
     private $distributor;
 
     /**
-     * @param DistributorInterface $distributor Distributor instance.
+     * @param StrategyInterface $strategy Optional cluster strategy.
      */
-    public function __construct(DistributorInterface $distributor = null)
+    public function __construct(StrategyInterface $strategy = null)
     {
-        $distributor = $distributor ?: new HashRing();
-
         $this->pool = array();
-        $this->strategy = new PredisClusterStrategy($distributor->getHashGenerator());
-        $this->distributor = $distributor;
+        $this->strategy = $strategy ?: new PredisStrategy();
+        $this->distributor = $this->strategy->getDistributor();
     }
 
     /**
@@ -133,15 +129,15 @@ class PredisCluster implements ClusterInterface, IteratorAggregate, Countable
      */
     public function getConnection(CommandInterface $command)
     {
-        $hash = $this->strategy->getHash($command);
+        $slot = $this->strategy->getSlot($command);
 
-        if (!isset($hash)) {
+        if (!isset($slot)) {
             throw new NotSupportedException(
                 "Cannot use '{$command->getId()}' over clusters of connections."
             );
         }
 
-        $node = $this->distributor->get($hash);
+        $node = $this->distributor->getBySlot($slot);
 
         return $node;
     }
@@ -162,8 +158,8 @@ class PredisCluster implements ClusterInterface, IteratorAggregate, Countable
      */
     public function getConnectionByKey($key)
     {
-        $hash = $this->strategy->getKeyHash($key);
-        $node = $this->distributor->get($hash);
+        $hash = $this->strategy->getSlotByKey($key);
+        $node = $this->distributor->getBySlot($hash);
 
         return $node;
     }

+ 4 - 6
src/Connection/Aggregate/RedisCluster.php

@@ -58,8 +58,8 @@ class RedisCluster implements ClusterInterface, IteratorAggregate, Countable
     private $connections;
 
     /**
-     * @param FactoryInterface  $connections Connection factory instance.
-     * @param StrategyInterface $strategy    Cluster strategy instance.
+     * @param FactoryInterface  $connections Optional connection factory.
+     * @param StrategyInterface $strategy    Optional cluster strategy.
      */
     public function __construct(
         FactoryInterface $connections = null,
@@ -292,16 +292,14 @@ class RedisCluster implements ClusterInterface, IteratorAggregate, Countable
      */
     public function getConnection(CommandInterface $command)
     {
-        $hash = $this->strategy->getHash($command);
+        $slot = $this->strategy->getSlot($command);
 
-        if (!isset($hash)) {
+        if (!isset($slot)) {
             throw new NotSupportedException(
                 "Cannot use '{$command->getId()}' with redis-cluster."
             );
         }
 
-        $slot = $hash & 0x3FFF;
-
         if (isset($this->slots[$slot])) {
             return $this->slots[$slot];
         } else {

+ 26 - 9
tests/PHPUnit/PredisDistributorTestCase.php

@@ -28,22 +28,39 @@ abstract class PredisDistributorTestCase extends PredisTestCase
     /**
      * Returns a list of nodes from the hashring.
      *
-     * @param  DistributorInterface $ring       Hashring instance.
-     * @param  int                  $iterations Number of nodes to fetch.
+     * @param  DistributorInterface $distributor Distributor instance.
+     * @param  int                  $iterations  Number of nodes to fetch.
      * @return array                Nodes from the hashring.
      */
-    protected function getNodes(DistributorInterface $ring, $iterations = 10)
+    protected function getNodes(DistributorInterface $distributor, $iterations = 10)
     {
         $nodes = array();
 
         for ($i = 0; $i < $iterations; $i++) {
-            $key = $ring->hash($i * $i);
-            $nodes[] = $ring->get($key);
+            $hash = $distributor->hash($i * $i);
+            $nodes[] = $distributor->getByHash($hash);
         }
 
         return $nodes;
     }
 
+    /**
+     * Returns a distributor instance with the specified nodes added.
+     *
+     * @param  array                $nodes Nodes to add to the distributor.
+     * @return DistributorInterface
+     */
+    protected function getSampleDistribution(array $nodes)
+    {
+        $distributor = $this->getDistributorInstance();
+
+        foreach ($nodes as $node) {
+            $distributor->add($node);
+        }
+
+        return $distributor;
+    }
+
     /**
      * @group disconnected
      */
@@ -51,8 +68,8 @@ abstract class PredisDistributorTestCase extends PredisTestCase
     {
         $this->setExpectedException('Predis\Cluster\Distributor\EmptyRingException');
 
-        $ring = $this->getDistributorInstance();
-        $ring->get('nodekey');
+        $distributor = $this->getDistributorInstance();
+        $distributor->getByHash('nodehash');
     }
 
     /**
@@ -60,8 +77,8 @@ abstract class PredisDistributorTestCase extends PredisTestCase
      */
     public function testRemoveOnEmptyRingDoesNotThrowException()
     {
-        $ring = $this->getDistributorInstance();
+        $distributor = $this->getDistributorInstance();
 
-        $this->assertNull($ring->remove('node'));
+        $this->assertNull($distributor->remove('node'));
     }
 }

+ 76 - 12
tests/Predis/Cluster/Distributor/HashRingTest.php

@@ -55,16 +55,11 @@ class HashRingTest extends PredisDistributorTestCase
      */
     public function testMultipleNodesInRing()
     {
-        $nodes = array(
+        $ring = $this->getSampleDistribution(array(
             '127.0.0.1:7000',
             '127.0.0.1:7001',
             '127.0.0.1:7002',
-        );
-
-        $ring = $this->getDistributorInstance();
-        foreach ($nodes as $node) {
-            $ring->add($node);
-        }
+        ));
 
         $expected = array(
             '127.0.0.1:7001',
@@ -131,13 +126,82 @@ class HashRingTest extends PredisDistributorTestCase
     }
 
     /**
-     * @todo This tests should be moved in Predis\Cluster\Distributor\DistributorTestCase
+     * @group disconnected
+     */
+    public function testGetByValue()
+    {
+        $ring = $this->getSampleDistribution(array(
+            '127.0.0.1:7000',
+            '127.0.0.1:7001',
+            '127.0.0.1:7002',
+        ));
+
+        $this->assertSame('127.0.0.1:7001', $ring->get('uid:256'));
+        $this->assertSame('127.0.0.1:7001', $ring->get('uid:281'));
+        $this->assertSame('127.0.0.1:7000', $ring->get('uid:312'));
+        $this->assertSame('127.0.0.1:7001', $ring->get('uid:432'));
+        $this->assertSame('127.0.0.1:7002', $ring->get('uid:500'));
+        $this->assertSame('127.0.0.1:7000', $ring->get('uid:641'));
+    }
+
+    /**
+     * @group disconnected
+     */
+    public function testGetByHash()
+    {
+        $ring = $this->getSampleDistribution(array(
+            '127.0.0.1:7000',
+            '127.0.0.1:7001',
+            '127.0.0.1:7002',
+        ));
+
+        $this->assertSame('127.0.0.1:7001', $ring->getByHash(PHP_INT_SIZE == 4 ? -1249390087 : 3045577209)); // uid:256
+        $this->assertSame('127.0.0.1:7001', $ring->getByHash(PHP_INT_SIZE == 4 ? -1639106025 : 2655861271)); // uid:281
+        $this->assertSame('127.0.0.1:7000', $ring->getByHash(PHP_INT_SIZE == 4 ?  -683361581 : 3611605715)); // uid:312
+        $this->assertSame('127.0.0.1:7001', $ring->getByHash(PHP_INT_SIZE == 4 ?  -532820268 : 3762147028)); // uid:432
+        $this->assertSame('127.0.0.1:7002', $ring->getByHash(PHP_INT_SIZE == 4 ?   618436108 :  618436108)); // uid:500
+        $this->assertSame('127.0.0.1:7000', $ring->getByHash(PHP_INT_SIZE == 4 ?   905043399 :  905043399)); // uid:641
+    }
+
+    /**
+     * @group disconnected
+     */
+    public function testGetBySlot()
+    {
+        $ring = $this->getSampleDistribution(array(
+            '127.0.0.1:7000',
+            '127.0.0.1:7001',
+            '127.0.0.1:7002',
+        ));
+
+/*
+$hash = $ring->hash('uid:641');
+$slot32 = $ring->getSlot($hash);
+$slot64 = sprintf("%u", $slot32 & 0xffffffff);
+echo "32: $slot32 - 64: $slot64\n";
+exit();
+*/
+        $this->assertSame('127.0.0.1:7001', $ring->getBySlot(PHP_INT_SIZE == 4 ? -1255075679 : 3039891617)); // uid:256
+        $this->assertSame('127.0.0.1:7001', $ring->getBySlot(PHP_INT_SIZE == 4 ? -1642314910 : 2652652386)); // uid:281
+        $this->assertSame('127.0.0.1:7000', $ring->getBySlot(PHP_INT_SIZE == 4 ?  -687739295 : 3607228001)); // uid:312
+        $this->assertSame('127.0.0.1:7001', $ring->getBySlot(PHP_INT_SIZE == 4 ?  -544842345 : 3750124951)); // uid:432
+        $this->assertSame('127.0.0.1:7002', $ring->getBySlot(PHP_INT_SIZE == 4 ?   609245004 :  609245004)); // uid:500
+        $this->assertSame('127.0.0.1:7000', $ring->getBySlot(PHP_INT_SIZE == 4 ?   902549909 :  902549909)); // uid:641
+
+        // Test first and last slots
+        $this->assertSame('127.0.0.1:7001', $ring->getBySlot(PHP_INT_SIZE == 4 ? -2096102881 : 2198864415));
+        $this->assertSame('127.0.0.1:7002', $ring->getBySlot(PHP_INT_SIZE == 4 ?  2146453549 : 2146453549));
+
+        // Test non-existing slot
+        $this->assertNull($ring->getBySlot(0));
+    }
+
+    /**
      * @group disconnected
      */
     public function testCallbackToGetNodeHash()
     {
         $node = '127.0.0.1:7000';
-        $replicas = HashRing::DEFAULT_REPLICAS;
         $callable = $this->getMock('stdClass', array('__invoke'));
 
         $callable->expects($this->once())
@@ -145,9 +209,9 @@ class HashRingTest extends PredisDistributorTestCase
                  ->with($node)
                  ->will($this->returnValue($node));
 
-        $ring = new HashRing($replicas, $callable);
-        $ring->add($node);
+        $distributor = new HashRing(HashRing::DEFAULT_REPLICAS, $callable);
+        $distributor->add($node);
 
-        $this->getNodes($ring);
+        $this->getNodes($distributor);
     }
 }

+ 69 - 11
tests/Predis/Cluster/Distributor/KetamaRingTest.php

@@ -56,16 +56,11 @@ class KetamaRingTest extends PredisDistributorTestCase
      */
     public function testMultipleNodesInRing()
     {
-        $nodes = array(
+        $ring = $this->getSampleDistribution(array(
             '127.0.0.1:7000',
             '127.0.0.1:7001',
             '127.0.0.1:7002',
-        );
-
-        $ring = $this->getDistributorInstance();
-        foreach ($nodes as $node) {
-            $ring->add($node);
-        }
+        ));
 
         $expected = array(
             '127.0.0.1:7000',
@@ -132,7 +127,70 @@ class KetamaRingTest extends PredisDistributorTestCase
     }
 
     /**
-     * @todo This tests should be moved in Predis\Cluster\Distributor\DistributorTestCase
+     * @group disconnected
+     */
+    public function testGetByValue()
+    {
+        $ring = $this->getSampleDistribution(array(
+            '127.0.0.1:7000',
+            '127.0.0.1:7001',
+            '127.0.0.1:7002',
+        ));
+
+        $this->assertSame('127.0.0.1:7001', $ring->get('uid:256'));
+        $this->assertSame('127.0.0.1:7002', $ring->get('uid:281'));
+        $this->assertSame('127.0.0.1:7001', $ring->get('uid:312'));
+        $this->assertSame('127.0.0.1:7000', $ring->get('uid:432'));
+        $this->assertSame('127.0.0.1:7000', $ring->get('uid:500'));
+        $this->assertSame('127.0.0.1:7002', $ring->get('uid:641'));
+    }
+
+    /**
+     * @group disconnected
+     */
+    public function testGetByHash()
+    {
+        $ring = $this->getSampleDistribution(array(
+            '127.0.0.1:7000',
+            '127.0.0.1:7001',
+            '127.0.0.1:7002',
+        ));
+
+        $this->assertSame('127.0.0.1:7001', $ring->getByHash(PHP_INT_SIZE == 4 ?  -591277534 : 3703689762)); // uid:256
+        $this->assertSame('127.0.0.1:7002', $ring->getByHash(PHP_INT_SIZE == 4 ? -1632011260 : 2662956036)); // uid:281
+        $this->assertSame('127.0.0.1:7001', $ring->getByHash(PHP_INT_SIZE == 4 ?   345494622 :  345494622)); // uid:312
+        $this->assertSame('127.0.0.1:7000', $ring->getByHash(PHP_INT_SIZE == 4 ? -1042625818 : 3252341478)); // uid:432
+        $this->assertSame('127.0.0.1:7000', $ring->getByHash(PHP_INT_SIZE == 4 ?  -465463623 : 3829503673)); // uid:500
+        $this->assertSame('127.0.0.1:7002', $ring->getByHash(PHP_INT_SIZE == 4 ?  2141928822 : 2141928822)); // uid:641
+    }
+
+    /**
+     * @group disconnected
+     */
+    public function testGetBySlot()
+    {
+        $ring = $this->getSampleDistribution(array(
+            '127.0.0.1:7000',
+            '127.0.0.1:7001',
+            '127.0.0.1:7002',
+        ));
+
+        $this->assertSame('127.0.0.1:7001', $ring->getBySlot(PHP_INT_SIZE == 4 ?  -585685153 : 3709282143)); // uid:256
+        $this->assertSame('127.0.0.1:7002', $ring->getBySlot(PHP_INT_SIZE == 4 ? -1617239533 : 2677727763)); // uid:281
+        $this->assertSame('127.0.0.1:7001', $ring->getBySlot(PHP_INT_SIZE == 4 ?   353009954 :  353009954)); // uid:312
+        $this->assertSame('127.0.0.1:7000', $ring->getBySlot(PHP_INT_SIZE == 4 ? -1037794023 : 3257173273)); // uid:432
+        $this->assertSame('127.0.0.1:7000', $ring->getBySlot(PHP_INT_SIZE == 4 ?  -458724341 : 3836242955)); // uid:500
+        $this->assertSame('127.0.0.1:7002', $ring->getBySlot(PHP_INT_SIZE == 4 ? -2143763192 : 2151204104)); // uid:641
+
+        // Test first and last slots
+        $this->assertSame('127.0.0.1:7002', $ring->getBySlot(PHP_INT_SIZE == 4 ? -2135629153 : 2159338143));
+        $this->assertSame('127.0.0.1:7000', $ring->getBySlot(PHP_INT_SIZE == 4 ?  2137506232 : 2137506232));
+
+        // Test non-existing slot
+        $this->assertNull($ring->getBySlot(0));
+    }
+
+    /**
      * @group disconnected
      */
     public function testCallbackToGetNodeHash()
@@ -145,9 +203,9 @@ class KetamaRingTest extends PredisDistributorTestCase
                  ->with($node)
                  ->will($this->returnValue($node));
 
-        $ring = new KetamaRing($callable);
-        $ring->add($node);
+        $distributor = new KetamaRing($callable);
+        $distributor->add($node);
 
-        $this->getNodes($ring);
+        $this->getNodes($distributor);
     }
 }

+ 26 - 26
tests/Predis/Cluster/PredisStrategyTest.php

@@ -25,21 +25,20 @@ class PredisStrategyTest extends PredisTestCase
     public function testSupportsKeyTags()
     {
         // NOTE: 32 and 64 bits PHP runtimes can produce different hash values.
-        $expected = PHP_INT_SIZE == 4 ? -1938594527 : 2356372769;
-
+        $expected = PHP_INT_SIZE == 4 ? -1954026732 : 2340940564;
         $strategy = $this->getClusterStrategy();
 
-        $this->assertSame($expected, $strategy->getKeyHash('{foo}'));
-        $this->assertSame($expected, $strategy->getKeyHash('{foo}:bar'));
-        $this->assertSame($expected, $strategy->getKeyHash('{foo}:baz'));
-        $this->assertSame($expected, $strategy->getKeyHash('bar:{foo}:baz'));
-        $this->assertSame($expected, $strategy->getKeyHash('bar:{foo}:{baz}'));
+        $this->assertSame($expected, $strategy->getSlotByKey('{foo}'));
+        $this->assertSame($expected, $strategy->getSlotByKey('{foo}:bar'));
+        $this->assertSame($expected, $strategy->getSlotByKey('{foo}:baz'));
+        $this->assertSame($expected, $strategy->getSlotByKey('bar:{foo}:baz'));
+        $this->assertSame($expected, $strategy->getSlotByKey('bar:{foo}:{baz}'));
 
-        $this->assertSame($expected, $strategy->getKeyHash('bar:{foo}:baz{}'));
-        $this->assertSame(PHP_INT_SIZE == 4 ? -1346986340 : 2947980956,  $strategy->getKeyHash('{}bar:{foo}:baz'));
+        $this->assertSame($expected, $strategy->getSlotByKey('bar:{foo}:baz{}'));
+        $this->assertSame(PHP_INT_SIZE == 4 ? -1355751440 : 2939215856,  $strategy->getSlotByKey('{}bar:{foo}:baz'));
 
-        $this->assertSame(0, $strategy->getKeyHash(''));
-        $this->assertSame(PHP_INT_SIZE == 4 ? -1549353149 : 2745614147, $strategy->getKeyHash('{}'));
+        $this->assertSame(PHP_INT_SIZE == 4 ?   -18873278 : 4276094018, $strategy->getSlotByKey(''));
+        $this->assertSame(PHP_INT_SIZE == 4 ? -1574052038 : 2720915258, $strategy->getSlotByKey('{}'));
     }
 
     /**
@@ -60,7 +59,7 @@ class PredisStrategyTest extends PredisTestCase
         $strategy = $this->getClusterStrategy();
         $command = Profile\Factory::getDevelopment()->createCommand('ping');
 
-        $this->assertNull($strategy->getHash($command));
+        $this->assertNull($strategy->getSlot($command));
     }
 
     /**
@@ -74,7 +73,7 @@ class PredisStrategyTest extends PredisTestCase
 
         foreach ($this->getExpectedCommands('keys-first') as $commandID) {
             $command = $profile->createCommand($commandID, $arguments);
-            $this->assertNotNull($strategy->getHash($command), $commandID);
+            $this->assertNotNull($strategy->getSlot($command), $commandID);
         }
     }
 
@@ -89,7 +88,7 @@ class PredisStrategyTest extends PredisTestCase
 
         foreach ($this->getExpectedCommands('keys-all') as $commandID) {
             $command = $profile->createCommand($commandID, $arguments);
-            $this->assertNotNull($strategy->getHash($command), $commandID);
+            $this->assertNotNull($strategy->getSlot($command), $commandID);
         }
     }
 
@@ -104,7 +103,7 @@ class PredisStrategyTest extends PredisTestCase
 
         foreach ($this->getExpectedCommands('keys-interleaved') as $commandID) {
             $command = $profile->createCommand($commandID, $arguments);
-            $this->assertNotNull($strategy->getHash($command), $commandID);
+            $this->assertNotNull($strategy->getSlot($command), $commandID);
         }
     }
 
@@ -119,7 +118,7 @@ class PredisStrategyTest extends PredisTestCase
 
         foreach ($this->getExpectedCommands('keys-blockinglist') as $commandID) {
             $command = $profile->createCommand($commandID, $arguments);
-            $this->assertNotNull($strategy->getHash($command), $commandID);
+            $this->assertNotNull($strategy->getSlot($command), $commandID);
         }
     }
 
@@ -134,7 +133,7 @@ class PredisStrategyTest extends PredisTestCase
 
         foreach ($this->getExpectedCommands('keys-zaggregated') as $commandID) {
             $command = $profile->createCommand($commandID, $arguments);
-            $this->assertNotNull($strategy->getHash($command), $commandID);
+            $this->assertNotNull($strategy->getSlot($command), $commandID);
         }
     }
 
@@ -149,7 +148,7 @@ class PredisStrategyTest extends PredisTestCase
 
         foreach ($this->getExpectedCommands('keys-bitop') as $commandID) {
             $command = $profile->createCommand($commandID, $arguments);
-            $this->assertNotNull($strategy->getHash($command), $commandID);
+            $this->assertNotNull($strategy->getSlot($command), $commandID);
         }
     }
 
@@ -164,7 +163,7 @@ class PredisStrategyTest extends PredisTestCase
 
         foreach ($this->getExpectedCommands('keys-script') as $commandID) {
             $command = $profile->createCommand($commandID, $arguments);
-            $this->assertNotNull($strategy->getHash($command), $commandID);
+            $this->assertNotNull($strategy->getSlot($command), $commandID);
         }
     }
 
@@ -185,7 +184,7 @@ class PredisStrategyTest extends PredisTestCase
                 ->will($this->returnValue(2));
         $command->setArguments($arguments);
 
-        $this->assertNotNull($strategy->getHash($command), "Script Command [{$command->getId()}]");
+        $this->assertNotNull($strategy->getSlot($command), "Script Command [{$command->getId()}]");
     }
 
     /**
@@ -200,10 +199,10 @@ class PredisStrategyTest extends PredisTestCase
         $strategy->setCommandHandler('get', null);
 
         $command = $profile->createCommand('set', array('key', 'value'));
-        $this->assertNull($strategy->getHash($command));
+        $this->assertNull($strategy->getSlot($command));
 
         $command = $profile->createCommand('get', array('key'));
-        $this->assertNull($strategy->getHash($command));
+        $this->assertNull($strategy->getSlot($command));
     }
 
     /**
@@ -223,7 +222,7 @@ class PredisStrategyTest extends PredisTestCase
         $strategy->setCommandHandler('get', $callable);
 
         $command = $profile->createCommand('get', array('key'));
-        $this->assertNotNull($strategy->getHash($command));
+        $this->assertNotNull($strategy->getSlot($command));
     }
 
     // ******************************************************************** //
@@ -237,9 +236,10 @@ class PredisStrategyTest extends PredisTestCase
      */
     protected function getClusterStrategy()
     {
-        $distributor = new Distributor\HashRing();
-        $hashGenerator = $distributor->getHashGenerator();
-        $strategy = new PredisStrategy($hashGenerator);
+        $strategy = new PredisStrategy();
+
+        $connection = $this->getMock('Predis\Connection\NodeConnectionInterface');
+        $strategy->getDistributor()->add($connection);
 
         return $strategy;
     }

+ 22 - 22
tests/Predis/Cluster/RedisStrategyTest.php

@@ -26,17 +26,17 @@ class RedisStrategyTest extends PredisTestCase
     {
         $strategy = $this->getClusterStrategy();
 
-        $this->assertSame(44950, $strategy->getKeyHash('{foo}'));
-        $this->assertSame(44950, $strategy->getKeyHash('{foo}:bar'));
-        $this->assertSame(44950, $strategy->getKeyHash('{foo}:baz'));
-        $this->assertSame(44950, $strategy->getKeyHash('bar:{foo}:baz'));
-        $this->assertSame(44950, $strategy->getKeyHash('bar:{foo}:{baz}'));
+        $this->assertSame(12182, $strategy->getSlotByKey('{foo}'));
+        $this->assertSame(12182, $strategy->getSlotByKey('{foo}:bar'));
+        $this->assertSame(12182, $strategy->getSlotByKey('{foo}:baz'));
+        $this->assertSame(12182, $strategy->getSlotByKey('bar:{foo}:baz'));
+        $this->assertSame(12182, $strategy->getSlotByKey('bar:{foo}:{baz}'));
 
-        $this->assertSame(44950, $strategy->getKeyHash('bar:{foo}:baz{}'));
-        $this->assertSame(9415,  $strategy->getKeyHash('{}bar:{foo}:baz'));
+        $this->assertSame(12182, $strategy->getSlotByKey('bar:{foo}:baz{}'));
+        $this->assertSame(9415,  $strategy->getSlotByKey('{}bar:{foo}:baz'));
 
-        $this->assertSame(0,     $strategy->getKeyHash(''));
-        $this->assertSame(31641, $strategy->getKeyHash('{}'));
+        $this->assertSame(0,     $strategy->getSlotByKey(''));
+        $this->assertSame(15257, $strategy->getSlotByKey('{}'));
     }
 
     /**
@@ -57,7 +57,7 @@ class RedisStrategyTest extends PredisTestCase
         $strategy = $this->getClusterStrategy();
         $command = Profile\Factory::getDevelopment()->createCommand('ping');
 
-        $this->assertNull($strategy->getHash($command));
+        $this->assertNull($strategy->getSlot($command));
     }
 
     /**
@@ -71,7 +71,7 @@ class RedisStrategyTest extends PredisTestCase
 
         foreach ($this->getExpectedCommands('keys-first') as $commandID) {
             $command = $profile->createCommand($commandID, $arguments);
-            $this->assertNotNull($strategy->getHash($command), $commandID);
+            $this->assertNotNull($strategy->getSlot($command), $commandID);
         }
     }
 
@@ -86,7 +86,7 @@ class RedisStrategyTest extends PredisTestCase
 
         foreach ($this->getExpectedCommands('keys-all') as $commandID) {
             $command = $profile->createCommand($commandID, $arguments);
-            $this->assertNotNull($strategy->getHash($command), $commandID);
+            $this->assertNotNull($strategy->getSlot($command), $commandID);
         }
     }
 
@@ -101,7 +101,7 @@ class RedisStrategyTest extends PredisTestCase
 
         foreach ($this->getExpectedCommands('keys-all') as $commandID) {
             $command = $profile->createCommand($commandID, $arguments);
-            $this->assertNull($strategy->getHash($command), $commandID);
+            $this->assertNull($strategy->getSlot($command), $commandID);
         }
     }
 
@@ -116,7 +116,7 @@ class RedisStrategyTest extends PredisTestCase
 
         foreach ($this->getExpectedCommands('keys-interleaved') as $commandID) {
             $command = $profile->createCommand($commandID, $arguments);
-            $this->assertNotNull($strategy->getHash($command), $commandID);
+            $this->assertNotNull($strategy->getSlot($command), $commandID);
         }
     }
 
@@ -131,7 +131,7 @@ class RedisStrategyTest extends PredisTestCase
 
         foreach ($this->getExpectedCommands('keys-interleaved') as $commandID) {
             $command = $profile->createCommand($commandID, $arguments);
-            $this->assertNull($strategy->getHash($command), $commandID);
+            $this->assertNull($strategy->getSlot($command), $commandID);
         }
     }
 
@@ -146,7 +146,7 @@ class RedisStrategyTest extends PredisTestCase
 
         foreach ($this->getExpectedCommands('keys-blockinglist') as $commandID) {
             $command = $profile->createCommand($commandID, $arguments);
-            $this->assertNotNull($strategy->getHash($command), $commandID);
+            $this->assertNotNull($strategy->getSlot($command), $commandID);
         }
     }
 
@@ -161,7 +161,7 @@ class RedisStrategyTest extends PredisTestCase
 
         foreach ($this->getExpectedCommands('keys-blockinglist') as $commandID) {
             $command = $profile->createCommand($commandID, $arguments);
-            $this->assertNull($strategy->getHash($command), $commandID);
+            $this->assertNull($strategy->getSlot($command), $commandID);
         }
     }
 
@@ -176,7 +176,7 @@ class RedisStrategyTest extends PredisTestCase
 
         foreach ($this->getExpectedCommands('keys-script') as $commandID) {
             $command = $profile->createCommand($commandID, $arguments);
-            $this->assertNotNull($strategy->getHash($command), $commandID);
+            $this->assertNotNull($strategy->getSlot($command), $commandID);
         }
     }
 
@@ -197,7 +197,7 @@ class RedisStrategyTest extends PredisTestCase
                 ->will($this->returnValue(1));
         $command->setArguments($arguments);
 
-        $this->assertNotNull($strategy->getHash($command), "Script Command [{$command->getId()}]");
+        $this->assertNotNull($strategy->getSlot($command), "Script Command [{$command->getId()}]");
     }
 
     /**
@@ -212,10 +212,10 @@ class RedisStrategyTest extends PredisTestCase
         $strategy->setCommandHandler('get', null);
 
         $command = $profile->createCommand('set', array('key', 'value'));
-        $this->assertNull($strategy->getHash($command));
+        $this->assertNull($strategy->getSlot($command));
 
         $command = $profile->createCommand('get', array('key'));
-        $this->assertNull($strategy->getHash($command));
+        $this->assertNull($strategy->getSlot($command));
     }
 
     /**
@@ -235,7 +235,7 @@ class RedisStrategyTest extends PredisTestCase
         $strategy->setCommandHandler('get', $callable);
 
         $command = $profile->createCommand('get', array('key'));
-        $this->assertNotNull($strategy->getHash($command));
+        $this->assertNotNull($strategy->getSlot($command));
     }
 
     // ******************************************************************** //

+ 8 - 8
tests/Predis/Command/CommandTest.php

@@ -97,24 +97,24 @@ class CommandTest extends PredisTestCase
     /**
      * @group disconnected
      */
-    public function testSetAndGetHash()
+    public function testSetAndGetSlot()
     {
-        $hash = "key-hash";
+        $slot = 1024;
 
         $command = $this->getMockForAbstractClass('Predis\Command\Command');
         $command->setRawArguments(array('key'));
 
-        $this->assertNull($command->getHash());
+        $this->assertNull($command->getSlot());
 
-        $command->setHash($hash);
-        $this->assertSame($hash, $command->getHash());
+        $command->setSlot($slot);
+        $this->assertSame($slot, $command->getSlot());
 
         $command->setArguments(array('key'));
-        $this->assertNull($command->getHash());
+        $this->assertNull($command->getSlot());
 
-        $command->setHash($hash);
+        $command->setSlot($slot);
         $command->setRawArguments(array('key'));
-        $this->assertNull($command->getHash());
+        $this->assertNull($command->getSlot());
     }
 
     /**

+ 5 - 5
tests/Predis/Command/RawCommandTest.php

@@ -116,17 +116,17 @@ class RawCommandTest extends PredisTestCase
      */
     public function testSetAndGetHash()
     {
-        $hash = "key-hash";
+        $slot = 1024;
         $arguments = array('SET', 'key', 'value');
         $command = new RawCommand($arguments);
 
-        $this->assertNull($command->getHash());
+        $this->assertNull($command->getSlot());
 
-        $command->setHash($hash);
-        $this->assertSame($hash, $command->getHash());
+        $command->setSlot($slot);
+        $this->assertSame($slot, $command->getSlot());
 
         $command->setArguments(array('hoge', 'piyo'));
-        $this->assertNull($command->getHash());
+        $this->assertNull($command->getSlot());
     }
 
     /**