redis-cluster-client 0.11.0 → 0.13.5

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.
@@ -7,7 +7,6 @@ require 'redis_client/cluster/errors'
7
7
  require 'redis_client/cluster/key_slot_converter'
8
8
  require 'redis_client/cluster/node'
9
9
  require 'redis_client/cluster/node_key'
10
- require 'redis_client/cluster/normalized_cmd_name'
11
10
  require 'redis_client/cluster/transaction'
12
11
  require 'redis_client/cluster/optimistic_locking'
13
12
  require 'redis_client/cluster/pipeline'
@@ -18,71 +17,121 @@ class RedisClient
18
17
  class Router
19
18
  ZERO_CURSOR_FOR_SCAN = '0'
20
19
  TSF = ->(f, x) { f.nil? ? x : f.call(x) }.curry
20
+ Ractor.make_shareable(TSF) if Object.const_defined?(:Ractor, false) && Ractor.respond_to?(:make_shareable)
21
+ DEDICATED_ACTIONS = lambda do # rubocop:disable Metrics/BlockLength
22
+ action = Struct.new('RedisCommandRoutingAction', :method_name, :reply_transformer, keyword_init: true)
23
+ pick_first = ->(reply) { reply.first } # rubocop:disable Style/SymbolProc
24
+ flatten_strings = ->(reply) { reply.flatten.sort_by(&:to_s) }
25
+ sum_num = ->(reply) { reply.select { |e| e.is_a?(Integer) }.sum }
26
+ sort_numbers = ->(reply) { reply.sort_by(&:to_i) }
27
+ if Object.const_defined?(:Ractor, false) && Ractor.respond_to?(:make_shareable)
28
+ Ractor.make_shareable(pick_first)
29
+ Ractor.make_shareable(flatten_strings)
30
+ Ractor.make_shareable(sum_num)
31
+ Ractor.make_shareable(sort_numbers)
32
+ end
33
+ multiple_key_action = action.new(method_name: :send_multiple_keys_command)
34
+ all_node_first_action = action.new(method_name: :send_command_to_all_nodes, reply_transformer: pick_first)
35
+ primary_first_action = action.new(method_name: :send_command_to_primaries, reply_transformer: pick_first)
36
+ not_supported_action = action.new(method_name: :fail_not_supported_command)
37
+ keyless_action = action.new(method_name: :fail_keyless_command)
38
+ {
39
+ 'ping' => action.new(method_name: :send_ping_command, reply_transformer: pick_first),
40
+ 'wait' => action.new(method_name: :send_wait_command),
41
+ 'keys' => action.new(method_name: :send_command_to_replicas, reply_transformer: flatten_strings),
42
+ 'dbsize' => action.new(method_name: :send_command_to_replicas, reply_transformer: sum_num),
43
+ 'scan' => action.new(method_name: :send_scan_command),
44
+ 'lastsave' => action.new(method_name: :send_command_to_all_nodes, reply_transformer: sort_numbers),
45
+ 'role' => action.new(method_name: :send_command_to_all_nodes),
46
+ 'config' => action.new(method_name: :send_config_command),
47
+ 'client' => action.new(method_name: :send_client_command),
48
+ 'cluster' => action.new(method_name: :send_cluster_command),
49
+ 'memory' => action.new(method_name: :send_memory_command),
50
+ 'script' => action.new(method_name: :send_script_command),
51
+ 'pubsub' => action.new(method_name: :send_pubsub_command),
52
+ 'watch' => action.new(method_name: :send_watch_command),
53
+ 'mget' => multiple_key_action,
54
+ 'mset' => multiple_key_action,
55
+ 'del' => multiple_key_action,
56
+ 'acl' => all_node_first_action,
57
+ 'auth' => all_node_first_action,
58
+ 'bgrewriteaof' => all_node_first_action,
59
+ 'bgsave' => all_node_first_action,
60
+ 'quit' => all_node_first_action,
61
+ 'save' => all_node_first_action,
62
+ 'flushall' => primary_first_action,
63
+ 'flushdb' => primary_first_action,
64
+ 'readonly' => not_supported_action,
65
+ 'readwrite' => not_supported_action,
66
+ 'shutdown' => not_supported_action,
67
+ 'discard' => keyless_action,
68
+ 'exec' => keyless_action,
69
+ 'multi' => keyless_action,
70
+ 'unwatch' => keyless_action
71
+ }.each_with_object({}) do |(k, v), acc|
72
+ acc[k] = v.freeze
73
+ acc[k.upcase] = v.freeze
74
+ end
75
+ end.call.freeze
76
+
77
+ private_constant :ZERO_CURSOR_FOR_SCAN, :TSF, :DEDICATED_ACTIONS
78
+
79
+ attr_reader :config
21
80
 
