redis-cluster-client 0.7.4 → 0.7.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.
@@ -2,6 +2,7 @@
2
2
 
3
3
  require 'redis_client'
4
4
  require 'redis_client/config'
5
+ require 'redis_client/cluster/error_identification'
5
6
  require 'redis_client/cluster/errors'
6
7
  require 'redis_client/cluster/node/primary_only'
7
8
  require 'redis_client/cluster/node/random_replica'
@@ -16,19 +17,16 @@ class RedisClient
16
17
  # It affects to strike a balance between load and stability in initialization or changed states.
17
18
  MAX_STARTUP_SAMPLE = Integer(ENV.fetch('REDIS_CLIENT_MAX_STARTUP_SAMPLE', 3))
18
19
 
19
- # It's used with slow queries of fetching meta data like CLUSTER NODES, COMMAND and so on.
20
- SLOW_COMMAND_TIMEOUT = Float(ENV.fetch('REDIS_CLIENT_SLOW_COMMAND_TIMEOUT', -1))
21
-
22
20
  # less memory consumption, but slow
23
21
  USE_CHAR_ARRAY_SLOT = Integer(ENV.fetch('REDIS_CLIENT_USE_CHAR_ARRAY_SLOT', 1)) == 1
24
22
 
25
23
  SLOT_SIZE = 16_384
26
24
  MIN_SLOT = 0
27
25
  MAX_SLOT = SLOT_SIZE - 1
28
- IGNORE_GENERIC_CONFIG_KEYS = %i[url host port path].freeze
29
26
  DEAD_FLAGS = %w[fail? fail handshake noaddr noflags].freeze
30
27
  ROLE_FLAGS = %w[master slave].freeze
31
28
  EMPTY_ARRAY = [].freeze
29
+ EMPTY_HASH = {}.freeze
32
30
 
33
31
  ReloadNeeded = Class.new(::RedisClient::Error)
34
32
 
@@ -81,9 +79,11 @@ class RedisClient
81
79
  end
82
80
 
83
81
  class Config < ::RedisClient::Config
84
- def initialize(scale_read: false, **kwargs)
82
+ def initialize(scale_read: false, middlewares: nil, **kwargs)
85
83
  @scale_read = scale_read
86
- super(**kwargs)
84
+ middlewares ||= []
85
+ middlewares.unshift ErrorIdentification::Middleware
86
+ super(middlewares: middlewares, **kwargs)
87
87
  end
88
88
 
89
89
  private
@@ -95,119 +95,19 @@ class RedisClient
95
95
  end
96
96
  end
97
97
 
