redis-cluster-client 0.0.8 → 0.0.11

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d13300ba8c7f294d3e4c86932d13775ad22060c172900c01d521b1ed8c2278d6
4
- data.tar.gz: d5b457978bb312a718ba57b48b56fac1ceb07a463889835c8190794007ecbf7d
3
+ metadata.gz: d7c3e9a3b764d9e5fc52670d17c175ee9ccda2dcd56b4a977b0e9a1e0b71d972
4
+ data.tar.gz: 191e1fe163f6f2fd111e4b2f69638aa28462d74e002f1ff0a11fc0f52cc740cd
5
5
  SHA512:
6
- metadata.gz: 37fb06428eed5a9cca5b42d27c034f057dd7d6f0c5d0c17e4b42c60a8fa3711127c5ed81f06e0565331f64aa762ccc30ee6674fb541e56a8b322ee50ab7a75ce
7
- data.tar.gz: 06d46f93c8c7b7116378d2ef9e4380ccce3aac74c071b709cbacd677ec0b302302b52160c04b2c3523771f69446b96d84e8b4f729765073386c5e5f1f3f27f5b
6
+ metadata.gz: a42c6a871302686e33026ed570575d5b51637b707501609fdf9e574a5a976e06af388bc69bebd5bb22a58a30741c4b3e43d883d154782675446ce88c49ac2b37
7
+ data.tar.gz: e2ae0a6dd3af75407cc4d795492afff75e5bc7c01fee84be2ef0761e67dda0d9f93cc103b99219bd1b5e8df9dc3ff1dc86b3406c3a23dc1d011b649c363ab163
@@ -7,15 +7,21 @@ class RedisClient
7
7
  class Cluster
8
8
  class Command
9
9
  class << self
10
- def load(nodes)
11
- errors = nodes&.map do |node|
10
+ def load(nodes) # rubocop:disable Metrics/MethodLength
11
+ errors = []
12
+ cmd = nil
13
+ nodes&.each do |node|
14
+ break unless cmd.nil?
15
+
12
16
  reply = node.call('COMMAND')
13
17
  details = parse_command_details(reply)
14
- return ::RedisClient::Cluster::Command.new(details)
15
- rescue ::RedisClient::ConnectionError, ::RedisClient::CommandError => e
16
- e
18
+ cmd = ::RedisClient::Cluster::Command.new(details)
19
+ rescue ::RedisClient::Error => e
20
+ errors << e
17
21
  end
18
22
 
23
+ return cmd unless cmd.nil?
24
+
19
25
  raise ::RedisClient::Cluster::InitialSetupError, errors
20
26
  end
21
27
 
@@ -75,7 +81,7 @@ class RedisClient
75
81
  when 'memory'
76
82
  command[1].to_s.casecmp('usage').zero? ? 2 : 0
77
83
  when 'migrate'
78
- command[3] == '""' ? determine_optional_key_position(command, 'keys') : 3
84
+ command[3].empty? ? determine_optional_key_position(command, 'keys') : 3
79
85
  when 'xread', 'xreadgroup'
80
86
  determine_optional_key_position(command, 'streams')
81
87
  else
@@ -6,8 +6,6 @@ class RedisClient
6
6
  class Cluster
7
7
  ERR_ARG_NORMALIZATION = ->(arg) { Array[arg].flatten.reject { |e| e.nil? || (e.respond_to?(:empty?) && e.empty?) } }
8
8
 
9
- # Raised when client connected to redis as cluster mode
10
- # and failed to fetch cluster state information by commands.
11
9
  class InitialSetupError < ::RedisClient::Error
12
10
  def initialize(errors)
13
11
  msg = ERR_ARG_NORMALIZATION.call(errors).map(&:message).uniq.join(',')
@@ -15,20 +13,17 @@ class RedisClient
15
13
  end
16
14
  end
17
15
 
18
- # Raised when client connected to redis as cluster mode
19
- # and some cluster subcommands were called.
20
16
  class OrchestrationCommandNotSupported < ::RedisClient::Error
21
17
  def initialize(command)
22
18
  str = ERR_ARG_NORMALIZATION.call(command).map(&:to_s).join(' ').upcase
23
- msg = "#{str} command should be used with care "\
24
- 'only by applications orchestrating Redis Cluster, like redis-cli, '\
25
- 'and the command if used out of the right context can leave the cluster '\
19
+ msg = "#{str} command should be used with care " \
20
+ 'only by applications orchestrating Redis Cluster, like redis-cli, ' \
21
+ 'and the command if used out of the right context can leave the cluster ' \
26
22
  'in a wrong state or cause data loss.'
27
23
  super(msg)
28
24
  end
29
25
  end
30
26
 
31
- # Raised when error occurs on any node of cluster.
32
27
  class ErrorCollection < ::RedisClient::Error
33
28
  attr_reader :errors
34
29
 
@@ -41,11 +36,10 @@ class RedisClient
41
36
 
42
37
  @errors = errors
43
38
  messages = @errors.map { |node_key, error| "#{node_key}: #{error.message}" }
44
- super("Command errors were replied on any node: #{messages.join(', ')}")
39
+ super("Errors occurred on any node: #{messages.join(', ')}")
45
40
  end
46
41
  end
47
42
 
48
- # Raised when cluster client can't select node.
49
43
  class AmbiguousNodeError < ::RedisClient::Error
50
44
  def initialize(command)
