redis 4.0.0.rc1 → 4.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (101) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +143 -3
  3. data/README.md +127 -18
  4. data/lib/redis/client.rb +150 -93
  5. data/lib/redis/cluster/command.rb +81 -0
  6. data/lib/redis/cluster/command_loader.rb +34 -0
  7. data/lib/redis/cluster/key_slot_converter.rb +72 -0
  8. data/lib/redis/cluster/node.rb +108 -0
  9. data/lib/redis/cluster/node_key.rb +31 -0
  10. data/lib/redis/cluster/node_loader.rb +37 -0
  11. data/lib/redis/cluster/option.rb +93 -0
  12. data/lib/redis/cluster/slot.rb +86 -0
  13. data/lib/redis/cluster/slot_loader.rb +49 -0
  14. data/lib/redis/cluster.rb +291 -0
  15. data/lib/redis/connection/command_helper.rb +3 -2
  16. data/lib/redis/connection/hiredis.rb +4 -3
  17. data/lib/redis/connection/registry.rb +2 -1
  18. data/lib/redis/connection/ruby.rb +123 -105
  19. data/lib/redis/connection/synchrony.rb +18 -5
  20. data/lib/redis/connection.rb +2 -0
  21. data/lib/redis/distributed.rb +955 -0
  22. data/lib/redis/errors.rb +48 -0
  23. data/lib/redis/hash_ring.rb +89 -0
  24. data/lib/redis/pipeline.rb +55 -9
  25. data/lib/redis/subscribe.rb +11 -12
  26. data/lib/redis/version.rb +3 -1
  27. data/lib/redis.rb +1242 -381
  28. metadata +34 -141
  29. data/.gitignore +0 -16
  30. data/.travis/Gemfile +0 -11
  31. data/.travis.yml +0 -71
  32. data/.yardopts +0 -3
  33. data/Gemfile +0 -3
  34. data/benchmarking/logging.rb +0 -71
  35. data/benchmarking/pipeline.rb +0 -51
  36. data/benchmarking/speed.rb +0 -21
  37. data/benchmarking/suite.rb +0 -24
  38. data/benchmarking/worker.rb +0 -71
  39. data/examples/basic.rb +0 -15
  40. data/examples/consistency.rb +0 -114
  41. data/examples/incr-decr.rb +0 -17
  42. data/examples/list.rb +0 -26
  43. data/examples/pubsub.rb +0 -37
  44. data/examples/sentinel/sentinel.conf +0 -9
  45. data/examples/sentinel/start +0 -49
  46. data/examples/sentinel.rb +0 -41
  47. data/examples/sets.rb +0 -36
  48. data/examples/unicorn/config.ru +0 -3
  49. data/examples/unicorn/unicorn.rb +0 -20
  50. data/makefile +0 -42
  51. data/redis.gemspec +0 -40
  52. data/test/bitpos_test.rb +0 -63
  53. data/test/blocking_commands_test.rb +0 -183
  54. data/test/client_test.rb +0 -59
  55. data/test/command_map_test.rb +0 -28
  56. data/test/commands_on_hashes_test.rb +0 -174
  57. data/test/commands_on_hyper_log_log_test.rb +0 -70
  58. data/test/commands_on_lists_test.rb +0 -154
  59. data/test/commands_on_sets_test.rb +0 -208
  60. data/test/commands_on_sorted_sets_test.rb +0 -444
  61. data/test/commands_on_strings_test.rb +0 -338
  62. data/test/commands_on_value_types_test.rb +0 -246
  63. data/test/connection_handling_test.rb +0 -275
  64. data/test/db/.gitkeep +0 -0
  65. data/test/encoding_test.rb +0 -14
  66. data/test/error_replies_test.rb +0 -57
  67. data/test/fork_safety_test.rb +0 -60
  68. data/test/helper.rb +0 -179
  69. data/test/helper_test.rb +0 -22
  70. data/test/internals_test.rb +0 -435
  71. data/test/persistence_control_commands_test.rb +0 -24
  72. data/test/pipelining_commands_test.rb +0 -238
  73. data/test/publish_subscribe_test.rb +0 -280
  74. data/test/remote_server_control_commands_test.rb +0 -175
  75. data/test/scanning_test.rb +0 -407
  76. data/test/scripting_test.rb +0 -76
  77. data/test/sentinel_command_test.rb +0 -78
  78. data/test/sentinel_test.rb +0 -253
  79. data/test/sorting_test.rb +0 -57
  80. data/test/ssl_test.rb +0 -69
  81. data/test/support/connection/hiredis.rb +0 -1
  82. data/test/support/connection/ruby.rb +0 -1
  83. data/test/support/connection/synchrony.rb +0 -17
  84. data/test/support/redis_mock.rb +0 -130
  85. data/test/support/ssl/gen_certs.sh +0 -31
  86. data/test/support/ssl/trusted-ca.crt +0 -25
  87. data/test/support/ssl/trusted-ca.key +0 -27
  88. data/test/support/ssl/trusted-cert.crt +0 -81
  89. data/test/support/ssl/trusted-cert.key +0 -28
  90. data/test/support/ssl/untrusted-ca.crt +0 -26
  91. data/test/support/ssl/untrusted-ca.key +0 -27
  92. data/test/support/ssl/untrusted-cert.crt +0 -82
  93. data/test/support/ssl/untrusted-cert.key +0 -28
  94. data/test/support/wire/synchrony.rb +0 -24
  95. data/test/support/wire/thread.rb +0 -5
  96. data/test/synchrony_driver.rb +0 -85
  97. data/test/test.conf.erb +0 -9
  98. data/test/thread_safety_test.rb +0 -60
  99. data/test/transactions_test.rb +0 -262
  100. data/test/unknown_commands_test.rb +0 -12
  101. data/test/url_param_test.rb +0 -136
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../errors'
4
+
5
+ class Redis
6
+ class Cluster
7
+ # Keep client list of node for Redis Cluster Client
8
+ class Node
9
+ include Enumerable
10
+
11
+ ReloadNeeded = Class.new(StandardError)
12
+
13
+ ROLE_SLAVE = 'slave'
14
+
15
+ def initialize(options, node_flags = {}, with_replica = false)
16
+ @with_replica = with_replica
17
+ @node_flags = node_flags
18
+ @clients = build_clients(options)
19
+ end
20
+
21
+ def each(&block)
22
+ @clients.values.each(&block)
23
+ end
24
+
25
+ def sample
26
+ @clients.values.sample
27
+ end
28
+
29
+ def find_by(node_key)
30
+ @clients.fetch(node_key)
31
+ rescue KeyError
32
+ raise ReloadNeeded
33
+ end
34
+
35
+ def call_all(command, &block)
36
+ try_map { |_, client| client.call(command, &block) }.values
37
+ end
38
+
39
+ def call_master(command, &block)
40
+ try_map do |node_key, client|
41
+ next if slave?(node_key)
42
+
43
+ client.call(command, &block)
44
+ end.values
45
+ end
46
+
47
+ def call_slave(command, &block)
48
+ return call_master(command, &block) if replica_disabled?
49
+
50
+ try_map do |node_key, client|
51
+ next if master?(node_key)
52
+
53
+ client.call(command, &block)
54
+ end.values
55
+ end
56
+
57
+ def process_all(commands, &block)
58
+ try_map { |_, client| client.process(commands, &block) }.values
59
+ end
60
+
61
+ private
62
+
63
+ def replica_disabled?
64
+ !@with_replica
65
+ end
66
+
67
+ def master?(node_key)
68
+ !slave?(node_key)
69
+ end
70
+
71
+ def slave?(node_key)
72
+ @node_flags[node_key] == ROLE_SLAVE
73
+ end
74
+
75
+ def build_clients(options)
76
+ clients = options.map do |node_key, option|
77
+ next if replica_disabled? && slave?(node_key)
78
+
79
+ option = option.merge(readonly: true) if slave?(node_key)
80
+
81
+ client = Client.new(option)
82
+ [node_key, client]
83
+ end
84
+
85
+ clients.compact.to_h
86
+ end
87
+
88
+ def try_map
89
+ errors = {}
90
+ results = {}
91
+
92
+ @clients.each do |node_key, client|
93
+ begin
94
+ reply = yield(node_key, client)
95
+ results[node_key] = reply unless reply.nil?
96
+ rescue CommandError => err
97
+ errors[node_key] = err
98
+ next
99
+ end
100
+ end
101
+
102
+ return results if errors.empty?
103
+
104
+ raise CommandErrorCollection, errors
105
+ end
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Redis
4
+ class Cluster
5
+ # Node key's format is `<ip>:<port>`.
6
+ # It is different from node id.
7
+ # Node id is internal identifying code in Redis Cluster.
8
+ module NodeKey
9
+ DELIMITER = ':'
10
+
11
+ module_function
12
+
13
+ def optionize(node_key)
14
+ host, port = split(node_key)
15
+ { host: host, port: port }
16
+ end
17
+
18
+ def split(node_key)
19
+ node_key.split(DELIMITER)
20
+ end
21
+
22
+ def build_from_uri(uri)
23
+ "#{uri.host}#{DELIMITER}#{uri.port}"
24
+ end
25
+
26
+ def build_from_host_port(host, port)
27
+ "#{host}#{DELIMITER}#{port}"
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../errors'
4
+
5
+ class Redis
6
+ class Cluster
7
+ # Load and hashify node info for Redis Cluster Client
8
+ module NodeLoader
9
+ module_function
10
+
11
+ def load_flags(nodes)
12
+ info = {}
13
+
14
+ nodes.each do |node|
15
+ info = fetch_node_info(node)
16
+ info.empty? ? next : break
17
+ end
18
+
19
+ return info unless info.empty?
20
+
21
+ raise CannotConnectError, 'Redis client could not connect to any cluster nodes'
22
+ end
23
+
24
+ def fetch_node_info(node)
25
+ node.call(%i[cluster nodes])
26
+ .split("\n")
27
+ .map { |str| str.split(' ') }
28
+ .map { |arr| [arr[1].split('@').first, (arr[2].split(',') & %w[master slave]).first] }
29
+ .to_h
30
+ rescue CannotConnectError, ConnectionError, CommandError
31
+ {} # can retry on another node
32
+ end
33
+
34
+ private_class_method :fetch_node_info
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../errors'
4
+ require_relative 'node_key'
5
+ require 'uri'
6
+
7
+ class Redis
8
+ class Cluster
9
+ # Keep options for Redis Cluster Client
10
+ class Option
11
+ DEFAULT_SCHEME = 'redis'
12
+ SECURE_SCHEME = 'rediss'
13
+ VALID_SCHEMES = [DEFAULT_SCHEME, SECURE_SCHEME].freeze
14
+
15
+ def initialize(options)
16
+ options = options.dup
17
+ node_addrs = options.delete(:cluster)
18
+ @node_opts = build_node_options(node_addrs)
19
+ @replica = options.delete(:replica) == true
20
+ add_common_node_option_if_needed(options, @node_opts, :scheme)
21
+ add_common_node_option_if_needed(options, @node_opts, :username)
22
+ add_common_node_option_if_needed(options, @node_opts, :password)
23
+ @options = options
24
+ end
25
+
26
+ def per_node_key
27
+ @node_opts.map { |opt| [NodeKey.build_from_host_port(opt[:host], opt[:port]), @options.merge(opt)] }
28
+ .to_h
29
+ end
30
+
31
+ def use_replica?
32
+ @replica
33
+ end
34
+
35
+ def update_node(addrs)
36
+ @node_opts = build_node_options(addrs)
37
+ end
38
+
39
+ def add_node(host, port)
40
+ @node_opts << { host: host, port: port }
41
+ end
42
+
43
+ private
44
+
45
+ def build_node_options(addrs)
46
+ raise InvalidClientOptionError, 'Redis option of `cluster` must be an Array' unless addrs.is_a?(Array)
47
+
48
+ addrs.map { |addr| parse_node_addr(addr) }
49
+ end
50
+
51
+ def parse_node_addr(addr)
52
+ case addr
53
+ when String
54
+ parse_node_url(addr)
55
+ when Hash
56
+ parse_node_option(addr)
57
+ else
58
+ raise InvalidClientOptionError, 'Redis option of `cluster` must includes String or Hash'
59
+ end
60
+ end
61
+
62
+ def parse_node_url(addr)
63
+ uri = URI(addr)
64
+ raise InvalidClientOptionError, "Invalid uri scheme #{addr}" unless VALID_SCHEMES.include?(uri.scheme)
65
+
66
+ db = uri.path.split('/')[1]&.to_i
67
+
68
+ { scheme: uri.scheme, username: uri.user, password: uri.password, host: uri.host, port: uri.port, db: db }
69
+ .reject { |_, v| v.nil? || v == '' }
70
+ rescue URI::InvalidURIError => err
71
+ raise InvalidClientOptionError, err.message
72
+ end
73
+
74
+ def parse_node_option(addr)
75
+ addr = addr.map { |k, v| [k.to_sym, v] }.to_h
76
+ if addr.values_at(:host, :port).any?(&:nil?)
77
+ raise InvalidClientOptionError, 'Redis option of `cluster` must includes `:host` and `:port` keys'
78
+ end
79
+
80
+ addr
81
+ end
82
+
83
+ # Redis cluster node returns only host and port information.
84
+ # So we should complement additional information such as:
85
+ # scheme, username, password and so on.
86
+ def add_common_node_option_if_needed(options, node_opts, key)
87
+ return options if options[key].nil? && node_opts.first[key].nil?
88
+
89
+ options[key] ||= node_opts.first[key]
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Redis
4
+ class Cluster
5
+ # Keep slot and node key map for Redis Cluster Client
6
+ class Slot
7
+ ROLE_SLAVE = 'slave'
8
+
9
+ def initialize(available_slots, node_flags = {}, with_replica = false)
10
+ @with_replica = with_replica
11
+ @node_flags = node_flags
12
+ @map = build_slot_node_key_map(available_slots)
13
+ end
14
+
15
+ def exists?(slot)
16
+ @map.key?(slot)
17
+ end
18
+
19
+ def find_node_key_of_master(slot)
20
+ return nil unless exists?(slot)
21
+
22
+ @map[slot][:master]
23
+ end
24
+
25
+ def find_node_key_of_slave(slot)
26
+ return nil unless exists?(slot)
27
+ return find_node_key_of_master(slot) if replica_disabled?
28
+
29
+ @map[slot][:slaves].sample
30
+ end
31
+
32
+ def put(slot, node_key)
33
+ # Since we're sharing a hash for build_slot_node_key_map, duplicate it
34
+ # if it already exists instead of preserving as-is.
35
+ @map[slot] = @map[slot] ? @map[slot].dup : { master: nil, slaves: [] }
36
+
37
+ if master?(node_key)
38
+ @map[slot][:master] = node_key
39
+ elsif !@map[slot][:slaves].include?(node_key)
40
+ @map[slot][:slaves] << node_key
41
+ end
42
+
43
+ nil
44
+ end
45
+
46
+ private
47
+
48
+ def replica_disabled?
49
+ !@with_replica
50
+ end
51
+
52
+ def master?(node_key)
53
+ !slave?(node_key)
54
+ end
55
+
56
+ def slave?(node_key)
57
+ @node_flags[node_key] == ROLE_SLAVE
58
+ end
59
+
60
+ # available_slots is mapping of node_key to list of slot ranges
61
+ def build_slot_node_key_map(available_slots)
62
+ by_ranges = {}
63
+ available_slots.each do |node_key, slots_arr|
64
+ by_ranges[slots_arr] ||= { master: nil, slaves: [] }
65
+
66
+ if master?(node_key)
67
+ by_ranges[slots_arr][:master] = node_key
68
+ elsif !by_ranges[slots_arr][:slaves].include?(node_key)
69
+ by_ranges[slots_arr][:slaves] << node_key
70
+ end
71
+ end
72
+
73
+ by_slot = {}
74
+ by_ranges.each do |slots_arr, nodes|
75
+ slots_arr.each do |slots|
76
+ slots.each do |slot|
77
+ by_slot[slot] = nodes
78
+ end
79
+ end
80
+ end
81
+
82
+ by_slot
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../errors'
4
+ require_relative 'node_key'
5
+
6
+ class Redis
7
+ class Cluster
8
+ # Load and hashify slot info for Redis Cluster Client
9
+ module SlotLoader
10
+ module_function
11
+
12
+ def load(nodes)
13
+ info = {}
14
+
15
+ nodes.each do |node|
16
+ info = fetch_slot_info(node)
17
+ info.empty? ? next : break
18
+ end
19
+
20
+ return info unless info.empty?
21
+
22
+ raise CannotConnectError, 'Redis client could not connect to any cluster nodes'
23
+ end
24
+
25
+ def fetch_slot_info(node)
26
+ hash_with_default_arr = Hash.new { |h, k| h[k] = [] }
27
+ node.call(%i[cluster slots])
28
+ .flat_map { |arr| parse_slot_info(arr, default_ip: node.host) }
29
+ .each_with_object(hash_with_default_arr) { |arr, h| h[arr[0]] << arr[1] }
30
+ rescue CannotConnectError, ConnectionError, CommandError
31
+ {} # can retry on another node
32
+ end
33
+
34
+ def parse_slot_info(arr, default_ip:)
35
+ first_slot, last_slot = arr[0..1]
36
+ slot_range = (first_slot..last_slot).freeze
37
+ arr[2..-1].map { |addr| [stringify_node_key(addr, default_ip), slot_range] }
38
+ end
39
+
40
+ def stringify_node_key(arr, default_ip)
41
+ ip, port = arr
42
+ ip = default_ip if ip.empty? # When cluster is down
43
+ NodeKey.build_from_host_port(ip, port)
44
+ end
45
+
46
+ private_class_method :fetch_slot_info, :parse_slot_info, :stringify_node_key
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,291 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'errors'
4
+ require_relative 'client'
5
+ require_relative 'cluster/command'
6
+ require_relative 'cluster/command_loader'
7
+ require_relative 'cluster/key_slot_converter'
8
+ require_relative 'cluster/node'
9
+ require_relative 'cluster/node_key'
10
+ require_relative 'cluster/node_loader'
11
+ require_relative 'cluster/option'
12
+ require_relative 'cluster/slot'
13
+ require_relative 'cluster/slot_loader'
14
+
15
+ class Redis
16
+ # Redis Cluster client
17
+ #
18
+ # @see https://github.com/antirez/redis-rb-cluster POC implementation
19
+ # @see https://redis.io/topics/cluster-spec Redis Cluster specification
20
+ # @see https://redis.io/topics/cluster-tutorial Redis Cluster tutorial
21
+ #
22
+ # Copyright (C) 2013 Salvatore Sanfilippo <antirez@gmail.com>
23
+ class Cluster
24
+ def initialize(options = {})
25
+ @option = Option.new(options)
26
+ @node, @slot = fetch_cluster_info!(@option)
27
+ @command = fetch_command_details(@node)
28
+ end
29
+
30
+ def id
31
+ @node.map(&:id).sort.join(' ')
32
+ end
33
+
34
+ # db feature is disabled in cluster mode
35
+ def db
36
+ 0
37
+ end
38
+
39
+ # db feature is disabled in cluster mode
40
+ def db=(_db); end
41
+
42
+ def timeout
43
+ @node.first.timeout
44
+ end
45
+
46
+ def connected?
47
+ @node.any?(&:connected?)
48
+ end
49
+
50
+ def disconnect
51
+ @node.each(&:disconnect)
52
+ true
53
+ end
54
+
55
+ def connection_info
56
+ @node.sort_by(&:id).map do |client|
57
+ {
58
+ host: client.host,
59
+ port: client.port,
60
+ db: client.db,
61
+ id: client.id,
62
+ location: client.location
63
+ }
64
+ end
65
+ end
66
+
67
+ def with_reconnect(val = true, &block)
68
+ try_send(@node.sample, :with_reconnect, val, &block)
69
+ end
70
+
71
+ def call(command, &block)
72
+ send_command(command, &block)
73
+ end
74
+
75
+ def call_loop(command, timeout = 0, &block)
76
+ node = assign_node(command)
77
+ try_send(node, :call_loop, command, timeout, &block)
78
+ end
79
+
80
+ def call_pipeline(pipeline)
81
+ node_keys = pipeline.commands.map { |cmd| find_node_key(cmd, primary_only: true) }.compact.uniq
82
+ if node_keys.size > 1
83
+ raise(CrossSlotPipeliningError,
84
+ pipeline.commands.map { |cmd| @command.extract_first_key(cmd) }.reject(&:empty?).uniq)
85
+ end
86
+
87
+ try_send(find_node(node_keys.first), :call_pipeline, pipeline)
88
+ end
89
+
90
+ def call_with_timeout(command, timeout, &block)
91
+ node = assign_node(command)
92
+ try_send(node, :call_with_timeout, command, timeout, &block)
93
+ end
94
+
95
+ def call_without_timeout(command, &block)
96
+ call_with_timeout(command, 0, &block)
97
+ end
98
+
99
+ def process(commands, &block)
100
+ if commands.size == 1 &&
101
+ %w[unsubscribe punsubscribe].include?(commands.first.first.to_s.downcase) &&
102
+ commands.first.size == 1
103
+
104
+ # Node is indeterminate. We do just a best-effort try here.
105
+ @node.process_all(commands, &block)
106
+ else
107
+ node = assign_node(commands.first)
108
+ try_send(node, :process, commands, &block)
109
+ end
110
+ end
111
+
112
+ private
113
+
114
+ def fetch_cluster_info!(option)
115
+ node = Node.new(option.per_node_key)
116
+ available_slots = SlotLoader.load(node)
117
+ node_flags = NodeLoader.load_flags(node)
118
+ option.update_node(available_slots.keys.map { |k| NodeKey.optionize(k) })
119
+ [Node.new(option.per_node_key, node_flags, option.use_replica?),
120
+ Slot.new(available_slots, node_flags, option.use_replica?)]
121
+ ensure
122
+ node&.each(&:disconnect)
123
+ end
124
+
125
+ def fetch_command_details(nodes)
126
+ details = CommandLoader.load(nodes)
127
+ Command.new(details)
128
+ end
129
+
130
+ def send_command(command, &block)
131
+ cmd = command.first.to_s.downcase
132
+ case cmd
133
+ when 'acl', 'auth', 'bgrewriteaof', 'bgsave', 'quit', 'save'
134
+ @node.call_all(command, &block).first
135
+ when 'flushall', 'flushdb'
136
+ @node.call_master(command, &block).first
137
+ when 'wait' then @node.call_master(command, &block).reduce(:+)
138
+ when 'keys' then @node.call_slave(command, &block).flatten.sort
139
+ when 'dbsize' then @node.call_slave(command, &block).reduce(:+)
140
+ when 'lastsave' then @node.call_all(command, &block).sort
141
+ when 'role' then @node.call_all(command, &block)
142
+ when 'config' then send_config_command(command, &block)
143
+ when 'client' then send_client_command(command, &block)
144
+ when 'cluster' then send_cluster_command(command, &block)
145
+ when 'readonly', 'readwrite', 'shutdown'
146
+ raise OrchestrationCommandNotSupported, cmd
147
+ when 'memory' then send_memory_command(command, &block)
148
+ when 'script' then send_script_command(command, &block)
149
+ when 'pubsub' then send_pubsub_command(command, &block)
150
+ when 'discard', 'exec', 'multi', 'unwatch'
151
+ raise AmbiguousNodeError, cmd
152
+ else
153
+ node = assign_node(command)
154
+ try_send(node, :call, command, &block)
155
+ end
156
+ end
157
+
158
+ def send_config_command(command, &block)
159
+ case command[1].to_s.downcase
160
+ when 'resetstat', 'rewrite', 'set'
161
+ @node.call_all(command, &block).first
162
+ else assign_node(command).call(command, &block)
163
+ end
164
+ end
165
+
166
+ def send_memory_command(command, &block)
167
+ case command[1].to_s.downcase
168
+ when 'stats' then @node.call_all(command, &block)
169
+ when 'purge' then @node.call_all(command, &block).first
170
+ else assign_node(command).call(command, &block)
171
+ end
172
+ end
173
+
174
+ def send_client_command(command, &block)
175
+ case command[1].to_s.downcase
176
+ when 'list' then @node.call_all(command, &block).flatten
177
+ when 'pause', 'reply', 'setname'
178
+ @node.call_all(command, &block).first
179
+ else assign_node(command).call(command, &block)
180
+ end
181
+ end
182
+
183
+ def send_cluster_command(command, &block)
184
+ subcommand = command[1].to_s.downcase
185
+ case subcommand
186
+ when 'addslots', 'delslots', 'failover', 'forget', 'meet', 'replicate',
187
+ 'reset', 'set-config-epoch', 'setslot'
188
+ raise OrchestrationCommandNotSupported, 'cluster', subcommand
189
+ when 'saveconfig' then @node.call_all(command, &block).first
190
+ else assign_node(command).call(command, &block)
191
+ end
192
+ end
193
+
194
+ def send_script_command(command, &block)
195
+ case command[1].to_s.downcase
196
+ when 'debug', 'kill'
197
+ @node.call_all(command, &block).first
198
+ when 'flush', 'load'
199
+ @node.call_master(command, &block).first
200
+ else assign_node(command).call(command, &block)
201
+ end
202
+ end
203
+
204
+ def send_pubsub_command(command, &block)
205
+ case command[1].to_s.downcase
206
+ when 'channels' then @node.call_all(command, &block).flatten.uniq.sort
207
+ when 'numsub'
208
+ @node.call_all(command, &block).reject(&:empty?).map { |e| Hash[*e] }
209
+ .reduce({}) { |a, e| a.merge(e) { |_, v1, v2| v1 + v2 } }
210
+ when 'numpat' then @node.call_all(command, &block).reduce(:+)
211
+ else assign_node(command).call(command, &block)
212
+ end
213
+ end
214
+
215
+ # @see https://redis.io/topics/cluster-spec#redirection-and-resharding
216
+ # Redirection and resharding
217
+ def try_send(node, method_name, *args, retry_count: 3, &block)
218
+ node.public_send(method_name, *args, &block)
219
+ rescue CommandError => err
220
+ if err.message.start_with?('MOVED')
221
+ raise if retry_count <= 0
222
+
223
+ node = assign_redirection_node(err.message)
224
+ retry_count -= 1
225
+ retry
226
+ elsif err.message.start_with?('ASK')
227
+ raise if retry_count <= 0
228
+
229
+ node = assign_asking_node(err.message)
230
+ node.call(%i[asking])
231
+ retry_count -= 1
232
+ retry
233
+ else
234
+ raise
235
+ end
236
+ rescue CannotConnectError
237
+ update_cluster_info!
238
+ raise
239
+ end
240
+
241
+ def assign_redirection_node(err_msg)
242
+ _, slot, node_key = err_msg.split(' ')
243
+ slot = slot.to_i
244
+ @slot.put(slot, node_key)
245
+ find_node(node_key)
246
+ end
247
+
248
+ def assign_asking_node(err_msg)
249
+ _, _, node_key = err_msg.split(' ')
250
+ find_node(node_key)
251
+ end
252
+
253
+ def assign_node(command)
254
+ node_key = find_node_key(command)
255
+ find_node(node_key)
256
+ end
257
+
258
+ def find_node_key(command, primary_only: false)
259
+ key = @command.extract_first_key(command)
260
+ return if key.empty?
261
+
262
+ slot = KeySlotConverter.convert(key)
263
+ return unless @slot.exists?(slot)
264
+
265
+ if @command.should_send_to_master?(command) || primary_only
266
+ @slot.find_node_key_of_master(slot)
267
+ else
268
+ @slot.find_node_key_of_slave(slot)
269
+ end
270
+ end
271
+
272
+ def find_node(node_key)
273
+ return @node.sample if node_key.nil?
274
+
275
+ @node.find_by(node_key)
276
+ rescue Node::ReloadNeeded
277
+ update_cluster_info!(node_key)
278
+ @node.find_by(node_key)
279
+ end
280
+
281
+ def update_cluster_info!(node_key = nil)
282
+ unless node_key.nil?
283
+ host, port = NodeKey.split(node_key)
284
+ @option.add_node(host, port)
285
+ end
286
+
287
+ @node.map(&:disconnect)
288
+ @node, @slot = fetch_cluster_info!(@option)
289
+ end
290
+ end
291
+ end