redis 4.0.1 → 4.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +3 -0
  3. data/.travis.yml +17 -29
  4. data/.travis/Gemfile +5 -0
  5. data/CHANGELOG.md +29 -0
  6. data/Gemfile +5 -0
  7. data/README.md +1 -1
  8. data/bin/build +71 -0
  9. data/lib/redis.rb +198 -12
  10. data/lib/redis/client.rb +26 -12
  11. data/lib/redis/cluster.rb +285 -0
  12. data/lib/redis/cluster/command.rb +81 -0
  13. data/lib/redis/cluster/command_loader.rb +32 -0
  14. data/lib/redis/cluster/key_slot_converter.rb +72 -0
  15. data/lib/redis/cluster/node.rb +104 -0
  16. data/lib/redis/cluster/node_key.rb +35 -0
  17. data/lib/redis/cluster/node_loader.rb +35 -0
  18. data/lib/redis/cluster/option.rb +76 -0
  19. data/lib/redis/cluster/slot.rb +69 -0
  20. data/lib/redis/cluster/slot_loader.rb +47 -0
  21. data/lib/redis/connection/ruby.rb +5 -2
  22. data/lib/redis/distributed.rb +10 -2
  23. data/lib/redis/errors.rb +46 -0
  24. data/lib/redis/pipeline.rb +9 -1
  25. data/lib/redis/version.rb +1 -1
  26. data/makefile +54 -22
  27. data/redis.gemspec +2 -1
  28. data/test/client_test.rb +17 -0
  29. data/test/cluster_abnormal_state_test.rb +38 -0
  30. data/test/cluster_blocking_commands_test.rb +15 -0
  31. data/test/cluster_client_internals_test.rb +77 -0
  32. data/test/cluster_client_key_hash_tags_test.rb +88 -0
  33. data/test/cluster_client_options_test.rb +147 -0
  34. data/test/cluster_client_pipelining_test.rb +59 -0
  35. data/test/cluster_client_replicas_test.rb +36 -0
  36. data/test/cluster_client_slots_test.rb +94 -0
  37. data/test/cluster_client_transactions_test.rb +71 -0
  38. data/test/cluster_commands_on_cluster_test.rb +165 -0
  39. data/test/cluster_commands_on_connection_test.rb +40 -0
  40. data/test/cluster_commands_on_geo_test.rb +74 -0
  41. data/test/cluster_commands_on_hashes_test.rb +11 -0
  42. data/test/cluster_commands_on_hyper_log_log_test.rb +17 -0
  43. data/test/cluster_commands_on_keys_test.rb +134 -0
  44. data/test/cluster_commands_on_lists_test.rb +15 -0
  45. data/test/cluster_commands_on_pub_sub_test.rb +101 -0
  46. data/test/cluster_commands_on_scripting_test.rb +56 -0
  47. data/test/cluster_commands_on_server_test.rb +221 -0
  48. data/test/cluster_commands_on_sets_test.rb +39 -0
  49. data/test/cluster_commands_on_sorted_sets_test.rb +35 -0
  50. data/test/cluster_commands_on_streams_test.rb +196 -0
  51. data/test/cluster_commands_on_strings_test.rb +15 -0
  52. data/test/cluster_commands_on_transactions_test.rb +41 -0
  53. data/test/cluster_commands_on_value_types_test.rb +14 -0
  54. data/test/commands_on_geo_test.rb +116 -0
  55. data/test/commands_on_hashes_test.rb +2 -14
  56. data/test/commands_on_hyper_log_log_test.rb +2 -14
  57. data/test/commands_on_lists_test.rb +2 -13
  58. data/test/commands_on_sets_test.rb +2 -70
  59. data/test/commands_on_sorted_sets_test.rb +2 -145
  60. data/test/commands_on_strings_test.rb +2 -94
  61. data/test/commands_on_value_types_test.rb +36 -0
  62. data/test/distributed_blocking_commands_test.rb +8 -0
  63. data/test/distributed_commands_on_hashes_test.rb +16 -3
  64. data/test/distributed_commands_on_hyper_log_log_test.rb +8 -13
  65. data/test/distributed_commands_on_lists_test.rb +4 -5
  66. data/test/distributed_commands_on_sets_test.rb +45 -46
  67. data/test/distributed_commands_on_sorted_sets_test.rb +51 -8
  68. data/test/distributed_commands_on_strings_test.rb +10 -0
  69. data/test/distributed_commands_on_value_types_test.rb +36 -0
  70. data/test/helper.rb +176 -32
  71. data/test/internals_test.rb +20 -1
  72. data/test/lint/blocking_commands.rb +40 -16
  73. data/test/lint/hashes.rb +41 -0
  74. data/test/lint/hyper_log_log.rb +15 -1
  75. data/test/lint/lists.rb +16 -0
  76. data/test/lint/sets.rb +142 -0
  77. data/test/lint/sorted_sets.rb +183 -2
  78. data/test/lint/strings.rb +102 -0
  79. data/test/pipelining_commands_test.rb +8 -0
  80. data/test/support/cluster/orchestrator.rb +199 -0
  81. data/test/support/redis_mock.rb +1 -1
  82. data/test/transactions_test.rb +10 -0
  83. metadata +81 -2