98
- class << self
99
- def load_info(options, concurrent_worker, **kwargs) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
100
- raise ::RedisClient::Cluster::InitialSetupError, [] if options.nil? || options.empty?
101
-
102
- startup_size = options.size > MAX_STARTUP_SAMPLE ? MAX_STARTUP_SAMPLE : options.size
103
- startup_options = options.to_a.sample(startup_size).to_h
104
- startup_nodes = ::RedisClient::Cluster::Node.new(startup_options, concurrent_worker, **kwargs)
105
- work_group = concurrent_worker.new_group(size: startup_size)
106
-
107
- startup_nodes.each_with_index do |raw_client, i|
108
- work_group.push(i, raw_client) do |client|
109
- regular_timeout = client.read_timeout
110
- client.read_timeout = SLOW_COMMAND_TIMEOUT > 0.0 ? SLOW_COMMAND_TIMEOUT : regular_timeout
111
- reply = client.call('CLUSTER', 'NODES')
112
- client.read_timeout = regular_timeout
113
- parse_cluster_node_reply(reply)
114
- rescue StandardError => e
115
- e
116
- ensure
117
- client&.close
118
- end
119
- end
120
-
121
- node_info_list = errors = nil
122
-
123
- work_group.each do |i, v|
124
- case v
125
- when StandardError
126
- errors ||= Array.new(startup_size)
127
- errors[i] = v
128
- else
129
- node_info_list ||= Array.new(startup_size)
130
- node_info_list[i] = v
131
- end
132
- end
133
-
134
- work_group.close
135
-
136
- raise ::RedisClient::Cluster::InitialSetupError, errors if node_info_list.nil?
137
-
138
- grouped = node_info_list.compact.group_by do |info_list|
139
- info_list.sort_by!(&:id)
140
- info_list.each_with_object(String.new(capacity: 128 * info_list.size)) do |e, a|
141
- a << e.id << e.node_key << e.role << e.primary_id << e.config_epoch
142
- end
143
- end
144
-
145
- grouped.max_by { |_, v| v.size }[1].first.freeze
146
- end
147
-
148
- private
149
-
150
- # @see https://redis.io/commands/cluster-nodes/
151
- # @see https://github.com/redis/redis/blob/78960ad57b8a5e6af743d789ed8fd767e37d42b8/src/cluster.c#L4660-L4683
152
- def parse_cluster_node_reply(reply) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
153
- reply.each_line("\n", chomp: true).filter_map do |line|
154
- fields = line.split
155
- flags = fields[2].split(',')
156
- next unless fields[7] == 'connected' && (flags & DEAD_FLAGS).empty?
157
-
158
- slots = if fields[8].nil?
159
- EMPTY_ARRAY
160
- else
161
- fields[8..].reject { |str| str.start_with?('[') }
162
- .map { |str| str.split('-').map { |s| Integer(s) } }
163
- .map { |a| a.size == 1 ? a << a.first : a }
164
- .map(&:sort)
165
- end
166
-
167
- ::RedisClient::Cluster::Node::Info.new(
168
- id: fields[0],
169
- node_key: parse_node_key(fields[1]),
170
- role: (flags & ROLE_FLAGS).first,
171
- primary_id: fields[3],
172
- ping_sent: fields[4],
173
- pong_recv: fields[5],
174
- config_epoch: fields[6],
175
- link_state: fields[7],
176
- slots: slots
177
- )
178
- end
179
- end
180
-
181
- # As redirection node_key is dependent on `cluster-preferred-endpoint-type` config,
182
- # node_key should use hostname if present in CLUSTER NODES output.
183
- #
184
- # See https://redis.io/commands/cluster-nodes/ for details on the output format.
185
- # node_address matches fhe format: <ip:port@cport[,hostname[,auxiliary_field=value]*]>
186
- def parse_node_key(node_address)
187
- ip_chunk, hostname, _auxiliaries = node_address.split(',')
188
- ip_port_string = ip_chunk.split('@').first
189
- return ip_port_string if hostname.nil? || hostname.empty?
190
-
191
- port = ip_port_string.split(':')[1]
192
- "#{hostname}:#{port}"
193
- end
194
- end
195
-
196
98
  def initialize(
197
- options,
198
99
  concurrent_worker,
199
- node_info_list: [],
200
- with_replica: false,
201
- replica_affinity: :random,
100
+ config:,
202
101
  pool: nil,
203
102
  **kwargs
204
103
  )
205
104
 
206
105
  @concurrent_worker = concurrent_worker
207
- @slots = build_slot_node_mappings(node_info_list)
208
- @replications = build_replication_mappings(node_info_list)
209
- klass = make_topology_class(with_replica, replica_affinity)
210
- @topology = klass.new(@replications, options, pool, @concurrent_worker, **kwargs)
106
+ @slots = build_slot_node_mappings(EMPTY_ARRAY)
107
+ @replications = build_replication_mappings(EMPTY_ARRAY)
108
+ klass = make_topology_class(config.use_replica?, config.replica_affinity)
109
+ @topology = klass.new(pool, @concurrent_worker, **kwargs)
110
+ @config = config
211
111
  @mutex = Mutex.new
212
112
  end
213
113
 
@@ -258,6 +158,14 @@ class RedisClient
258
158
  @topology.clients_for_scanning(seed: seed).values.sort_by { |c| "#{c.config.host}-#{c.config.port}" }
259
159
  end
260
160
 
161
+ def clients
162
+ @topology.clients.values
163
+ end
164
+
165
+ def primary_clients
166
+ @topology.primary_clients.values
167
+ end
168
+
261
169
  def replica_clients
262
170
  @topology.replica_clients.values
263
171
  end
@@ -295,6 +203,24 @@ class RedisClient
295
203
  end
