浏览代码

Merge branch 'v1.0/refactor-cluster-internals'

Daniele Alessandri 10 年之前
父节点
当前提交
558e10963c

+ 30 - 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;
     },

+ 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,
@@ -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());
     }
 
     /**