@@ -267,7 +267,10 @@ class Redis
267
267
  ssl_sock = new(tcp_sock, ctx)
268
268
  ssl_sock.hostname = host
269
269
  ssl_sock.connect
270
- ssl_sock.post_connection_check(host)
270
+
271
+ unless ctx.verify_mode == OpenSSL::SSL::VERIFY_NONE
272
+ ssl_sock.post_connection_check(host)
273
+ end
271
274
 
272
275
  ssl_sock
273
276
  end
@@ -294,7 +297,7 @@ class Redis
294
297
  end
295
298
 
296
299
  instance = new(sock)
297
- instance.timeout = config[:timeout]
300
+ instance.timeout = config[:read_timeout]
298
301
  instance.write_timeout = config[:write_timeout]
299
302
  instance.set_tcp_keepalive config[:tcp_keepalive]
300
303
  instance
@@ -161,6 +161,14 @@ class Redis
161
161
  end
162
162
  end
163
163
 
164
+ # Unlink keys.
165
+ def unlink(*args)
166
+ keys_per_node = args.group_by { |key| node_for(key) }
167
+ keys_per_node.inject(0) do |sum, (node, keys)|
168
+ sum + node.unlink(*keys)
169
+ end
170
+ end
171
+
164
172
  # Determine if a key exists.
165
173
  def exists(key)
166
174
  node_for(key).exists(key)
@@ -692,8 +700,8 @@ class Redis
692
700
  end
693
701
 
694
702
  # Delete one or more hash fields.
695
- def hdel(key, field)
696
- node_for(key).hdel(key, field)
703
+ def hdel(key, *fields)
704
+ node_for(key).hdel(key, *fields)
697
705
  end
698
706
 
699
707
  # Determine if a hash field exists.
@@ -37,4 +37,50 @@ class Redis
37
37
  # Raised when the connection was inherited by a child process.
38
38
  class InheritedError < BaseConnectionError
39
39
  end