296
204
  end
297
205
 
206
+ def reload!
207
+ with_reload_lock do
208
+ with_startup_clients(MAX_STARTUP_SAMPLE) do |startup_clients|
209
+ @node_info = refetch_node_info_list(startup_clients)
210
+ @node_configs = @node_info.to_h do |node_info|
211
+ [node_info.node_key, @config.client_config_for_node(node_info.node_key)]
212
+ end
213
+ @slots = build_slot_node_mappings(@node_info)
214
+ @replications = build_replication_mappings(@node_info)
215
+ @topology.process_topology_update!(@replications, @node_configs)
216
+ end
217
+ end
218
+ end
219
+
220
+ def owns_error?(err)
221
+ any? { |c| ErrorIdentification.client_owns_error?(err, c) }
222
+ end
223
+
298
224
  private
299
225
 
300
226
  def make_topology_class(with_replica, replica_affinity)
@@ -381,6 +307,137 @@ class RedisClient
381
307
 
382
308
  [results, errors]
383
309
  end
310
+
311
+ def refetch_node_info_list(startup_clients) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
312
+ startup_size = startup_clients.size
313
+ work_group = @concurrent_worker.new_group(size: startup_size)
314
+
315
+ startup_clients.each_with_index do |raw_client, i|
316
+ work_group.push(i, raw_client) do |client|
317
+ regular_timeout = client.read_timeout
318
+ client.read_timeout = @config.slow_command_timeout > 0.0 ? @config.slow_command_timeout : regular_timeout
319
+ reply = client.call('CLUSTER', 'NODES')
320
+ client.read_timeout = regular_timeout
321
+ parse_cluster_node_reply(reply)
322
+ rescue StandardError => e
323
+ e
324
+ ensure
325
+ client&.close
326
+ end
327
+ end
328
+
329
+ node_info_list = errors = nil
330
+
331
+ work_group.each do |i, v|
332
+ case v
333
+ when StandardError
334
+ errors ||= Array.new(startup_size)
335
+ errors[i] = v
336
+ else
337
+ node_info_list ||= Array.new(startup_size)
338
+ node_info_list[i] = v
339
+ end
340
+ end
341
+
342
+ work_group.close
343
+
344
+ raise ::RedisClient::Cluster::InitialSetupError, errors if node_info_list.nil?
345
+
346
+ grouped = node_info_list.compact.group_by do |info_list|
347
+ info_list.sort_by!(&:id)
348
+ info_list.each_with_object(String.new(capacity: 128 * info_list.size)) do |e, a|
349
+ a << e.id << e.node_key << e.role << e.primary_id << e.config_epoch
350
+ end
351
+ end
352
+
353
+ grouped.max_by { |_, v| v.size }[1].first
354
+ end
355
+
356
+ def parse_cluster_node_reply(reply) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
357
+ reply.each_line("\n", chomp: true).filter_map do |line|
358
+ fields = line.split
359
+ flags = fields[2].split(',')
360
+ next unless fields[7] == 'connected' && (flags & DEAD_FLAGS).empty?
361
+
362
+ slots = if fields[8].nil?
363
+ EMPTY_ARRAY
364
+ else
365
+ fields[8..].reject { |str| str.start_with?('[') }
366
+ .map { |str| str.split('-').map { |s| Integer(s) } }
367
+ .map { |a| a.size == 1 ? a << a.first : a }
368
+ .map(&:sort)
369
+ end
370
+
371
+ ::RedisClient::Cluster::Node::Info.new(
372
+ id: fields[0],
373
+ node_key: parse_node_key(fields[1]),
374
+ role: (flags & ROLE_FLAGS).first,
375
+ primary_id: fields[3],
376
+ ping_sent: fields[4],
377
+ pong_recv: fields[5],
378
+ config_epoch: fields[6],
379
+ link_state: fields[7],
380
+ slots: slots
381
+ )
382
+ end
383
+ end
384
+
385
+ # As redirection node_key is dependent on `cluster-preferred-endpoint-type` config,
386
+ # node_key should use hostname if present in CLUSTER NODES output.
387
+ #
388
+ # See https://redis.io/commands/cluster-nodes/ for details on the output format.
389
+ # node_address matches fhe format: <ip:port@cport[,hostname[,auxiliary_field=value]*]>
390
+ def parse_node_key(node_address)
391
+ ip_chunk, hostname, _auxiliaries = node_address.split(',')
392
+ ip_port_string = ip_chunk.split('@').first
393
+ return ip_port_string if hostname.nil? || hostname.empty?
394
+
395
+ port = ip_port_string.split(':')[1]
396
+ "#{hostname}:#{port}"
397
+ end
398
+
399
+ def with_startup_clients(count) # rubocop:disable Metrics/AbcSize
400
+ if @config.connect_with_original_config
401
+ # If connect_with_original_config is set, that means we need to build actual client objects
402
+ # and close them, so that we e.g. re-resolve a DNS entry with the cluster nodes in it.
403
+ begin
404
+ # Memoize the startup clients, so we maintain RedisClient's internal circuit breaker configuration
405
+ # if it's set.
406
+ @startup_clients ||= @config.startup_nodes.values.sample(count).map do |node_config|
407
+ ::RedisClient::Cluster::Node::Config.new(**node_config).new_client
408
+ end
409
+ yield @startup_clients
410
+ ensure
411
+ # Close the startup clients when we're done, so we don't maintain pointless open connections to
412
+ # the cluster though
413
+ @startup_clients&.each(&:close)
414
+ end
415
+ else
416
+ # (re-)connect using nodes we already know about.
417
+ # If this is the first time we're connecting to the cluster, we need to seed the topology with the
418
+ # startup clients though.
419
+ @topology.process_topology_update!({}, @config.startup_nodes) if @topology.clients.empty?
420
+ yield @topology.clients.values.sample(count)
421
+ end
422
+ end
423
+
424
+ def with_reload_lock
425
+ # What should happen with concurrent calls #reload? This is a realistic possibility if the cluster goes into
426
+ # a CLUSTERDOWN state, and we're using a pooled backend. Every thread will independently discover this, and
427
+ # call reload!.
428
+ # For now, if a reload is in progress, wait for that to complete, and consider that the same as us having
429
+ # performed the reload.
430
+ # Probably in the future we should add a circuit breaker to #reload itself, and stop trying if the cluster is
431
+ # obviously not working.
432
+ wait_start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
433
+ @mutex.synchronize do
434
+ return if @last_reloaded_at && @last_reloaded_at > wait_start
435
+
436
+ r = yield
437
+ @last_reloaded_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
438
+ r
439
+ end
440
+ end
384
441
  end