22
81
  def initialize(config, concurrent_worker, pool: nil, **kwargs)
23
- @config = config.dup
24
- @original_config = config.dup if config.connect_with_original_config
25
- @connect_with_original_config = config.connect_with_original_config
82
+ @config = config
26
83
  @concurrent_worker = concurrent_worker
27
84
  @pool = pool
28
85
  @client_kwargs = kwargs
29
86
  @node = ::RedisClient::Cluster::Node.new(concurrent_worker, config: config, pool: pool, **kwargs)
30
- update_cluster_info!
87
+ @node.reload!
31
88
  @command = ::RedisClient::Cluster::Command.load(@node.replica_clients.shuffle, slow_command_timeout: config.slow_command_timeout)
32
89
  @command_builder = @config.command_builder
90
+ rescue ::RedisClient::Cluster::InitialSetupError => e
91
+ e.with_config(config)
92
+ raise
33
93
  end
34
94
 
35
95
  def send_command(method, command, *args, &block) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
36
- cmd = ::RedisClient::Cluster::NormalizedCmdName.instance.get_by_command(command)
37
- case cmd
38
- when 'ping' then @node.send_ping(method, command, args).first.then(&TSF.call(block))
39
- when 'wait' then send_wait_command(method, command, args, &block)
40
- when 'keys' then @node.call_replicas(method, command, args).flatten.sort_by(&:to_s).then(&TSF.call(block))
41
- when 'dbsize' then @node.call_replicas(method, command, args).select { |e| e.is_a?(Integer) }.sum.then(&TSF.call(block))
42
- when 'scan' then scan(command, seed: 1)
43
- when 'lastsave' then @node.call_all(method, command, args).sort_by(&:to_i).then(&TSF.call(block))
44
- when 'role' then @node.call_all(method, command, args, &block)
45
- when 'config' then send_config_command(method, command, args, &block)
46
- when 'client' then send_client_command(method, command, args, &block)
47
- when 'cluster' then send_cluster_command(method, command, args, &block)
48
- when 'memory' then send_memory_command(method, command, args, &block)
49
- when 'script' then send_script_command(method, command, args, &block)
50
- when 'pubsub' then send_pubsub_command(method, command, args, &block)
51
- when 'watch' then send_watch_command(command, &block)
52
- when 'mset', 'mget', 'del'
53
- send_multiple_keys_command(cmd, method, command, args, &block)
54
- when 'acl', 'auth', 'bgrewriteaof', 'bgsave', 'quit', 'save'
55
- @node.call_all(method, command, args).first.then(&TSF.call(block))
56
- when 'flushall', 'flushdb'
57
- @node.call_primaries(method, command, args).first.then(&TSF.call(block))
58
- when 'readonly', 'readwrite', 'shutdown'
59
- raise ::RedisClient::Cluster::OrchestrationCommandNotSupported, cmd
60
- when 'discard', 'exec', 'multi', 'unwatch'
61
- raise ::RedisClient::Cluster::AmbiguousNodeError, cmd
62
- else
63
- node = assign_node(command)
64
- try_send(node, method, command, args, &block)
65
- end
96
+ return assign_node_and_send_command(method, command, args, &block) unless DEDICATED_ACTIONS.key?(command.first)
97
+
98
+ action = DEDICATED_ACTIONS[command.first]
99
+ return send(action.method_name, method, command, args, &block) if action.reply_transformer.nil?
100
+
101
+ reply = send(action.method_name, method, command, args)
102
+ action.reply_transformer.call(reply).then(&TSF.call(block))
66
103
  rescue ::RedisClient::CircuitBreaker::OpenCircuitError
