redis-cluster-client 0.16.4 → 0.16.6

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: 80b5e1d6525a06ecf8747239269b110528bf9efc99ff3424108033c85cdc011a
4
- data.tar.gz: 35b5aba54bc61e3731509c7ba3959289ddcfb30e40193f365fc26f62f4c7e60d
3
+ metadata.gz: 201f719d07b33c0df8cde529564564e2993495a7817419730b6699047e40c068
4
+ data.tar.gz: 45790a9f41cd3708e8ffc7c2729e767204711c966cf9dd19aed419b7abc9fb5c
5
5
  SHA512:
6
- metadata.gz: ab546dc6dc75d1ec1733c5cd5f29a451881d71db8c21e587be92ab77c2f12f602100468626b742a1d87caa98aa27699906f9aefd7d9cff02512c3be1e355bdd4
7
- data.tar.gz: 741326d16416834940e08c7084019080188d3e6b252c6dafdf3037bb9a40e2f9aafc0dc771fce7ac4edbb50af6d630067fb3646f4e7f1016ab72d7ceafb04940
6
+ metadata.gz: '085310367927882a2e2d6547419ca4edc254ea2296411ed0c8f0f7352b48d98c54a6e005cc4e11ab0b5455810fa9b086dfefa9786373f926de520c7972bae5fe'
7
+ data.tar.gz: 5d3db4f8ecee72af9d44c239bab164c581b4983c21a7eca1063d3724f0f70d1ed5ef73d5b141a43d34a32edc6419210fd839c0d48b61efac18ac88d5306a5a03
@@ -9,15 +9,25 @@ class RedisClient
9
9
  class Command
10
10
  EMPTY_STRING = ''
11
11
  EMPTY_HASH = {}.freeze
12
+ EMPTY_POLICIES = [nil, nil].freeze
13
+ REQUEST_POLICY_PREFIX = 'request_policy:'
14
+ RESPONSE_POLICY_PREFIX = 'response_policy:'
15
+ SUBCOMMAND_DELIMITER = '|'
12
16
 
13
- private_constant :EMPTY_HASH
17
+ private_constant :EMPTY_HASH, :EMPTY_POLICIES, :REQUEST_POLICY_PREFIX,
18
+ :RESPONSE_POLICY_PREFIX, :SUBCOMMAND_DELIMITER
14
19
 
20
+ # @see https://redis.io/docs/latest/commands/command/ The reply of the COMMAND command
21
+ # @see https://redis.io/docs/latest/develop/reference/command-tips/ Command tips
15
22
  Spec = Struct.new(
16
23
  'RedisCommandSpec',
17
24
  :first_key_position,
18
25
  :key_step,
19
26
  :write?,
20
27
  :readonly?,
28
+ :request_policy,
29
+ :response_policy,
30
+ :subcommands,
21
31
  keyword_init: true
22
32
  ) do
23
33
  def extract_first_key(command)
@@ -86,40 +96,92 @@ class RedisClient
86
96
 
87
97
  private
88
98
 
89
- def parse_command_reply(rows) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity
99
+ def parse_command_reply(rows)
90
100
  rows&.each_with_object({}) do |row, acc|
91
101
  next if row.first.nil?
92
102
 
93
- # TODO: in redis 7.0 or later, subcommand information included in the command reply
94
-
95
- pos = case row.first
96
- when 'eval', 'evalsha', 'zinterstore', 'zunionstore' then 3
97
- when 'object', 'xgroup' then 2
98
- when 'migrate', 'xread', 'xreadgroup' then 0
99
- else row[3]
100
- end
101
-
102
- writable = case row.first
103
- when 'xgroup' then true
104
- else row[2].include?('write')
105
- end
106
-
107
- acc[row.first] = ::RedisClient::Cluster::Command::Spec.new(
108
- first_key_position: pos,
109
- key_step: row[5],
110
- write?: writable,
111
- readonly?: row[2].include?('readonly')
112
- )
103
+ acc[row.first] = build_spec(row)
113
104
  end.freeze || EMPTY_HASH
114
105
  end