385
442
  end
386
443
  end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ class RedisClient
4
+ class Cluster
5
+ class PinningNode
6
+ def initialize(client)
7
+ @client = client
8
+ end
9
+
10
+ def call(*args, **kwargs, &block)
11
+ @client.call(*args, **kwargs, &block)
12
+ end
13
+
14
+ def call_v(args, &block)
15
+ @client.call_v(args, &block)
16
+ end
17
+
18
+ def call_once(*args, **kwargs, &block)
19
+ @client.call_once(*args, **kwargs, &block)
20
+ end
21
+
22
+ def call_once_v(args, &block)
23
+ @client.call_once_v(args, &block)
24
+ end
25
+
26
+ def blocking_call(timeout, *args, **kwargs, &block)
27
+ @client.blocking_call(timeout, *args, **kwargs, &block)
28
+ end
29
+
30
+ def blocking_call_v(timeout, args, &block)
31
+ @client.blocking_call_v(timeout, args, &block)
32
+ end
33
+ end
34
+ end
35
+ end
@@ -50,28 +50,31 @@ class RedisClient
50
50
  end
51
51
 
52
52
  ::RedisClient::ConnectionMixin.module_eval do
53
- def call_pipelined_aware_of_redirection(commands, timeouts) # rubocop:disable Metrics/AbcSize
53
+ def call_pipelined_aware_of_redirection(commands, timeouts) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
54
54
  size = commands.size
55
55
  results = Array.new(commands.size)
56
56
  @pending_reads += size
57
57
  write_multi(commands)
58
58
 
59
59
  redirection_indices = nil
60
+ exception = nil
60
61
  size.times do |index|