67
104
  raise
68
105
  rescue ::RedisClient::Cluster::Node::ReloadNeeded
69
- update_cluster_info!
70
- raise ::RedisClient::Cluster::NodeMightBeDown
106
+ renew_cluster_state
107
+ raise ::RedisClient::Cluster::NodeMightBeDown.new.with_config(@config)
108
+ rescue ::RedisClient::ConnectionError
109
+ renew_cluster_state
110
+ raise
111
+ rescue ::RedisClient::CommandError => e
112
+ renew_cluster_state if e.message.start_with?('CLUSTERDOWN')
113
+ raise
71
114
  rescue ::RedisClient::Cluster::ErrorCollection => e
115
+ e.with_config(@config)
72
116
  raise if e.errors.any?(::RedisClient::CircuitBreaker::OpenCircuitError)
73
117
 
74
- update_cluster_info! if e.errors.values.any? do |err|
118
+ renew_cluster_state if e.errors.values.any? do |err|
75
119
  next false if ::RedisClient::Cluster::ErrorIdentification.identifiable?(err) && @node.none? { |c| ::RedisClient::Cluster::ErrorIdentification.client_owns_error?(err, c) }
76
120
 
77
- err.message.start_with?('CLUSTERDOWN Hash slot not served')
121
+ err.message.start_with?('CLUSTERDOWN') || err.is_a?(::RedisClient::ConnectionError)
78
122
  end
79
123
 
80
124
  raise
81
125
  end
82
126
 
83
127
  # @see https://redis.io/docs/reference/cluster-spec/#redirection-and-resharding Redirection and resharding
84
- def try_send(node, method, command, args, retry_count: 3, &block)
85
- handle_redirection(node, retry_count: retry_count) do |on_node|
128
+ def assign_node_and_send_command(method, command, args, retry_count: 3, &block)
129
+ node = assign_node(command)
130
+ send_command_to_node(node, method, command, args, retry_count: retry_count, &block)
131
+ end
132
+
133
+ def send_command_to_node(node, method, command, args, retry_count: 3, &block)
134
+ handle_redirection(node, command, retry_count: retry_count) do |on_node|
86
135
  if args.empty?
87
136
  # prevent memory allocation for variable-length args
88
137
  on_node.public_send(method, command, &block)
@@ -92,50 +141,50 @@ class RedisClient
92
141
  end
93
142
  end
94
143
 
95
- def try_delegate(node, method, *args, retry_count: 3, **kwargs, &block)
96
- handle_redirection(node, retry_count: retry_count) do |on_node|
97
- on_node.public_send(method, *args, **kwargs, &block)
98
- end
99
- end
100
-
101
- def handle_redirection(node, retry_count:) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
144
+ def handle_redirection(node, command, retry_count:) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
102
145
  yield node
103
146
  rescue ::RedisClient::CircuitBreaker::OpenCircuitError
104
147
  raise
105
148
  rescue ::RedisClient::CommandError => e
106
149
  raise unless ::RedisClient::Cluster::ErrorIdentification.client_owns_error?(e, node)
107
150
 
151
+ retry_count -= 1
108
152
  if e.message.start_with?('MOVED')
109
153
  node = assign_redirection_node(e.message)
110
- retry_count -= 1
111
154
  retry if retry_count >= 0
112
155
  elsif e.message.start_with?('ASK')
113
156
  node = assign_asking_node(e.message)
114
- retry_count -= 1
115
157
  if retry_count >= 0