106
+
107
+ def build_spec(row)
108
+ request_policy, response_policy = parse_tips(row[7])
109
+
110
+ ::RedisClient::Cluster::Command::Spec.new(
111
+ first_key_position: parse_first_key_position(row),
112
+ key_step: row[5],
113
+ write?: parse_writability(row),
114
+ readonly?: row[2].include?('readonly'),
115
+ request_policy: request_policy,
116
+ response_policy: response_policy,
117
+ subcommands: parse_subcommands(row[9])
118
+ ).freeze
119
+ end
120
+
121
+ # The redis 6.2 or earlier doesn't include the information of the subcommands in the reply.
122
+ # These hard-coded positions are the fallback for such old versions.
123
+ def parse_first_key_position(row)
124
+ case row.first
125
+ when 'eval', 'evalsha', 'zinterstore', 'zunionstore' then 3
126
+ when 'object', 'xgroup' then 2
127
+ when 'migrate', 'xread', 'xreadgroup' then 0
128
+ else row[3]
129
+ end
130
+ end
131
+
132
+ def parse_writability(row)
133
+ case row.first
134
+ when 'xgroup' then true
135
+ else row[2].include?('write')
136
+ end
137
+ end
138
+
139
+ # The command tips are available in the redis 7.0 or later.
140
+ def parse_tips(tips)
141
+ return EMPTY_POLICIES if tips.nil? || tips.empty?
142
+
143
+ request_policy = response_policy = nil
144
+
145
+ tips.each do |tip|
146
+ if tip.start_with?(REQUEST_POLICY_PREFIX)
147
+ request_policy = -tip[REQUEST_POLICY_PREFIX.size..]
148
+ elsif tip.start_with?(RESPONSE_POLICY_PREFIX)
149
+ response_policy = -tip[RESPONSE_POLICY_PREFIX.size..]
150
+ end
151
+ end
152
+
153
+ [request_policy, response_policy]
154
+ end
155
+
156
+ # The information of the subcommands is available in the redis 7.0 or later.
157
+ # A container command such as XINFO reports its key positions per subcommand.
158
+ def parse_subcommands(rows)
159
+ return if rows.nil? || rows.empty?
160
+
161
+ rows.each_with_object({}) do |row, acc|
162
+ name = row.first
163
+ next if name.nil?
164
+
165
+ # The server replies with a full name of the subcommand such as `xinfo|stream`.
166
+ i = name.index(SUBCOMMAND_DELIMITER)
167
+ acc[i.nil? ? name : name[(i + 1)..]] = build_spec(row)
168
+ end.freeze
169
+ end
115
170
  end
116
171
 
117
172
  def initialize(commands)
118
173
  @commands = commands || EMPTY_HASH
119
174
  end
120
175
 
121
- def get_spec(name)
122
- @commands[name] || @commands[name.to_s.downcase(:ascii)]
176
+ def get_spec(command) # rubocop:disable Metrics/AbcSize
177
+ name = command.first
178
+ spec = @commands[name] || @commands[name.to_s.downcase(:ascii)]
179
+ return spec if spec.nil? || spec.subcommands.nil?
180
+
181
+ subcommand = command[1]
182
+ return spec if subcommand.nil?
183
+
184
+ spec.subcommands[subcommand] || spec.subcommands[subcommand.to_s.downcase(:ascii)] || spec
123
185
  end
124
186
 
125
187
  def exists?(name)
@@ -53,15 +53,10 @@ class RedisClient
53
53
  end
54
54
 
55
55
  class CharArray
56
- BASE = ''
57
- PADDING = '0'
58
-
59
- private_constant :BASE, :PADDING
60
-
61
56
  def initialize(size, elements)
62
57
  @elements = elements
63
- @string = String.new(BASE, encoding: Encoding::BINARY, capacity: size)
64
- size.times { @string << PADDING }
58
+ @string = String.new('', encoding: Encoding::BINARY, capacity: size)
59
+ size.times { @string << 0xff }
65
60
  end
66
61
 
67
62
  def [](index)
@@ -146,6 +141,16 @@ class RedisClient
146
141
  call_multiple_nodes!(@topology.replica_clients, method, command, args, &block)
147
142
  end
148
143
 
144
+ # for the `response_policy:one_succeeded` command tip
145
+ def call_all_leniently(method, command, args, &block)
146
+ call_multiple_nodes_leniently(@topology.clients, method, command, args, &block)
147
+ end
148
+
149
+ # for the `response_policy:one_succeeded` command tip
150
+ def call_primaries_leniently(method, command, args, &block)
151
+ call_multiple_nodes_leniently(@topology.primary_clients, method, command, args, &block)
152
+ end
153
+
149
154
  def send_ping(method, command, args, &block)