61
62
  timeout = timeouts && timeouts[index]
62
63
  result = read(timeout)
63
64
  @pending_reads -= 1
64
- if result.is_a?(CommandError)
65
+ if result.is_a?(::RedisClient::Error)
65
66
  result._set_command(commands[index])
66
- if result.message.start_with?('MOVED', 'ASK')
67
+ if result.is_a?(::RedisClient::CommandError) && result.message.start_with?('MOVED', 'ASK')
67
68
  redirection_indices ||= []
68
69
  redirection_indices << index
70
+ else
71
+ exception ||= result
69
72
  end
70
73
  end
71
-
72
74
  results[index] = result
73
75
  end
74
76
 
77
+ raise exception if exception
75
78
  return results if redirection_indices.nil?
76
79
 
77
80
  err = ::RedisClient::Cluster::Pipeline::RedirectionNeeded.new
@@ -158,15 +161,13 @@ class RedisClient
158
161
  end
159
162
  end
160
163
 
161
- all_replies = errors = nil
164
+ all_replies = errors = required_redirections = nil
162
165
 
163
166
  work_group.each do |node_key, v|
164
167
  case v
165
168
  when ::RedisClient::Cluster::Pipeline::RedirectionNeeded
166
- all_replies ||= Array.new(@size)
167
- pipeline = @pipelines[node_key]
168
- v.indices.each { |i| v.replies[i] = handle_redirection(v.replies[i], pipeline, i) }
169
- pipeline.outer_indices.each_with_index { |outer, inner| all_replies[outer] = v.replies[inner] }
169
+ required_redirections ||= {}
170
+ required_redirections[node_key] = v
170
171
  when StandardError
171
172
  errors ||= {}
172
173
  errors[node_key] = v
@@ -177,9 +178,15 @@ class RedisClient
177
178
  end
178
179
 
179
180
  work_group.close
180
-
181
181
  raise ::RedisClient::Cluster::ErrorCollection, errors unless errors.nil?
182
182
 
183
+ required_redirections&.each do |node_key, v|
184
+ all_replies ||= Array.new(@size)
185
+ pipeline = @pipelines[node_key]
186
+ v.indices.each { |i| v.replies[i] = handle_redirection(v.replies[i], pipeline, i) }
187
+ pipeline.outer_indices.each_with_index { |outer, inner| all_replies[outer] = v.replies[inner] }
188
+ end
189
+
183
190
  all_replies
184
191
  end
185
192
 
@@ -217,21 +224,15 @@ class RedisClient
217
224
 
218
225
  if err.message.start_with?('MOVED')
219
226
  node = @router.assign_redirection_node(err.message)
220
- try_redirection(node, pipeline, inner_index)
227
+ redirect_command(node, pipeline, inner_index)
221
228
  elsif err.message.start_with?('ASK')
222
229
  node = @router.assign_asking_node(err.message)
223
- try_asking(node) ? try_redirection(node, pipeline, inner_index) : err
230
+ try_asking(node) ? redirect_command(node, pipeline, inner_index) : err
224
231
  else
225
232
  err
226
233
  end
227
234
  end
228
235
 
229
- def try_redirection(node, pipeline, inner_index)
230
- redirect_command(node, pipeline, inner_index)
231
- rescue StandardError => e
232
- e
233
- end
234
-
235
236
  def redirect_command(node, pipeline, inner_index)
236
237
  method = pipeline.get_callee_method(inner_index)
237
238
  command = pipeline.get_command(inner_index)
@@ -13,17 +13,18 @@ class RedisClient
13
13
  class Cluster
14
14
  class Router
15
15
  ZERO_CURSOR_FOR_SCAN = '0'
16
- METHODS_FOR_BLOCKING_CMD = %i[blocking_call_v blocking_call].freeze
17
16
  TSF = ->(f, x) { f.nil? ? x : f.call(x) }.curry
18
17
 
19
18
  def initialize(config, concurrent_worker, pool: nil, **kwargs)
20
19
  @config = config.dup
20
+ @original_config = config.dup if config.connect_with_original_config
21
+ @connect_with_original_config = config.connect_with_original_config
21
22
  @concurrent_worker = concurrent_worker
