redis-cluster-client 0.7.5 → 0.7.6

Sign up to get free protection for your applications and to get access to all the features.
@@ -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