51
45
  super("Cluster client doesn't know which node the #{command} command should be sent to.")
@@ -55,8 +49,8 @@ class RedisClient
55
49
  class NodeMightBeDown < ::RedisClient::Error
56
50
  def initialize(_ = '')
57
51
  super(
58
- 'The client is trying to fetch the latest cluster state '\
59
- 'because a subset of nodes might be down. '\
52
+ 'The client is trying to fetch the latest cluster state ' \
53
+ 'because a subset of nodes might be down. ' \
60
54
  'It might continue to raise errors for a while.'
61
55
  )
62
56
  end
@@ -77,9 +77,8 @@ class RedisClient
77
77
  arr[8] = []
78
78
  next
79
79
  end
80
-
81
- arr[8] = arr[8].split(',').map { |r| r.split('-').map { |s| Integer(s) } }
82
- arr[8] = arr[8].map { |a| a.size == 1 ? a << a.first : a }.map(&:sort)
80
+ arr[8] = arr[8..].filter_map { |str| str.start_with?('[') ? nil : str.split('-').map { |s| Integer(s) } }
81
+ .map { |a| a.size == 1 ? a << a.first : a }.map(&:sort)
83
82
  end
84
83
 
85
84
  rows.map do |arr|
@@ -102,7 +101,7 @@ class RedisClient
102
101
  end
103
102
 
104
103
  def each(&block)
105
- @clients.values.each(&block)
104
+ @clients.each_value(&block)
106
105
  end
107
106
 
108
107
  def sample
@@ -129,9 +128,9 @@ class RedisClient
129
128
  @clients.fetch(node_key)
130
129
  end
131
130
 
132
- def call_all(method, *command, **kwargs, &block)
131
+ def call_all(method, *args, **kwargs, &block)
133
132
  results, errors = try_map do |_, client|
134
- client.send(method, *command, **kwargs, &block)
133
+ client.send(method, *args, **kwargs, &block)
135
134
  end
136
135
 
137
136
  return results.values if errors.empty?
@@ -139,11 +138,11 @@ class RedisClient
139
138
  raise ::RedisClient::Cluster::ErrorCollection, errors
140
139
  end
141
140
 
142
- def call_primaries(method, *command, **kwargs, &block)
141
+ def call_primaries(method, *args, **kwargs, &block)
143
142
  results, errors = try_map do |node_key, client|
144
143
  next if replica?(node_key)
145
144
 
146
- client.send(method, *command, **kwargs, &block)
145
+ client.send(method, *args, **kwargs, &block)
147
146
  end
148
147
 
149
148
  return results.values if errors.empty?
@@ -151,14 +150,14 @@ class RedisClient
151
150
  raise ::RedisClient::Cluster::ErrorCollection, errors
152
151
  end
153
152
 
154
- def call_replicas(method, *command, **kwargs, &block)
155
- return call_primaries(method, *command, **kwargs, &block) if replica_disabled?
153
+ def call_replicas(method, *args, **kwargs, &block)
154
+ return call_primaries(method, *args, **kwargs, &block) if replica_disabled?
156
155
 
157
156
  replica_node_keys = @replications.values.map(&:sample)
158
157
  results, errors = try_map do |node_key, client|
159
158
  next if primary?(node_key) || !replica_node_keys.include?(node_key)
160
159
 
161
- client.send(method, *command, **kwargs, &block)
160
+ client.send(method, *args, **kwargs, &block)
162
161
  end
163
162
 
164
163
  return results.values if errors.empty?
@@ -166,9 +165,9 @@ class RedisClient
166
165
  raise ::RedisClient::Cluster::ErrorCollection, errors
167
166
  end
168
167
 
169
- def send_ping(method, *command, **kwargs, &block)
168
+ def send_ping(method, *args, **kwargs, &block)
170
169
  results, errors = try_map do |_, client|
171
- client.send(method, *command, **kwargs, &block)
170
+ client.send(method, *args, **kwargs, &block)
172
171
  end
173
172
 
174
173
  return results.values if errors.empty?
@@ -181,7 +180,7 @@ class RedisClient
181
180
  def scale_reading_clients
182
181
  keys = replica_disabled? ? @replications.keys : @replications.values.map(&:first)
183
182
  @clients.select { |k, _| keys.include?(k) }.values.sort_by do |client|
184
- ::RedisClient::Cluster::NodeKey.build_from_host_port(client.config.host, client.config.port)
183
+ "#{client.config.host}-#{client.config.port}"
185
184
  end
186
185
  end