150
155
  result_values, errors = call_multiple_nodes(@topology.clients, method, command, args, &block)
151
156
  return result_values if errors.nil? || errors.empty?
@@ -270,6 +275,13 @@ class RedisClient
270
275
  raise ::RedisClient::Cluster::ErrorCollection.with_errors(errors)
271
276
  end
272
277
 
278
+ def call_multiple_nodes_leniently(clients, method, command, args, &block)
279
+ result_values, errors = call_multiple_nodes(clients, method, command, args, &block)
280
+ return result_values unless result_values.nil? || result_values.empty?
281
+
282
+ raise ::RedisClient::Cluster::ErrorCollection.with_errors(errors)
283
+ end
284
+
273
285
  def try_map(clients, &block) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity
274
286
  return [{}, {}] if clients.empty?
275
287
 
@@ -314,7 +326,6 @@ class RedisClient
314
326
  e
315
327
  ensure
316
328
  client.read_timeout = regular_timeout
317
- client&.close
318
329
  end
319
330
  end
320
331
 
@@ -378,7 +389,7 @@ class RedisClient
378
389
  config_epoch: fields[6],
379
390
  link_state: fields[7],
380
391
  slots: slots
381
- )
392
+ ).freeze
382
393
  end
383
394
  end
384
395
 
@@ -398,7 +409,7 @@ class RedisClient
398
409
  role: role,
399
410
  primary_id: role == 'master' ? EMPTY_STRING : primary_id,
400
411
  slots: role == 'master' ? slots : EMPTY_ARRAY
401
- )
412
+ ).freeze
402
413
  end
403
414
  end
404
415
  end
@@ -409,7 +420,7 @@ class RedisClient
409
420
  shard = shard.each_slice(2).to_h if resp2
410
421
  nodes = shard.fetch('nodes')
411
422
  nodes = nodes.map { |n| n.each_slice(2).to_h } if resp2
412
- primary_id = nodes.find { |n| n.fetch('role') == 'master' }.fetch('id')
423
+ primary_id = nodes.find { |n| n.fetch('role') == 'master' }&.fetch('id') || EMPTY_STRING
413
424
 
414
425
  nodes.each do |node|
415
426
  host = pick_shard_host(node)
@@ -422,7 +433,7 @@ class RedisClient
422
433
  role: role == 'master' ? role : 'slave',
423
434
  primary_id: role == 'master' ? EMPTY_STRING : primary_id,
424
435
  slots: role == 'master' ? shard.fetch('slots').each_slice(2).to_a.freeze : EMPTY_ARRAY
425
- )
436
+ ).freeze
426
437
  end
427
438
  end
428
439
  end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require 'redis_client'
4
4
  require 'redis_client/cluster/transaction'
5
+ require 'redis_client/cluster/error_identification'
5
6
 
6
7
  class RedisClient
7
8
  class Cluster
@@ -54,7 +55,7 @@ class RedisClient
54
55
  rescue ::RedisClient::ConnectionError
55
56
  # Deduct the number of retries that happened _inside_ router#handle_redirection from our remaining
56
57
  # _external_ retries. Always deduct at least one in case handle_redirection raises without trying the block.
57
- retry_count -= times_block_executed == 0 ? 1 : [times_block_executed, 1].min
58
+ retry_count -= times_block_executed > 1 ? times_block_executed : 1
58
59
  raise if retry_count < 0
59
60
 
60
61
  retry
@@ -23,10 +23,6 @@ class RedisClient
23
23
  @outer_indices << index
24
24
  end
25
25
 
26
- def get_inner_index(outer_index)
27
- @outer_indices&.find_index(outer_index)
28
- end
29
-
30
26
  def get_callee_method(inner_index)
31
27
  if @timeouts.is_a?(Array) && !@timeouts[inner_index].nil?
32
28
  :blocking_call_v
@@ -66,6 +62,7 @@ class RedisClient
66
62
  if result.is_a?(::RedisClient::Error)
67
63
  result._set_command(commands[index])
68
64
  result._set_config(config)
65
+ result._set_retry_attempt(@retry_attempt)
69
66
 
70
67
  if result.is_a?(::RedisClient::CommandError) && result.message.start_with?('MOVED', 'ASK')
71
68
  redirection_indices ||= []