116
- node.call('ASKING')
158
+ node.call('asking')
117
159
  retry
118
160
  end
119
- elsif e.message.start_with?('CLUSTERDOWN Hash slot not served')
120
- update_cluster_info!
121
- retry_count -= 1
161
+ elsif e.message.start_with?('CLUSTERDOWN')
162
+ renew_cluster_state
122
163
  retry if retry_count >= 0
123
164
  end
165
+
124
166
  raise
125
167
  rescue ::RedisClient::ConnectionError => e
126
168
  raise unless ::RedisClient::Cluster::ErrorIdentification.client_owns_error?(e, node)
127
169
 
128
- update_cluster_info!
129
-
130
- raise if retry_count <= 0
131
-
132
170
  retry_count -= 1
133
- retry
134
- end
171
+ renew_cluster_state
172
+
173
+ if retry_count >= 0
174
+ # Find the node to use for this command - if this fails for some reason, though, re-use
175
+ # the old node.
176
+ begin
177
+ node = find_node(find_node_key(command)) if command
178
+ rescue StandardError # rubocop:disable Lint/SuppressedException
179
+ end
180
+ retry
181
+ end
135
182
 
136
- def scan(*command, seed: nil, **kwargs) # rubocop:disable Metrics/AbcSize
137
- command = @command_builder.generate(command, kwargs)
183
+ retry if retry_count >= 0
184
+ raise
185
+ end
138
186
 
187
+ def scan(command, seed: nil) # rubocop:disable Metrics/AbcSize
139
188
  command[1] = ZERO_CURSOR_FOR_SCAN if command.size == 1
140
189
  input_cursor = Integer(command[1])
141
190
 
@@ -155,25 +204,47 @@ class RedisClient
155
204
  client_index += 1 if result_cursor == 0
156
205
 
157
206
  [((result_cursor << 8) + client_index).to_s, result_keys]
207
+ rescue ::RedisClient::ConnectionError
208
+ renew_cluster_state
209
+ raise
210
+ end
211
+
212
+ def scan_single_key(command, arity:, &block)
213
+ node = assign_node(command)
214
+ loop do
215
+ cursor, values = handle_redirection(node, nil, retry_count: 3) { |n| n.call_v(command) }
216
+ command[2] = cursor
217
+ arity < 2 ? values.each(&block) : values.each_slice(arity, &block)
218
+ break if cursor == ZERO_CURSOR_FOR_SCAN
219
+ end
158
220
  end
159
221
 
160
222
  def assign_node(command)
161
- node_key = find_node_key(command)
162
- find_node(node_key)
223
+ handle_node_reload_error do
224
+ node_key = find_node_key(command)
225
+ @node.find_by(node_key)
226
+ end
163
227
  end
164
228
 
165
229
  def find_node_key_by_key(key, seed: nil, primary: false)
166
230
  if key && !key.empty?
167
231
  slot = ::RedisClient::Cluster::KeySlotConverter.convert(key)
168
- primary ? @node.find_node_key_of_primary(slot) : @node.find_node_key_of_replica(slot)
232
+ node_key = primary ? @node.find_node_key_of_primary(slot) : @node.find_node_key_of_replica(slot)
233
+ if node_key.nil?
234
+ renew_cluster_state
235
+ raise ::RedisClient::Cluster::NodeMightBeDown.new.with_config(@config)
236
+ end
237
+ node_key
169
238
  else
170
239
  primary ? @node.any_primary_node_key(seed: seed) : @node.any_replica_node_key(seed: seed)
171
240
  end
172
241
  end
173
242
 
174
243
  def find_primary_node_by_slot(slot)
175
- node_key = @node.find_node_key_of_primary(slot)
176
- find_node(node_key)
244
+ handle_node_reload_error do
245
+ node_key = @node.find_node_key_of_primary(slot)
246
+ @node.find_by(node_key)
247
+ end
177
248
  end
178
249
 
179
250
  def find_node_key(command, seed: nil)
