Эх сурвалжийг харах

Extract a strategy class from the replication connection.

Daniele Alessandri 12 жил өмнө
parent
commit
732d7f96d1

+ 4 - 0
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.

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

+ 24 - 184
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 <suppakilla@gmail.com>
  */
 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');
     }
 }

+ 218 - 0
lib/Predis/Replication/ReplicationStrategy.php

@@ -0,0 +1,218 @@
+<?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\Replication;
+
+use Predis\NotSupportedException;
+use Predis\Command\CommandInterface;
+
+/**
+ * Defines a strategy for master/reply replication.
+ *
+ * @author Daniele Alessandri <suppakilla@gmail.com>
+ */
+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'),
+        );
+    }
+}

+ 18 - 4
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
      */

+ 374 - 0
tests/Predis/Replication/ReplicationStrategyTest.php

@@ -0,0 +1,374 @@
+<?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\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);
+    }
+}