redis-cluster-client 0.7.4 → 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'
@@ -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