diff --git a/CHANGELOG.md b/CHANGELOG.md index 2531dc73..b997d435 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -45,6 +45,10 @@ v0.8.0 (201x-xx-xx) - Cluster and replication connections now extend a new common interface, `Predis\Connection\AggregatedConnectionInterface`. +- The `Predis\Connection\MasterSlaveReplication` connection class now uses an + external strategy class to handle the logic for checking readable / writable + commands or Lua scripts. + - Command pipelines have been optimized for both speed and code cleanness, but at the cost of bringing a breaking change in the signature of the interface for pipeline executors. diff --git a/examples/MasterSlaveReplicationComplex.php b/examples/MasterSlaveReplicationComplex.php index 802a65b0..02b2e086 100644 --- a/examples/MasterSlaveReplicationComplex.php +++ b/examples/MasterSlaveReplicationComplex.php @@ -21,6 +21,7 @@ require 'SharedConfigurations.php'; use Predis\Command\ScriptedCommand; use Predis\Connection\MasterSlaveReplication; use Predis\Profile\ServerProfile; +use Predis\Replication\ReplicationStrategy; // ------------------------------------------------------------------------- // @@ -57,8 +58,10 @@ $options = array( return $profile; }, 'replication' => function ($options) { - $replication = new MasterSlaveReplication(); - $replication->setScriptReadOnly(HashMultipleGetAll::BODY); + $strategy = new ReplicationStrategy(); + $strategy->setScriptReadOnly(HashMultipleGetAll::BODY); + + $replication = new MasterSlaveReplication($strategy); return $replication; }, diff --git a/lib/Predis/Connection/MasterSlaveReplication.php b/lib/Predis/Connection/MasterSlaveReplication.php index 4d5cda76..d5f9e739 100644 --- a/lib/Predis/Connection/MasterSlaveReplication.php +++ b/lib/Predis/Connection/MasterSlaveReplication.php @@ -13,30 +13,28 @@ namespace Predis\Connection; use Predis\NotSupportedException; use Predis\Command\CommandInterface; +use Predis\Replication\ReplicationStrategy; /** - * Defines the standard virtual connection class that is used - * by Predis to handle replication with a group of servers in - * a master/slave configuration. + * Aggregated connection class used by to handle replication with a + * group of servers in a master/slave configuration. * * @author Daniele Alessandri */ class MasterSlaveReplication implements ReplicationConnectionInterface { - private $disallowed = array(); - private $readonly = array(); - private $readonlySHA1 = array(); - private $current = null; - private $master = null; - private $slaves = array(); + protected $strategy; + protected $master; + protected $slaves; + protected $current; /** * */ - public function __construct() + public function __construct(ReplicationStrategy $strategy = null) { - $this->disallowed = $this->getDisallowedOperations(); - $this->readonly = $this->getReadOnlyOperations(); + $this->slaves = array(); + $this->strategy = $strategy ?: new ReplicationStrategy(); } /** @@ -102,15 +100,16 @@ class MasterSlaveReplication implements ReplicationConnectionInterface { if ($this->current === null) { $this->check(); - $this->current = $this->isReadOperation($command) ? $this->pickSlave() : $this->master; + $this->current = $this->strategy->isReadOperation($command) ? $this->pickSlave() : $this->master; return $this->current; } + if ($this->current === $this->master) { return $this->current; } - if (!$this->isReadOperation($command)) { + if (!$this->strategy->isReadOperation($command)) { $this->current = $this->master; } @@ -174,6 +173,16 @@ class MasterSlaveReplication implements ReplicationConnectionInterface return array_values($this->slaves); } + /** + * Returns the underlying replication strategy. + * + * @return ReplicationStrategy + */ + public function getReplicationStrategy() + { + return $this->strategy; + } + /** * Returns a random slave. * @@ -243,180 +252,11 @@ class MasterSlaveReplication implements ReplicationConnectionInterface return $this->getConnection($command)->executeCommand($command); } - /** - * Returns if the specified command performs a read-only operation - * against a key stored on Redis. - * - * @param CommandInterface $command Instance of Redis command. - * @return Boolean - */ - protected function isReadOperation(CommandInterface $command) - { - if (isset($this->disallowed[$id = $command->getId()])) { - throw new NotSupportedException("The command $id is not allowed in replication mode"); - } - - if (isset($this->readonly[$id])) { - if (true === $readonly = $this->readonly[$id]) { - return true; - } - - return call_user_func($readonly, $command); - } - - if (($eval = $id === 'EVAL') || $id === 'EVALSHA') { - $sha1 = $eval ? sha1($command->getArgument(0)) : $command->getArgument(0); - - if (isset($this->readonlySHA1[$sha1])) { - if (true === $readonly = $this->readonlySHA1[$sha1]) { - return true; - } - - return call_user_func($readonly, $command); - } - } - - return false; - } - - /** - * Checks if a SORT command is a readable operation by parsing the arguments - * array of the specified commad instance. - * - * @param CommandInterface $command Instance of Redis command. - * @return Boolean - */ - private function isSortReadOnly(CommandInterface $command) - { - $arguments = $command->getArguments(); - return ($c = count($arguments)) === 1 ? true : $arguments[$c - 2] !== 'STORE'; - } - - /** - * Marks a command as a read-only operation. When the behaviour of a - * command can be decided only at runtime depending on its arguments, - * a callable object can be provided to dinamically check if the passed - * instance of a command performs write operations or not. - * - * @param string $commandID ID of the command. - * @param mixed $readonly A boolean or a callable object. - */ - public function setCommandReadOnly($commandID, $readonly = true) - { - $commandID = strtoupper($commandID); - - if ($readonly) { - $this->readonly[$commandID] = $readonly; - } else { - unset($this->readonly[$commandID]); - } - } - - /** - * Marks a Lua script for EVAL and EVALSHA as a read-only operation. When - * the behaviour of a script can be decided only at runtime depending on - * its arguments, a callable object can be provided to dinamically check - * if the passed instance of EVAL or EVALSHA performs write operations or - * not. - * - * @param string $script Body of the Lua script. - * @param mixed $readonly A boolean or a callable object. - */ - public function setScriptReadOnly($script, $readonly = true) - { - $sha1 = sha1($script); - - if ($readonly) { - $this->readonlySHA1[$sha1] = $readonly; - } else { - unset($this->readonlySHA1[$sha1]); - } - } - - /** - * Returns the default list of disallowed commands. - * - * @return array - */ - protected function getDisallowedOperations() - { - return array( - 'SHUTDOWN' => true, - 'INFO' => true, - 'DBSIZE' => true, - 'LASTSAVE' => true, - 'CONFIG' => true, - 'MONITOR' => true, - 'SLAVEOF' => true, - 'SAVE' => true, - 'BGSAVE' => true, - 'BGREWRITEAOF' => true, - 'SLOWLOG' => true, - ); - } - - /** - * Returns the default list of commands performing read-only operations. - * - * @return array - */ - protected function getReadOnlyOperations() - { - return array( - 'EXISTS' => true, - 'TYPE' => true, - 'KEYS' => true, - 'RANDOMKEY' => true, - 'TTL' => true, - 'GET' => true, - 'MGET' => true, - 'SUBSTR' => true, - 'STRLEN' => true, - 'GETRANGE' => true, - 'GETBIT' => true, - 'LLEN' => true, - 'LRANGE' => true, - 'LINDEX' => true, - 'SCARD' => true, - 'SISMEMBER' => true, - 'SINTER' => true, - 'SUNION' => true, - 'SDIFF' => true, - 'SMEMBERS' => true, - 'SRANDMEMBER' => true, - 'ZRANGE' => true, - 'ZREVRANGE' => true, - 'ZRANGEBYSCORE' => true, - 'ZREVRANGEBYSCORE' => true, - 'ZCARD' => true, - 'ZSCORE' => true, - 'ZCOUNT' => true, - 'ZRANK' => true, - 'ZREVRANK' => true, - 'HGET' => true, - 'HMGET' => true, - 'HEXISTS' => true, - 'HLEN' => true, - 'HKEYS' => true, - 'HVELS' => true, - 'HGETALL' => true, - 'PING' => true, - 'AUTH' => true, - 'SELECT' => true, - 'ECHO' => true, - 'QUIT' => true, - 'OBJECT' => true, - 'BITCOUNT' => true, - 'TIME' => true, - 'SORT' => array($this, 'isSortReadOnly'), - ); - } - /** * {@inheritdoc} */ public function __sleep() { - return array('master', 'slaves', 'disallowed', 'readonly', 'readonlySHA1'); + return array('master', 'slaves', 'strategy'); } } diff --git a/lib/Predis/Replication/ReplicationStrategy.php b/lib/Predis/Replication/ReplicationStrategy.php new file mode 100644 index 00000000..de84cfe7 --- /dev/null +++ b/lib/Predis/Replication/ReplicationStrategy.php @@ -0,0 +1,218 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Replication; + +use Predis\NotSupportedException; +use Predis\Command\CommandInterface; + +/** + * Defines a strategy for master/reply replication. + * + * @author Daniele Alessandri + */ +class ReplicationStrategy +{ + protected $disallowed; + protected $readonly; + protected $readonlySHA1; + + /** + * + */ + public function __construct() + { + $this->disallowed = $this->getDisallowedOperations(); + $this->readonly = $this->getReadOnlyOperations(); + $this->readonlySHA1 = array(); + } + + /** + * Returns if the specified command performs a read-only operation + * against a key stored on Redis. + * + * @param CommandInterface $command Instance of Redis command. + * @return Boolean + */ + public function isReadOperation(CommandInterface $command) + { + if (isset($this->disallowed[$id = $command->getId()])) { + throw new NotSupportedException("The command $id is not allowed in replication mode"); + } + + if (isset($this->readonly[$id])) { + if (true === $readonly = $this->readonly[$id]) { + return true; + } + + return call_user_func($readonly, $command); + } + + if (($eval = $id === 'EVAL') || $id === 'EVALSHA') { + $sha1 = $eval ? sha1($command->getArgument(0)) : $command->getArgument(0); + + if (isset($this->readonlySHA1[$sha1])) { + if (true === $readonly = $this->readonlySHA1[$sha1]) { + return true; + } + + return call_user_func($readonly, $command); + } + } + + return false; + } + + /** + * Returns if the specified command is disallowed in a master/slave + * replication context. + * + * @param CommandInterface $command Instance of Redis command. + * @return Boolean + */ + public function isDisallowedOperation(CommandInterface $command) + { + return isset($this->disallowed[$command->getId()]); + } + + /** + * Checks if a SORT command is a readable operation by parsing the arguments + * array of the specified commad instance. + * + * @param CommandInterface $command Instance of Redis command. + * @return Boolean + */ + protected function isSortReadOnly(CommandInterface $command) + { + $arguments = $command->getArguments(); + return ($c = count($arguments)) === 1 ? true : $arguments[$c - 2] !== 'STORE'; + } + + /** + * Marks a command as a read-only operation. When the behaviour of a + * command can be decided only at runtime depending on its arguments, + * a callable object can be provided to dinamically check if the passed + * instance of a command performs write operations or not. + * + * @param string $commandID ID of the command. + * @param mixed $readonly A boolean or a callable object. + */ + public function setCommandReadOnly($commandID, $readonly = true) + { + $commandID = strtoupper($commandID); + + if ($readonly) { + $this->readonly[$commandID] = $readonly; + } else { + unset($this->readonly[$commandID]); + } + } + + /** + * Marks a Lua script for EVAL and EVALSHA as a read-only operation. When + * the behaviour of a script can be decided only at runtime depending on + * its arguments, a callable object can be provided to dinamically check + * if the passed instance of EVAL or EVALSHA performs write operations or + * not. + * + * @param string $script Body of the Lua script. + * @param mixed $readonly A boolean or a callable object. + */ + public function setScriptReadOnly($script, $readonly = true) + { + $sha1 = sha1($script); + + if ($readonly) { + $this->readonlySHA1[$sha1] = $readonly; + } else { + unset($this->readonlySHA1[$sha1]); + } + } + + /** + * Returns the default list of disallowed commands. + * + * @return array + */ + protected function getDisallowedOperations() + { + return array( + 'SHUTDOWN' => true, + 'INFO' => true, + 'DBSIZE' => true, + 'LASTSAVE' => true, + 'CONFIG' => true, + 'MONITOR' => true, + 'SLAVEOF' => true, + 'SAVE' => true, + 'BGSAVE' => true, + 'BGREWRITEAOF' => true, + 'SLOWLOG' => true, + ); + } + + /** + * Returns the default list of commands performing read-only operations. + * + * @return array + */ + protected function getReadOnlyOperations() + { + return array( + 'EXISTS' => true, + 'TYPE' => true, + 'KEYS' => true, + 'RANDOMKEY' => true, + 'TTL' => true, + 'GET' => true, + 'MGET' => true, + 'SUBSTR' => true, + 'STRLEN' => true, + 'GETRANGE' => true, + 'GETBIT' => true, + 'LLEN' => true, + 'LRANGE' => true, + 'LINDEX' => true, + 'SCARD' => true, + 'SISMEMBER' => true, + 'SINTER' => true, + 'SUNION' => true, + 'SDIFF' => true, + 'SMEMBERS' => true, + 'SRANDMEMBER' => true, + 'ZRANGE' => true, + 'ZREVRANGE' => true, + 'ZRANGEBYSCORE' => true, + 'ZREVRANGEBYSCORE' => true, + 'ZCARD' => true, + 'ZSCORE' => true, + 'ZCOUNT' => true, + 'ZRANK' => true, + 'ZREVRANK' => true, + 'HGET' => true, + 'HMGET' => true, + 'HEXISTS' => true, + 'HLEN' => true, + 'HKEYS' => true, + 'HVALS' => true, + 'HGETALL' => true, + 'PING' => true, + 'AUTH' => true, + 'SELECT' => true, + 'ECHO' => true, + 'QUIT' => true, + 'OBJECT' => true, + 'BITCOUNT' => true, + 'TIME' => true, + 'SORT' => array($this, 'isSortReadOnly'), + ); + } +} diff --git a/tests/Predis/Connection/MasterSlaveReplicationTest.php b/tests/Predis/Connection/MasterSlaveReplicationTest.php index ca736255..5acf0d77 100644 --- a/tests/Predis/Connection/MasterSlaveReplicationTest.php +++ b/tests/Predis/Connection/MasterSlaveReplicationTest.php @@ -14,6 +14,7 @@ namespace Predis\Connection; use \PHPUnit_Framework_TestCase as StandardTestCase; use Predis\Profile\ServerProfile; +use Predis\Replication\ReplicationStrategy; /** * @@ -467,8 +468,8 @@ class MasterSlaveReplicationTest extends StandardTestCase $replication->add($master); $replication->add($slave1); - $replication->setCommandReadOnly($cmdSet->getId(), true); - $replication->setCommandReadOnly($cmdGet->getId(), false); + $replication->getReplicationStrategy()->setCommandReadOnly($cmdSet->getId(), true); + $replication->getReplicationStrategy()->setCommandReadOnly($cmdGet->getId(), false); $replication->executeCommand($cmdSet); $replication->executeCommand($cmdGet); @@ -493,7 +494,7 @@ class MasterSlaveReplicationTest extends StandardTestCase $replication->add($master); $replication->add($slave1); - $replication->setCommandReadOnly('exists', function ($cmd) { + $replication->getReplicationStrategy()->setCommandReadOnly('exists', function ($cmd) { list($arg1) = $cmd->getArguments(); return $arg1 === 'foo'; }); @@ -524,12 +525,25 @@ class MasterSlaveReplicationTest extends StandardTestCase $replication->add($master); $replication->add($slave1); - $replication->setScriptReadOnly($script); + $replication->getReplicationStrategy()->setScriptReadOnly($script); $replication->executeCommand($cmdEval); $replication->executeCommand($cmdEvalSha); } + /** + * @group disconnected + */ + public function testExposesReplicationStrategy() + { + $replication = new MasterSlaveReplication(); + $this->assertInstanceOf('Predis\Replication\ReplicationStrategy', $replication->getReplicationStrategy()); + + $strategy = new ReplicationStrategy(); + $replication = new MasterSlaveReplication($strategy); + $this->assertSame($strategy, $replication->getReplicationStrategy()); + } + /** * @group disconnected */ diff --git a/tests/Predis/Replication/ReplicationStrategyTest.php b/tests/Predis/Replication/ReplicationStrategyTest.php new file mode 100644 index 00000000..10365a24 --- /dev/null +++ b/tests/Predis/Replication/ReplicationStrategyTest.php @@ -0,0 +1,374 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Replication; + +use \PHPUnit_Framework_TestCase as StandardTestCase; + +use Predis\Command\CommandInterface; +use Predis\Profile\ServerProfile; + +/** + * + */ +class ReplicationStrategyTest extends StandardTestCase +{ + /** + * @group disconnected + */ + public function testReadCommands() + { + $profile = ServerProfile::getDevelopment(); + $strategy = new ReplicationStrategy(); + + foreach ($this->getExpectedCommands('read') as $commandId) { + $command = $profile->createCommand($commandId); + $this->assertTrue($strategy->isReadOperation($command)); + } + } + + /** + * @group disconnected + */ + public function testWriteCommands() + { + $profile = ServerProfile::getDevelopment(); + $strategy = new ReplicationStrategy(); + + foreach ($this->getExpectedCommands('write') as $commandId) { + $command = $profile->createCommand($commandId); + $this->assertFalse($strategy->isReadOperation($command), $commandId); + } + } + + /** + * @group disconnected + */ + public function testDisallowedCommands() + { + $profile = ServerProfile::getDevelopment(); + $strategy = new ReplicationStrategy(); + + foreach ($this->getExpectedCommands('disallowed') as $commandId) { + $command = $profile->createCommand($commandId); + $this->assertTrue($strategy->isDisallowedOperation($command), $commandId); + } + } + + /** + * @group disconnected + */ + public function testSortCommand() + { + $profile = ServerProfile::getDevelopment(); + $strategy = new ReplicationStrategy(); + + $cmdReadSort = $profile->createCommand('SORT', array('key:list')); + $this->assertTrue($strategy->isReadOperation($cmdReadSort), 'SORT [read-only]'); + + $cmdWriteSort = $profile->createCommand('SORT', array('key:list', array('store' => 'key:stored'))); + $this->assertFalse($strategy->isReadOperation($cmdWriteSort), 'SORT [write with STORE]'); + } + + /** + * @group disconnected + * @expectedException Predis\NotSupportedException + * @expectedExceptionMessage The command INFO is not allowed in replication mode + */ + public function testUsingDisallowedCommandThrowsException() + { + $profile = ServerProfile::getDevelopment(); + $strategy = new ReplicationStrategy(); + + $command = $profile->createCommand('INFO'); + $strategy->isReadOperation($command); + } + + /** + * @group disconnected + */ + public function testDefaultIsWriteOperation() + { + $strategy = new ReplicationStrategy(); + + $command = $this->getMock('Predis\Command\CommandInterface'); + $command->expects($this->any()) + ->method('getId') + ->will($this->returnValue('CMDTEST')); + + $this->assertFalse($strategy->isReadOperation($command)); + } + + /** + * @group disconnected + */ + public function testCanSetCommandAsReadOperation() + { + $strategy = new ReplicationStrategy(); + + $command = $this->getMock('Predis\Command\CommandInterface'); + $command->expects($this->any()) + ->method('getId') + ->will($this->returnValue('CMDTEST')); + + + $strategy->setCommandReadOnly('CMDTEST', true); + $this->assertTrue($strategy->isReadOperation($command)); + } + + /** + * @group disconnected + */ + public function testCanSetCommandAsWriteOperation() + { + $strategy = new ReplicationStrategy(); + + $command = $this->getMock('Predis\Command\CommandInterface'); + $command->expects($this->any()) + ->method('getId') + ->will($this->returnValue('CMDTEST')); + + + $strategy->setCommandReadOnly('CMDTEST', false); + $this->assertFalse($strategy->isReadOperation($command)); + + $strategy->setCommandReadOnly('GET', false); + $this->assertFalse($strategy->isReadOperation($command)); + } + + /** + * @group disconnected + */ + public function testCanUseCallableToCheckCommand() + { + $strategy = new ReplicationStrategy(); + $profile = ServerProfile::getDevelopment(); + + $strategy->setCommandReadOnly('SET', function ($command) { + return $command->getArgument(1) === true; + }); + + $command = $profile->createCommand('SET', array('trigger', false)); + $this->assertFalse($strategy->isReadOperation($command)); + + $command = $profile->createCommand('SET', array('trigger', true)); + $this->assertTrue($strategy->isReadOperation($command)); + } + + /** + * @group disconnected + */ + public function testSetLuaScriptAsReadOperation() + { + $strategy = new ReplicationStrategy(); + $profile = ServerProfile::getDevelopment(); + + $writeScript = 'redis.call("set", "foo", "bar")'; + $readScript = 'return true'; + + $strategy->setScriptReadOnly($readScript, true); + + $cmdEval = $profile->createCommand('EVAL', array($writeScript)); + $cmdEvalSHA = $profile->createCommand('EVALSHA', array(sha1($writeScript))); + $this->assertFalse($strategy->isReadOperation($cmdEval)); + $this->assertFalse($strategy->isReadOperation($cmdEvalSHA)); + + $cmdEval = $profile->createCommand('EVAL', array($readScript)); + $cmdEvalSHA = $profile->createCommand('EVALSHA', array(sha1($readScript))); + $this->assertTrue($strategy->isReadOperation($cmdEval)); + $this->assertTrue($strategy->isReadOperation($cmdEvalSHA)); + } + + /** + * @group disconnected + */ + public function testSetLuaScriptAsReadOperationWorksWithScriptedCommand() + { + $strategy = new ReplicationStrategy(); + + $command = $this->getMock('Predis\Command\ScriptedCommand', array('getScript')); + $command->expects($this->any()) + ->method('getScript') + ->will($this->returnValue($script = 'return true')); + + $strategy->setScriptReadOnly($script, function ($command) { + return $command->getArgument(2) === true; + }); + + $command->setArguments(array(false)); + $this->assertFalse($strategy->isReadOperation($command)); + + $command->setArguments(array(true)); + $this->assertTrue($strategy->isReadOperation($command)); + } + + /** + * @group disconnected + */ + public function testSetLuaScriptAsReadOperationWorksWithScriptedCommandAndCallableCheck() + { + $strategy = new ReplicationStrategy(); + + $command = $this->getMock('Predis\Command\ScriptedCommand', array('getScript')); + $command->expects($this->any()) + ->method('getScript') + ->will($this->returnValue($script = 'return true')); + + $command->setArguments(array('trigger', false)); + + $strategy->setScriptReadOnly($script, true); + + $this->assertTrue($strategy->isReadOperation($command)); + } + + // ******************************************************************** // + // ---- HELPER METHODS ------------------------------------------------ // + // ******************************************************************** // + + /** + * 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 connection */ + 'EXISTS' => 'read', + 'AUTH' => 'read', + 'SELECT' => 'read', + 'ECHO' => 'read', + 'QUIT' => 'read', + 'OBJECT' => 'read', + 'BITCOUNT' => 'read', + 'TIME' => 'read', + 'SHUTDOWN' => 'disallowed', + 'INFO' => 'disallowed', + 'DBSIZE' => 'disallowed', + 'LASTSAVE' => 'disallowed', + 'CONFIG' => 'disallowed', + 'MONITOR' => 'disallowed', + 'SLAVEOF' => 'disallowed', + 'SAVE' => 'disallowed', + 'BGSAVE' => 'disallowed', + 'BGREWRITEAOF' => 'disallowed', + 'SLOWLOG' => 'disallowed', + + /* commands operating on the key space */ + 'EXISTS' => 'read', + 'DEL' => 'write', + 'TYPE' => 'read', + 'EXPIRE' => 'write', + 'EXPIREAT' => 'write', + 'PERSIST' => 'write', + 'PEXPIRE' => 'write', + 'PEXPIREAT' => 'write', + 'TTL' => 'read', + 'PTTL' => 'write', + 'SORT' => 'variable', + 'KEYS' => 'read', + 'RANDOMKEY' => 'read', + + /* commands operating on string values */ + 'APPEND' => 'write', + 'DECR' => 'write', + 'DECRBY' => 'write', + 'GET' => 'read', + 'GETBIT' => 'read', + 'MGET' => 'read', + 'SET' => 'write', + 'GETRANGE' => 'read', + 'GETSET' => 'write', + 'INCR' => 'write', + 'INCRBY' => 'write', + 'SETBIT' => 'write', + 'SETEX' => 'write', + 'MSET' => 'write', + 'MSETNX' => 'write', + 'SETNX' => 'write', + 'SETRANGE' => 'write', + 'STRLEN' => 'read', + 'SUBSTR' => 'read', + + /* commands operating on lists */ + 'LINSERT' => 'write', + 'LINDEX' => 'read', + 'LLEN' => 'read', + 'LPOP' => 'write', + 'RPOP' => 'write', + 'BLPOP' => 'write', + 'BRPOP' => 'write', + 'LPUSH' => 'write', + 'LPUSHX' => 'write', + 'RPUSH' => 'write', + 'RPUSHX' => 'write', + 'LRANGE' => 'read', + 'LREM' => 'write', + 'LSET' => 'write', + 'LTRIM' => 'write', + + /* commands operating on sets */ + 'SADD' => 'write', + 'SCARD' => 'read', + 'SISMEMBER' => 'read', + 'SMEMBERS' => 'read', + 'SPOP' => 'write', + 'SRANDMEMBER' => 'read', + 'SREM' => 'write', + 'SINTER' => 'read', + 'SUNION' => 'read', + 'SDIFF' => 'read', + + /* commands operating on sorted sets */ + 'ZADD' => 'write', + 'ZCARD' => 'read', + 'ZCOUNT' => 'read', + 'ZINCRBY' => 'write', + 'ZRANGE' => 'read', + 'ZRANGEBYSCORE' => 'read', + 'ZRANK' => 'read', + 'ZREM' => 'write', + 'ZREMRANGEBYRANK' => 'write', + 'ZREMRANGEBYSCORE' => 'write', + 'ZREVRANGE' => 'read', + 'ZREVRANGEBYSCORE' => 'read', + 'ZREVRANK' => 'read', + 'ZSCORE' => 'read', + + /* commands operating on hashes */ + 'HDEL' => 'write', + 'HEXISTS' => 'read', + 'HGET' => 'read', + 'HGETALL' => 'read', + 'HMGET' => 'read', + 'HINCRBY' => 'write', + 'HINCRBYFLOAT' => 'write', + 'HKEYS' => 'read', + 'HLEN' => 'read', + 'HSET' => 'write', + 'HSETNX' => 'write', + 'HVALS' => 'read', + + /* scripting */ + 'EVAL' => 'write', + 'EVALSHA' => 'write', + ); + + if (isset($type)) { + $commands = array_filter($commands, function ($expectedType) use ($type) { + return $expectedType === $type; + }); + } + + return array_keys($commands); + } +}