@@ -198,14 +269,8 @@ class RedisClient
198
269
  ::RedisClient::Cluster::KeySlotConverter.convert(key)
199
270
  end
200
271
 
201
- def find_node(node_key, retry_count: 3)
202
- @node.find_by(node_key)
203
- rescue ::RedisClient::Cluster::Node::ReloadNeeded
204
- raise ::RedisClient::Cluster::NodeMightBeDown if retry_count <= 0
205
-
206
- update_cluster_info!
207
- retry_count -= 1
208
- retry
272
+ def find_node(node_key)
273
+ handle_node_reload_error { @node.find_by(node_key) }
209
274
  end
210
275
 
211
276
  def command_exists?(name)
@@ -216,109 +281,178 @@ class RedisClient
216
281
  _, slot, node_key = err_msg.split
217
282
  slot = slot.to_i
218
283
  @node.update_slot(slot, node_key)
219
- find_node(node_key)
284
+ handle_node_reload_error { @node.find_by(node_key) }
220
285
  end
221
286
 
222
287
  def assign_asking_node(err_msg)
223
288
  _, _, node_key = err_msg.split
224
- find_node(node_key)
289
+ handle_node_reload_error { @node.find_by(node_key) }
225
290
  end
226
291
 
227
292
  def node_keys
228
293
  @node.node_keys
229
294
  end
230
295
 
296
+ def renew_cluster_state
297
+ @node.reload!
298
+ rescue ::RedisClient::Cluster::InitialSetupError
299
+ # ignore
300
+ end
301
+
231
302
  def close
232
303
  @node.each(&:close)
233
304
  end
234
305
 
235
306
  private
236
307
 
237
- def send_wait_command(method, command, args, retry_count: 3, &block) # rubocop:disable Metrics/AbcSize
308
+ def send_command_to_all_nodes(method, command, args, &block)
309
+ @node.call_all(method, command, args, &block)
310
+ end
311
+
312
+ def send_command_to_primaries(method, command, args, &block)
313
+ @node.call_primaries(method, command, args, &block)
314
+ end
315
+
316
+ def send_command_to_replicas(method, command, args, &block)
317
+ @node.call_replicas(method, command, args, &block)
318
+ end
319
+
320
+ def send_ping_command(method, command, args, &block)
321
+ @node.send_ping(method, command, args, &block)
322
+ end
323
+
324
+ def send_scan_command(_method, command, _args, &_block)
325
+ scan(command, seed: 1)
326
+ end
327
+
328
+ def fail_not_supported_command(_method, command, _args, &_block)
329
+ raise ::RedisClient::Cluster::OrchestrationCommandNotSupported.from_command(command.first).with_config(@config)
330
+ end
331
+
332
+ def fail_keyless_command(_method, command, _args, &_block)
333
+ raise ::RedisClient::Cluster::AmbiguousNodeError.from_command(command.first).with_config(@config)
334
+ end
335
+
336
+ def send_wait_command(method, command, args, retry_count: 1, &block) # rubocop:disable Metrics/AbcSize
238
337
  @node.call_primaries(method, command, args).select { |r| r.is_a?(Integer) }.sum.then(&TSF.call(block))
239
338
  rescue ::RedisClient::Cluster::ErrorCollection => e
240
339
  raise if e.errors.any?(::RedisClient::CircuitBreaker::OpenCircuitError)
241
340
  raise if retry_count <= 0
242
- raise if e.errors.values.none? do |err|
243
- err.message.include?('WAIT cannot be used with replica instances')
244
- end
341
+ raise if e.errors.values.none? { |err| err.message.include?('WAIT cannot be used with replica instances') }
245
342
 
246
- update_cluster_info!
247
343
  retry_count -= 1
344
+ renew_cluster_state
248
345
  retry
249
346
  end
250
347
 