187
186
 
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'redis_client'
4
+ require 'redis_client/cluster/errors'
5
+
6
+ class RedisClient
7
+ class Cluster
8
+ class Pipeline
9
+ ReplySizeError = Class.new(::RedisClient::Error)
10
+
11
+ def initialize(router)
12
+ @router = router
13
+ @grouped = Hash.new([].freeze)
14
+ @size = 0
15
+ end
16
+
17
+ def call(*command, **kwargs)
18
+ node_key = @router.find_node_key(*command, primary_only: true)
19
+ @grouped[node_key] += [[@size, :call, command, kwargs]]
20
+ @size += 1
21
+ end
22
+
23
+ def call_once(*command, **kwargs)
24
+ node_key = @router.find_node_key(*command, primary_only: true)
25
+ @grouped[node_key] += [[@size, :call_once, command, kwargs]]
26
+ @size += 1
27
+ end
28
+
29
+ def blocking_call(timeout, *command, **kwargs)
30
+ node_key = @router.find_node_key(*command, primary_only: true)
31
+ @grouped[node_key] += [[@size, :blocking_call, timeout, command, kwargs]]
32
+ @size += 1
33
+ end
34
+
35
+ def empty?
36
+ @size.zero?
37
+ end
38
+
39
+ # TODO: https://github.com/redis-rb/redis-cluster-client/issues/37 handle redirections
40
+ def execute # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
41
+ all_replies = Array.new(@size)
42
+ errors = {}
43
+ threads = @grouped.map do |k, v|
44
+ Thread.new(@router, k, v) do |router, node_key, rows|
45
+ Thread.pass
46
+ replies = router.find_node(node_key).pipelined do |pipeline|
47
+ rows.each do |row|
48
+ case row[1]
49
+ when :call then pipeline.call(*row[2], **row[3])
50
+ when :call_once then pipeline.call_once(*row[2], **row[3])
51
+ when :blocking_call then pipeline.blocking_call(row[2], *row[3], **row[4])
52
+ else raise NotImplementedError, row[1]
53
+ end
54
+ end
55
+ end
56
+
57
+ raise ReplySizeError, "commands: #{rows.size}, replies: #{replies.size}" if rows.size != replies.size
58
+
59
+ rows.each_with_index { |row, idx| all_replies[row.first] = replies[idx] }
60
+ rescue StandardError => e
61
+ errors[node_key] = e
62
+ end
63
+ end
64
+
65
+ threads.each(&:join)
66
+ return all_replies if errors.empty?
67
+
68
+ raise ::RedisClient::Cluster::ErrorCollection, errors
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ class RedisClient
4
+ class Cluster
5
+ class PubSub
6
+ def initialize(router)
7
+ @router = router
8
+ @pubsub = nil
9
+ end
10
+
11
+ def call(*command, **kwargs)
12
+ close
13
+ @pubsub = @router.assign_node(*command).pubsub
14
+ @pubsub.call(*command, **kwargs)
15
+ end
16
+
17
+ def close
18
+ @pubsub&.close
19
+ @pubsub = nil
20
+ end
21
+
22
+ def next_event(timeout = nil)
23
+ @pubsub&.next_event(timeout)
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,257 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'redis_client'
4
+ require 'redis_client/cluster/command'
5
+ require 'redis_client/cluster/errors'
6
+ require 'redis_client/cluster/key_slot_converter'
7
+ require 'redis_client/cluster/node'
8
+ require 'redis_client/cluster/node_key'
9
+
10
+ class RedisClient
11
+ class Cluster
12
+ class Router
13
+ ZERO_CURSOR_FOR_SCAN = '0'
14
+
15
+ attr_reader :node
16
+
17
+ def initialize(config, pool: nil, **kwargs)
18
+ @config = config.dup
19
+ @pool = pool
20
+ @client_kwargs = kwargs
21
+ @node = fetch_cluster_info(@config, pool: @pool, **@client_kwargs)
22
+ @command = ::RedisClient::Cluster::Command.load(@node)
23
+ @mutex = Mutex.new
24
+ end
25
+
26
+ def send_command(method, *args, **kwargs, &block) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
27
+ command = method == :blocking_call && args.size > 1 ? args[1..] : args
28
+
29
+ cmd = command.first.to_s.downcase
30
+ case cmd
31
+ when 'acl', 'auth', 'bgrewriteaof', 'bgsave', 'quit', 'save'
32
+ @node.call_all(method, *args, **kwargs, &block).first
33
+ when 'flushall', 'flushdb'
34
+ @node.call_primaries(method, *args, **kwargs, &block).first
35
+ when 'ping' then @node.send_ping(method, *args, **kwargs, &block).first
36
+ when 'wait' then send_wait_command(method, *args, **kwargs, &block)
37
+ when 'keys' then @node.call_replicas(method, *args, **kwargs, &block).flatten.sort
38
+ when 'dbsize' then @node.call_replicas(method, *args, **kwargs, &block).sum
39
+ when 'scan' then scan(*command, **kwargs)
40
+ when 'lastsave' then @node.call_all(method, *args, **kwargs, &block).sort
41
+ when 'role' then @node.call_all(method, *args, **kwargs, &block)
42
+ when 'config' then send_config_command(method, *args, **kwargs, &block)
43
+ when 'client' then send_client_command(method, *args, **kwargs, &block)
44
+ when 'cluster' then send_cluster_command(method, *args, **kwargs, &block)
45
+ when 'readonly', 'readwrite', 'shutdown'
46
+ raise ::RedisClient::Cluster::OrchestrationCommandNotSupported, cmd
47
+ when 'memory' then send_memory_command(method, *args, **kwargs, &block)
48
+ when 'script' then send_script_command(method, *args, **kwargs, &block)
49
+ when 'pubsub' then send_pubsub_command(method, *args, **kwargs, &block)
50
+ when 'discard', 'exec', 'multi', 'unwatch'
51
+ raise ::RedisClient::Cluster::AmbiguousNodeError, cmd
52
+ else
53
+ node = assign_node(*command)
54
+ try_send(node, method, *args, **kwargs, &block)
55
+ end
56
+ rescue ::RedisClient::Cluster::Node::ReloadNeeded
57
+ update_cluster_info!
58
+ raise ::RedisClient::Cluster::NodeMightBeDown
59
+ end
60
+
61
+ # @see https://redis.io/topics/cluster-spec#redirection-and-resharding
62
+ # Redirection and resharding
63
+ def try_send(node, method, *args, retry_count: 3, **kwargs, &block) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
64
+ node.send(method, *args, **kwargs, &block)
65
+ rescue ::RedisClient::CommandError => e
66
+ raise if retry_count <= 0
67
+
68
+ if e.message.start_with?('MOVED')
69
+ node = assign_redirection_node(e.message)
70
+ retry_count -= 1
71
+ retry
72
+ elsif e.message.start_with?('ASK')
73
+ node = assign_asking_node(e.message)
74
+ node.call('ASKING')
75
+ retry_count -= 1
76
+ retry
77
+ else
78
+ raise
79
+ end
80
+ rescue ::RedisClient::ConnectionError
81
+ raise if retry_count <= 0
82
+
83
+ update_cluster_info!
84
+ retry_count -= 1
85
+ retry
86
+ end
87
+
88
+ def scan(*command, **kwargs) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
89
+ command[1] = ZERO_CURSOR_FOR_SCAN if command.size == 1
90
+ input_cursor = Integer(command[1])
91
+
92
+ client_index = input_cursor % 256
93
+ raw_cursor = input_cursor >> 8
94
+
95
+ clients = @node.scale_reading_clients
96
+
97
+ client = clients[client_index]
98
+ return [ZERO_CURSOR_FOR_SCAN, []] unless client
99
+
100
+ command[1] = raw_cursor.to_s
101
+
102
+ result_cursor, result_keys = client.call(*command, **kwargs)
103
+ result_cursor = Integer(result_cursor)
104
+
105
+ client_index += 1 if result_cursor == 0
106
+
107
+ [((result_cursor << 8) + client_index).to_s, result_keys]
108
+ end
109
+
110
+ def assign_node(*command)
111
+ node_key = find_node_key(*command)
112
+ find_node(node_key)
113
+ end
114
+
115
+ def find_node_key(*command, primary_only: false)
116
+ key = @command.extract_first_key(command)
117
+ slot = key.empty? ? nil : ::RedisClient::Cluster::KeySlotConverter.convert(key)
118
+
119
+ if @command.should_send_to_primary?(command) || primary_only
120
+ @node.find_node_key_of_primary(slot) || @node.primary_node_keys.sample
121
+ else
122
+ @node.find_node_key_of_replica(slot) || @node.replica_node_keys.sample
123
+ end
124
+ end
125
+
126
+ def find_node(node_key, retry_count: 3)
127
+ @node.find_by(node_key)
128
+ rescue ::RedisClient::Cluster::Node::ReloadNeeded
129
+ raise ::RedieClient::Cluster::NodeMightBeDown if retry_count <= 0
130
+
131
+ update_cluster_info!
132
+ retry_count -= 1
133
+ retry
134
+ end
135
+
136
+ private
137
+
138
+ def send_wait_command(method, *args, retry_count: 3, **kwargs, &block)
139
+ @node.call_primaries(method, *args, **kwargs, &block).select { |r| r.is_a?(Integer) }.sum
140
+ rescue ::RedisClient::Cluster::ErrorCollection => e
141
+ raise if retry_count <= 0
142
+ raise if e.errors.values.none? do |err|
143
+ err.message.include?('WAIT cannot be used with replica instances')
144
+ end
145
+
146
+ update_cluster_info!
147
+ retry_count -= 1
148
+ retry
149
+ end
150
+
151
+ def send_config_command(method, *args, **kwargs, &block)
152
+ command = method == :blocking_call && args.size > 1 ? args[1..] : args
153
+
154
+ case command[1].to_s.downcase
155
+ when 'resetstat', 'rewrite', 'set'
156
+ @node.call_all(method, *args, **kwargs, &block).first
157
+ else assign_node(*command).send(method, *args, **kwargs, &block)
158
+ end
159
+ end
160
+
161
+ def send_memory_command(method, *args, **kwargs, &block)
162
+ command = method == :blocking_call && args.size > 1 ? args[1..] : args
163
+
164
+ case command[1].to_s.downcase
165
+ when 'stats' then @node.call_all(method, *args, **kwargs, &block)
166
+ when 'purge' then @node.call_all(method, *args, **kwargs, &block).first
167
+ else assign_node(*command).send(method, *args, **kwargs, &block)
168
+ end
169
+ end
170
+
171
+ def send_client_command(method, *args, **kwargs, &block)
172
+ command = method == :blocking_call && args.size > 1 ? args[1..] : args
173
+
174
+ case command[1].to_s.downcase
175
+ when 'list' then @node.call_all(method, *args, **kwargs, &block).flatten
176
+ when 'pause', 'reply', 'setname'
177
+ @node.call_all(method, *args, **kwargs, &block).first
178
+ else assign_node(*command).send(method, *args, **kwargs, &block)
179
+ end
180
+ end
181
+
182
+ def send_cluster_command(method, *args, **kwargs, &block) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
183
+ command = method == :blocking_call && args.size > 1 ? args[1..] : args
184
+ subcommand = command[1].to_s.downcase
185
+
186
+ case subcommand
187
+ when 'addslots', 'delslots', 'failover', 'forget', 'meet', 'replicate',
188
+ 'reset', 'set-config-epoch', 'setslot'
189
+ raise ::RedisClient::Cluster::OrchestrationCommandNotSupported, ['cluster', subcommand]
190
+ when 'saveconfig' then @node.call_all(method, *args, **kwargs, &block).first
191
+ when 'getkeysinslot'
192
+ raise ArgumentError, command.join(' ') if command.size != 4
193
+
194
+ find_node(@node.find_node_key_of_replica(command[2])).send(method, *args, **kwargs, &block)
195
+ else assign_node(*command).send(method, *args, **kwargs, &block)
196
+ end
197
+ end
198
+
199
+ def send_script_command(method, *args, **kwargs, &block)
200
+ command = method == :blocking_call && args.size > 1 ? args[1..] : args
201
+
202
+ case command[1].to_s.downcase
203
+ when 'debug', 'kill'
204
+ @node.call_all(method, *args, **kwargs, &block).first
205
+ when 'flush', 'load'
206
+ @node.call_primaries(method, *args, **kwargs, &block).first
207
+ else assign_node(*command).send(method, *args, **kwargs, &block)
208
+ end
209
+ end
210
+
211
+ def send_pubsub_command(method, *args, **kwargs, &block) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
212
+ command = method == :blocking_call && args.size > 1 ? args[1..] : args
213
+
214
+ case command[1].to_s.downcase
215
+ when 'channels' then @node.call_all(method, *args, **kwargs, &block).flatten.uniq.sort
216
+ when 'numsub'
217
+ @node.call_all(method, *args, **kwargs, &block).reject(&:empty?).map { |e| Hash[*e] }
218
+ .reduce({}) { |a, e| a.merge(e) { |_, v1, v2| v1 + v2 } }
219
+ when 'numpat' then @node.call_all(method, *args, **kwargs, &block).sum
220
+ else assign_node(*command).send(method, *args, **kwargs, &block)
221
+ end
222
+ end
223
+
224
+ def assign_redirection_node(err_msg)
225
+ _, slot, node_key = err_msg.split
226
+ slot = slot.to_i
227
+ @node.update_slot(slot, node_key)
228
+ find_node(node_key)
229
+ end
230
+
231
+ def assign_asking_node(err_msg)
232
+ _, _, node_key = err_msg.split
233
+ find_node(node_key)
234
+ end
235
+
236
+ def fetch_cluster_info(config, pool: nil, **kwargs)
237
+ node_info = ::RedisClient::Cluster::Node.load_info(config.per_node_key, **kwargs)
238
+ node_addrs = node_info.map { |info| ::RedisClient::Cluster::NodeKey.hashify(info[:node_key]) }
239
+ config.update_node(node_addrs)
240
+ ::RedisClient::Cluster::Node.new(config.per_node_key,
241
+ node_info: node_info, pool: pool, with_replica: config.use_replica?, **kwargs)
242
+ end
243
+
244
+ def update_cluster_info!
245
+ @mutex.synchronize do
246
+ begin
247
+ @node.call_all(:close)
248
+ rescue ::RedisClient::Cluster::ErrorCollection
249
+ # ignore
250
+ end
251
+
252
+ @node = fetch_cluster_info(@config, pool: @pool, **@client_kwargs)
253
+ end
254
+ end
255
+ end
256
+ end
257
+ end
@@ -1,134 +1,32 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'redis_client'
4
- require 'redis_client/cluster/command'
5
- require 'redis_client/cluster/errors'
6
- require 'redis_client/cluster/key_slot_converter'
7
- require 'redis_client/cluster/node'
8
- require 'redis_client/cluster/node_key'
4
+ require 'redis_client/cluster/pipeline'
5
+ require 'redis_client/cluster/pub_sub'
6
+ require 'redis_client/cluster/router'
9
7
 
