redis-cluster-client 0.7.5 → 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'
@@ -22,10 +23,10 @@ class RedisClient
22
23
  SLOT_SIZE = 16_384
23
24
  MIN_SLOT = 0
24
25
  MAX_SLOT = SLOT_SIZE - 1
25
- IGNORE_GENERIC_CONFIG_KEYS = %i[url host port path].freeze
26
26
  DEAD_FLAGS = %w[fail? fail handshake noaddr noflags].freeze
27
27
  ROLE_FLAGS = %w[master slave].freeze
28
28
  EMPTY_ARRAY = [].freeze
29
+ EMPTY_HASH = {}.freeze
29
30
 
30
31
  ReloadNeeded = Class.new(::RedisClient::Error)
31
32
 
@@ -78,9 +79,11 @@ class RedisClient
78
79
  end
79
80
 
80
81
  class Config < ::RedisClient::Config
81
- def initialize(scale_read: false, **kwargs)
82
+ def initialize(scale_read: false, middlewares: nil, **kwargs)
82
83
  @scale_read = scale_read
83
- super(**kwargs)
84
+ middlewares ||= []
85
+ middlewares.unshift ErrorIdentification::Middleware
86
+ super(middlewares: middlewares, **kwargs)
84
87
  end
85
88
 
86
89
  private
@@ -92,119 +95,19 @@ class RedisClient
92
95
  end
93
96
  end
94
97
 
95
- class << self
96
- def load_info(options, concurrent_worker, slow_command_timeout: -1, **kwargs) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
97
- raise ::RedisClient::Cluster::InitialSetupError, [] if options.nil? || options.empty?
98
-
99
- startup_size = options.size > MAX_STARTUP_SAMPLE ? MAX_STARTUP_SAMPLE : options.size
100
- startup_options = options.to_a.sample(startup_size).to_h
101
- startup_nodes = ::RedisClient::Cluster::Node.new(startup_options, concurrent_worker, **kwargs)
102
- work_group = concurrent_worker.new_group(size: startup_size)
103
-
104
- startup_nodes.each_with_index do |raw_client, i|
105
- work_group.push(i, raw_client) do |client|
106
- regular_timeout = client.read_timeout
107
- client.read_timeout = slow_command_timeout > 0.0 ? slow_command_timeout : regular_timeout
108
- reply = client.call('CLUSTER', 'NODES')
109
- client.read_timeout = regular_timeout
110
- parse_cluster_node_reply(reply)
111
- rescue StandardError => e
112
- e
113
- ensure
114
- client&.close
115
- end
116
- end
117
-
118
- node_info_list = errors = nil
119
-
120
- work_group.each do |i, v|
121
- case v
122
- when StandardError
123
- errors ||= Array.new(startup_size)
124
- errors[i] = v
125
- else
126
- node_info_list ||= Array.new(startup_size)
127
- node_info_list[i] = v
128
- end
129
- end
130
-
131
- work_group.close
132
-
133
- raise ::RedisClient::Cluster::InitialSetupError, errors if node_info_list.nil?
134
-
135
- grouped = node_info_list.compact.group_by do |info_list|
136
- info_list.sort_by!(&:id)
137
- info_list.each_with_object(String.new(capacity: 128 * info_list.size)) do |e, a|
138
- a << e.id << e.node_key << e.role << e.primary_id << e.config_epoch
139
- end
140
- end
141
-
142
- grouped.max_by { |_, v| v.size }[1].first.freeze
143
- end
144
-
145
- private
146
-
147
- # @see https://redis.io/commands/cluster-nodes/
148
- # @see https://github.com/redis/redis/blob/78960ad57b8a5e6af743d789ed8fd767e37d42b8/src/cluster.c#L4660-L4683
149
- def parse_cluster_node_reply(reply) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
150
- reply.each_line("\n", chomp: true).filter_map do |line|
151
- fields = line.split
152
- flags = fields[2].split(',')
153
- next unless fields[7] == 'connected' && (flags & DEAD_FLAGS).empty?
154
-
155
- slots = if fields[8].nil?
156
- EMPTY_ARRAY
157
- else
158
- fields[8..].reject { |str| str.start_with?('[') }
159
- .map { |str| str.split('-').map { |s| Integer(s) } }
160
- .map { |a| a.size == 1 ? a << a.first : a }
161
- .map(&:sort)
162
- end
163
-
164
- ::RedisClient::Cluster::Node::Info.new(
165
- id: fields[0],
166
- node_key: parse_node_key(fields[1]),
167
- role: (flags & ROLE_FLAGS).first,
168
- primary_id: fields[3],
169
- ping_sent: fields[4],
170
- pong_recv: fields[5],
171
- config_epoch: fields[6],
172
- link_state: fields[7],
173
- slots: slots
174
- )
175
- end
176
- end
177
-
178
- # As redirection node_key is dependent on `cluster-preferred-endpoint-type` config,
179
- # node_key should use hostname if present in CLUSTER NODES output.
180
- #
181
- # See https://redis.io/commands/cluster-nodes/ for details on the output format.
182
- # node_address matches fhe format: <ip:port@cport[,hostname[,auxiliary_field=value]*]>
183
- def parse_node_key(node_address)
184
- ip_chunk, hostname, _auxiliaries = node_address.split(',')
185
- ip_port_string = ip_chunk.split('@').first
186
- return ip_port_string if hostname.nil? || hostname.empty?
187
-
188
- port = ip_port_string.split(':')[1]
189
- "#{hostname}:#{port}"
190
- end
191
- end
192
-
193
98
  def initialize(
194
- options,
195
99
  concurrent_worker,
196
- node_info_list: [],
197
- with_replica: false,
198
- replica_affinity: :random,
100
+ config:,
199
101
  pool: nil,
200
102
  **kwargs
201
103
  )