40
+
41
+ # Raised when client options are invalid.
42
+ class InvalidClientOptionError < BaseError
43
+ end
44
+
45
+ class Cluster
46
+ # Raised when client connected to redis as cluster mode
47
+ # and some cluster subcommands were called.
48
+ class OrchestrationCommandNotSupported < BaseError
49
+ def initialize(command, subcommand = '')
50
+ str = [command, subcommand].map(&:to_s).reject(&:empty?).join(' ').upcase
51
+ msg = "#{str} command should be used with care "\
52
+ 'only by applications orchestrating Redis Cluster, like redis-trib, '\
53
+ 'and the command if used out of the right context can leave the cluster '\
54
+ 'in a wrong state or cause data loss.'
55
+ super(msg)
56
+ end
57
+ end
58
+
59
+ # Raised when error occurs on any node of cluster.
60
+ class CommandErrorCollection < BaseError
61
+ attr_reader :errors
62
+
63
+ # @param errors [Hash{String => Redis::CommandError}]
64
+ # @param error_message [String]
65
+ def initialize(errors, error_message = 'Command errors were replied on any node')
66
+ @errors = errors
67
+ super(error_message)
68
+ end
69
+ end
70
+
71
+ # Raised when cluster client can't select node.
72
+ class AmbiguousNodeError < BaseError
73
+ def initialize(command)
74
+ super("Cluster client doesn't know which node the #{command} command should be sent to.")
75
+ end
76
+ end
77
+
78
+ # Raised when commands in pipelining include cross slot keys.
79
+ class CrossSlotPipeliningError < BaseError
80
+ def initialize(keys)
81
+ super("Cluster client couldn't send pipelining to single node. "\
82
+ "The commands include cross slot keys. #{keys}")
83
+ end
84
+ end
85
+ end
40
86
  end
@@ -22,6 +22,10 @@ class Redis
22
22
  @shutdown
23
23
  end
24
24
 
25
+ def empty?
26
+ @futures.empty?
27
+ end
28
+
25
29
  def call(command, &block)
26
30
  # A pipeline that contains a shutdown should not raise ECONNRESET when
27
31
  # the connection is gone.
@@ -86,7 +90,11 @@ class Redis
86
90
  end
87
91
 
88
92
  def commands
89
- [[:multi]] + super + [[:exec]]
93
+ if empty?
94
+ []
95
+ else
96
+ [[:multi]] + super + [[:exec]]
97
+ end
90
98
  end
91
99
  end
92
100
  end
@@ -1,3 +1,3 @@
1
1
  class Redis
2
- VERSION = "4.0.1"
2
+ VERSION = "4.0.3"
3
3
  end
data/makefile CHANGED
@@ -1,33 +1,43 @@
1
- TEST_FILES := $(shell find test -name *_test.rb -type f)
2
- REDIS_BRANCH := unstable
3
- TMP := tmp
4
- BUILD_DIR := ${TMP}/redis-${REDIS_BRANCH}
5
- TARBALL := ${TMP}/redis-${REDIS_BRANCH}.tar.gz
6
- BINARY := ${BUILD_DIR}/src/redis-server
7
- PID_PATH := ${BUILD_DIR}/redis.pid
8
- SOCKET_PATH := ${BUILD_DIR}/redis.sock
9
- PORT := 6381
1
+ TEST_FILES := $(shell find ./test -name *_test.rb -type f)
2
+ REDIS_BRANCH ?= unstable
3
+ TMP := tmp
4
+ BUILD_DIR := ${TMP}/cache/redis-${REDIS_BRANCH}
5
+ TARBALL := ${TMP}/redis-${REDIS_BRANCH}.tar.gz
6
+ BINARY := ${BUILD_DIR}/src/redis-server
7
+ REDIS_CLIENT := ${BUILD_DIR}/src/redis-cli
8
+ REDIS_TRIB := ${BUILD_DIR}/src/redis-trib.rb
9
+ PID_PATH := ${BUILD_DIR}/redis.pid
10
+ SOCKET_PATH := ${BUILD_DIR}/redis.sock
11
+ PORT := 6381
12
+ CLUSTER_PORTS := 7000 7001 7002 7003 7004 7005
13
+ CLUSTER_PID_PATHS := $(addprefix ${TMP}/redis,$(addsuffix .pid,${CLUSTER_PORTS}))
14
+ CLUSTER_CONF_PATHS := $(addprefix ${TMP}/nodes,$(addsuffix .conf,${CLUSTER_PORTS}))
15
+ CLUSTER_ADDRS := $(addprefix 127.0.0.1:,${CLUSTER_PORTS})
10
16
 