10
8
  class RedisClient
11
9
  class Cluster
12
- class Pipeline
13
- ReplySizeError = Class.new(::RedisClient::Error)
14
-
15
- def initialize(client)
16
- @client = client
17
- @grouped = Hash.new([].freeze)
18
- @size = 0
19
- end
20
-
21
- def call(*command, **kwargs)
22
- node_key = @client.send(:find_node_key, *command, primary_only: true)
23
- @grouped[node_key] += [[@size, :call, command, kwargs]]
24
- @size += 1
25
- end
26
-
27
- def call_once(*command, **kwargs)
28
- node_key = @client.send(:find_node_key, *command, primary_only: true)
29
- @grouped[node_key] += [[@size, :call_once, command, kwargs]]
30
- @size += 1
31
- end
32
-
33
- def blocking_call(timeout, *command, **kwargs)
34
- node_key = @client.send(:find_node_key, *command, primary_only: true)
35
- @grouped[node_key] += [[@size, :blocking_call, timeout, command, kwargs]]
36
- @size += 1
37
- end
38
-
39
- def empty?
40
- @size.zero?
41
- end
42
-
43
- # TODO: https://github.com/redis-rb/redis-cluster-client/issues/37 handle redirections
44
- def execute # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
45
- all_replies = Array.new(@size)
46
- errors = {}
47
- threads = @grouped.map do |k, v|
48
- Thread.new(@client, k, v) do |client, node_key, rows|
49
- Thread.pass
50
- replies = client.send(:find_node, node_key).pipelined do |pipeline|
51
- rows.each do |row|
52
- case row[1]
53
- when :call then pipeline.call(*row[2], **row[3])
54
- when :call_once then pipeline.call_once(*row[2], **row[3])
55
- when :blocking_call then pipeline.blocking_call(row[2], *row[3], **row[4])
56
- else raise NotImplementedError, row[1]
57
- end
58
- end
59
- end
60
-
61
- raise ReplySizeError, "commands: #{rows.size}, replies: #{replies.size}" if rows.size != replies.size
62
-
63
- rows.each_with_index { |row, idx| all_replies[row.first] = replies[idx] }
64
- rescue StandardError => e
65
- errors[node_key] = e
66
- end
67
- end
68
-
69
- threads.each(&:join)
70
- return all_replies if errors.empty?
71
-
72
- raise ::RedisClient::Cluster::ErrorCollection, errors
73
- end
74
- end
75
-
76
- class PubSub
77
- def initialize(client)
78
- @client = client
79
- @pubsub = nil
80
- end
81
-
82
- def call(*command, **kwargs)
83
- close
84
- @pubsub = @client.send(:assign_node, *command).pubsub
85
- @pubsub.call(*command, **kwargs)
86
- end
87
-
88
- def close
89
- @pubsub&.close
90
- @pubsub = nil
91
- end
92
-
93
- def next_event(timeout = nil)
94
- @pubsub&.next_event(timeout)
95
- end
96
- end
97
-
98
10
  ZERO_CURSOR_FOR_SCAN = '0'