202
104
 
203
105
  @concurrent_worker = concurrent_worker
204
- @slots = build_slot_node_mappings(node_info_list)
205
- @replications = build_replication_mappings(node_info_list)
206
- klass = make_topology_class(with_replica, replica_affinity)
207
- @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
208
111
  @mutex = Mutex.new
209
112
  end
210
113
 
@@ -255,6 +158,14 @@ class RedisClient
255
158
  @topology.clients_for_scanning(seed: seed).values.sort_by { |c| "#{c.config.host}-#{c.config.port}" }
256
159
  end
257
160
 
161
+ def clients
162
+ @topology.clients.values
163
+ end
164
+
165
+ def primary_clients
166
+ @topology.primary_clients.values
167
+ end
168
+
258
169
  def replica_clients
259
170
  @topology.replica_clients.values
260
171
  end
@@ -292,6 +203,24 @@ class RedisClient
292
203
  end
293
204
  end
294
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
+
295
224
  private
296
225
 
297
226
  def make_topology_class(with_replica, replica_affinity)
@@ -378,6 +307,137 @@ class RedisClient
378
307
 
379
308
  [results, errors]
380
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
381
441
  end
382
442
  end
383
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,7 +13,6 @@ 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)
@@ -23,9 +22,9 @@ class RedisClient
23
22
  @concurrent_worker = concurrent_worker
24
23
  @pool = pool
25
24
  @client_kwargs = kwargs
26
- @node = fetch_cluster_info(@config, @concurrent_worker, pool: @pool, **@client_kwargs)
25
+ @node = ::RedisClient::Cluster::Node.new(concurrent_worker, config: config, pool: pool, **kwargs)
26
+ update_cluster_info!
27
27
  @command = ::RedisClient::Cluster::Command.load(@node.replica_clients.shuffle, slow_command_timeout: config.slow_command_timeout)
28
- @mutex = Mutex.new
29
28
  @command_builder = @config.command_builder
30
29
  end
31
30
 
@@ -66,77 +65,61 @@ class RedisClient
66
65
  raise if e.errors.any?(::RedisClient::CircuitBreaker::OpenCircuitError)
67
66
 
68
67
  update_cluster_info! if e.errors.values.any? do |err|
69
- err.message.start_with?('CLUSTERDOWN Hash slot not served')
68
+ @node.owns_error?(err) && err.message.start_with?('CLUSTERDOWN Hash slot not served')
70
69
  end
71
70
 
72
71
  raise
73
72
  end
74
73
 
75
74
  # @see https://redis.io/docs/reference/cluster-spec/#redirection-and-resharding Redirection and resharding
76
- def try_send(node, method, command, args, retry_count: 3, &block) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
77
- if args.empty?
78
- # prevent memory allocation for variable-length args
79
- node.public_send(method, command, &block)
80
- else
81
- 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
82
83
  end
83
- rescue ::RedisClient::CircuitBreaker::OpenCircuitError
84
- raise
85
- rescue ::RedisClient::CommandError => e
86
- raise if retry_count <= 0
84
+ end
87
85
 
88
- if e.message.start_with?('MOVED')
89
- node = assign_redirection_node(e.message)
90
- retry_count -= 1
91
- retry
92
- elsif e.message.start_with?('ASK')
93
- node = assign_asking_node(e.message)
94
- node.call('ASKING')
95
- retry_count -= 1
96
- retry
97
- elsif e.message.start_with?('CLUSTERDOWN Hash slot not served')
98
- update_cluster_info!
99
- retry_count -= 1
100
- retry
101
- else
102
- 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)
103
89
  end