@@ -61,17 +61,7 @@ class RedisClient
61
61
  end
62
62
 
63
63
  BUF_SIZE = Integer(ENV.fetch('REDIS_CLIENT_PUBSUB_BUF_SIZE', 1024))
64
- RECOVERY_BASE_INTERVAL = Float(ENV.fetch('REDIS_CLIENT_PUBSUB_RECOVERY_INTERVAL_SEC', 1.0))
65
- RECOVERY_MAX_INTERVAL = Float(ENV.fetch('REDIS_CLIENT_PUBSUB_RECOVERY_MAX_INTERVAL_SEC', 30.0))
66
- RECOVERY_MAX_ATTEMPTS = Integer(ENV.fetch('REDIS_CLIENT_PUBSUB_RECOVERY_MAX_ATTEMPTS', 10))
67
- SUBSCRIBE_COMMANDS = %w[subscribe psubscribe ssubscribe].freeze
68
- UNSUBSCRIBE_TO_SUBSCRIBE = {
69
- 'unsubscribe' => 'subscribe',
70
- 'punsubscribe' => 'psubscribe',
71
- 'sunsubscribe' => 'ssubscribe'
72
- }.freeze
73
- private_constant :BUF_SIZE, :RECOVERY_BASE_INTERVAL, :RECOVERY_MAX_INTERVAL, :RECOVERY_MAX_ATTEMPTS,
74
- :SUBSCRIBE_COMMANDS, :UNSUBSCRIBE_TO_SUBSCRIBE
64
+ private_constant :BUF_SIZE
75
65
 
76
66
  def initialize(router, command_builder)
77
67
  @router = router
@@ -84,14 +74,14 @@ class RedisClient
84
74
  def call(*args, **kwargs)
85
75
  command = @command_builder.generate(args, kwargs)
86
76
  _call(command)
87
- remember(command)
77
+ remember_subscription(command)
88
78
  nil
89
79
  end
90
80
 
91
81
  def call_v(command)
92
82
  command = @command_builder.generate(command)
93
83
  _call(command)
94
- remember(command)
84
+ remember_subscription(command)
95
85
  nil
96
86
  end
97
87
 
@@ -128,35 +118,6 @@ class RedisClient
128
118
 
129
119
  private
130
120
 
131
- def remember(command)
132
- cmd = command.first.to_s.downcase
133
- if SUBSCRIBE_COMMANDS.include?(cmd)
134
- @commands << command
135
- elsif (subscribe_cmd = UNSUBSCRIBE_TO_SUBSCRIBE[cmd])
136
- forget_subscriptions(subscribe_cmd, command[1..])
137
- else
138
- @commands << command
139
- end
140
- end
141
-
142
- def forget_subscriptions(subscribe_cmd, channels)
143
- if channels.nil? || channels.empty?
144
- @commands.reject! { |c| c.first.to_s.casecmp(subscribe_cmd).zero? }
145
- else
146
- @commands.reject! { |c| prune_entry(c, subscribe_cmd, channels) }
147
- end
148
- end
149
-
150
- def prune_entry(entry, subscribe_cmd, channels)
151
- return false unless entry.first.to_s.casecmp(subscribe_cmd).zero?
152
-
153
- remaining = entry[1..] - channels
154
- return true if remaining.empty?
155
-
156
- entry.replace([entry.first, *remaining])
157
- false
158
- end
159
-
160
121
  def _call(command) # rubocop:disable Metrics/AbcSize
161
122
  if command.first.casecmp('subscribe').zero?
162
123
  call_to_single_state(command)
@@ -227,14 +188,44 @@ class RedisClient
227
188
  break
228
189
  rescue ::RedisClient::ConnectionError, ::RedisClient::Cluster::NodeMightBeDown
229
190
  attempt += 1
230
- raise if attempt >= RECOVERY_MAX_ATTEMPTS
191
+ raise if attempt >= 10
231
192
 
232
193
  sleep recovery_interval(attempt)
233
194
  end
234
195
  end
235
196
 
236
197
  def recovery_interval(attempt)