11
- test: ${TEST_FILES}
17
+ define kill-redis
18
+ (ls $1 2> /dev/null && kill $$(cat $1) && rm -f $1) || true
19
+ endef
20
+
21
+ all:
12
22
  make start
13
- env SOCKET_PATH=${SOCKET_PATH} \
14
- ruby -v $$(echo $? | tr ' ' '\n' | awk '{ print "-r./" $$0 }') -e ''
23
+ make start_cluster
24
+ make create_cluster
25
+ make test
15
26
  make stop
27
+ make stop_cluster
16
28
 
17
29
  ${TMP}:
18
- mkdir $@
30
+ mkdir -p $@
19
31
 
20
- ${TARBALL}: ${TMP}
21
- wget https://github.com/antirez/redis/archive/${REDIS_BRANCH}.tar.gz -O $@
32
+ ${BINARY}: ${TMP}
33
+ bin/build ${REDIS_BRANCH} $<
22
34
 
23
- ${BINARY}: ${TARBALL} ${TMP}
24
- rm -rf ${BUILD_DIR}
25
- mkdir -p ${BUILD_DIR}
26
- tar xf ${TARBALL} -C ${TMP}
27
- cd ${BUILD_DIR} && make
35
+ test: ${TEST_FILES}
36
+ env SOCKET_PATH=${SOCKET_PATH} \
37
+ bundle exec ruby -v -e 'ARGV.each { |test_file| require test_file }' ${TEST_FILES}
28
38
 
29
39
  stop:
30
- (test -f ${PID_PATH} && (kill $$(cat ${PID_PATH}) || true) && rm -f ${PID_PATH}) || true
40
+ $(call kill-redis,${PID_PATH})
31
41
 
32
42
  start: ${BINARY}
33
43
  ${BINARY} \
@@ -36,7 +46,29 @@ start: ${BINARY}
36
46
  --port ${PORT} \
37
47
  --unixsocket ${SOCKET_PATH}
38
48
 
49
+ stop_cluster:
50
+ $(call kill-redis,${CLUSTER_PID_PATHS})
51
+ rm -f appendonly.aof || true
52
+ rm -f ${CLUSTER_CONF_PATHS} || true
53
+
54
+ start_cluster: ${BINARY}
55
+ for port in ${CLUSTER_PORTS}; do \
56
+ ${BINARY} \
57
+ --daemonize yes \
58
+ --appendonly yes \
59
+ --cluster-enabled yes \
60
+ --cluster-config-file ${TMP}/nodes$$port.conf \
61
+ --cluster-node-timeout 5000 \
62
+ --pidfile ${TMP}/redis$$port.pid \
63
+ --port $$port \
64
+ --unixsocket ${TMP}/redis$$port.sock; \
65
+ done
66
+
67
+ create_cluster:
68
+ yes yes | ((bundle exec ruby ${REDIS_TRIB} create --replicas 1 ${CLUSTER_ADDRS}) || \
69
+ (${REDIS_CLIENT} --cluster create ${CLUSTER_ADDRS} --cluster-replicas 1))
70
+
39
71
  clean:
40
72
  (test -d ${BUILD_DIR} && cd ${BUILD_DIR}/src && make clean distclean) || true
41
73
 
42
- .PHONY: test start stop
74
+ .PHONY: all test stop start stop_cluster start_cluster create_cluster clean
@@ -32,11 +32,12 @@ Gem::Specification.new do |s|
32
32
 
33
33
  s.files = `git ls-files`.split("\n")
34
34
  s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
35
- s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
35
+ s.executables = `git ls-files -- exe/*`.split("\n").map{ |f| File.basename(f) }
36
36
 
37
37
  s.required_ruby_version = '>= 2.2.2'
38
38
 