104
- rescue ::RedisClient::ConnectionError => e
105
- raise if METHODS_FOR_BLOCKING_CMD.include?(method) && e.is_a?(RedisClient::ReadTimeoutError)
106
- raise if retry_count <= 0
107
-
108
- update_cluster_info!
109
- retry_count -= 1
110
- retry
111
90
  end
112
91
 
113
- def try_delegate(node, method, *args, retry_count: 3, **kwargs, &block) # rubocop:disable Metrics/AbcSize
114
- 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
115
94
  rescue ::RedisClient::CircuitBreaker::OpenCircuitError
116
95
  raise
117
96
  rescue ::RedisClient::CommandError => e
118
- raise if retry_count <= 0
97
+ raise unless ErrorIdentification.client_owns_error?(e, node)
119
98
 
120
99
  if e.message.start_with?('MOVED')
121
100
  node = assign_redirection_node(e.message)
122
101
  retry_count -= 1
123
- retry
102
+ retry if retry_count >= 0
124
103
  elsif e.message.start_with?('ASK')
125
104
  node = assign_asking_node(e.message)
126
- node.call('ASKING')
127
105
  retry_count -= 1
128
- retry
106
+ if retry_count >= 0
107
+ node.call('ASKING')
108
+ retry
109
+ end
129
110
  elsif e.message.start_with?('CLUSTERDOWN Hash slot not served')
130
111
  update_cluster_info!
131
112
  retry_count -= 1
132
- retry
133
- else
134
- raise
113
+ retry if retry_count >= 0
135
114
  end
136
- rescue ::RedisClient::ConnectionError
137
- raise if retry_count <= 0
115
+ raise
116
+ rescue ::RedisClient::ConnectionError => e
117
+ raise unless ErrorIdentification.client_owns_error?(e, node)
138
118
 
139
119
  update_cluster_info!
120
+
121
+ raise if retry_count <= 0
122
+
140
123
  retry_count -= 1
141
124
  retry
142
125
  end
@@ -170,21 +153,30 @@ class RedisClient
170
153
  find_node(node_key)
171
154
  end
172
155
 
173
- def find_node_key(command, seed: nil)
174
- key = @command.extract_first_key(command)
175
- slot = key.empty? ? nil : ::RedisClient::Cluster::KeySlotConverter.convert(key)
176
-
177
- if @command.should_send_to_primary?(command)
178
- @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)
179
160
  else
180
- @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)
181
162
  end
182
163
  end
183
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
+
184
175
  def find_primary_node_key(command)
185
176
  key = @command.extract_first_key(command)
186
- slot = key.empty? ? nil : ::RedisClient::Cluster::KeySlotConverter.convert(key)
187
- @node.find_node_key_of_primary(slot)
177
+ return nil unless key&.size&.> 0
178
+
179
+ find_node_key_by_key(key, primary: true)
188
180
  end
189
181
 
190
182
  def find_node(node_key, retry_count: 3)
@@ -306,34 +298,8 @@ class RedisClient
306
298
  end
307
299
  end
308
300
 
309
- def fetch_cluster_info(config, concurrent_worker, pool: nil, **kwargs)
310
- node_info_list = ::RedisClient::Cluster::Node.load_info(config.per_node_key, concurrent_worker, slow_command_timeout: config.slow_command_timeout, **kwargs)
311
- node_addrs = node_info_list.map { |i| ::RedisClient::Cluster::NodeKey.hashify(i.node_key) }
312
- config.update_node(node_addrs)
313
- ::RedisClient::Cluster::Node.new(
314
- config.per_node_key,
315
- concurrent_worker,
316
- node_info_list: node_info_list,
317
- pool: pool,
318
- with_replica: config.use_replica?,
319
- replica_affinity: config.replica_affinity,
320
- **kwargs
321
- )
322
- end
323
-
324
301
  def update_cluster_info!
325
- return if @mutex.locked?
326
-
327
- @mutex.synchronize do
328
- begin
329
- @node.each(&:close)
330
- rescue ::RedisClient::Cluster::ErrorCollection
331
- # ignore
332
- end
333
-
334
- @config = @original_config.dup if @connect_with_original_config
335
- @node = fetch_cluster_info(@config, @concurrent_worker, pool: @pool, **@client_kwargs)
336
- end
302
+ @node.reload!
337
303
  end
338
304
  end
339
305
  end