Skip to content

Commit f9e7410

Browse files
authored
fix: make some errors identifiable to which cluster related (#401)
1 parent 972f03e commit f9e7410

File tree

8 files changed

+77
-47
lines changed

8 files changed

+77
-47
lines changed

lib/redis_client/cluster/command.rb

+2-2
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ class Command
2525
)
2626

2727
class << self
28-
def load(nodes, slow_command_timeout: -1)
28+
def load(nodes, slow_command_timeout: -1) # rubocop:disable Metrics/AbcSize
2929
cmd = errors = nil
3030

3131
nodes&.each do |node|
@@ -43,7 +43,7 @@ def load(nodes, slow_command_timeout: -1)
4343

4444
return cmd unless cmd.nil?
4545

46-
raise ::RedisClient::Cluster::InitialSetupError, errors
46+
raise ::RedisClient::Cluster::InitialSetupError.from_errors(errors)
4747
end
4848

4949
private

lib/redis_client/cluster/errors.rb

+27-18
Original file line numberDiff line numberDiff line change
@@ -4,51 +4,60 @@
44

55
class RedisClient
66
class Cluster
7+
class Error < ::RedisClient::Error
8+
def with_config(config)
9+
@config = config
10+
self
11+
end
12+
end
13+
714
ERR_ARG_NORMALIZATION = ->(arg) { Array[arg].flatten.reject { |e| e.nil? || (e.respond_to?(:empty?) && e.empty?) } }
815

916
private_constant :ERR_ARG_NORMALIZATION
1017

11-
class InitialSetupError < ::RedisClient::Error
12-
def initialize(errors)
18+
class InitialSetupError < Error
19+
def self.from_errors(errors)
1320
msg = ERR_ARG_NORMALIZATION.call(errors).map(&:message).uniq.join(',')
14-
super("Redis client could not fetch cluster information: #{msg}")
21+
new("Redis client could not fetch cluster information: #{msg}")
1522
end
1623
end
1724

18-
class OrchestrationCommandNotSupported < ::RedisClient::Error
19-
def initialize(command)
25+
class OrchestrationCommandNotSupported < Error
26+
def self.from_command(command)
2027
str = ERR_ARG_NORMALIZATION.call(command).map(&:to_s).join(' ').upcase
2128
msg = "#{str} command should be used with care " \
2229
'only by applications orchestrating Redis Cluster, like redis-cli, ' \
2330
'and the command if used out of the right context can leave the cluster ' \
2431
'in a wrong state or cause data loss.'
25-
super(msg)
32+
new(msg)
2633
end
2734
end
2835

29-
class ErrorCollection < ::RedisClient::Error
36+
class ErrorCollection < Error
3037
attr_reader :errors
3138

32-
def initialize(errors)
33-
@errors = {}
39+
def self.with_errors(errors)
3440
if !errors.is_a?(Hash) || errors.empty?
35-
super(errors.to_s)
36-
return
41+
new(errors.to_s).with_errors({})
42+
else
43+
messages = errors.map { |node_key, error| "#{node_key}: (#{error.class}) #{error.message}" }
44+
new(messages.join(', ')).with_errors(errors)
3745
end
46+
end
3847

39-
@errors = errors
40-
messages = @errors.map { |node_key, error| "#{node_key}: (#{error.class}) #{error.message}" }
41-
super(messages.join(', '))
48+
def with_errors(errors)
49+
@errors = errors if @errors.nil?
50+
self
4251
end
4352
end
4453

45-
class AmbiguousNodeError < ::RedisClient::Error
46-
def initialize(command)
47-
super("Cluster client doesn't know which node the #{command} command should be sent to.")
54+
class AmbiguousNodeError < Error
55+
def self.from_command(command)
56+
new("Cluster client doesn't know which node the #{command} command should be sent to.")
4857
end
4958
end
5059

