redis-cluster-client 0.12.1 → 0.13.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5d90c095f5058e808331eaa9219fb52b9d4a27de639a9c54d194c68ecb253611
4
- data.tar.gz: 8cd8092db87c7cfa42e00c6cabd30747bdea74130d8eb1e19b971b95637665fa
3
+ metadata.gz: 2bc3348020af7012ab193ef18128f2cd37028c9225f1e7fa9555d3609ff92362
4
+ data.tar.gz: 3c08890f1c50f297f0d60f0503bd268742bbcd242a32ac73df733496a013660e
5
5
  SHA512:
6
- metadata.gz: 4821b78b0d5766566fa3943f65271908d3e52e3f82269bd2ea0a789e571df7818b5e0275c88d8833eb7b449c7c0eb3ed672034c1caf03fac35528114f31adb73
7
- data.tar.gz: a4bb1d4a766cd742645c61342c2822e2bd337c792fb67b9793bfbb06ec0a889185eaa8b3ed34f96591f9e57997ecfe83cc901ce65451577b7e8f13d7d23f64b3
6
+ metadata.gz: 8a4036ef8be3c29c747abc7deb59698fbf5d5e581ab9913fd3e06d530a9bae7eba13a397929bb9e3eaf611d5ee01960378ffef54bc6f69706aaf672bc9f38f89
7
+ data.tar.gz: '08897fd0c13bb175399b9118783e812f20555e0e09d021cf24fab236168d8ffd5a3ad4599f2ba1db56ccebaf501f9319c605940069c963f54b2268e1acad5dae'
@@ -3,7 +3,6 @@
3
3
  require 'redis_client'
4
4
  require 'redis_client/cluster/errors'
5
5
  require 'redis_client/cluster/key_slot_converter'
6
- require 'redis_client/cluster/normalized_cmd_name'
7
6
 
8
7
  class RedisClient
9
8
  class Cluster
@@ -30,7 +29,7 @@ class RedisClient
30
29
  nodes&.each do |node|
31
30
  regular_timeout = node.read_timeout
32
31
  node.read_timeout = slow_command_timeout > 0.0 ? slow_command_timeout : regular_timeout
33
- reply = node.call('COMMAND')
32
+ reply = node.call('command')
34
33
  node.read_timeout = regular_timeout
35
34
  commands = parse_command_reply(reply)
36
35
  cmd = ::RedisClient::Cluster::Command.new(commands)
@@ -47,14 +46,28 @@ class RedisClient
47
46
 
48
47
  private
49
48
 
50
- def parse_command_reply(rows)
49
+ def parse_command_reply(rows) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity
51
50
  rows&.each_with_object({}) do |row, acc|
52
- next if row[0].nil?
51
+ next if row.first.nil?
53
52
 