22
23
  @pool = pool
23
24
  @client_kwargs = kwargs
24
- @node = fetch_cluster_info(@config, @concurrent_worker, pool: @pool, **@client_kwargs)
25
- @command = ::RedisClient::Cluster::Command.load(@node.replica_clients.shuffle)
26
- @mutex = Mutex.new
25
+ @node = ::RedisClient::Cluster::Node.new(concurrent_worker, config: config, pool: pool, **kwargs)
26
+ update_cluster_info!
27
+ @command = ::RedisClient::Cluster::Command.load(@node.replica_clients.shuffle, slow_command_timeout: config.slow_command_timeout)
27
28
  @command_builder = @config.command_builder
28
29
  end
29
30
 
@@ -64,77 +65,61 @@ class RedisClient
64
65
  raise if e.errors.any?(::RedisClient::CircuitBreaker::OpenCircuitError)
65
66
 
66
67
  update_cluster_info! if e.errors.values.any? do |err|
67
- err.message.start_with?('CLUSTERDOWN Hash slot not served')
68
+ @node.owns_error?(err) && err.message.start_with?('CLUSTERDOWN Hash slot not served')
68
69
  end
69
70
 
70
71
  raise
71
72
  end
72
73
 
73
74
  # @see https://redis.io/docs/reference/cluster-spec/#redirection-and-resharding Redirection and resharding
74
- def try_send(node, method, command, args, retry_count: 3, &block) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
75
- if args.empty?
76
- # prevent memory allocation for variable-length args
77
- node.public_send(method, command, &block)
78
- else
79
- node.public_send(method, *args, command, &block)
75
+ def try_send(node, method, command, args, retry_count: 3, &block)
76
+ handle_redirection(node, retry_count: retry_count) do |on_node|
77
+ if args.empty?
78
+ # prevent memory allocation for variable-length args
79
+ on_node.public_send(method, command, &block)
80
+ else
81
+ on_node.public_send(method, *args, command, &block)
82
+ end
80
83
  end
81
- rescue ::RedisClient::CircuitBreaker::OpenCircuitError
82
- raise
83
- rescue ::RedisClient::CommandError => e
84
- raise if retry_count <= 0
84
+ end
85
85
 
86
- if e.message.start_with?('MOVED')
87
- node = assign_redirection_node(e.message)
88
- retry_count -= 1
89
- retry
90
- elsif e.message.start_with?('ASK')
91
- node = assign_asking_node(e.message)
92
- node.call('ASKING')
93
- retry_count -= 1
94
- retry
95
- elsif e.message.start_with?('CLUSTERDOWN Hash slot not served')
96
- update_cluster_info!
97
- retry_count -= 1
98
- retry
99
- else
100
- raise
86
+ def try_delegate(node, method, *args, retry_count: 3, **kwargs, &block)
87
+ handle_redirection(node, retry_count: retry_count) do |on_node|
88
+ on_node.public_send(method, *args, **kwargs, &block)
101
89
  end
102
- rescue ::RedisClient::ConnectionError => e
103
- raise if METHODS_FOR_BLOCKING_CMD.include?(method) && e.is_a?(RedisClient::ReadTimeoutError)
104
- raise if retry_count <= 0
105
-
106
- update_cluster_info!
107
- retry_count -= 1
108
- retry
109
90
  end
110
91
 
111
- def try_delegate(node, method, *args, retry_count: 3, **kwargs, &block) # rubocop:disable Metrics/AbcSize
112
- node.public_send(method, *args, **kwargs, &block)
92
+ def handle_redirection(node, retry_count:) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
93
+ yield node
113
94
  rescue ::RedisClient::CircuitBreaker::OpenCircuitError
114
95
  raise
115
96
  rescue ::RedisClient::CommandError => e
116
- raise if retry_count <= 0
97
+ raise unless ErrorIdentification.client_owns_error?(e, node)
117
98
 
118
99
  if e.message.start_with?('MOVED')
119
100
  node = assign_redirection_node(e.message)
120
101
  retry_count -= 1
121
- retry
102
+ retry if retry_count >= 0
122
103
  elsif e.message.start_with?('ASK')
123
104
  node = assign_asking_node(e.message)
