redis 3.3.5 → 4.5.1

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 (130) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +161 -2
  3. data/README.md +144 -79
  4. data/lib/redis/client.rb +166 -90
  5. data/lib/redis/cluster/command.rb +81 -0
  6. data/lib/redis/cluster/command_loader.rb +33 -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 +7 -10
  16. data/lib/redis/connection/hiredis.rb +6 -5
  17. data/lib/redis/connection/registry.rb +2 -1
  18. data/lib/redis/connection/ruby.rb +128 -129
  19. data/lib/redis/connection/synchrony.rb +21 -8
  20. data/lib/redis/connection.rb +4 -2
  21. data/lib/redis/distributed.rb +194 -72
  22. data/lib/redis/errors.rb +48 -0
  23. data/lib/redis/hash_ring.rb +30 -73
  24. data/lib/redis/pipeline.rb +55 -15
  25. data/lib/redis/subscribe.rb +11 -12
  26. data/lib/redis/version.rb +3 -1
  27. data/lib/redis.rb +1451 -403
  28. metadata +49 -202
  29. data/.gitignore +0 -16
  30. data/.travis/Gemfile +0 -11
  31. data/.travis.yml +0 -89
  32. data/.yardopts +0 -3
  33. data/Gemfile +0 -4
  34. data/Rakefile +0 -87
  35. data/benchmarking/logging.rb +0 -71
  36. data/benchmarking/pipeline.rb +0 -51
  37. data/benchmarking/speed.rb +0 -21
  38. data/benchmarking/suite.rb +0 -24
  39. data/benchmarking/worker.rb +0 -71
  40. data/examples/basic.rb +0 -15
  41. data/examples/consistency.rb +0 -114
  42. data/examples/dist_redis.rb +0 -43
  43. data/examples/incr-decr.rb +0 -17
  44. data/examples/list.rb +0 -26
  45. data/examples/pubsub.rb +0 -37
  46. data/examples/sentinel/sentinel.conf +0 -9
  47. data/examples/sentinel/start +0 -49
  48. data/examples/sentinel.rb +0 -41
  49. data/examples/sets.rb +0 -36
  50. data/examples/unicorn/config.ru +0 -3
  51. data/examples/unicorn/unicorn.rb +0 -20
  52. data/redis.gemspec +0 -44
  53. data/test/bitpos_test.rb +0 -69
  54. data/test/blocking_commands_test.rb +0 -42
  55. data/test/client_test.rb +0 -59
  56. data/test/command_map_test.rb +0 -30
  57. data/test/commands_on_hashes_test.rb +0 -21
  58. data/test/commands_on_hyper_log_log_test.rb +0 -21
  59. data/test/commands_on_lists_test.rb +0 -20
  60. data/test/commands_on_sets_test.rb +0 -77
  61. data/test/commands_on_sorted_sets_test.rb +0 -137
  62. data/test/commands_on_strings_test.rb +0 -101
  63. data/test/commands_on_value_types_test.rb +0 -133
  64. data/test/connection_handling_test.rb +0 -277
  65. data/test/connection_test.rb +0 -57
  66. data/test/db/.gitkeep +0 -0
  67. data/test/distributed_blocking_commands_test.rb +0 -46
  68. data/test/distributed_commands_on_hashes_test.rb +0 -10
  69. data/test/distributed_commands_on_hyper_log_log_test.rb +0 -33
  70. data/test/distributed_commands_on_lists_test.rb +0 -22
  71. data/test/distributed_commands_on_sets_test.rb +0 -83
  72. data/test/distributed_commands_on_sorted_sets_test.rb +0 -18
  73. data/test/distributed_commands_on_strings_test.rb +0 -59
  74. data/test/distributed_commands_on_value_types_test.rb +0 -95
  75. data/test/distributed_commands_requiring_clustering_test.rb +0 -164
  76. data/test/distributed_connection_handling_test.rb +0 -23
  77. data/test/distributed_internals_test.rb +0 -79
  78. data/test/distributed_key_tags_test.rb +0 -52
  79. data/test/distributed_persistence_control_commands_test.rb +0 -26
  80. data/test/distributed_publish_subscribe_test.rb +0 -92
  81. data/test/distributed_remote_server_control_commands_test.rb +0 -66
  82. data/test/distributed_scripting_test.rb +0 -102
  83. data/test/distributed_sorting_test.rb +0 -20
  84. data/test/distributed_test.rb +0 -58
  85. data/test/distributed_transactions_test.rb +0 -32
  86. data/test/encoding_test.rb +0 -18
  87. data/test/error_replies_test.rb +0 -59
  88. data/test/fork_safety_test.rb +0 -65
  89. data/test/helper.rb +0 -232
  90. data/test/helper_test.rb +0 -24
  91. data/test/internals_test.rb +0 -417
  92. data/test/lint/blocking_commands.rb +0 -150
  93. data/test/lint/hashes.rb +0 -162
  94. data/test/lint/hyper_log_log.rb +0 -60
  95. data/test/lint/lists.rb +0 -143
  96. data/test/lint/sets.rb +0 -140
  97. data/test/lint/sorted_sets.rb +0 -316
  98. data/test/lint/strings.rb +0 -260
  99. data/test/lint/value_types.rb +0 -122
  100. data/test/persistence_control_commands_test.rb +0 -26
  101. data/test/pipelining_commands_test.rb +0 -242
  102. data/test/publish_subscribe_test.rb +0 -282
  103. data/test/remote_server_control_commands_test.rb +0 -118
  104. data/test/scanning_test.rb +0 -413
  105. data/test/scripting_test.rb +0 -78
  106. data/test/sentinel_command_test.rb +0 -80
  107. data/test/sentinel_test.rb +0 -255
  108. data/test/sorting_test.rb +0 -59
  109. data/test/ssl_test.rb +0 -73
  110. data/test/support/connection/hiredis.rb +0 -1
  111. data/test/support/connection/ruby.rb +0 -1
  112. data/test/support/connection/synchrony.rb +0 -17
  113. data/test/support/redis_mock.rb +0 -130
  114. data/test/support/ssl/gen_certs.sh +0 -31
  115. data/test/support/ssl/trusted-ca.crt +0 -25
  116. data/test/support/ssl/trusted-ca.key +0 -27
  117. data/test/support/ssl/trusted-cert.crt +0 -81
  118. data/test/support/ssl/trusted-cert.key +0 -28
  119. data/test/support/ssl/untrusted-ca.crt +0 -26
  120. data/test/support/ssl/untrusted-ca.key +0 -27
  121. data/test/support/ssl/untrusted-cert.crt +0 -82
  122. data/test/support/ssl/untrusted-cert.key +0 -28
  123. data/test/support/wire/synchrony.rb +0 -24
  124. data/test/support/wire/thread.rb +0 -5
  125. data/test/synchrony_driver.rb +0 -88
  126. data/test/test.conf.erb +0 -9
  127. data/test/thread_safety_test.rb +0 -62
  128. data/test/transactions_test.rb +0 -264
  129. data/test/unknown_commands_test.rb +0 -14
  130. data/test/url_param_test.rb +0 -138
@@ -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