251
- def send_config_command(method, command, args, &block)
252
- case ::RedisClient::Cluster::NormalizedCmdName.instance.get_by_subcommand(command)
253
- when 'resetstat', 'rewrite', 'set'
348
+ def send_config_command(method, command, args, &block) # rubocop:disable Metrics/AbcSize
349
+ if command[1].casecmp('resetstat').zero?
350
+ @node.call_all(method, command, args).first.then(&TSF.call(block))
351
+ elsif command[1].casecmp('rewrite').zero?
254
352
  @node.call_all(method, command, args).first.then(&TSF.call(block))
255
- else assign_node(command).public_send(method, *args, command, &block)
353
+ elsif command[1].casecmp('set').zero?
354
+ @node.call_all(method, command, args).first.then(&TSF.call(block))
355
+ else
356
+ assign_node(command).public_send(method, *args, command, &block)
256
357
  end
257
358
  end
258
359
 
259
360
  def send_memory_command(method, command, args, &block)
260
- case ::RedisClient::Cluster::NormalizedCmdName.instance.get_by_subcommand(command)
261
- when 'stats' then @node.call_all(method, command, args, &block)
262
- when 'purge' then @node.call_all(method, command, args).first.then(&TSF.call(block))
263
- else assign_node(command).public_send(method, *args, command, &block)
361
+ if command[1].casecmp('stats').zero?
362
+ @node.call_all(method, command, args, &block)
363
+ elsif command[1].casecmp('purge').zero?
364
+ @node.call_all(method, command, args).first.then(&TSF.call(block))
365
+ else
366
+ assign_node(command).public_send(method, *args, command, &block)
264
367
  end
265
368
  end
266
369
 
267
- def send_client_command(method, command, args, &block)
268
- case ::RedisClient::Cluster::NormalizedCmdName.instance.get_by_subcommand(command)
269
- when 'list' then @node.call_all(method, command, args, &block).flatten
270
- when 'pause', 'reply', 'setname'
370
+ def send_client_command(method, command, args, &block) # rubocop:disable Metrics/AbcSize
371
+ if command[1].casecmp('list').zero?
372
+ @node.call_all(method, command, args, &block).flatten
373
+ elsif command[1].casecmp('pause').zero?
374
+ @node.call_all(method, command, args).first.then(&TSF.call(block))
375
+ elsif command[1].casecmp('reply').zero?
271
376
  @node.call_all(method, command, args).first.then(&TSF.call(block))
272
- else assign_node(command).public_send(method, *args, command, &block)
377
+ elsif command[1].casecmp('setname').zero?
378
+ @node.call_all(method, command, args).first.then(&TSF.call(block))
379
+ else
380
+ assign_node(command).public_send(method, *args, command, &block)
273
381
  end
274
382
  end
275
383
 
276
- def send_cluster_command(method, command, args, &block)
277
- case subcommand = ::RedisClient::Cluster::NormalizedCmdName.instance.get_by_subcommand(command)
278
- when 'addslots', 'delslots', 'failover', 'forget', 'meet', 'replicate',
279
- 'reset', 'set-config-epoch', 'setslot'
280
- raise ::RedisClient::Cluster::OrchestrationCommandNotSupported, ['cluster', subcommand]
281
- when 'saveconfig' then @node.call_all(method, command, args).first.then(&TSF.call(block))
282
- when 'getkeysinslot'
384
+ def send_cluster_command(method, command, args, &block) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
385
+ if command[1].casecmp('addslots').zero?
386
+ fail_not_supported_command(method, command, args, &block)
387
+ elsif command[1].casecmp('delslots').zero?
388
+ fail_not_supported_command(method, command, args, &block)
389
+ elsif command[1].casecmp('failover').zero?
390
+ fail_not_supported_command(method, command, args, &block)
391
+ elsif command[1].casecmp('forget').zero?
392
+ fail_not_supported_command(method, command, args, &block)
393
+ elsif command[1].casecmp('meet').zero?
394
+ fail_not_supported_command(method, command, args, &block)
395
+ elsif command[1].casecmp('replicate').zero?
396
+ fail_not_supported_command(method, command, args, &block)
397
+ elsif command[1].casecmp('reset').zero?
398
+ fail_not_supported_command(method, command, args, &block)
399
+ elsif command[1].casecmp('set-config-epoch').zero?
400
+ fail_not_supported_command(method, command, args, &block)
401
+ elsif command[1].casecmp('setslot').zero?
402
+ fail_not_supported_command(method, command, args, &block)
403
+ elsif command[1].casecmp('saveconfig').zero?
404
+ @node.call_all(method, command, args).first.then(&TSF.call(block))
405
+ elsif command[1].casecmp('getkeysinslot').zero?
283
406
  raise ArgumentError, command.join(' ') if command.size != 4