51-
class NodeMightBeDown < ::RedisClient::Error
60+
class NodeMightBeDown < Error
5261
def initialize(_ = '')
5362
super(
5463
'The client is trying to fetch the latest cluster state ' \

lib/redis_client/cluster/node.rb

+4-4
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ class Node
2828
private_constant :USE_CHAR_ARRAY_SLOT, :SLOT_SIZE, :MIN_SLOT, :MAX_SLOT,
2929
:DEAD_FLAGS, :ROLE_FLAGS, :EMPTY_ARRAY, :EMPTY_HASH
3030

31-
ReloadNeeded = Class.new(::RedisClient::Error)
31+
ReloadNeeded = Class.new(::RedisClient::Cluster::Error)
3232

3333
Info = Struct.new(
3434
'RedisClusterNode',
@@ -148,7 +148,7 @@ def send_ping(method, command, args, &block)
148148

149149
raise ReloadNeeded if errors.values.any?(::RedisClient::ConnectionError)
150150

151-
raise ::RedisClient::Cluster::ErrorCollection, errors
151+
raise ::RedisClient::Cluster::ErrorCollection.with_errors(errors)
152152
end
153153

154154
def clients_for_scanning(seed: nil)
@@ -267,7 +267,7 @@ def call_multiple_nodes!(clients, method, command, args, &block)
267267
result_values, errors = call_multiple_nodes(clients, method, command, args, &block)
268268
return result_values if errors.nil? || errors.empty?
269269

270-
raise ::RedisClient::Cluster::ErrorCollection, errors
270+
raise ::RedisClient::Cluster::ErrorCollection.with_errors(errors)
271271
end
272272

273273
def try_map(clients, &block) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity
@@ -334,7 +334,7 @@ def refetch_node_info_list(startup_clients) # rubocop:disable Metrics/AbcSize, M
334334

335335
work_group.close
336336

337-
raise ::RedisClient::Cluster::InitialSetupError, errors if node_info_list.nil?
337+
raise ::RedisClient::Cluster::InitialSetupError.from_errors(errors) if node_info_list.nil?
338338

339339
grouped = node_info_list.compact.group_by do |info_list|
340340
info_list.sort_by!(&:id)

lib/redis_client/cluster/pipeline.rb

+4-4
Original file line numberDiff line numberDiff line change
@@ -108,13 +108,13 @@ def ensure_connected_cluster_scoped(retryable: true, &block)
108108
end
109109
end
110110

111-
ReplySizeError = Class.new(::RedisClient::Error)
111+
ReplySizeError = Class.new(::RedisClient::Cluster::Error)
112112

113-
class StaleClusterState < ::RedisClient::Error
113+
class StaleClusterState < ::RedisClient::Cluster::Error
114114
attr_accessor :replies, :first_exception
115115
end
116116

117-
class RedirectionNeeded < ::RedisClient::Error
117+
class RedirectionNeeded < ::RedisClient::Cluster::Error
118118
attr_accessor :replies, :indices, :first_exception
119119
end
120120

@@ -204,7 +204,7 @@ def execute # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Met
204204

205205
work_group.close
206206
@router.renew_cluster_state if cluster_state_errors
207-
raise ::RedisClient::Cluster::ErrorCollection, errors unless errors.nil?
207+
raise ::RedisClient::Cluster::ErrorCollection.with_errors(errors).with_config(@router.config) unless errors.nil?
208208

209209
required_redirections&.each do |node_key, v|
210210
raise v.first_exception if v.first_exception

lib/redis_client/cluster/router.rb

+17-10
Original file line numberDiff line numberDiff line change
@@ -21,17 +21,20 @@ class Router
2121

2222
private_constant :ZERO_CURSOR_FOR_SCAN, :TSF
2323

24+
attr_reader :config
25+
2426
def initialize(config, concurrent_worker, pool: nil, **kwargs)
25-
@config = config.dup
26-
@original_config = config.dup if config.connect_with_original_config
27-
@connect_with_original_config = config.connect_with_original_config
27+
@config = config
2828
@concurrent_worker = concurrent_worker
2929
@pool = pool
3030
@client_kwargs = kwargs
3131
@node = ::RedisClient::Cluster::Node.new(concurrent_worker, config: config, pool: pool, **kwargs)
3232
@node.reload!
3333
@command = ::RedisClient::Cluster::Command.load(@node.replica_clients.shuffle, slow_command_timeout: config.slow_command_timeout)
3434
@command_builder = @config.command_builder
35+
rescue ::RedisClient::Cluster::InitialSetupError => e
36+
e.with_config(config)
37+
raise
3538
end
3639

3740
def send_command(method, command, *args, &block) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
@@ -58,9 +61,9 @@ def send_command(method, command, *args, &block) # rubocop:disable Metrics/AbcSi
5861
when 'flushall', 'flushdb'
5962
@node.call_primaries(method, command, args).first.then(&TSF.call(block))
6063
when 'readonly', 'readwrite', 'shutdown'
61-
raise ::RedisClient::Cluster::OrchestrationCommandNotSupported, cmd
64+
raise ::RedisClient::Cluster::OrchestrationCommandNotSupported.from_command(cmd).with_config(@config)
6265
when 'discard', 'exec', 'multi', 'unwatch'
63-
raise ::RedisClient::Cluster::AmbiguousNodeError, cmd
66+
raise ::RedisClient::Cluster::AmbiguousNodeError.from_command(cmd).with_config(@config)
6467
else
6568
node = assign_node(command)
6669
try_send(node, method, command, args, &block)
@@ -69,14 +72,15 @@ def send_command(method, command, *args, &block) # rubocop:disable Metrics/AbcSi
6972
raise
7073
rescue ::RedisClient::Cluster::Node::ReloadNeeded
7174
renew_cluster_state
72-
raise ::RedisClient::Cluster::NodeMightBeDown
75+
raise ::RedisClient::Cluster::NodeMightBeDown.new.with_config(@config)
7376
rescue ::RedisClient::ConnectionError
7477
renew_cluster_state
7578
raise
7679
rescue ::RedisClient::CommandError => e
7780
renew_cluster_state if e.message.start_with?('CLUSTERDOWN')
7881
raise
7982
rescue ::RedisClient::Cluster::ErrorCollection => e
83+
e.with_config(@config)
8084
raise if e.errors.any?(::RedisClient::CircuitBreaker::OpenCircuitError)
8185

8286
renew_cluster_state if e.errors.values.any? do |err|
@@ -189,7 +193,7 @@ def find_node_key_by_key(key, seed: nil, primary: false)
189193
node_key = primary ? @node.find_node_key_of_primary(slot) : @node.find_node_key_of_replica(slot)
190194
if node_key.nil?
191195
renew_cluster_state
192-
raise ::RedisClient::Cluster::NodeMightBeDown
196+
raise ::RedisClient::Cluster::NodeMightBeDown.new.with_config(@config)
193197
end
194198
node_key
195199
else
@@ -303,7 +307,7 @@ def send_cluster_command(method, command, args, &block) # rubocop:disable Metric
303307
case subcommand = ::RedisClient::Cluster::NormalizedCmdName.instance.get_by_subcommand(command)
304308
when 'addslots', 'delslots', 'failover', 'forget', 'meet', 'replicate',
305309
'reset', 'set-config-epoch', 'setslot'
306-
raise ::RedisClient::Cluster::OrchestrationCommandNotSupported, ['cluster', subcommand]
310+
raise ::RedisClient::Cluster::OrchestrationCommandNotSupported.from_command(['cluster', subcommand]).with_config(@config)
307311
when 'saveconfig' then @node.call_all(method, command, args).first.then(&TSF.call(block))
308312
when 'getkeysinslot'
309313
raise ArgumentError, command.join(' ') if command.size != 4
@@ -347,7 +351,10 @@ def send_pubsub_command(method, command, args, &block) # rubocop:disable Metrics
347351
end
348352

349353
def send_watch_command(command)
350-
raise ::RedisClient::Cluster::Transaction::ConsistencyError, 'A block required. And you need to use the block argument as a client for the transaction.' unless block_given?
354+
unless block_given?
355+
msg = 'A block required. And you need to use the block argument as a client for the transaction.'
356+
raise ::RedisClient::Cluster::Transaction::ConsistencyError.new(msg).with_config(@config)
357+
end
351358

352359
::RedisClient::Cluster::OptimisticLocking.new(self).watch(command[1..]) do |c, slot, asking|
353360
transaction = ::RedisClient::Cluster::Transaction.new(
@@ -401,7 +408,7 @@ def send_multiple_keys_command(cmd, method, command, args, &block) # rubocop:dis
401408
def handle_node_reload_error(retry_count: 1)
402409
yield
403410
rescue ::RedisClient::Cluster::Node::ReloadNeeded
404-
raise ::RedisClient::Cluster::NodeMightBeDown if retry_count <= 0
411+
raise ::RedisClient::Cluster::NodeMightBeDown.new.with_config(@config) if retry_count <= 0
405412

406413
retry_count -= 1
407414
renew_cluster_state

lib/redis_client/cluster/transaction.rb

+5-4
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
# frozen_string_literal: true
22

33
require 'redis_client'
4+
require 'redis_client/cluster/errors'
45
require 'redis_client/cluster/pipeline'
56

67
class RedisClient
78
class Cluster
89
class Transaction
9-
ConsistencyError = Class.new(::RedisClient::Error)
10+
ConsistencyError = Class.new(::RedisClient::Cluster::Error)
1011

1112
MAX_REDIRECTION = 2
1213
EMPTY_ARRAY = [].freeze
@@ -67,7 +68,7 @@ def execute
6768
@pending_commands.each(&:call)
6869

6970
return EMPTY_ARRAY if @pipeline._empty?
70-
raise ConsistencyError, "couldn't determine the node: #{@pipeline._commands}" if @node.nil?
71+
raise ConsistencyError.new("couldn't determine the node: #{@pipeline._commands}").with_config(@router.config) if @node.nil?
7172

7273
commit
7374
end
@@ -163,7 +164,7 @@ def coerce_results!(results, offset: 1)
163164

164165
def handle_command_error!(err, redirect:) # rubocop:disable Metrics/AbcSize
165166
if err.message.start_with?('CROSSSLOT')
166-
raise ConsistencyError, "#{err.message}: #{err.command}"
167+
raise ConsistencyError.new("#{err.message}: #{err.command}").with_config(@router.config)
167168
elsif err.message.start_with?('MOVED')
168169
node = @router.assign_redirection_node(err.message)
169170
send_transaction(node, redirect: redirect - 1)
@@ -183,7 +184,7 @@ def ensure_the_same_slot!(commands)
183184
return if slots.size == 1 && @watching_slot.nil?
184185
return if slots.size == 1 && @watching_slot == slots.first
185186

186-
raise(ConsistencyError, "the transaction should be executed to a slot in a node: #{commands}")
187+
raise ConsistencyError.new("the transaction should be executed to a slot in a node: #{commands}").with_config(@router.config)
187188
end
188189

189190
def try_asking(node)

lib/redis_client/cluster_config.rb

+14-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
require 'uri'
44
require 'redis_client'
55
require 'redis_client/cluster'
6+
require 'redis_client/cluster/errors'
67
require 'redis_client/cluster/node_key'
78
require 'redis_client/command_builder'
89

@@ -27,7 +28,7 @@ class ClusterConfig
2728
:VALID_SCHEMES, :VALID_NODES_KEYS, :MERGE_CONFIG_KEYS, :IGNORE_GENERIC_CONFIG_KEYS,
2829
:MAX_WORKERS, :SLOW_COMMAND_TIMEOUT, :MAX_STARTUP_SAMPLE
2930

30-
InvalidClientConfigError = Class.new(::RedisClient::Error)
31+
InvalidClientConfigError = Class.new(::RedisClient::Cluster::Error)
3132

3233
attr_reader :command_builder, :client_config, :replica_affinity, :slow_command_timeout,
3334
:connect_with_original_config, :startup_nodes, :max_startup_sample
@@ -92,6 +93,18 @@ def client_config_for_node(node_key)
9293
augment_client_config(config)
9394
end
9495

96+
def resolved?
97+
true
98+
end
99+
100+
def sentinel?
101+
false
102+
end
103+
104+
def server_url
105+
nil
106+
end
107+
95108
private
96109

97110
def merge_concurrency_option(option)

test/redis_client/cluster/test_errors.rb

+4-4
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ def test_initial_setup_error
2121
{ errors: '', want: 'Redis client could not fetch cluster information: ' },
2222
{ errors: nil, want: 'Redis client could not fetch cluster information: ' }
2323
].each_with_index do |c, idx|
24-
raise ::RedisClient::Cluster::InitialSetupError, c[:errors]
24+
raise ::RedisClient::Cluster::InitialSetupError.from_errors(c[:errors])
2525
rescue StandardError => e
2626
assert_equal(c[:want], e.message, "Case: #{idx}")
2727
end
@@ -34,7 +34,7 @@ def test_orchestration_command_not_supported_error
3434
{ command: '', want: ' command should be' },
3535
{ command: nil, want: ' command should be' }
3636
].each_with_index do |c, idx|
37-
raise ::RedisClient::Cluster::OrchestrationCommandNotSupported, c[:command]
37+
raise ::RedisClient::Cluster::OrchestrationCommandNotSupported.from_command(c[:command])
3838
rescue StandardError => e
3939
assert(e.message.start_with?(c[:want]), "Case: #{idx}")
4040
end
@@ -55,7 +55,7 @@ def test_error_collection_error
5555
{ errors: '', want: { msg: '', size: 0 } },
5656
{ errors: nil, want: { msg: '', size: 0 } }
5757
].each_with_index do |c, idx|
58-
raise ::RedisClient::Cluster::ErrorCollection, c[:errors]
58+
raise ::RedisClient::Cluster::ErrorCollection.with_errors(c[:errors])
5959
rescue StandardError => e
6060
assert_equal(c[:want][:msg], e.message, "Case: #{idx}")
6161
assert_equal(c[:want][:size], e.errors.size, "Case: #{idx}")
@@ -67,7 +67,7 @@ def test_ambiguous_node_error
6767
{ command: 'MULTI', want: "Cluster client doesn't know which node the MULTI command should be sent to." },
6868
{ command: nil, want: "Cluster client doesn't know which node the command should be sent to." }
6969
].each_with_index do |c, idx|
70-
raise ::RedisClient::Cluster::AmbiguousNodeError, c[:command]
70+
raise ::RedisClient::Cluster::AmbiguousNodeError.from_command(c[:command])
7171
rescue StandardError => e
7272
assert_equal(e.message, c[:want], "Case: #{idx}")
7373
end

0 commit comments

Comments
 (0)