99
- CMD_SCAN = 'SCAN'
100
- CMD_SSCAN = 'SSCAN'
101
- CMD_HSCAN = 'HSCAN'
102
- CMD_ZSCAN = 'ZSCAN'
103
- CMD_ASKING = 'ASKING'
104
- REPLY_OK = 'OK'
105
- REPLY_MOVED = 'MOVED'
106
- REPLY_ASK = 'ASK'
107
11
 
108
12
  def initialize(config, pool: nil, **kwargs)
109
- @config = config.dup
110
- @pool = pool
111
- @client_kwargs = kwargs
112
- @node = fetch_cluster_info!(@config, pool: @pool, **@client_kwargs)
113
- @command = ::RedisClient::Cluster::Command.load(@node)
114
- @mutex = Mutex.new
13
+ @router = ::RedisClient::Cluster::Router.new(config, pool: pool, **kwargs)
115
14
  end
116
15
 
117
16
  def inspect
118
- "#<#{self.class.name} #{@node.node_keys.join(', ')}>"
17
+ "#<#{self.class.name} #{@router.node.node_keys.join(', ')}>"
119
18
  end
120
19
 
121
20
  def call(*command, **kwargs)
122
- send_command(:call, *command, **kwargs)
21
+ @router.send_command(:call, *command, **kwargs)
123
22
  end