54
- acc[row[0].downcase] = ::RedisClient::Cluster::Command::Detail.new(
55
- first_key_position: row[3],
53
+ # TODO: in redis 7.0 or later, subcommand information included in the command reply
54
+
55
+ pos = case row.first
56
+ when 'eval', 'evalsha', 'zinterstore', 'zunionstore' then 3
57
+ when 'object', 'xgroup' then 2
58
+ when 'migrate', 'xread', 'xreadgroup' then 0
59
+ else row[3]
60
+ end
61
+
62
+ writable = case row.first
63
+ when 'xgroup' then true
64
+ else row[2].include?('write')
65
+ end
66
+
67
+ acc[row.first] = ::RedisClient::Cluster::Command::Detail.new(
68
+ first_key_position: pos,
56
69
  key_step: row[5],
57
- write?: row[2].include?('write'),
70
+ write?: writable,
58
71
  readonly?: row[2].include?('readonly')
59
72
  )
60
73
  end.freeze || EMPTY_HASH
@@ -73,39 +86,47 @@ class RedisClient
73
86
  end
74
87
 
75
88
  def should_send_to_primary?(command)
76
- name = ::RedisClient::Cluster::NormalizedCmdName.instance.get_by_command(command)
77
- @commands[name]&.write?
89
+ find_command_info(command.first)&.write?
78
90
  end
79
91
 
80
92
  def should_send_to_replica?(command)
81
- name = ::RedisClient::Cluster::NormalizedCmdName.instance.get_by_command(command)
82
- @commands[name]&.readonly?
93
+ find_command_info(command.first)&.readonly?
83
94
  end
84
95
 
85
96
  def exists?(name)
86
- @commands.key?(::RedisClient::Cluster::NormalizedCmdName.instance.get_by_name(name))
97
+ @commands.key?(name) || @commands.key?(name.to_s.downcase(:ascii))
87
98
  end
88
99
 
89
100
  private
90
101
 
91
- def determine_first_key_position(command) # rubocop:disable Metrics/CyclomaticComplexity
92
- case name = ::RedisClient::Cluster::NormalizedCmdName.instance.get_by_command(command)
93
- when 'eval', 'evalsha', 'zinterstore', 'zunionstore' then 3
94
- when 'object' then 2
95
- when 'memory'
96
- command[1].to_s.casecmp('usage').zero? ? 2 : 0
97
- when 'migrate'
98
- command[3].empty? ? determine_optional_key_position(command, 'keys') : 3
99
- when 'xread', 'xreadgroup'
102
+ def find_command_info(name)
103
+ @commands[name] || @commands[name.to_s.downcase(:ascii)]
104
+ end
105
+
106
+ def determine_first_key_position(command) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/AbcSize, Metrics/PerceivedComplexity
107
+ i = find_command_info(command.first)&.first_key_position.to_i
108
+ return i if i > 0
109
+
110
+ cmd_name = command.first
111
+ if cmd_name.casecmp('xread').zero?
112
+ determine_optional_key_position(command, 'streams')
113
+ elsif cmd_name.casecmp('xreadgroup').zero?
100
114
  determine_optional_key_position(command, 'streams')
115
+ elsif cmd_name.casecmp('migrate').zero?
116
+ command[3].empty? ? determine_optional_key_position(command, 'keys') : 3
117
+ elsif cmd_name.casecmp('memory').zero?
118
+ command[1].to_s.casecmp('usage').zero? ? 2 : 0
101
119
  else
102
- @commands[name]&.first_key_position.to_i
120
+ i
103
121
  end
104
122
  end
105
123
 
106
124
  def determine_optional_key_position(command, option_name)
107
- idx = command.map { |e| e.to_s.downcase }.index(option_name&.downcase)
108
- idx.nil? ? 0 : idx + 1
125
+ command.each_with_index do |e, i|
126
+ return i + 1 if e.to_s.downcase(:ascii) == option_name
127
+ end
128
+
129
+ 0
109
130
  end
110
131
  end
111
132
  end
@@ -5,6 +5,8 @@ class RedisClient
5
5
  module ConcurrentWorker
6
6
  class OnDemand
7
7
  def initialize(size:)
8
+ raise ArgumentError, "size must be positive: #{size}" unless size.positive?
9
+
8
10
  @q = SizedQueue.new(size)
9
11
  end
10
12
 
@@ -11,6 +11,8 @@ class RedisClient
11
11
  # So it consumes memory 1 MB multiplied a number of workers.
12
12
  class Pooled
13
13
  def initialize(size:)
14
+ raise ArgumentError, "size must be positive: #{size}" unless size.positive?
15
+
14
16
  @size = size
15
17
  setup
16
18
  end
@@ -71,14 +71,12 @@ class RedisClient
71
71
 
72
72
  module_function
73
73
 
74
- def create(model: :on_demand, size: 5)
75
- size = size.positive? ? size : 5
76
-
74
+ def create(model: :none, size: 5)
77
75
  case model
78
- when :on_demand, nil then ::RedisClient::Cluster::ConcurrentWorker::OnDemand.new(size: size)
79
- when :pooled then ::RedisClient::Cluster::ConcurrentWorker::Pooled.new(size: size)
80
76
  when :none then ::RedisClient::Cluster::ConcurrentWorker::None.new
81
- else raise ArgumentError, "Unknown model: #{model}"
77
+ when :on_demand then ::RedisClient::Cluster::ConcurrentWorker::OnDemand.new(size: size)
78
+ when :pooled then ::RedisClient::Cluster::ConcurrentWorker::Pooled.new(size: size)
79
+ else raise ArgumentError, "unknown model: #{model}"
82
80
  end
83
81
  end
84
82
  end
@@ -43,11 +43,16 @@ class RedisClient
43
43
  if !errors.is_a?(Hash) || errors.empty?
44
44
  new(errors.to_s).with_errors(EMPTY_HASH)
45
45
  else
46
- messages = errors.map { |node_key, error| "#{node_key}: (#{error.class}) #{error.message}" }
46
+ messages = errors.map { |node_key, error| "#{node_key}: (#{error.class}) #{error.message}" }.freeze
47
47
  new(messages.join(', ')).with_errors(errors)
48
48
  end
49
49
  end
50
50
 
51
+ def initialize(error_message = nil)
52
+ @errors = nil
53
+ super
54
+ end
55
+
51
56
  def with_errors(errors)
52
57
  @errors = errors if @errors.nil?
53
58
  self
@@ -61,7 +66,7 @@ class RedisClient
61
66
  end
62
67
 
63
68
  class NodeMightBeDown < Error
64
- def initialize(_ = '')
69
+ def initialize(_error_message = nil)
65
70
  super(
66
71
  'The client is trying to fetch the latest cluster state ' \
67
72
  'because a subset of nodes might be down. ' \
@@ -47,7 +47,7 @@ class RedisClient
47
47
  min = DUMMY_LATENCY_MSEC
48
48
  MEASURE_ATTEMPT_COUNT.times do
49
49
  starting = obtain_current_time
50
- cli.call_once('PING')
50
+ cli.call_once('ping')
51
51
  duration = obtain_current_time - starting
52
52
  min = duration if duration < min
53
53
  end
@@ -7,6 +7,7 @@ require 'redis_client/cluster/node/primary_only'
7
7
  require 'redis_client/cluster/node/random_replica'
8
8
  require 'redis_client/cluster/node/random_replica_or_primary'
9
9
  require 'redis_client/cluster/node/latency_replica'
10
+ require 'redis_client/cluster/node_key'
10
11
 
11
12
  class RedisClient
12
13
  class Cluster
@@ -43,6 +44,10 @@ class RedisClient
43
44
  def replica?
44
45
  role == 'slave'
45
46
  end
47
+
48
+ def serialize(str)
49
+ str << id << node_key << role << primary_id << config_epoch
50
+ end
46
51
  end
47
52
 
48
53
  class CharArray
@@ -90,7 +95,7 @@ class RedisClient
90
95
 
91
96
  def build_connection_prelude
92
97
  prelude = super.dup
93
- prelude << ['READONLY'] if @scale_read
98
+ prelude << ['readonly'] if @scale_read
94
99
  prelude.freeze
95
100
  end
96
101
  end
@@ -309,7 +314,7 @@ class RedisClient
309
314
  work_group.push(i, raw_client) do |client|
310
315
  regular_timeout = client.read_timeout
311
316
  client.read_timeout = @config.slow_command_timeout > 0.0 ? @config.slow_command_timeout : regular_timeout
312
- reply = client.call_once('CLUSTER', 'NODES')
317
+ reply = client.call_once('cluster', 'nodes')
313
318
  client.read_timeout = regular_timeout
314
319
  parse_cluster_node_reply(reply)
315
320
  rescue StandardError => e
@@ -338,9 +343,7 @@ class RedisClient
338
343
 
339
344
  grouped = node_info_list.compact.group_by do |info_list|
340
345
  info_list.sort_by!(&:id)
341
- info_list.each_with_object(String.new(capacity: 128 * info_list.size)) do |e, a|
342
- a << e.id << e.node_key << e.role << e.primary_id << e.config_epoch
343
- end
346
+ info_list.each_with_object(String.new(capacity: 128 * info_list.size)) { |e, a| e.serialize(a) }
344
347
  end
345
348
 
346
349
  grouped.max_by { |_, v| v.size }[1].first
@@ -375,6 +378,48 @@ class RedisClient
375
378
  end
376
379
  end
377
380
 
381
+ def parse_cluster_slots_reply(reply) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
382
+ reply.group_by { |e| e[2][2] }.each_with_object([]) do |(primary_id, group), acc|
383
+ slots = group.map { |e| e[0, 2] }.freeze
384
+
385
+ group.first[2..].each do |arr|
386
+ ip = arr[0]
387
+ next if ip.nil? || ip.empty? || ip == '?'
388
+
389
+ id = arr[2]
390
+ role = id == primary_id ? 'master' : 'slave'
391
+ acc << ::RedisClient::Cluster::Node::Info.new(
392
+ id: id,
393
+ node_key: NodeKey.build_from_host_port(ip, arr[1]),
394
+ role: role,
395
+ primary_id: role == 'master' ? nil : primary_id,
396
+ slots: role == 'master' ? slots : EMPTY_ARRAY
397
+ )
398
+ end
399
+ end.freeze
400
+ end
401
+
402
+ def parse_cluster_shards_reply(reply) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
403
+ reply.each_with_object([]) do |shard, acc|
404
+ nodes = shard.fetch('nodes')
405
+ primary_id = nodes.find { |n| n.fetch('role') == 'master' }.fetch('id')
406
+
407
+ nodes.each do |node|
408
+ ip = node.fetch('ip')
409
+ next if node.fetch('health') != 'online' || ip.nil? || ip.empty? || ip == '?'
410
+
411
+ role = node.fetch('role')
412
+ acc << ::RedisClient::Cluster::Node::Info.new(
413
+ id: node.fetch('id'),
414
+ node_key: NodeKey.build_from_host_port(ip, node['port'] || node['tls-port']),
415
+ role: role == 'master' ? role : 'slave',
416
+ primary_id: role == 'master' ? nil : primary_id,
417
+ slots: role == 'master' ? shard.fetch('slots').each_slice(2).to_a.freeze : EMPTY_ARRAY
418
+ )
419
+ end
420
+ end.freeze
421
+ end
422
+
378
423
  # As redirection node_key is dependent on `cluster-preferred-endpoint-type` config,
379
424
  # node_key should use hostname if present in CLUSTER NODES output.
380
425
  #
@@ -18,15 +18,15 @@ class RedisClient
18
18
  handle_redirection(slot, retry_count: 1) do |nd|
19
19
  nd.with do |c|
20
20
  c.ensure_connected_cluster_scoped(retryable: false) do
21
- c.call('ASKING') if @asking
22
- c.call('WATCH', *keys)
21
+ c.call('asking') if @asking
22
+ c.call('watch', *keys)
23
23
  begin
24
24
  yield(c, slot, @asking)
25
25
  rescue ::RedisClient::ConnectionError
26
26
  # No need to unwatch on a connection error.
27
27
  raise
28
28
  rescue StandardError
29
- c.call('UNWATCH')
29
+ c.call('unwatch')
30
30
  raise
31
31
  end
32
32
  rescue ::RedisClient::CommandError => e
@@ -283,14 +283,14 @@ class RedisClient
283
283
  args = timeout.nil? ? [] : [timeout]
284
284
 
285
285
  if block.nil?
286
- @router.try_send(node, method, command, args)
286
+ @router.send_command_to_node(node, method, command, args)
287
287
  else
288
- @router.try_send(node, method, command, args, &block)
288
+ @router.send_command_to_node(node, method, command, args, &block)
289
289
  end
290
290
  end
291
291
 
292
292
  def try_asking(node)
293
- node.call('ASKING') == 'OK'
293
+ node.call('asking') == 'OK'
294
294
  rescue StandardError
295
295
  false
296
296
  end
@@ -2,7 +2,6 @@
2
2
 
3
3
  require 'redis_client'
4
4
  require 'redis_client/cluster/errors'
5
- require 'redis_client/cluster/normalized_cmd_name'
6
5
 
7
6
  class RedisClient
8
7
  class Cluster
@@ -108,12 +107,21 @@ class RedisClient
108
107
 
109
108
  private
110
109
 
111
- def _call(command)
112
- case ::RedisClient::Cluster::NormalizedCmdName.instance.get_by_command(command)
113
- when 'subscribe', 'psubscribe', 'ssubscribe' then call_to_single_state(command)
114
- when 'unsubscribe', 'punsubscribe' then call_to_all_states(command)
115
- when 'sunsubscribe' then call_for_sharded_states(command)
116
- else call_to_single_state(command)
110
+ def _call(command) # rubocop:disable Metrics/AbcSize
111
+ if command.first.casecmp('subscribe').zero?
112
+ call_to_single_state(command)
113
+ elsif command.first.casecmp('psubscribe').zero?
114
+ call_to_single_state(command)
115
+ elsif command.first.casecmp('ssubscribe').zero?
116
+ call_to_single_state(command)
117
+ elsif command.first.casecmp('unsubscribe').zero?
118
+ call_to_all_states(command)
119
+ elsif command.first.casecmp('punsubscribe').zero?
120
+ call_to_all_states(command)
121
+ elsif command.first.casecmp('sunsubscribe').zero?
122
+ call_for_sharded_states(command)
123
+ else
124
+ call_to_single_state(command)
117
125
  end
118
126
  end
119
127
 
@@ -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'
@@ -23,6 +22,13 @@ class RedisClient
23
22
 
24
23
  attr_reader :config
25
24
 
25
+ Action = Struct.new(
26
+ 'RedisCommandRoutingAction',
27
+ :method_name,
28
+ :reply_transformer,
29
+ keyword_init: true
30
+ )
31
+
26
32
  def initialize(config, concurrent_worker, pool: nil, **kwargs)
27
33
  @config = config
28
34
  @concurrent_worker = concurrent_worker
@@ -32,42 +38,20 @@ class RedisClient
32
38
  @node.reload!
33
39
  @command = ::RedisClient::Cluster::Command.load(@node.replica_clients.shuffle, slow_command_timeout: config.slow_command_timeout)
34
40
  @command_builder = @config.command_builder
41
+ @dedicated_actions = build_dedicated_actions
35
42
  rescue ::RedisClient::Cluster::InitialSetupError => e
36
43
  e.with_config(config)
37
44
  raise
38
45
  end
39
46
 
40
47
  def send_command(method, command, *args, &block) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
41
- cmd = ::RedisClient::Cluster::NormalizedCmdName.instance.get_by_command(command)
42
- case cmd
43
- when 'ping' then @node.send_ping(method, command, args).first.then(&TSF.call(block))
44
- when 'wait' then send_wait_command(method, command, args, &block)
45
- when 'keys' then @node.call_replicas(method, command, args).flatten.sort_by(&:to_s).then(&TSF.call(block))
46
- when 'dbsize' then @node.call_replicas(method, command, args).select { |e| e.is_a?(Integer) }.sum.then(&TSF.call(block))
47
- when 'scan' then scan(command, seed: 1)
48
- when 'lastsave' then @node.call_all(method, command, args).sort_by(&:to_i).then(&TSF.call(block))
49
- when 'role' then @node.call_all(method, command, args, &block)
50
- when 'config' then send_config_command(method, command, args, &block)
51
- when 'client' then send_client_command(method, command, args, &block)
52
- when 'cluster' then send_cluster_command(method, command, args, &block)
53
- when 'memory' then send_memory_command(method, command, args, &block)
54
- when 'script' then send_script_command(method, command, args, &block)
55
- when 'pubsub' then send_pubsub_command(method, command, args, &block)
56
- when 'watch' then send_watch_command(command, &block)
57
- when 'mset', 'mget', 'del'
58
- send_multiple_keys_command(cmd, method, command, args, &block)
59
- when 'acl', 'auth', 'bgrewriteaof', 'bgsave', 'quit', 'save'
60
- @node.call_all(method, command, args).first.then(&TSF.call(block))
61
- when 'flushall', 'flushdb'
62
- @node.call_primaries(method, command, args).first.then(&TSF.call(block))
63
- when 'readonly', 'readwrite', 'shutdown'
64
- raise ::RedisClient::Cluster::OrchestrationCommandNotSupported.from_command(cmd).with_config(@config)
65
- when 'discard', 'exec', 'multi', 'unwatch'
66
- raise ::RedisClient::Cluster::AmbiguousNodeError.from_command(cmd).with_config(@config)
67
- else
68
- node = assign_node(command)
69
- try_send(node, method, command, args, &block)
70
- end
48
+ return assign_node_and_send_command(method, command, args, &block) unless @dedicated_actions.key?(command.first)
49
+
50
+ action = @dedicated_actions[command.first]
51
+ return send(action.method_name, method, command, args, &block) if action.reply_transformer.nil?
52
+
53
+ reply = send(action.method_name, method, command, args)
54
+ action.reply_transformer.call(reply).then(&TSF.call(block))
71
55
  rescue ::RedisClient::CircuitBreaker::OpenCircuitError
72
56
  raise
73
57
  rescue ::RedisClient::Cluster::Node::ReloadNeeded
@@ -93,7 +77,12 @@ class RedisClient
93
77
  end
94
78
 
95
79
  # @see https://redis.io/docs/reference/cluster-spec/#redirection-and-resharding Redirection and resharding
96
- def try_send(node, method, command, args, retry_count: 3, &block)
80
+ def assign_node_and_send_command(method, command, args, retry_count: 3, &block)
81
+ node = assign_node(command)
82
+ send_command_to_node(node, method, command, args, retry_count: retry_count, &block)
83
+ end
84
+
85
+ def send_command_to_node(node, method, command, args, retry_count: 3, &block)
97
86
  handle_redirection(node, command, retry_count: retry_count) do |on_node|
98
87
  if args.empty?
99
88
  # prevent memory allocation for variable-length args
@@ -118,7 +107,7 @@ class RedisClient
118
107
  elsif e.message.start_with?('ASK')
119
108
  node = assign_asking_node(e.message)
120
109
  if retry_count >= 0
121
- node.call('ASKING')
110
+ node.call('asking')
122
111
  retry
123
112
  end
124
113
  elsif e.message.start_with?('CLUSTERDOWN')
@@ -268,6 +257,81 @@ class RedisClient
268
257
 
269
258
  private
270
259
 
260
+ def build_dedicated_actions # rubocop:disable Metrics/AbcSize
261
+ pick_first = ->(reply) { reply.first } # rubocop:disable Style/SymbolProc
262
+ multiple_key_action = Action.new(method_name: :send_multiple_keys_command)
263
+ all_node_first_action = Action.new(method_name: :send_command_to_all_nodes, reply_transformer: pick_first)
264
+ primary_first_action = Action.new(method_name: :send_command_to_primaries, reply_transformer: pick_first)
265
+ not_supported_action = Action.new(method_name: :fail_not_supported_command)
266
+ keyless_action = Action.new(method_name: :fail_keyless_command)
267
+ actions = {
268
+ 'ping' => Action.new(method_name: :send_ping_command, reply_transformer: pick_first),
269
+ 'wait' => Action.new(method_name: :send_wait_command),
270
+ 'keys' => Action.new(method_name: :send_command_to_replicas, reply_transformer: ->(reply) { reply.flatten.sort_by(&:to_s) }),
271
+ 'dbsize' => Action.new(method_name: :send_command_to_replicas, reply_transformer: ->(reply) { reply.select { |e| e.is_a?(Integer) }.sum }),
272
+ 'scan' => Action.new(method_name: :send_scan_command),
273
+ 'lastsave' => Action.new(method_name: :send_command_to_all_nodes, reply_transformer: ->(reply) { reply.sort_by(&:to_i) }),
274
+ 'role' => Action.new(method_name: :send_command_to_all_nodes),
275
+ 'config' => Action.new(method_name: :send_config_command),
276
+ 'client' => Action.new(method_name: :send_client_command),
277
+ 'cluster' => Action.new(method_name: :send_cluster_command),
278
+ 'memory' => Action.new(method_name: :send_memory_command),
279
+ 'script' => Action.new(method_name: :send_script_command),
280
+ 'pubsub' => Action.new(method_name: :send_pubsub_command),
281
+ 'watch' => Action.new(method_name: :send_watch_command),
282
+ 'mget' => multiple_key_action,
283
+ 'mset' => multiple_key_action,
284
+ 'del' => multiple_key_action,
285
+ 'acl' => all_node_first_action,
286
+ 'auth' => all_node_first_action,
287
+ 'bgrewriteaof' => all_node_first_action,
288
+ 'bgsave' => all_node_first_action,
289
+ 'quit' => all_node_first_action,
290
+ 'save' => all_node_first_action,
291
+ 'flushall' => primary_first_action,
292
+ 'flushdb' => primary_first_action,
293
+ 'readonly' => not_supported_action,
294
+ 'readwrite' => not_supported_action,
295
+ 'shutdown' => not_supported_action,
296
+ 'discard' => keyless_action,
297
+ 'exec' => keyless_action,
298
+ 'multi' => keyless_action,
299
+ 'unwatch' => keyless_action
300
+ }.freeze
301
+ actions.each_with_object({}) do |(k, v), acc|
302
+ acc[k] = v
303
+ acc[k.upcase] = v
304
+ end.freeze
305
+ end
306
+
307
+ def send_command_to_all_nodes(method, command, args, &block)
308
+ @node.call_all(method, command, args, &block)
309
+ end
310
+
311
+ def send_command_to_primaries(method, command, args, &block)
312
+ @node.call_primaries(method, command, args, &block)
313
+ end
314
+
315
+ def send_command_to_replicas(method, command, args, &block)
316
+ @node.call_replicas(method, command, args, &block)
317
+ end
318
+
319
+ def send_ping_command(method, command, args, &block)
320
+ @node.send_ping(method, command, args, &block)
321
+ end
322
+
323
+ def send_scan_command(_method, command, _args, &_block)
324
+ scan(command, seed: 1)
325
+ end
326
+
327
+ def fail_not_supported_command(_method, command, _args, &_block)
328
+ raise ::RedisClient::Cluster::OrchestrationCommandNotSupported.from_command(command.first).with_config(@config)
329
+ end
330
+
331
+ def fail_keyless_command(_method, command, _args, &_block)
332
+ raise ::RedisClient::Cluster::AmbiguousNodeError.from_command(command.first).with_config(@config)
333
+ end
334
+
271
335
  def send_wait_command(method, command, args, retry_count: 1, &block) # rubocop:disable Metrics/AbcSize
272
336
  @node.call_primaries(method, command, args).select { |r| r.is_a?(Integer) }.sum.then(&TSF.call(block))
273
337
  rescue ::RedisClient::Cluster::ErrorCollection => e
@@ -280,79 +344,110 @@ class RedisClient
280
344
  retry
281
345
  end
282
346
 
283
- def send_config_command(method, command, args, &block)
284
- case ::RedisClient::Cluster::NormalizedCmdName.instance.get_by_subcommand(command)
285
- when 'resetstat', 'rewrite', 'set'
347
+ def send_config_command(method, command, args, &block) # rubocop:disable Metrics/AbcSize
348
+ if command[1].casecmp('resetstat').zero?
286
349
  @node.call_all(method, command, args).first.then(&TSF.call(block))
287
- else assign_node(command).public_send(method, *args, command, &block)
350
+ elsif command[1].casecmp('rewrite').zero?
351
+ @node.call_all(method, command, args).first.then(&TSF.call(block))
352
+ elsif command[1].casecmp('set').zero?
353
+ @node.call_all(method, command, args).first.then(&TSF.call(block))
354
+ else
355
+ assign_node(command).public_send(method, *args, command, &block)
288
356
  end
289
357
  end
290
358
 
291
359
  def send_memory_command(method, command, args, &block)
292
- case ::RedisClient::Cluster::NormalizedCmdName.instance.get_by_subcommand(command)
293
- when 'stats' then @node.call_all(method, command, args, &block)
294
- when 'purge' then @node.call_all(method, command, args).first.then(&TSF.call(block))
295
- else assign_node(command).public_send(method, *args, command, &block)
360
+ if command[1].casecmp('stats').zero?
361
+ @node.call_all(method, command, args, &block)
362
+ elsif command[1].casecmp('purge').zero?
363
+ @node.call_all(method, command, args).first.then(&TSF.call(block))
364
+ else
365
+ assign_node(command).public_send(method, *args, command, &block)
296
366
  end
297
367
  end
298
368
 
299
- def send_client_command(method, command, args, &block)
300
- case ::RedisClient::Cluster::NormalizedCmdName.instance.get_by_subcommand(command)
301
- when 'list' then @node.call_all(method, command, args, &block).flatten
302
- when 'pause', 'reply', 'setname'
369
+ def send_client_command(method, command, args, &block) # rubocop:disable Metrics/AbcSize
370
+ if command[1].casecmp('list').zero?
371
+ @node.call_all(method, command, args, &block).flatten
372
+ elsif command[1].casecmp('pause').zero?
303
373
  @node.call_all(method, command, args).first.then(&TSF.call(block))
304
- else assign_node(command).public_send(method, *args, command, &block)
374
+ elsif command[1].casecmp('reply').zero?
375
+ @node.call_all(method, command, args).first.then(&TSF.call(block))
376
+ elsif command[1].casecmp('setname').zero?
377
+ @node.call_all(method, command, args).first.then(&TSF.call(block))
378
+ else
379
+ assign_node(command).public_send(method, *args, command, &block)
305
380
  end
306
381
  end
307
382
 
308
- def send_cluster_command(method, command, args, &block) # rubocop:disable Metrics/AbcSize
309
- case subcommand = ::RedisClient::Cluster::NormalizedCmdName.instance.get_by_subcommand(command)
310
- when 'addslots', 'delslots', 'failover', 'forget', 'meet', 'replicate',
311
- 'reset', 'set-config-epoch', 'setslot'
312
- raise ::RedisClient::Cluster::OrchestrationCommandNotSupported.from_command(['cluster', subcommand]).with_config(@config)
313
- when 'saveconfig' then @node.call_all(method, command, args).first.then(&TSF.call(block))
314
- when 'getkeysinslot'
383
+ def send_cluster_command(method, command, args, &block) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
384
+ if command[1].casecmp('addslots').zero?
385
+ fail_not_supported_command(method, command, args, &block)
386
+ elsif command[1].casecmp('delslots').zero?
387
+ fail_not_supported_command(method, command, args, &block)
388
+ elsif command[1].casecmp('failover').zero?
389
+ fail_not_supported_command(method, command, args, &block)
390
+ elsif command[1].casecmp('forget').zero?
391
+ fail_not_supported_command(method, command, args, &block)
392
+ elsif command[1].casecmp('meet').zero?
393
+ fail_not_supported_command(method, command, args, &block)
394
+ elsif command[1].casecmp('replicate').zero?
395
+ fail_not_supported_command(method, command, args, &block)
396
+ elsif command[1].casecmp('reset').zero?
397
+ fail_not_supported_command(method, command, args, &block)
398
+ elsif command[1].casecmp('set-config-epoch').zero?
399
+ fail_not_supported_command(method, command, args, &block)
400
+ elsif command[1].casecmp('setslot').zero?
401
+ fail_not_supported_command(method, command, args, &block)
402
+ elsif command[1].casecmp('saveconfig').zero?
403
+ @node.call_all(method, command, args).first.then(&TSF.call(block))
404
+ elsif command[1].casecmp('getkeysinslot').zero?
315
405
  raise ArgumentError, command.join(' ') if command.size != 4
316
406
 
317
407
  handle_node_reload_error do
318
408
  node_key = @node.find_node_key_of_replica(command[2])
319
409
  @node.find_by(node_key).public_send(method, *args, command, &block)
320
410
  end
321
- else assign_node(command).public_send(method, *args, command, &block)
411
+ else
412
+ assign_node(command).public_send(method, *args, command, &block)
322
413
  end
323
414
  end
324
415
 
325
- def send_script_command(method, command, args, &block) # rubocop:disable Metrics/AbcSize
326
- case ::RedisClient::Cluster::NormalizedCmdName.instance.get_by_subcommand(command)
327
- when 'debug', 'kill'
416
+ def send_script_command(method, command, args, &block) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
417
+ if command[1].casecmp('debug').zero?
328
418
  @node.call_all(method, command, args).first.then(&TSF.call(block))
329
- when 'flush', 'load'
419
+ elsif command[1].casecmp('kill').zero?
420
+ @node.call_all(method, command, args).first.then(&TSF.call(block))
421
+ elsif command[1].casecmp('flush').zero?
422
+ @node.call_primaries(method, command, args).first.then(&TSF.call(block))
423
+ elsif command[1].casecmp('load').zero?
330
424
  @node.call_primaries(method, command, args).first.then(&TSF.call(block))
331
- when 'exists'
425
+ elsif command[1].casecmp('exists').zero?
332
426
  @node.call_all(method, command, args).transpose.map { |arr| arr.any?(&:zero?) ? 0 : 1 }.then(&TSF.call(block))
333
- else assign_node(command).public_send(method, *args, command, &block)
427
+ else
428
+ assign_node(command).public_send(method, *args, command, &block)
334
429
  end
335
430
  end
336
431
 
337
432
  def send_pubsub_command(method, command, args, &block) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
338
- case ::RedisClient::Cluster::NormalizedCmdName.instance.get_by_subcommand(command)
339
- when 'channels'
433
+ if command[1].casecmp('channels').zero?
340
434
  @node.call_all(method, command, args).flatten.uniq.sort_by(&:to_s).then(&TSF.call(block))
341
- when 'shardchannels'
435
+ elsif command[1].casecmp('shardchannels').zero?
342
436
  @node.call_replicas(method, command, args).flatten.uniq.sort_by(&:to_s).then(&TSF.call(block))
343
- when 'numpat'
437
+ elsif command[1].casecmp('numpat').zero?
344
438
  @node.call_all(method, command, args).select { |e| e.is_a?(Integer) }.sum.then(&TSF.call(block))
345
- when 'numsub'
439
+ elsif command[1].casecmp('numsub').zero?
346
440
  @node.call_all(method, command, args).reject(&:empty?).map { |e| Hash[*e] }
347
441
  .reduce({}) { |a, e| a.merge(e) { |_, v1, v2| v1 + v2 } }.then(&TSF.call(block))
348
- when 'shardnumsub'
442
+ elsif command[1].casecmp('shardnumsub').zero?
349
443
  @node.call_replicas(method, command, args).reject(&:empty?).map { |e| Hash[*e] }
350
444
  .reduce({}) { |a, e| a.merge(e) { |_, v1, v2| v1 + v2 } }.then(&TSF.call(block))
351
- else assign_node(command).public_send(method, *args, command, &block)
445
+ else
446
+ assign_node(command).public_send(method, *args, command, &block)
352
447
  end
353
448
  end
354
449
 
355
- def send_watch_command(command)
450
+ def send_watch_command(_method, command, _args, &_block)
356
451
  unless block_given?
357
452
  msg = 'A block required. And you need to use the block argument as a client for the transaction.'
358
453
  raise ::RedisClient::Cluster::Transaction::ConsistencyError.new(msg).with_config(@config)
@@ -367,22 +462,23 @@ class RedisClient
367
462
  end
368
463
  end
369
464
 
370
- def send_multiple_keys_command(cmd, method, command, args, &block) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
465
+ def send_multiple_keys_command(method, command, args, &block) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
371
466
  # This implementation is prioritized performance rather than readability or so.
372
- case cmd
373
- when 'mget'
467
+ cmd = command.first
468
+ if cmd.casecmp('mget').zero?
374
469
  single_key_cmd = 'get'
375
470
  keys_step = 1
376
- when 'mset'
471
+ elsif cmd.casecmp('mset').zero?
377
472
  single_key_cmd = 'set'
378
473
  keys_step = 2
379
- when 'del'
474
+ elsif cmd.casecmp('del').zero?
380
475
  single_key_cmd = 'del'
381
476
  keys_step = 1
382
- else raise NotImplementedError, cmd
477
+ else
478
+ raise NotImplementedError, cmd
383
479
  end
384
480
 
385
- return try_send(assign_node(command), method, command, args, &block) if command.size <= keys_step + 1 || ::RedisClient::Cluster::KeySlotConverter.hash_tag_included?(command[1])
481
+ return assign_node_and_send_command(method, command, args, &block) if command.size <= keys_step + 1 || ::RedisClient::Cluster::KeySlotConverter.hash_tag_included?(command[1])
386
482
 
387
483
  seed = @config.use_replica? && @config.replica_affinity == :random ? nil : Random.new_seed
388
484
  pipeline = ::RedisClient::Cluster::Pipeline.new(self, @command_builder, @concurrent_worker, exception: true, seed: seed)
@@ -402,10 +498,12 @@ class RedisClient
402
498
  end
403
499
 
404
500
  replies = pipeline.execute
405
- result = case cmd
406
- when 'mset' then replies.first
407
- when 'del' then replies.sum
408
- else replies
501
+ result = if cmd.casecmp('mset').zero?
502
+ replies.first
503
+ elsif cmd.casecmp('del').zero?
504
+ replies.sum
505
+ else
506
+ replies
409
507
  end
410
508
  block_given? ? yield(result) : result
411
509
  end
@@ -93,24 +93,24 @@ class RedisClient
93
93
  end
94
94
 
95
95
  def prepare_tx
96
- @pipeline.call('MULTI')
96
+ @pipeline.call('multi')
97
97
  @pending_commands.each(&:call)
98
98
  @pending_commands.clear
99
99
  end
100
100
 
101
101
  def commit
102
- @pipeline.call('EXEC')
102
+ @pipeline.call('exec')
103
103
  settle
104
104
  end
105
105
 
106
106
  def cancel
107
- @pipeline.call('DISCARD')
107
+ @pipeline.call('discard')
108
108
  settle
109
109
  end
110
110
 
111
111
  def settle
112
112
  # If we needed ASKING on the watch, we need ASKING on the multi as well.
113
- @node.call('ASKING') if @asking
113
+ @node.call('asking') if @asking
114
114
  # Don't handle redirections at this level if we're in a watch (the watcher handles redirections
115
115
  # at the whole-transaction level.)
116
116
  send_transaction(@node, redirect: !!@watching_slot ? 0 : MAX_REDIRECTION)
@@ -189,7 +189,7 @@ class RedisClient
189
189
  end
190
190
 
191
191
  def try_asking(node)
192
- node.call('ASKING') == 'OK'
192
+ node.call('asking') == 'OK'
193
193
  rescue StandardError
194
194
  false
195
195
  end
@@ -64,7 +64,7 @@ class RedisClient
64
64
  def scan(*args, **kwargs, &block)
65
65
  return to_enum(__callee__, *args, **kwargs) unless block_given?
66
66
 
67
- command = @command_builder.generate(['SCAN', ZERO_CURSOR_FOR_SCAN] + args, kwargs)
67
+ command = @command_builder.generate(['scan', ZERO_CURSOR_FOR_SCAN] + args, kwargs)
68
68
  seed = Random.new_seed
69
69
  loop do
70
70
  cursor, keys = router.scan(command, seed: seed)
@@ -77,21 +77,21 @@ class RedisClient
77
77
  def sscan(key, *args, **kwargs, &block)
78
78
  return to_enum(__callee__, key, *args, **kwargs) unless block_given?
79
79
 
80
- command = @command_builder.generate(['SSCAN', key, ZERO_CURSOR_FOR_SCAN] + args, kwargs)
80
+ command = @command_builder.generate(['sscan', key, ZERO_CURSOR_FOR_SCAN] + args, kwargs)
81
81
  router.scan_single_key(command, arity: 1, &block)
82
82
  end
83
83
 
84
84
  def hscan(key, *args, **kwargs, &block)
85
85
  return to_enum(__callee__, key, *args, **kwargs) unless block_given?
86
86
 
87
- command = @command_builder.generate(['HSCAN', key, ZERO_CURSOR_FOR_SCAN] + args, kwargs)
87
+ command = @command_builder.generate(['hscan', key, ZERO_CURSOR_FOR_SCAN] + args, kwargs)
88
88
  router.scan_single_key(command, arity: 2, &block)
89
89
  end
90
90
 
91
91
  def zscan(key, *args, **kwargs, &block)
92
92
  return to_enum(__callee__, key, *args, **kwargs) unless block_given?
93
93
 
94
- command = @command_builder.generate(['ZSCAN', key, ZERO_CURSOR_FOR_SCAN] + args, kwargs)
94
+ command = @command_builder.generate(['zscan', key, ZERO_CURSOR_FOR_SCAN] + args, kwargs)
95
95
  router.scan_single_key(command, arity: 2, &block)
96
96
  end
97
97
 
@@ -152,8 +152,9 @@ class RedisClient
152
152
  end
153
153
 
154
154
  def method_missing(name, *args, **kwargs, &block)
155
- if router.command_exists?(name)
156
- args.unshift(name)
155
+ cmd = name.respond_to?(:name) ? name.name : name.to_s
156
+ if router.command_exists?(cmd)
157
+ args.unshift(cmd)
157
158
  command = @command_builder.generate(args, kwargs)
158
159
  return router.send_command(:call_v, command, &block)
159
160
  end
@@ -19,7 +19,7 @@ class RedisClient
19
19
  VALID_NODES_KEYS = %i[ssl username password host port db].freeze
20
20
  MERGE_CONFIG_KEYS = %i[ssl username password].freeze
21
21
  IGNORE_GENERIC_CONFIG_KEYS = %i[url host port path].freeze
22
- MAX_WORKERS = Integer(ENV.fetch('REDIS_CLIENT_MAX_THREADS', 5))
22
+ MAX_WORKERS = Integer(ENV.fetch('REDIS_CLIENT_MAX_THREADS', -1)) # for backward compatibility
23
23
  # It's used with slow queries of fetching meta data like CLUSTER NODES, COMMAND and so on.
24
24
  SLOW_COMMAND_TIMEOUT = Float(ENV.fetch('REDIS_CLIENT_SLOW_COMMAND_TIMEOUT', -1))
25
25
  # It affects to strike a balance between load and stability in initialization or changed states.
@@ -47,7 +47,6 @@ class RedisClient
47
47
  max_startup_sample: MAX_STARTUP_SAMPLE,
48
48
  **client_config
49
49
  )
50
-
51
50
  @replica = true & replica
52
51
  @replica_affinity = replica_affinity.to_s.to_sym
53
52
  @fixed_hostname = fixed_hostname.to_s
@@ -110,12 +109,16 @@ class RedisClient
110
109
  private
111
110
 
112
111
  def merge_concurrency_option(option)
113
- case option
114
- when Hash
115
- option = option.transform_keys(&:to_sym)
116
- { size: MAX_WORKERS }.merge(option)
117
- else { size: MAX_WORKERS }
112
+ opts = {}
113
+
114
+ if MAX_WORKERS.positive?
115
+ opts[:model] = :on_demand
116
+ opts[:size] = MAX_WORKERS
118
117
  end
118
+
119
+ opts.merge!(option.transform_keys(&:to_sym)) if option.is_a?(Hash)
120
+ opts[:model] = :none if opts.empty?
121
+ opts.freeze
119
122
  end
120
123
 
121
124
  def build_node_configs(addrs)
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.12.1
4
+ version: 0.13.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Taishi Kasuga
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-11-18 00:00:00.000000000 Z
11
+ date: 2024-12-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: redis-client
@@ -49,7 +49,6 @@ files:
49
49
  - lib/redis_client/cluster/node/random_replica_or_primary.rb
50
50
  - lib/redis_client/cluster/node_key.rb
51
51
  - lib/redis_client/cluster/noop_command_builder.rb
52
- - lib/redis_client/cluster/normalized_cmd_name.rb
53
52
  - lib/redis_client/cluster/optimistic_locking.rb
54
53
  - lib/redis_client/cluster/pipeline.rb
55
54
  - lib/redis_client/cluster/pub_sub.rb
@@ -1,71 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'singleton'
4
-
5
- class RedisClient
6
- class Cluster
7
- class NormalizedCmdName
8
- include Singleton
9
-
10
- EMPTY_STRING = ''
11
-
12
- private_constant :EMPTY_STRING
13
-
14
- def initialize
15
- @cache = {}
16
- @mutex = Mutex.new
17
- end
18
-
19
- def get_by_command(command)
20
- get(command, index: 0)
21
- end
22
-
23
- def get_by_subcommand(command)
24
- get(command, index: 1)
25
- end
26
-
27
- def get_by_name(name)
28
- get(name, index: 0)
29
- end
30
-
31
- def clear
32
- @mutex.synchronize { @cache.clear }
33
- true
34
- end
35
-
36
- private
37
-
38
- def get(command, index:)
39
- name = extract_name(command, index: index)
40
- return EMPTY_STRING if name.nil? || name.empty?
41
-
42
- normalize(name)
43
- end
44
-
45
- def extract_name(command, index:)
46
- case command
47
- when String, Symbol then index.zero? ? command : nil
48
- when Array then extract_name_from_array(command, index: index)
49
- end
50
- end
51
-
52
- def extract_name_from_array(command, index:)
53
- return if command.size - 1 < index
54
-
55
- case e = command[index]
56
- when String, Symbol then e
57
- when Array then e[index]
58
- end
59
- end
60
-
61
- def normalize(name)
62
- return @cache[name] || name.to_s.downcase if @cache.key?(name)
63
- return name.to_s.downcase if @mutex.locked?
64
-
65
- str = name.to_s.downcase
66
- @mutex.synchronize { @cache[name] = str }
67
- str
68
- end
69
- end
70
- end
71
- end