237
- [RECOVERY_BASE_INTERVAL * (2**(attempt - 1)), RECOVERY_MAX_INTERVAL].min
198
+ [1.0 * (2**(attempt - 1)), 30.0].min
199
+ end
200
+
201
+ def remember_subscription(command) # rubocop:disable Metrics/AbcSize
202
+ if command.first.casecmp('subscribe').zero?
203
+ @commands << command
204
+ elsif command.first.casecmp('psubscribe').zero?
205
+ @commands << command
206
+ elsif command.first.casecmp('ssubscribe').zero?
207
+ @commands << command
208
+ elsif command.first.casecmp('unsubscribe').zero?
209
+ forget_subscriptions('subscribe', command[1, command.size])
210
+ elsif command.first.casecmp('punsubscribe').zero?
211
+ forget_subscriptions('psubscribe', command[1, command.size])
212
+ elsif command.first.casecmp('sunsubscribe').zero?
213
+ forget_subscriptions('ssubscribe', command[1, command.size])
214
+ end
215
+ end
216
+
217
+ def forget_subscriptions(subscribe_cmd, channels)
218
+ @commands.map! do |command|
219
+ next command unless command.first.casecmp(subscribe_cmd).zero?
220
+ next if channels.nil? || channels.empty?
221
+
222
+ remaining = command[1, command.size] - channels
223
+ next if remaining.empty?
224
+
225
+ [command.first, *remaining]
226
+ end
227
+
228
+ @commands.compact!
238
229
  end
239
230
  end
240
231
  end
@@ -17,38 +17,39 @@ class RedisClient
17
17
  class Router
18
18
  ZERO_CURSOR_FOR_SCAN = '0'
19
19
  TSF = ->(f, x) { f.nil? ? x : f.call(x) }.curry
20
+ RoutingAction = Struct.new('RedisCommandRoutingAction', :method_name, :reply_transformer, keyword_init: true)
21
+ PICK_FIRST = ->(reply) { reply.first } # rubocop:disable Style/SymbolProc
22
+ FLATTEN_STRINGS = ->(reply) { reply.flatten.sort_by(&:to_s) }
23
+ SUM_NUM = ->(reply) { reply.select { |e| e.is_a?(Integer) }.sum }
24
+ SORT_NUMBERS = ->(reply) { reply.sort_by(&:to_i) }
25
+ if Object.const_defined?(:Ractor, false) && Ractor.respond_to?(:make_shareable)
26
+ Ractor.make_shareable(PICK_FIRST)
27
+ Ractor.make_shareable(FLATTEN_STRINGS)
28
+ Ractor.make_shareable(SUM_NUM)
29
+ Ractor.make_shareable(SORT_NUMBERS)
30
+ end
20
31
  DEDICATED_ACTIONS = lambda do # rubocop:disable Metrics/BlockLength