124
23
 
125
24
  def call_once(*command, **kwargs)
126
- send_command(:call_once, *command, **kwargs)
25
+ @router.send_command(:call_once, *command, **kwargs)
127
26
  end
128
27
 
129
28
  def blocking_call(timeout, *command, **kwargs)
130
- node = assign_node(*command)
131
- try_send(node, :blocking_call, timeout, *command, **kwargs)
29
+ @router.send_command(:blocking_call, timeout, *command, **kwargs)
132
30
  end
133
31
 
134
32
  def scan(*args, **kwargs, &block)
@@ -136,29 +34,29 @@ class RedisClient
136
34
 
137
35
  cursor = ZERO_CURSOR_FOR_SCAN
138
36
  loop do
139
- cursor, keys = _scan(CMD_SCAN, cursor, *args, **kwargs)
37
+ cursor, keys = @router.scan('SCAN', cursor, *args, **kwargs)
140
38
  keys.each(&block)
141
39
  break if cursor == ZERO_CURSOR_FOR_SCAN
142
40
  end
143
41
  end
144
42
 
145
43
  def sscan(key, *args, **kwargs, &block)
146
- node = assign_node(CMD_SSCAN, key)
147
- try_send(node, :sscan, key, *args, **kwargs, &block)
44
+ node = @router.assign_node('SSCAN', key)
45
+ @router.try_send(node, :sscan, key, *args, **kwargs, &block)
148
46
  end
149
47
 
150
48
  def hscan(key, *args, **kwargs, &block)
151
- node = assign_node(CMD_HSCAN, key)
152
- try_send(node, :hscan, key, *args, **kwargs, &block)
49
+ node = @router.assign_node('HSCAN', key)
50
+ @router.try_send(node, :hscan, key, *args, **kwargs, &block)
153
51
  end
154
52
 
155
53
  def zscan(key, *args, **kwargs, &block)
156
- node = assign_node(CMD_ZSCAN, key)
157
- try_send(node, :zscan, key, *args, **kwargs, &block)
54
+ node = @router.assign_node('ZSCAN', key)
55
+ @router.try_send(node, :zscan, key, *args, **kwargs, &block)
158
56
  end
159
57
 
160
58
  def pipelined
161
- pipeline = ::RedisClient::Cluster::Pipeline.new(self)
59
+ pipeline = ::RedisClient::Cluster::Pipeline.new(@router)
162
60
  yield pipeline
163
61
  return [] if pipeline.empty? == 0
164
62
 
@@ -166,225 +64,12 @@ class RedisClient
166
64
  end
167
65
 
168
66
  def pubsub
169
- ::RedisClient::Cluster::PubSub.new(self)
67
+ ::RedisClient::Cluster::PubSub.new(@router)
170
68
  end