39
39
  s.add_development_dependency("test-unit", ">= 3.1.5")
40
+ s.add_development_dependency("mocha")
40
41
  s.add_development_dependency("hiredis")
41
42
  s.add_development_dependency("em-synchrony")
42
43
  end
@@ -56,4 +56,21 @@ class TestClient < Test::Unit::TestCase
56
56
 
57
57
  assert_equal result, ["OK", 1]
58
58
  end
59
+
60
+ def test_client_with_custom_connector
61
+ custom_connector = Class.new(Redis::Client::Connector) do
62
+ def resolve
63
+ @options[:host] = '127.0.0.5'
64
+ @options[:port] = '999'
65
+ @options
66
+ end
67
+ end
68
+
69
+ assert_raise_message(
70
+ 'Error connecting to Redis on 127.0.0.5:999 (Errno::ECONNREFUSED)'
71
+ ) do
72
+ new_redis = _new_client(connector: custom_connector)
73
+ new_redis.ping
74
+ end
75
+ end
59
76
  end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'helper'
4
+
5
+ # ruby -w -Itest test/cluster_abnormal_state_test.rb
6
+ class TestClusterAbnormalState < Test::Unit::TestCase
7
+ include Helper::Cluster
8
+
9
+ def test_the_state_of_cluster_down
10
+ redis_cluster_down do
11
+ assert_raise(Redis::CommandError, 'CLUSTERDOWN Hash slot not served') do
12
+ redis.set('key1', 1)
13
+ end
14
+
15
+ assert_equal 'fail', redis.cluster(:info).fetch('cluster_state')
16
+ end
17
+ end
18
+
19
+ def test_the_state_of_cluster_failover
20
+ redis_cluster_failover do
21
+ 100.times do |i|
22
+ assert_equal 'OK', r.set("key#{i}", i)
23
+ end
24
+
25
+ 100.times do |i|
26
+ assert_equal i.to_s, r.get("key#{i}")
27
+ end
28
+
29
+ assert_equal 'ok', redis.cluster(:info).fetch('cluster_state')
30
+ end
31
+ end
32
+
33
+ def test_raising_error_when_nodes_are_not_cluster_mode
34
+ assert_raise(Redis::CannotConnectError, 'Redis client could not connect to any cluster nodes') do
35
+ build_another_client(cluster: %W[redis://127.0.0.1:#{PORT}])
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'helper'
4
+ require_relative 'lint/blocking_commands'
5
+
6
+ # ruby -w -Itest test/cluster_blocking_commands_test.rb
7
+ class TestClusterBlockingCommands < Test::Unit::TestCase
8
+ include Helper::Cluster
9
+ include Lint::BlockingCommands
10
+
11
+ def mock(options = {}, &blk)
12
+ commands = build_mock_commands(options)
13
+ redis_cluster_mock(commands, &blk)
14
+ end
15
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'helper'
4
+
5
+ # ruby -w -Itest test/cluster_client_internals_test.rb
6
+ class TestClusterClientInternals < Test::Unit::TestCase
7
+ include Helper::Cluster
8
+
9
+ def test_handle_multiple_servers
10
+ 100.times { |i| redis.set(i.to_s, "hogehoge#{i}") }
11
+ 100.times { |i| assert_equal "hogehoge#{i}", redis.get(i.to_s) }
12
+ end
13
+
14
+ def test_info_of_cluster_mode_is_enabled
15
+ assert_equal '1', redis.info['cluster_enabled']
16
+ end
17
+
18
+ def test_unknown_commands_does_not_work_by_default
19
+ assert_raise(Redis::CommandError) do
20
+ redis.not_yet_implemented_command('boo', 'foo')
21
+ end
22
+ end
23
+
24
+ def test_with_reconnect
25
+ assert_equal('Hello World', redis.with_reconnect { 'Hello World' })
26
+ end
27
+
28
+ def test_without_reconnect
29
+ assert_equal('Hello World', redis.without_reconnect { 'Hello World' })
30
+ end
31
+
32
+ def test_connected?
33
+ assert_equal true, redis.connected?
34
+ end
35
+
36
+ def test_close
37
+ assert_equal true, redis.close
38
+ end
39
+
40
+ def test_disconnect!
41
+ assert_equal true, redis.disconnect!
42
+ end
43
+
44
+ def test_asking
45
+ assert_equal 'OK', redis.asking
46
+ end
47
+
48
+ def test_id
49
+ expected = 'redis://127.0.0.1:7000/0 '\
50
+ 'redis://127.0.0.1:7001/0 '\
51
+ 'redis://127.0.0.1:7002/0'
52
+ assert_equal expected, redis.id
53
+ end
54
+
55
+ def test_inspect
56
+ expected = "#<Redis client v#{Redis::VERSION} for "\
57
+ 'redis://127.0.0.1:7000/0 '\
58
+ 'redis://127.0.0.1:7001/0 '\
59
+ 'redis://127.0.0.1:7002/0>'
60
+
61
+ assert_equal expected, redis.inspect
62
+ end
63
+
64
+ def test_dup
65
+ assert_instance_of Redis, redis.dup
66
+ end
67
+
68
+ def test_connection
69
+ expected = [
70
+ { host: '127.0.0.1', port: 7000, db: 0, id: 'redis://127.0.0.1:7000/0', location: '127.0.0.1:7000' },
71
+ { host: '127.0.0.1', port: 7001, db: 0, id: 'redis://127.0.0.1:7001/0', location: '127.0.0.1:7001' },
72
+ { host: '127.0.0.1', port: 7002, db: 0, id: 'redis://127.0.0.1:7002/0', location: '127.0.0.1:7002' }
73
+ ]
74
+
75
+ assert_equal expected, redis.connection
76
+ end
77
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'helper'
4
+
5
+ # ruby -w -Itest test/cluster_client_key_hash_tags_test.rb
6
+ class TestClusterClientKeyHashTags < Test::Unit::TestCase
7
+ include Helper::Cluster
8
+
9
+ def build_described_class
10
+ option = Redis::Cluster::Option.new(cluster: ['redis://127.0.0.1:7000'])
11
+ node = Redis::Cluster::Node.new(option.per_node_key)
12
+ details = Redis::Cluster::CommandLoader.load(node)
13
+ Redis::Cluster::Command.new(details)
14
+ end
15
+
16
+ def test_key_extraction
17
+ described_class = build_described_class
18
+
19
+ assert_equal 'dogs:1', described_class.extract_first_key(%w[get dogs:1])
20
+ assert_equal 'user1000', described_class.extract_first_key(%w[get {user1000}.following])
21
+ assert_equal 'user1000', described_class.extract_first_key(%w[get {user1000}.followers])
22
+ assert_equal 'foo{}{bar}', described_class.extract_first_key(%w[get foo{}{bar}])
23
+ assert_equal '{bar', described_class.extract_first_key(%w[get foo{{bar}}zap])
24
+ assert_equal 'bar', described_class.extract_first_key(%w[get foo{bar}{zap}])
25
+
26
+ assert_equal '', described_class.extract_first_key([:get, ''])
27
+ assert_equal '', described_class.extract_first_key([:get, nil])
28
+ assert_equal '', described_class.extract_first_key([:get])
29
+
30
+ assert_equal '', described_class.extract_first_key([:set, '', 1])
31
+ assert_equal '', described_class.extract_first_key([:set, nil, 1])
32
+ assert_equal '', described_class.extract_first_key([:set])
33
+
34
+ # Keyless commands
35
+ assert_equal '', described_class.extract_first_key([:auth, 'password'])
36
+ assert_equal '', described_class.extract_first_key(%i[client kill])
37
+ assert_equal '', described_class.extract_first_key(%i[cluster addslots])
38
+ assert_equal '', described_class.extract_first_key(%i[command])
39
+ assert_equal '', described_class.extract_first_key(%i[command count])
40
+ assert_equal '', described_class.extract_first_key(%i[config get])
41
+ assert_equal '', described_class.extract_first_key(%i[debug segfault])
42
+ assert_equal '', described_class.extract_first_key([:echo, 'Hello World'])
43
+ assert_equal '', described_class.extract_first_key([:flushall, 'ASYNC'])
44
+ assert_equal '', described_class.extract_first_key([:flushdb, 'ASYNC'])
45
+ assert_equal '', described_class.extract_first_key([:info, 'cluster'])
46
+ assert_equal '', described_class.extract_first_key(%i[memory doctor])
47
+ assert_equal '', described_class.extract_first_key([:ping, 'Hi'])
48
+ assert_equal '', described_class.extract_first_key([:psubscribe, 'channel'])
49
+ assert_equal '', described_class.extract_first_key([:pubsub, 'channels', '*'])
50
+ assert_equal '', described_class.extract_first_key([:publish, 'channel', 'Hi'])
51
+ assert_equal '', described_class.extract_first_key([:punsubscribe, 'channel'])
52
+ assert_equal '', described_class.extract_first_key([:subscribe, 'channel'])
53
+ assert_equal '', described_class.extract_first_key([:unsubscribe, 'channel'])
54
+ assert_equal '', described_class.extract_first_key(%w[script exists sha1 sha1])
55
+ assert_equal '', described_class.extract_first_key([:select, 1])
56
+ assert_equal '', described_class.extract_first_key([:shutdown, 'SAVE'])
57
+ assert_equal '', described_class.extract_first_key([:slaveof, '127.0.0.1', 6379])
58
+ assert_equal '', described_class.extract_first_key([:slowlog, 'get', 2])
59
+ assert_equal '', described_class.extract_first_key([:swapdb, 0, 1])
60
+ assert_equal '', described_class.extract_first_key([:wait, 1, 0])
61
+
62
+ # 2nd argument is not a key
63
+ assert_equal 'key1', described_class.extract_first_key([:eval, 'script', 2, 'key1', 'key2', 'first', 'second'])
64
+ assert_equal '', described_class.extract_first_key([:eval, 'return 0', 0])
65
+ assert_equal 'key1', described_class.extract_first_key([:evalsha, 'sha1', 2, 'key1', 'key2', 'first', 'second'])
66
+ assert_equal '', described_class.extract_first_key([:evalsha, 'return 0', 0])
67
+ assert_equal 'key1', described_class.extract_first_key([:migrate, '127.0.0.1', 6379, 'key1', 0, 5000])
68
+ assert_equal 'key1', described_class.extract_first_key([:memory, :usage, 'key1'])
69
+ assert_equal 'key1', described_class.extract_first_key([:object, 'refcount', 'key1'])
70
+ assert_equal 'mystream', described_class.extract_first_key([:xread, 'COUNT', 2, 'STREAMS', 'mystream', 0])
71
+ assert_equal 'mystream', described_class.extract_first_key([:xreadgroup, 'GROUP', 'mygroup', 'Bob', 'COUNT', 2, 'STREAMS', 'mystream', '>'])
72
+ end
73
+
74
+ def test_whether_the_command_effect_is_readonly_or_not
75
+ described_class = build_described_class
76
+
77
+ assert_equal true, described_class.should_send_to_master?([:set])
78
+ assert_equal false, described_class.should_send_to_slave?([:set])
79
+
80
+ assert_equal false, described_class.should_send_to_master?([:get])
81
+ assert_equal true, described_class.should_send_to_slave?([:get])
82
+
83
+ target_version('3.2.0') do
84
+ assert_equal false, described_class.should_send_to_master?([:info])
85
+ assert_equal false, described_class.should_send_to_slave?([:info])
86
+ end
87
+ end
88
+ end