284
407
 
285
- find_node(@node.find_node_key_of_replica(command[2])).public_send(method, *args, command, &block)
286
- else assign_node(command).public_send(method, *args, command, &block)
408
+ handle_node_reload_error do
409
+ node_key = @node.find_node_key_of_replica(command[2])
410
+ @node.find_by(node_key).public_send(method, *args, command, &block)
411
+ end
412
+ else
413
+ assign_node(command).public_send(method, *args, command, &block)
287
414
  end
288
415
  end
289
416
 
290
- def send_script_command(method, command, args, &block) # rubocop:disable Metrics/AbcSize
291
- case ::RedisClient::Cluster::NormalizedCmdName.instance.get_by_subcommand(command)
292
- when 'debug', 'kill'
417
+ def send_script_command(method, command, args, &block) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
418
+ if command[1].casecmp('debug').zero?
419
+ @node.call_all(method, command, args).first.then(&TSF.call(block))
420
+ elsif command[1].casecmp('kill').zero?
293
421
  @node.call_all(method, command, args).first.then(&TSF.call(block))
294
- when 'flush', 'load'
422
+ elsif command[1].casecmp('flush').zero?
295
423
  @node.call_primaries(method, command, args).first.then(&TSF.call(block))
296
- when 'exists'
424
+ elsif command[1].casecmp('load').zero?
425
+ @node.call_primaries(method, command, args).first.then(&TSF.call(block))
426
+ elsif command[1].casecmp('exists').zero?
297
427
  @node.call_all(method, command, args).transpose.map { |arr| arr.any?(&:zero?) ? 0 : 1 }.then(&TSF.call(block))
298
- else assign_node(command).public_send(method, *args, command, &block)
428
+ else
429
+ assign_node(command).public_send(method, *args, command, &block)
299
430
  end
300
431
  end
301
432
 
302
433
  def send_pubsub_command(method, command, args, &block) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
303
- case ::RedisClient::Cluster::NormalizedCmdName.instance.get_by_subcommand(command)
304
- when 'channels'
434
+ if command[1].casecmp('channels').zero?
305
435
  @node.call_all(method, command, args).flatten.uniq.sort_by(&:to_s).then(&TSF.call(block))
306
- when 'shardchannels'
436
+ elsif command[1].casecmp('shardchannels').zero?
307
437
  @node.call_replicas(method, command, args).flatten.uniq.sort_by(&:to_s).then(&TSF.call(block))
308
- when 'numpat'
438
+ elsif command[1].casecmp('numpat').zero?
309
439
  @node.call_all(method, command, args).select { |e| e.is_a?(Integer) }.sum.then(&TSF.call(block))
310
- when 'numsub'
440
+ elsif command[1].casecmp('numsub').zero?
311
441
  @node.call_all(method, command, args).reject(&:empty?).map { |e| Hash[*e] }
312
442
  .reduce({}) { |a, e| a.merge(e) { |_, v1, v2| v1 + v2 } }.then(&TSF.call(block))
313
- when 'shardnumsub'
443
+ elsif command[1].casecmp('shardnumsub').zero?
314
444
  @node.call_replicas(method, command, args).reject(&:empty?).map { |e| Hash[*e] }