171
69
 
172
70
  def close
173
- @node.call_all(:close)
71
+ @router.node.call_all(:close)
174
72
  nil
175
- rescue StandardError
176
- # ignore
177
- end
178
-
179
- private
180
-
181
- def fetch_cluster_info!(config, pool: nil, **kwargs)
182
- node_info = ::RedisClient::Cluster::Node.load_info(config.per_node_key, **kwargs)
183
- node_addrs = node_info.map { |info| ::RedisClient::Cluster::NodeKey.hashify(info[:node_key]) }
184
- config.update_node(node_addrs)
185
- ::RedisClient::Cluster::Node.new(config.per_node_key,
186
- node_info: node_info, pool: pool, with_replica: config.use_replica?, **kwargs)
187
- end
188
-
189
- def send_command(method, *command, **kwargs, &block) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
190
- cmd = command.first.to_s.downcase
191
- case cmd
192
- when 'acl', 'auth', 'bgrewriteaof', 'bgsave', 'quit', 'save'
193
- @node.call_all(method, *command, **kwargs, &block).first
194
- when 'flushall', 'flushdb'
195
- @node.call_primaries(method, *command, **kwargs, &block).first
196
- when 'ping' then @node.send_ping(method, *command, **kwargs, &block).first
197
- when 'wait' then send_wait_command(method, *command, **kwargs, &block)
198
- when 'keys' then @node.call_replicas(method, *command, **kwargs, &block).flatten.sort
199
- when 'dbsize' then @node.call_replicas(method, *command, **kwargs, &block).sum
200
- when 'scan' then _scan(*command, **kwargs)
201
- when 'lastsave' then @node.call_all(method, *command, **kwargs, &block).sort
202
- when 'role' then @node.call_all(method, *command, **kwargs, &block)
203
- when 'config' then send_config_command(method, *command, **kwargs, &block)
204
- when 'client' then send_client_command(method, *command, **kwargs, &block)
205
- when 'cluster' then send_cluster_command(method, *command, **kwargs, &block)
206
- when 'readonly', 'readwrite', 'shutdown'
207
- raise ::RedisClient::Cluster::OrchestrationCommandNotSupported, cmd
208
- when 'memory' then send_memory_command(method, *command, **kwargs, &block)
209
- when 'script' then send_script_command(method, *command, **kwargs, &block)
210
- when 'pubsub' then send_pubsub_command(method, *command, **kwargs, &block)
211
- when 'discard', 'exec', 'multi', 'unwatch'
212
- raise ::RedisClient::Cluster::AmbiguousNodeError, cmd
213
- else
214
- node = assign_node(*command)
215
- try_send(node, method, *command, **kwargs, &block)
216
- end
217
- rescue RedisClient::Cluster::Node::ReloadNeeded
218
- update_cluster_info!
219
- raise ::RedisClient::Cluster::NodeMightBeDown
220
- end
221
-
222
- def send_wait_command(method, *command, retry_count: 3, **kwargs, &block)
223
- @node.call_primaries(method, *command, **kwargs, &block).sum
224
- rescue RedisClient::Cluster::ErrorCollection => e
225
- raise if retry_count <= 0
226
- raise if e.errors.values.none? do |err|
227
- err.message.include?('WAIT cannot be used with replica instances')
228
- end
229
-
230
- update_cluster_info!
231
- retry_count -= 1
232
- retry
233
- end
234
-
235
- def send_config_command(method, *command, **kwargs, &block)
236
- case command[1].to_s.downcase
237
- when 'resetstat', 'rewrite', 'set'
238
- @node.call_all(method, *command, **kwargs, &block).first
239
- else assign_node(*command).send(method, *command, **kwargs, &block)
240
- end
241
- end
242
-
243
- def send_memory_command(method, *command, **kwargs, &block)
244
- case command[1].to_s.downcase
245
- when 'stats' then @node.call_all(method, *command, **kwargs, &block)
246
- when 'purge' then @node.call_all(method, *command, **kwargs, &block).first
247
- else assign_node(*command).send(method, *command, **kwargs, &block)
248
- end
249
- end
250
-
251
- def send_client_command(method, *command, **kwargs, &block)
252
- case command[1].to_s.downcase
253
- when 'list' then @node.call_all(method, *command, **kwargs, &block).flatten
254
- when 'pause', 'reply', 'setname'
255
- @node.call_all(method, *command, **kwargs, &block).first
256
- else assign_node(*command).send(method, *command, **kwargs, &block)
257
- end
258
- end
259
-
260
- def send_cluster_command(method, *command, **kwargs, &block) # rubocop:disable Metrics/MethodLength
261
- subcommand = command[1].to_s.downcase
262
- case subcommand
263
- when 'addslots', 'delslots', 'failover', 'forget', 'meet', 'replicate',
264
- 'reset', 'set-config-epoch', 'setslot'
265
- raise ::RedisClient::Cluster::OrchestrationCommandNotSupported, ['cluster', subcommand]
266
- when 'saveconfig' then @node.call_all(method, *command, **kwargs, &block).first
267
- when 'getkeysinslot'
268
- raise ArgumentError, command.join(' ') if command.size != 4
269
-
270
- find_node(@node.find_node_key_of_replica(command[2])).send(method, *command, **kwargs, &block)
271
- else assign_node(*command).send(method, *command, **kwargs, &block)
272
- end
273
- end
274
-
275
- def send_script_command(method, *command, **kwargs, &block)
276
- case command[1].to_s.downcase
277
- when 'debug', 'kill'
278
- @node.call_all(method, *command, **kwargs, &block).first
279
- when 'flush', 'load'
280
- @node.call_primaries(method, *command, **kwargs, &block).first
281
- else assign_node(*command).send(method, *command, **kwargs, &block)
282
- end
283
- end
284
-
285
- def send_pubsub_command(method, *command, **kwargs, &block) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity
286
- case command[1].to_s.downcase
287
- when 'channels' then @node.call_all(method, *command, **kwargs, &block).flatten.uniq.sort
288
- when 'numsub'
289
- @node.call_all(method, *command, **kwargs, &block).reject(&:empty?).map { |e| Hash[*e] }
290
- .reduce({}) { |a, e| a.merge(e) { |_, v1, v2| v1 + v2 } }
291
- when 'numpat' then @node.call_all(method, *command, **kwargs, &block).sum
292
- else assign_node(*command).send(method, *command, **kwargs, &block)
293
- end
294
- end
295
-
296
- # @see https://redis.io/topics/cluster-spec#redirection-and-resharding
297
- # Redirection and resharding
298
- def try_send(node, method, *args, retry_count: 3, **kwargs, &block) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
299
- node.send(method, *args, **kwargs, &block)
300
- rescue ::RedisClient::CommandError => e
301
- raise if retry_count <= 0
302
-
303
- if e.message.start_with?(REPLY_MOVED)
304
- node = assign_redirection_node(e.message)
305
- retry_count -= 1
306
- retry
307
- elsif e.message.start_with?(REPLY_ASK)
308
- node = assign_asking_node(e.message)
309
- node.call(CMD_ASKING)
310
- retry_count -= 1
311
- retry
312
- else
313
- raise
314
- end
315
- rescue ::RedisClient::ConnectionError
316
- raise if retry_count <= 0
317
-
318
- update_cluster_info!
319
- retry_count -= 1
320
- retry
321
- end
322
-
323
- def _scan(*command, **kwargs) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
324
- command[1] = ZERO_CURSOR_FOR_SCAN if command.size == 1
325
- input_cursor = Integer(command[1])
326
-
327
- client_index = input_cursor % 256
328
- raw_cursor = input_cursor >> 8
329
-
330
- clients = @node.scale_reading_clients
331
-
332
- client = clients[client_index]
333
- return [ZERO_CURSOR_FOR_SCAN, []] unless client
334
-
335
- command[1] = raw_cursor.to_s
336
-
337
- result_cursor, result_keys = client.call(*command, **kwargs)
338
- result_cursor = Integer(result_cursor)
339
-
340
- client_index += 1 if result_cursor == 0
341
-
342
- [((result_cursor << 8) + client_index).to_s, result_keys]
343
- end
344
-
345
- def assign_redirection_node(err_msg)
346
- _, slot, node_key = err_msg.split
347
- slot = slot.to_i
348
- @node.update_slot(slot, node_key)
349
- find_node(node_key)
350
- end
351
-
352
- def assign_asking_node(err_msg)
353
- _, _, node_key = err_msg.split
354
- find_node(node_key)
355
- end
356
-
357
- def assign_node(*command)
358
- node_key = find_node_key(*command)
359
- find_node(node_key)
360
- end
361
-
362
- def find_node_key(*command, primary_only: false)
363
- key = @command.extract_first_key(command)
364
- slot = key.empty? ? nil : ::RedisClient::Cluster::KeySlotConverter.convert(key)
365
-
366
- if @command.should_send_to_primary?(command) || primary_only
367
- @node.find_node_key_of_primary(slot) || @node.primary_node_keys.sample
368
- else
369
- @node.find_node_key_of_replica(slot) || @node.replica_node_keys.sample
370
- end
371
- end
372
-
373
- def find_node(node_key, retry_count: 3)
374
- @node.find_by(node_key)
375
- rescue ::RedisClient::Cluster::Node::ReloadNeeded
376
- raise ::RedieClient::Cluster::NodeMightBeDown if retry_count <= 0
377
-
378
- update_cluster_info!
379
- retry_count -= 1
380
- retry
381
- end
382
-
383
- def update_cluster_info!
384
- @mutex.synchronize do
385
- close
386
- @node = fetch_cluster_info!(@config, pool: @pool, **@client_kwargs)
387
- end
388
73
  end
389
74
  end
390
75
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: redis-cluster-client
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.8
4
+ version: 0.0.11
5
5
  platform: ruby
6
6
  authors:
7
7
  - Taishi Kasuga
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-06-21 00:00:00.000000000 Z
11
+ date: 2022-06-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: redis-client
@@ -37,6 +37,9 @@ files:
37
37
  - lib/redis_client/cluster/key_slot_converter.rb
38
38
  - lib/redis_client/cluster/node.rb
39
39
  - lib/redis_client/cluster/node_key.rb
40
+ - lib/redis_client/cluster/pipeline.rb
41
+ - lib/redis_client/cluster/pub_sub.rb
42
+ - lib/redis_client/cluster/router.rb
40
43
  - lib/redis_client/cluster_config.rb
41
44
  - lib/redis_cluster_client.rb
42
45
  homepage: https://github.com/redis-rb/redis-cluster-client