124
- node.call('ASKING')
125
105
  retry_count -= 1
126
- retry
106
+ if retry_count >= 0
107
+ node.call('ASKING')
108
+ retry
109
+ end
127
110
  elsif e.message.start_with?('CLUSTERDOWN Hash slot not served')
128
111
  update_cluster_info!
129
112
  retry_count -= 1
130
- retry
131
- else
132
- raise
113
+ retry if retry_count >= 0
133
114
  end
134
- rescue ::RedisClient::ConnectionError
135
- raise if retry_count <= 0
115
+ raise
116
+ rescue ::RedisClient::ConnectionError => e
117
+ raise unless ErrorIdentification.client_owns_error?(e, node)
136
118
 
137
119
  update_cluster_info!
120
+
121
+ raise if retry_count <= 0
122
+
138
123
  retry_count -= 1
139
124
  retry
140
125
  end
@@ -168,21 +153,30 @@ class RedisClient
168
153
  find_node(node_key)
169
154
  end
170
155
 
171
- def find_node_key(command, seed: nil)
172
- key = @command.extract_first_key(command)
173
- slot = key.empty? ? nil : ::RedisClient::Cluster::KeySlotConverter.convert(key)
174
-
175
- if @command.should_send_to_primary?(command)
176
- @node.find_node_key_of_primary(slot) || @node.any_primary_node_key(seed: seed)
156
+ def find_node_key_by_key(key, seed: nil, primary: false)
157
+ if key && !key.empty?
158
+ slot = ::RedisClient::Cluster::KeySlotConverter.convert(key)
159
+ primary ? @node.find_node_key_of_primary(slot) : @node.find_node_key_of_replica(slot)
177
160
  else
178
- @node.find_node_key_of_replica(slot, seed: seed) || @node.any_replica_node_key(seed: seed)
161
+ primary ? @node.any_primary_node_key(seed: seed) : @node.any_replica_node_key(seed: seed)
179
162
  end
180
163
  end
181
164
 
165
+ def find_primary_node_by_slot(slot)
166
+ node_key = @node.find_node_key_of_primary(slot)
167
+ find_node(node_key)
168
+ end
169
+
170
+ def find_node_key(command, seed: nil)
171
+ key = @command.extract_first_key(command)
172
+ find_node_key_by_key(key, seed: seed, primary: @command.should_send_to_primary?(command))
173
+ end
174
+
182
175
  def find_primary_node_key(command)
183
176
  key = @command.extract_first_key(command)
184
- slot = key.empty? ? nil : ::RedisClient::Cluster::KeySlotConverter.convert(key)
185
- @node.find_node_key_of_primary(slot)
177
+ return nil unless key&.size&.> 0
178
+
179
+ find_node_key_by_key(key, primary: true)
186
180
  end
187
181
 
188
182
  def find_node(node_key, retry_count: 3)
@@ -304,33 +298,8 @@ class RedisClient
304
298
  end
305
299
  end
306
300
 
307
- def fetch_cluster_info(config, concurrent_worker, pool: nil, **kwargs)
308
- node_info_list = ::RedisClient::Cluster::Node.load_info(config.per_node_key, concurrent_worker, **kwargs)
309
- node_addrs = node_info_list.map { |i| ::RedisClient::Cluster::NodeKey.hashify(i.node_key) }
310
- config.update_node(node_addrs)
311
- ::RedisClient::Cluster::Node.new(
312
- config.per_node_key,
313
- concurrent_worker,
314
- node_info_list: node_info_list,
315
- pool: pool,
316
- with_replica: config.use_replica?,
317
- replica_affinity: config.replica_affinity,
318
- **kwargs
319
- )
320
- end
321
-
322
301
  def update_cluster_info!
323
- return if @mutex.locked?
324
-
325
- @mutex.synchronize do
326
- begin
327
- @node.each(&:close)
328
- rescue ::RedisClient::Cluster::ErrorCollection
329
- # ignore
330
- end
331
-
332
- @node = fetch_cluster_info(@config, @concurrent_worker, pool: @pool, **@client_kwargs)
333
- end
302
+ @node.reload!
334
303
  end
335
304
  end
336
305
  end