Files
predis/tests/Predis/Cluster/RedisStrategyTest.php
T
Daniele Alessandri 2ff8c37104 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.
2014-07-22 18:13:12 +02:00

397 lines
14 KiB
PHP

<?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 PredisTestCase;
use Predis\Profile;
/**
*
*/
class RedisStrategyTest extends PredisTestCase
{
/**
* @group disconnected
*/
public function testSupportsKeyTags()
{
$strategy = $this->getClusterStrategy();
$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(12182, $strategy->getSlotByKey('bar:{foo}:baz{}'));
$this->assertSame(9415, $strategy->getSlotByKey('{}bar:{foo}:baz'));
$this->assertSame(0, $strategy->getSlotByKey(''));
$this->assertSame(15257, $strategy->getSlotByKey('{}'));
}
/**
* @group disconnected
*/
public function testSupportedCommands()
{
$strategy = $this->getClusterStrategy();
$this->assertSame($this->getExpectedCommands(), $strategy->getSupportedCommands());
}
/**
* @group disconnected
*/
public function testReturnsNullOnUnsupportedCommand()
{
$strategy = $this->getClusterStrategy();
$command = Profile\Factory::getDevelopment()->createCommand('ping');
$this->assertNull($strategy->getSlot($command));
}
/**
* @group disconnected
*/
public function testFirstKeyCommands()
{
$strategy = $this->getClusterStrategy();
$profile = Profile\Factory::getDevelopment();
$arguments = array('key');
foreach ($this->getExpectedCommands('keys-first') as $commandID) {
$command = $profile->createCommand($commandID, $arguments);
$this->assertNotNull($strategy->getSlot($command), $commandID);
}
}
/**
* @group disconnected
*/
public function testAllKeysCommandsWithOneKey()
{
$strategy = $this->getClusterStrategy();
$profile = Profile\Factory::getDevelopment();
$arguments = array('key');
foreach ($this->getExpectedCommands('keys-all') as $commandID) {
$command = $profile->createCommand($commandID, $arguments);
$this->assertNotNull($strategy->getSlot($command), $commandID);
}
}
/**
* @group disconnected
*/
public function testAllKeysCommandsWithMoreKeys()
{
$strategy = $this->getClusterStrategy();
$profile = Profile\Factory::getDevelopment();
$arguments = array('key1', 'key2');
foreach ($this->getExpectedCommands('keys-all') as $commandID) {
$command = $profile->createCommand($commandID, $arguments);
$this->assertNull($strategy->getSlot($command), $commandID);
}
}
/**
* @group disconnected
*/
public function testInterleavedKeysCommandsWithOneKey()
{
$strategy = $this->getClusterStrategy();
$profile = Profile\Factory::getDevelopment();
$arguments = array('key:1', 'value1');
foreach ($this->getExpectedCommands('keys-interleaved') as $commandID) {
$command = $profile->createCommand($commandID, $arguments);
$this->assertNotNull($strategy->getSlot($command), $commandID);
}
}
/**
* @group disconnected
*/
public function testInterleavedKeysCommandsWithMoreKeys()
{
$strategy = $this->getClusterStrategy();
$profile = Profile\Factory::getDevelopment();
$arguments = array('key:1', 'value1', 'key:2', 'value2');
foreach ($this->getExpectedCommands('keys-interleaved') as $commandID) {
$command = $profile->createCommand($commandID, $arguments);
$this->assertNull($strategy->getSlot($command), $commandID);
}
}
/**
* @group disconnected
*/
public function testKeysForBlockingListCommandsWithOneKey()
{
$strategy = $this->getClusterStrategy();
$profile = Profile\Factory::getDevelopment();
$arguments = array('key:1', 10);
foreach ($this->getExpectedCommands('keys-blockinglist') as $commandID) {
$command = $profile->createCommand($commandID, $arguments);
$this->assertNotNull($strategy->getSlot($command), $commandID);
}
}
/**
* @group disconnected
*/
public function testKeysForBlockingListCommandsWithMoreKeys()
{
$strategy = $this->getClusterStrategy();
$profile = Profile\Factory::getDevelopment();
$arguments = array('key:1', 'key:2', 10);
foreach ($this->getExpectedCommands('keys-blockinglist') as $commandID) {
$command = $profile->createCommand($commandID, $arguments);
$this->assertNull($strategy->getSlot($command), $commandID);
}
}
/**
* @group disconnected
*/
public function testKeysForEvalCommand()
{
$strategy = $this->getClusterStrategy();
$profile = Profile\Factory::getDevelopment();
$arguments = array('%SCRIPT%', 1, 'key:1', 'value1');
foreach ($this->getExpectedCommands('keys-script') as $commandID) {
$command = $profile->createCommand($commandID, $arguments);
$this->assertNotNull($strategy->getSlot($command), $commandID);
}
}
/**
* @group disconnected
*/
public function testKeysForScriptCommand()
{
$strategy = $this->getClusterStrategy();
$arguments = array('key:1', 'value1');
$command = $this->getMock('Predis\Command\ScriptCommand', array('getScript', 'getKeysCount'));
$command->expects($this->once())
->method('getScript')
->will($this->returnValue('return true'));
$command->expects($this->exactly(2))
->method('getKeysCount')
->will($this->returnValue(1));
$command->setArguments($arguments);
$this->assertNotNull($strategy->getSlot($command), "Script Command [{$command->getId()}]");
}
/**
* @group disconnected
*/
public function testUnsettingCommandHandler()
{
$strategy = $this->getClusterStrategy();
$profile = Profile\Factory::getDevelopment();
$strategy->setCommandHandler('set');
$strategy->setCommandHandler('get', null);
$command = $profile->createCommand('set', array('key', 'value'));
$this->assertNull($strategy->getSlot($command));
$command = $profile->createCommand('get', array('key'));
$this->assertNull($strategy->getSlot($command));
}
/**
* @group disconnected
*/
public function testSettingCustomCommandHandler()
{
$strategy = $this->getClusterStrategy();
$profile = Profile\Factory::getDevelopment();
$callable = $this->getMock('stdClass', array('__invoke'));
$callable->expects($this->once())
->method('__invoke')
->with($this->isInstanceOf('Predis\Command\CommandInterface'))
->will($this->returnValue('key'));
$strategy->setCommandHandler('get', $callable);
$command = $profile->createCommand('get', array('key'));
$this->assertNotNull($strategy->getSlot($command));
}
// ******************************************************************** //
// ---- HELPER METHODS ------------------------------------------------ //
// ******************************************************************** //
/**
* Creates the default cluster strategy object.
*
* @return StrategyInterface
*/
protected function getClusterStrategy()
{
$strategy = new RedisStrategy();
return $strategy;
}
/**
* Returns the list of expected supported commands.
*
* @param string $type Optional type of command (based on its keys)
* @return array
*/
protected function getExpectedCommands($type = null)
{
$commands = array(
/* commands operating on the key space */
'EXISTS' => 'keys-first',
'DEL' => 'keys-all',
'TYPE' => 'keys-first',
'EXPIRE' => 'keys-first',
'EXPIREAT' => 'keys-first',
'PERSIST' => 'keys-first',
'PEXPIRE' => 'keys-first',
'PEXPIREAT' => 'keys-first',
'TTL' => 'keys-first',
'PTTL' => 'keys-first',
'SORT' => 'keys-first', // TODO
'DUMP' => 'keys-first',
'RESTORE' => 'keys-first',
/* commands operating on string values */
'APPEND' => 'keys-first',
'DECR' => 'keys-first',
'DECRBY' => 'keys-first',
'GET' => 'keys-first',
'GETBIT' => 'keys-first',
'MGET' => 'keys-all',
'SET' => 'keys-first',
'GETRANGE' => 'keys-first',
'GETSET' => 'keys-first',
'INCR' => 'keys-first',
'INCRBY' => 'keys-first',
'INCRBYFLOAT' => 'keys-first',
'SETBIT' => 'keys-first',
'SETEX' => 'keys-first',
'MSET' => 'keys-interleaved',
'MSETNX' => 'keys-interleaved',
'SETNX' => 'keys-first',
'SETRANGE' => 'keys-first',
'STRLEN' => 'keys-first',
'SUBSTR' => 'keys-first',
'BITOP' => 'keys-bitop',
'BITCOUNT' => 'keys-first',
/* commands operating on lists */
'LINSERT' => 'keys-first',
'LINDEX' => 'keys-first',
'LLEN' => 'keys-first',
'LPOP' => 'keys-first',
'RPOP' => 'keys-first',
'RPOPLPUSH' => 'keys-all',
'BLPOP' => 'keys-blockinglist',
'BRPOP' => 'keys-blockinglist',
'BRPOPLPUSH' => 'keys-blockinglist',
'LPUSH' => 'keys-first',
'LPUSHX' => 'keys-first',
'RPUSH' => 'keys-first',
'RPUSHX' => 'keys-first',
'LRANGE' => 'keys-first',
'LREM' => 'keys-first',
'LSET' => 'keys-first',
'LTRIM' => 'keys-first',
/* commands operating on sets */
'SADD' => 'keys-first',
'SCARD' => 'keys-first',
'SDIFF' => 'keys-all',
'SDIFFSTORE' => 'keys-all',
'SINTER' => 'keys-all',
'SINTERSTORE' => 'keys-all',
'SUNION' => 'keys-all',
'SUNIONSTORE' => 'keys-all',
'SISMEMBER' => 'keys-first',
'SMEMBERS' => 'keys-first',
'SSCAN' => 'keys-first',
'SPOP' => 'keys-first',
'SRANDMEMBER' => 'keys-first',
'SREM' => 'keys-first',
/* commands operating on sorted sets */
'ZADD' => 'keys-first',
'ZCARD' => 'keys-first',
'ZCOUNT' => 'keys-first',
'ZINCRBY' => 'keys-first',
'ZINTERSTORE' => 'keys-zaggregated',
'ZRANGE' => 'keys-first',
'ZRANGEBYSCORE' => 'keys-first',
'ZRANK' => 'keys-first',
'ZREM' => 'keys-first',
'ZREMRANGEBYRANK' => 'keys-first',
'ZREMRANGEBYSCORE' => 'keys-first',
'ZREVRANGE' => 'keys-first',
'ZREVRANGEBYSCORE' => 'keys-first',
'ZREVRANK' => 'keys-first',
'ZSCORE' => 'keys-first',
'ZUNIONSTORE' => 'keys-zaggregated',
'ZSCAN' => 'keys-first',
'ZLEXCOUNT' => 'keys-first',
'ZRANGEBYLEX' => 'keys-first',
'ZREMRANGEBYLEX' => 'keys-first',
/* commands operating on hashes */
'HDEL' => 'keys-first',
'HEXISTS' => 'keys-first',
'HGET' => 'keys-first',
'HGETALL' => 'keys-first',
'HMGET' => 'keys-first',
'HMSET' => 'keys-first',
'HINCRBY' => 'keys-first',
'HINCRBYFLOAT' => 'keys-first',
'HKEYS' => 'keys-first',
'HLEN' => 'keys-first',
'HSET' => 'keys-first',
'HSETNX' => 'keys-first',
'HVALS' => 'keys-first',
'HSCAN' => 'keys-first',
/* commands operating on HyperLogLog */
'PFADD' => 'keys-first',
'PFCOUNT' => 'keys-all',
'PFMERGE' => 'keys-all',
/* scripting */
'EVAL' => 'keys-script',
'EVALSHA' => 'keys-script',
);
if (isset($type)) {
$commands = array_filter($commands, function ($expectedType) use ($type) {
return $expectedType === $type;
});
}
return array_keys($commands);
}
}