21
- action = Struct.new('RedisCommandRoutingAction', :method_name, :reply_transformer, keyword_init: true)
22
- pick_first = ->(reply) { reply.first } # rubocop:disable Style/SymbolProc
23
- flatten_strings = ->(reply) { reply.flatten.sort_by(&:to_s) }
24
- sum_num = ->(reply) { reply.select { |e| e.is_a?(Integer) }.sum }
25
- sort_numbers = ->(reply) { reply.sort_by(&:to_i) }
26
- if Object.const_defined?(:Ractor, false) && Ractor.respond_to?(:make_shareable)
27
- Ractor.make_shareable(pick_first)
28
- Ractor.make_shareable(flatten_strings)
29
- Ractor.make_shareable(sum_num)
30
- Ractor.make_shareable(sort_numbers)
31
- end
32
- multiple_key_action = action.new(method_name: :send_multiple_keys_command)
33
- all_node_first_action = action.new(method_name: :send_command_to_all_nodes, reply_transformer: pick_first)
34
- primary_first_action = action.new(method_name: :send_command_to_primaries, reply_transformer: pick_first)
35
- not_supported_action = action.new(method_name: :fail_not_supported_command)
36
- keyless_action = action.new(method_name: :fail_keyless_command)
32
+ multiple_key_action = RoutingAction.new(method_name: :send_multiple_keys_command)
33
+ all_node_first_action = RoutingAction.new(method_name: :send_command_to_all_nodes, reply_transformer: PICK_FIRST)
34
+ primary_first_action = RoutingAction.new(method_name: :send_command_to_primaries, reply_transformer: PICK_FIRST)
35
+ not_supported_action = RoutingAction.new(method_name: :fail_not_supported_command)
36
+ keyless_action = RoutingAction.new(method_name: :fail_keyless_command)
37
+ single_node_action = RoutingAction.new(method_name: :assign_node_and_send_command)
37
38
  {
38
- 'ping' => action.new(method_name: :send_ping_command, reply_transformer: pick_first),
39
- 'wait' => action.new(method_name: :send_wait_command),
40
- 'keys' => action.new(method_name: :send_command_to_replicas, reply_transformer: flatten_strings),
41
- 'dbsize' => action.new(method_name: :send_command_to_replicas, reply_transformer: sum_num),
42
- 'scan' => action.new(method_name: :send_scan_command),
43
- 'lastsave' => action.new(method_name: :send_command_to_all_nodes, reply_transformer: sort_numbers),
44
- 'role' => action.new(method_name: :send_command_to_all_nodes),
45
- 'config' => action.new(method_name: :send_config_command),
46
- 'client' => action.new(method_name: :send_client_command),
47
- 'cluster' => action.new(method_name: :send_cluster_command),
48
- 'memory' => action.new(method_name: :send_memory_command),
49
- 'script' => action.new(method_name: :send_script_command),
50
- 'pubsub' => action.new(method_name: :send_pubsub_command),
51
- 'watch' => action.new(method_name: :send_watch_command),
39
+ 'ping' => RoutingAction.new(method_name: :send_ping_command, reply_transformer: PICK_FIRST),
40
+ 'wait' => RoutingAction.new(method_name: :send_wait_command),
41
+ 'keys' => RoutingAction.new(method_name: :send_command_to_replicas, reply_transformer: FLATTEN_STRINGS),
42
+ 'dbsize' => RoutingAction.new(method_name: :send_command_to_replicas, reply_transformer: SUM_NUM),
43
+ 'scan' => RoutingAction.new(method_name: :send_scan_command),
44
+ 'lastsave' => RoutingAction.new(method_name: :send_command_to_all_nodes, reply_transformer: SORT_NUMBERS),
45
+ 'role' => RoutingAction.new(method_name: :send_command_to_all_nodes),
46
+ 'config' => RoutingAction.new(method_name: :send_config_command),
47
+ 'client' => RoutingAction.new(method_name: :send_client_command),
48
+ 'cluster' => RoutingAction.new(method_name: :send_cluster_command),
49
+ 'memory' => RoutingAction.new(method_name: :send_memory_command),
50
+ 'script' => RoutingAction.new(method_name: :send_script_command),
51
+ 'pubsub' => RoutingAction.new(method_name: :send_pubsub_command),
52
+ 'watch' => RoutingAction.new(method_name: :send_watch_command),
52
53
  'mget' => multiple_key_action,
53
54
  'mset' => multiple_key_action,
54
55
  'del' => multiple_key_action,
@@ -61,6 +62,10 @@ class RedisClient
61
62
  'select' => all_node_first_action,
62
63
  'flushall' => primary_first_action,
63
64
  'flushdb' => primary_first_action,
65
+ # The redis 7.0 tags RANDOMKEY with `request_policy:all_shards` but without any response policy.
66
+ # It was corrected to `response_policy:special` in the redis 7.2.
67
+ # This entry keeps the historical single node routing for the redis 7.0.
68
+ 'randomkey' => single_node_action,
64
69
  'readonly' => not_supported_action,
65
70
  'readwrite' => not_supported_action,
66
71
  'shutdown' => not_supported_action,
@@ -74,7 +79,33 @@ class RedisClient
74
79
  end
75
80
  end.call.freeze
76
81
 
77
- private_constant :ZERO_CURSOR_FOR_SCAN, :TSF, :DEDICATED_ACTIONS
82
+ # The routing which the command tips of the COMMAND command reply instruct.
83
+ # The `multi_shard` and the `special` request policies are out of scope.
84
+ # The `special` response policy is also out of scope because the aggregation is undefined,
85
+ # and the fan-out would change the return value of commands such as INFO.
86
+ # The `agg_min`, `agg_max` and `agg_logical_*` response policies are out of scope too:
87
+ # the only reachable command with them today is WAITAOF, whose reply is an array,
88
+ # and the aggregation of array replies is undefined. Such a command falls back to
89
+ # the single node routing until a command with a settled semantics appears.
90
+ # The entries of the DEDICATED_ACTIONS take precedence over these to keep the existing behavior.
91
+ # @see https://redis.io/docs/latest/develop/reference/command-tips/
92
+ POLICY_ACTIONS = {
93
+ 'all_shards' => {
94
+ nil => RoutingAction.new(method_name: :send_command_to_primaries).freeze,
95
+ 'all_succeeded' => RoutingAction.new(method_name: :send_command_to_primaries, reply_transformer: PICK_FIRST).freeze,
96
+ 'one_succeeded' => RoutingAction.new(method_name: :send_command_to_primaries_leniently, reply_transformer: PICK_FIRST).freeze,
97
+ 'agg_sum' => RoutingAction.new(method_name: :send_command_to_primaries, reply_transformer: SUM_NUM).freeze
98
+ }.freeze,
99
+ 'all_nodes' => {
100
+ nil => RoutingAction.new(method_name: :send_command_to_all_nodes).freeze,
101
+ 'all_succeeded' => RoutingAction.new(method_name: :send_command_to_all_nodes, reply_transformer: PICK_FIRST).freeze,
102
+ 'one_succeeded' => RoutingAction.new(method_name: :send_command_to_all_nodes_leniently, reply_transformer: PICK_FIRST).freeze,
103
+ 'agg_sum' => RoutingAction.new(method_name: :send_command_to_all_nodes, reply_transformer: SUM_NUM).freeze
104
+ }.freeze
105
+ }.freeze
106
+
107
+ private_constant :ZERO_CURSOR_FOR_SCAN, :TSF, :RoutingAction, :PICK_FIRST, :FLATTEN_STRINGS,
108
+ :SUM_NUM, :SORT_NUMBERS, :DEDICATED_ACTIONS, :POLICY_ACTIONS
78
109
 
79
110
  attr_reader :config
80
111
 
@@ -94,7 +125,12 @@ class RedisClient
94
125
 
95
126
  def send_command(method, command, *args, &block) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
96
127
  action = DEDICATED_ACTIONS[command.first]
97
- return assign_node_and_send_command(method, command, args, &block) if action.nil?
128
+ if action.nil?
129
+ cmd_spec = @command.get_spec(command)
130
+ action = find_policy_action(cmd_spec)
131
+ return assign_node_and_send_command(method, command, args, cmd_spec: cmd_spec, &block) if action.nil?
132
+ end
133
+
98
134
  return send(action.method_name, method, command, args, &block) if action.reply_transformer.nil?
99
135
 
100
136
  reply = send(action.method_name, method, command, args)
@@ -124,8 +160,8 @@ class RedisClient
124
160
  end
125
161
 
126
162
  # @see https://redis.io/docs/reference/cluster-spec/#redirection-and-resharding Redirection and resharding
127
- def assign_node_and_send_command(method, command, args, retry_count: 3, &block)
128
- node = assign_node(command)
163
+ def assign_node_and_send_command(method, command, args, retry_count: 3, cmd_spec: nil, &block)
164
+ node = assign_node(command, cmd_spec: cmd_spec)
129
165
  send_command_to_node(node, method, command, args, retry_count: retry_count, &block)
130
166
  end
131
167
 
@@ -167,19 +203,20 @@ class RedisClient
167
203
  raise unless ::RedisClient::Cluster::ErrorIdentification.client_owns_error?(e, node)
168
204
 
169
205
  renew_cluster_state
170
- retry_count -= 1
206
+ raise if command.nil? || command.empty?
171
207
 
172
- if retry_count >= 0
173
- # Find the node to use for this command - if this fails for some reason, though, re-use
174
- # the old node.
175
- begin
176
- node = find_node(find_node_key(command)) if command
177
- rescue StandardError # rubocop:disable Lint/SuppressedException
178
- end
179
- retry
208
+ retry_count -= 1
209
+ raise if retry_count < 0
210
+
211
+ # Find the node to use for this command - if this fails for some reason, though, re-use
212
+ # the old node.
213
+ begin
214
+ node = find_node(find_node_key(command))
215
+ rescue StandardError
216
+ raise e
180
217
  end
181
218
 
182
- raise
219
+ retry
183
220
  end
184
221
 
185
222
  def scan(command, seed: nil) # rubocop:disable Metrics/AbcSize
@@ -217,9 +254,9 @@ class RedisClient
217
254
  end
218
255
  end
219
256
 
220
- def assign_node(command)
257
+ def assign_node(command, cmd_spec: nil)
221
258
  handle_node_reload_error do
222
- node_key = find_node_key(command)
259
+ node_key = find_node_key(command, cmd_spec: cmd_spec)
223
260
  @node.find_by(node_key)
224
261
  end
225
262
  end
@@ -227,7 +264,7 @@ class RedisClient
227
264
  def find_node_key_by_key(key, seed: nil, primary: false)
228
265
  if key && !key.empty?
229
266
  slot = ::RedisClient::Cluster::KeySlotConverter.convert(key)
230
- node_key = primary ? @node.find_node_key_of_primary(slot) : @node.find_node_key_of_replica(slot)
267
+ node_key = primary ? @node.find_node_key_of_primary(slot) : @node.find_node_key_of_replica(slot, seed: seed)
231
268
  if node_key.nil?
232
269
  renew_cluster_state
233
270
  raise ::RedisClient::Cluster::NodeMightBeDown.new.with_config(@config)
@@ -245,8 +282,8 @@ class RedisClient
245
282
  end
246
283
  end
247
284
 
248
- def find_node_key(command, seed: nil)
249
- cmd_spec = @command.get_spec(command.first)
285
+ def find_node_key(command, seed: nil, cmd_spec: nil)
286
+ cmd_spec = @command.get_spec(command) if cmd_spec.nil?
250
287
  find_node_key_by_key(
251
288
  cmd_spec&.extract_first_key(command),
252
289
  seed: seed,
@@ -255,14 +292,14 @@ class RedisClient
255
292
  end
256
293
 
257
294
  def find_primary_node_key(command)
258
- key = @command.get_spec(command.first)&.extract_first_key(command)
295
+ key = @command.get_spec(command)&.extract_first_key(command)
259
296
  return unless key&.size&.> 0
260
297
 
261
298
  find_node_key_by_key(key, primary: true)
262
299
  end
263
300
 
264
301
  def find_slot(command)
265
- find_slot_by_key(@command.get_spec(command.first)&.extract_first_key(command))
302
+ find_slot_by_key(@command.get_spec(command)&.extract_first_key(command))
266
303
  end
267
304
 
268
305
  def find_slot_by_key(key)
@@ -307,6 +344,15 @@ class RedisClient
307
344
 
308
345
  private
309
346
 
347
+ def find_policy_action(cmd_spec)
348
+ return if cmd_spec.nil?
349
+
350
+ request_policy = cmd_spec.request_policy
351
+ return if request_policy.nil?
352
+
353
+ POLICY_ACTIONS.dig(request_policy, cmd_spec.response_policy)
354
+ end
355
+
310
356
  def send_command_to_all_nodes(method, command, args, &block)
311
357
  @node.call_all(method, command, args, &block)
312
358
  end
@@ -315,6 +361,14 @@ class RedisClient
315
361
  @node.call_primaries(method, command, args, &block)
316
362
  end
317
363
 
364
+ def send_command_to_all_nodes_leniently(method, command, args, &block)
365
+ @node.call_all_leniently(method, command, args, &block)
366
+ end
367
+
368
+ def send_command_to_primaries_leniently(method, command, args, &block)
369
+ @node.call_primaries_leniently(method, command, args, &block)
370
+ end
371
+
318
372
  def send_command_to_replicas(method, command, args, &block)
319
373
  @node.call_replicas(method, command, args, &block)
320
374
  end
@@ -171,7 +171,7 @@ class RedisClient
171
171
  send_transaction(node, redirect: redirect - 1)
172
172
  elsif err.message.start_with?('ASK')
173
173
  node = @router.assign_asking_node(err.message)
174
- try_asking(node) ? send_transaction(node, redirect: redirect - 1) : err
174
+ try_asking(node) ? send_transaction(node, redirect: redirect - 1) : raise(err)
175
175
  elsif err.message.start_with?('CLUSTERDOWN')
176
176
  @router.renew_cluster_state if @watching_slot.nil?
177
177
  raise err
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: redis-cluster-client
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.16.4
4
+ version: 0.16.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Taishi Kasuga
@@ -74,7 +74,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
74
74
  - !ruby/object:Gem::Version
75
75
  version: '0'
76
76
  requirements: []
77
- rubygems_version: 4.0.6
77
+ rubygems_version: 4.0.16
78
78
  specification_version: 4
79
79
  summary: Redis cluster-aware client for Ruby
80
80
  test_files: []