315
445
  .reduce({}) { |a, e| a.merge(e) { |_, v1, v2| v1 + v2 } }.then(&TSF.call(block))
316
- else assign_node(command).public_send(method, *args, command, &block)
446
+ else
447
+ assign_node(command).public_send(method, *args, command, &block)
317
448
  end
318
449
  end
319
450
 
320
- def send_watch_command(command)
321
- raise ::RedisClient::Cluster::Transaction::ConsistencyError, 'A block required. And you need to use the block argument as a client for the transaction.' unless block_given?
451
+ def send_watch_command(_method, command, _args, &_block)
452
+ unless block_given?
453
+ msg = 'A block required. And you need to use the block argument as a client for the transaction.'
454
+ raise ::RedisClient::Cluster::Transaction::ConsistencyError.new(msg).with_config(@config)
455
+ end
322
456
 
323
457
  ::RedisClient::Cluster::OptimisticLocking.new(self).watch(command[1..]) do |c, slot, asking|
324
458
  transaction = ::RedisClient::Cluster::Transaction.new(
@@ -329,17 +463,23 @@ class RedisClient
329
463
  end
330
464
  end
331
465
 
332
- MULTIPLE_KEYS_COMMAND_TO_SINGLE = {
333
- 'mget' => ['get', 1].freeze,
334
- 'mset' => ['set', 2].freeze,
335
- 'del' => ['del', 1].freeze
336
- }.freeze
337
-
338
- def send_multiple_keys_command(cmd, method, command, args, &block) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
466
+ def send_multiple_keys_command(method, command, args, &block) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
339
467
  # This implementation is prioritized performance rather than readability or so.
340
- single_key_cmd, keys_step = MULTIPLE_KEYS_COMMAND_TO_SINGLE.fetch(cmd)
468
+ cmd = command.first
469
+ if cmd.casecmp('mget').zero?
470
+ single_key_cmd = 'get'
471
+ keys_step = 1
472
+ elsif cmd.casecmp('mset').zero?
473
+ single_key_cmd = 'set'
474
+ keys_step = 2
475
+ elsif cmd.casecmp('del').zero?
476
+ single_key_cmd = 'del'
477
+ keys_step = 1
478
+ else
479
+ raise NotImplementedError, cmd
480
+ end
341
481
 
342
- return try_send(assign_node(command), method, command, args, &block) if command.size <= keys_step + 1 || ::RedisClient::Cluster::KeySlotConverter.hash_tag_included?(command[1])
482
+ return assign_node_and_send_command(method, command, args, &block) if command.size <= keys_step + 1 || ::RedisClient::Cluster::KeySlotConverter.hash_tag_included?(command[1])
343
483
 
344
484
  seed = @config.use_replica? && @config.replica_affinity == :random ? nil : Random.new_seed
345
485
  pipeline = ::RedisClient::Cluster::Pipeline.new(self, @command_builder, @concurrent_worker, exception: true, seed: seed)
@@ -359,16 +499,24 @@ class RedisClient
359
499
  end
360
500
 
361
501
  replies = pipeline.execute
362
- result = case cmd
363
- when 'mset' then replies.first
364
- when 'del' then replies.sum
365
- else replies
502
+ result = if cmd.casecmp('mset').zero?
503
+ replies.first
504
+ elsif cmd.casecmp('del').zero?
505
+ replies.sum
506
+ else
507
+ replies
366
508
  end
367
509
  block_given? ? yield(result) : result
368
510
  end
369
511
 
370
- def update_cluster_info!
371
- @node.reload!
512
+ def handle_node_reload_error(retry_count: 1)
513
+ yield
514
+ rescue ::RedisClient::Cluster::Node::ReloadNeeded
515
+ raise ::RedisClient::Cluster::NodeMightBeDown.new.with_config(@config) if retry_count <= 0
516
+
517
+ retry_count -= 1
518
+ renew_cluster_state
519
+ retry
372
520
  end
373
521
  end
374
522
  end