redis-cluster-client 0.11.5 → 0.12.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 175bb2bf9b82d2b1d0b95e8d7e3e74e3952165f2edea957303f8b16716a17273
4
- data.tar.gz: 1e094b0d0ae859cb4d77cc872203c21f2f408b739a748d518fd60ca7c33e7451
3
+ metadata.gz: 2465b865cd79c78cca3a1e7e3950989cce94f6675b0497763e03e261d3fa9c6f
4
+ data.tar.gz: df607279a58171aca75835b767f3bf5965256db0fcb7b540bfcb3a8f302eb638
5
5
  SHA512:
6
- metadata.gz: 8b8a13a4df4b5d96da3bcb618bada62d5882fb688b7fd7a9d618c6733a44913af24222c48d40f4db0a316d58b00aa608de99467114dac22711eb7b421f7e2e8a
7
- data.tar.gz: 2c3ebd371f3a23388e6600192a6403c5f97a90f7a7c06bab747310fa729d8be8f9d7d793763a8e7829f2e4699893d76964a96bd0ea40e727d0bdb499e1ef9f67
6
+ metadata.gz: bfb9031660b642bb14768dc8edc66fb15073be9ca23ddc061d279a351c497a83ff8a3aad21903e2d0a37aa6824da35761188ba4dc1fe8ae0182b54584edaa5e5
7
+ data.tar.gz: 9fab63d0595bfb7dac85a07c255a41573ab8d44598605919fd4182e99cf3da585bf931abe63da9e2f470815c6ef0d34ab24a5b7f2de346ba29870fb894c556ee
@@ -25,7 +25,7 @@ class RedisClient
25
25
  )
26
26
 
27
27
  class << self
28
- def load(nodes, slow_command_timeout: -1)
28
+ def load(nodes, slow_command_timeout: -1) # rubocop:disable Metrics/AbcSize
29
29
  cmd = errors = nil
30
30
 
31
31
  nodes&.each do |node|
@@ -43,7 +43,7 @@ class RedisClient
43
43
 
44
44
  return cmd unless cmd.nil?
45
45
 
46
- raise ::RedisClient::Cluster::InitialSetupError, errors
46
+ raise ::RedisClient::Cluster::InitialSetupError.from_errors(errors)
47
47
  end
48
48
 
49
49
  private
@@ -4,51 +4,60 @@ require 'redis_client'
4
4
 
5
5
  class RedisClient
6
6
  class Cluster
7
+ class Error < ::RedisClient::Error
8
+ def with_config(config)
9
+ @config = config
10
+ self
11
+ end
12
+ end
13
+
7
14
  ERR_ARG_NORMALIZATION = ->(arg) { Array[arg].flatten.reject { |e| e.nil? || (e.respond_to?(:empty?) && e.empty?) } }
8
15
 
9
16
  private_constant :ERR_ARG_NORMALIZATION
10
17
 
11
- class InitialSetupError < ::RedisClient::Error
12
- def initialize(errors)
18
+ class InitialSetupError < Error
19
+ def self.from_errors(errors)
13
20
  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}")
15
22
  end
16
23
  end
17
24
 
18
- class OrchestrationCommandNotSupported < ::RedisClient::Error
19
- def initialize(command)
25
+ class OrchestrationCommandNotSupported < Error
26
+ def self.from_command(command)
20
27
  str = ERR_ARG_NORMALIZATION.call(command).map(&:to_s).join(' ').upcase
21
28
  msg = "#{str} command should be used with care " \
22
29
  'only by applications orchestrating Redis Cluster, like redis-cli, ' \
23
30
  'and the command if used out of the right context can leave the cluster ' \
24
31
  'in a wrong state or cause data loss.'
25
- super(msg)
32
+ new(msg)
26
33
  end
27
34
  end
28
35
 
29
- class ErrorCollection < ::RedisClient::Error
36
+ class ErrorCollection < Error
30
37
  attr_reader :errors
31
38
 
32
- def initialize(errors)
33
- @errors = {}
39
+ def self.with_errors(errors)
34
40
  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)
37
45
  end
46
+ end
38
47
 
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
42
51
  end
43
52
  end
44
53
 
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.")
48
57
  end
49
58
  end
50
59
 
51
- class NodeMightBeDown < ::RedisClient::Error
60
+ class NodeMightBeDown < Error
52
61
  def initialize(_ = '')
53
62
  super(
54
63
  'The client is trying to fetch the latest cluster state ' \
@@ -28,7 +28,7 @@ class RedisClient
28
28
  private_constant :USE_CHAR_ARRAY_SLOT, :SLOT_SIZE, :MIN_SLOT, :MAX_SLOT,
29
29
  :DEAD_FLAGS, :ROLE_FLAGS, :EMPTY_ARRAY, :EMPTY_HASH
30
30
 
31
- ReloadNeeded = Class.new(::RedisClient::Error)
31
+ ReloadNeeded = Class.new(::RedisClient::Cluster::Error)
32
32
 
33
33
  Info = Struct.new(
34
34
  'RedisClusterNode',
@@ -148,7 +148,7 @@ class RedisClient
148
148
 
149
149
  raise ReloadNeeded if errors.values.any?(::RedisClient::ConnectionError)
150
150
 
151
- raise ::RedisClient::Cluster::ErrorCollection, errors
151
+ raise ::RedisClient::Cluster::ErrorCollection.with_errors(errors)
152
152
  end
153
153
 
154
154
  def clients_for_scanning(seed: nil)
@@ -267,7 +267,7 @@ class RedisClient
267
267
  result_values, errors = call_multiple_nodes(clients, method, command, args, &block)
268
268
  return result_values if errors.nil? || errors.empty?
269
269
 
270
- raise ::RedisClient::Cluster::ErrorCollection, errors
270
+ raise ::RedisClient::Cluster::ErrorCollection.with_errors(errors)
271
271
  end
272
272
 
273
273
  def try_map(clients, &block) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity
@@ -334,7 +334,7 @@ class RedisClient
334
334
 
335
335
  work_group.close
336
336
 
337
- raise ::RedisClient::Cluster::InitialSetupError, errors if node_info_list.nil?
337
+ raise ::RedisClient::Cluster::InitialSetupError.from_errors(errors) if node_info_list.nil?
338
338
 
339
339
  grouped = node_info_list.compact.group_by do |info_list|
340
340
  info_list.sort_by!(&:id)
@@ -15,10 +15,7 @@ class RedisClient
15
15
  slot = find_slot(keys)
16
16
  raise ::RedisClient::Cluster::Transaction::ConsistencyError, "unsafe watch: #{keys.join(' ')}" if slot.nil?
17
17
 
18
- # We have not yet selected a node for this transaction, initially, which means we can handle
19
- # redirections freely initially (i.e. for the first WATCH call)
20
- node = @router.find_primary_node_by_slot(slot)
21
- handle_redirection(node, retry_count: 1) do |nd|
18
+ handle_redirection(slot, retry_count: 1) do |nd|
22
19
  nd.with do |c|
23
20
  c.ensure_connected_cluster_scoped(retryable: false) do
24
21
  c.call('ASKING') if @asking
@@ -45,10 +42,22 @@ class RedisClient
45
42
 
46
43
  private
47
44
 
48
- def handle_redirection(node, retry_count: 1, &blk)
49
- @router.handle_redirection(node, retry_count: retry_count) do |nd|
45
+ def handle_redirection(slot, retry_count: 1, &blk)
46
+ # We have not yet selected a node for this transaction, initially, which means we can handle
47
+ # redirections freely initially (i.e. for the first WATCH call)
48
+ node = @router.find_primary_node_by_slot(slot)
49
+ times_block_executed = 0
50
+ @router.handle_redirection(node, nil, retry_count: retry_count) do |nd|
51
+ times_block_executed += 1
50
52
  handle_asking_once(nd, &blk)
51
53
  end
54
+ rescue ::RedisClient::ConnectionError
55
+ # Deduct the number of retries that happened _inside_ router#handle_redirection from our remaining
56
+ # _external_ retries. Always deduct at least one in case handle_redirection raises without trying the block.
57
+ retry_count -= [times_block_executed, 1].min
58
+ raise if retry_count < 0
59
+
60
+ retry
52
61
  end
53
62
 
54
63
  def handle_asking_once(node)
@@ -108,13 +108,13 @@ class RedisClient
108
108
  end
109
109
  end
110
110
 
111
- ReplySizeError = Class.new(::RedisClient::Error)
111
+ ReplySizeError = Class.new(::RedisClient::Cluster::Error)
112
112
 
113
- class StaleClusterState < ::RedisClient::Error
113
+ class StaleClusterState < ::RedisClient::Cluster::Error
114
114
  attr_accessor :replies, :first_exception
115
115
  end
116
116
 
117
- class RedirectionNeeded < ::RedisClient::Error
117
+ class RedirectionNeeded < ::RedisClient::Cluster::Error
118
118
  attr_accessor :replies, :indices, :first_exception
119
119
  end
120
120
 
@@ -204,7 +204,7 @@ class RedisClient
204
204
 
205
205
  work_group.close
206
206
  @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?
208
208
 
209
209
  required_redirections&.each do |node_key, v|
210
210
  raise v.first_exception if v.first_exception
@@ -21,10 +21,10 @@ class RedisClient
21
21
 
22
22
  private_constant :ZERO_CURSOR_FOR_SCAN, :TSF
23
23
 
24
+ attr_reader :config
25
+
24
26
  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
28
28
  @concurrent_worker = concurrent_worker
29
29
  @pool = pool
30
30
  @client_kwargs = kwargs
@@ -32,6 +32,9 @@ class RedisClient
32
32
  @node.reload!
33
33
  @command = ::RedisClient::Cluster::Command.load(@node.replica_clients.shuffle, slow_command_timeout: config.slow_command_timeout)
34
34
  @command_builder = @config.command_builder
35
+ rescue ::RedisClient::Cluster::InitialSetupError => e
36
+ e.with_config(config)
37
+ raise
35
38
  end
36
39
 
37
40
  def send_command(method, command, *args, &block) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
@@ -58,9 +61,9 @@ class RedisClient
58
61
  when 'flushall', 'flushdb'
59
62
  @node.call_primaries(method, command, args).first.then(&TSF.call(block))
60
63
  when 'readonly', 'readwrite', 'shutdown'
61
- raise ::RedisClient::Cluster::OrchestrationCommandNotSupported, cmd
64
+ raise ::RedisClient::Cluster::OrchestrationCommandNotSupported.from_command(cmd).with_config(@config)
62
65
  when 'discard', 'exec', 'multi', 'unwatch'
63
- raise ::RedisClient::Cluster::AmbiguousNodeError, cmd
66
+ raise ::RedisClient::Cluster::AmbiguousNodeError.from_command(cmd).with_config(@config)
64
67
  else
65
68
  node = assign_node(command)
66
69
  try_send(node, method, command, args, &block)
@@ -69,7 +72,7 @@ class RedisClient
69
72
  raise
70
73
  rescue ::RedisClient::Cluster::Node::ReloadNeeded
71
74
  renew_cluster_state
72
- raise ::RedisClient::Cluster::NodeMightBeDown
75
+ raise ::RedisClient::Cluster::NodeMightBeDown.new.with_config(@config)
73
76
  rescue ::RedisClient::ConnectionError
74
77
  renew_cluster_state
75
78
  raise
@@ -77,6 +80,7 @@ class RedisClient
77
80
  renew_cluster_state if e.message.start_with?('CLUSTERDOWN')
78
81
  raise
79
82
  rescue ::RedisClient::Cluster::ErrorCollection => e
83
+ e.with_config(@config)
80
84
  raise if e.errors.any?(::RedisClient::CircuitBreaker::OpenCircuitError)
81
85
 
82
86
  renew_cluster_state if e.errors.values.any? do |err|
@@ -90,7 +94,7 @@ class RedisClient
90
94
 
91
95
  # @see https://redis.io/docs/reference/cluster-spec/#redirection-and-resharding Redirection and resharding
92
96
  def try_send(node, method, command, args, retry_count: 3, &block)
93
- handle_redirection(node, retry_count: retry_count) do |on_node|
97
+ handle_redirection(node, command, retry_count: retry_count) do |on_node|
94
98
  if args.empty?
95
99
  # prevent memory allocation for variable-length args
96
100
  on_node.public_send(method, command, &block)
@@ -101,12 +105,12 @@ class RedisClient
101
105
  end
102
106
 
103
107
  def try_delegate(node, method, *args, retry_count: 3, **kwargs, &block)
104
- handle_redirection(node, retry_count: retry_count) do |on_node|
108
+ handle_redirection(node, nil, retry_count: retry_count) do |on_node|
105
109
  on_node.public_send(method, *args, **kwargs, &block)
106
110
  end
107
111
  end
108
112
 
109
- def handle_redirection(node, retry_count:) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
113
+ def handle_redirection(node, command, retry_count:) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
110
114
  yield node
111
115
  rescue ::RedisClient::CircuitBreaker::OpenCircuitError
112
116
  raise
@@ -134,6 +138,17 @@ class RedisClient
134
138
 
135
139
  retry_count -= 1
136
140
  renew_cluster_state
141
+
142
+ if retry_count >= 0
143
+ # Find the node to use for this command - if this fails for some reason, though, re-use
144
+ # the old node.
145
+ begin
146
+ node = find_node(find_node_key(command)) if command
147
+ rescue StandardError # rubocop:disable Lint/SuppressedException
148
+ end
149
+ retry
150
+ end
151
+
137
152
  retry if retry_count >= 0
138
153
  raise
139
154
  end
@@ -178,7 +193,7 @@ class RedisClient
178
193
  node_key = primary ? @node.find_node_key_of_primary(slot) : @node.find_node_key_of_replica(slot)
179
194
  if node_key.nil?
180
195
  renew_cluster_state
181
- raise ::RedisClient::Cluster::NodeMightBeDown
196
+ raise ::RedisClient::Cluster::NodeMightBeDown.new.with_config(@config)
182
197
  end
183
198
  node_key
184
199
  else
@@ -292,7 +307,7 @@ class RedisClient
292
307
  case subcommand = ::RedisClient::Cluster::NormalizedCmdName.instance.get_by_subcommand(command)
293
308
  when 'addslots', 'delslots', 'failover', 'forget', 'meet', 'replicate',
294
309
  'reset', 'set-config-epoch', 'setslot'
295
- raise ::RedisClient::Cluster::OrchestrationCommandNotSupported, ['cluster', subcommand]
310
+ raise ::RedisClient::Cluster::OrchestrationCommandNotSupported.from_command(['cluster', subcommand]).with_config(@config)
296
311
  when 'saveconfig' then @node.call_all(method, command, args).first.then(&TSF.call(block))
297
312
  when 'getkeysinslot'
298
313
  raise ArgumentError, command.join(' ') if command.size != 4
@@ -336,7 +351,10 @@ class RedisClient
336
351
  end
337
352
 
338
353
  def send_watch_command(command)
339
- 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
340
358
 
341
359
  ::RedisClient::Cluster::OptimisticLocking.new(self).watch(command[1..]) do |c, slot, asking|
342
360
  transaction = ::RedisClient::Cluster::Transaction.new(
@@ -390,7 +408,7 @@ class RedisClient
390
408
  def handle_node_reload_error(retry_count: 1)
391
409
  yield
392
410
  rescue ::RedisClient::Cluster::Node::ReloadNeeded
393
- raise ::RedisClient::Cluster::NodeMightBeDown if retry_count <= 0
411
+ raise ::RedisClient::Cluster::NodeMightBeDown.new.with_config(@config) if retry_count <= 0
394
412
 
395
413
  retry_count -= 1
396
414
  renew_cluster_state
@@ -1,12 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'redis_client'
4
+ require 'redis_client/cluster/errors'
4
5
  require 'redis_client/cluster/pipeline'
5
6
 
6
7
  class RedisClient
7
8
  class Cluster
8
9
  class Transaction
9
- ConsistencyError = Class.new(::RedisClient::Error)
10
+ ConsistencyError = Class.new(::RedisClient::Cluster::Error)
10
11
 
11
12
  MAX_REDIRECTION = 2
12
13
  EMPTY_ARRAY = [].freeze
@@ -67,7 +68,7 @@ class RedisClient
67
68
  @pending_commands.each(&:call)
68
69
 
69
70
  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?
71
72
 
72
73
  commit
73
74
  end
@@ -163,7 +164,7 @@ class RedisClient
163
164
 
164
165
  def handle_command_error!(err, redirect:) # rubocop:disable Metrics/AbcSize
165
166
  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)
167
168
  elsif err.message.start_with?('MOVED')
168
169
  node = @router.assign_redirection_node(err.message)
169
170
  send_transaction(node, redirect: redirect - 1)
@@ -183,7 +184,7 @@ class RedisClient
183
184
  return if slots.size == 1 && @watching_slot.nil?
184
185
  return if slots.size == 1 && @watching_slot == slots.first
185
186
 
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)
187
188
  end
188
189
 
189
190
  def try_asking(node)
@@ -3,6 +3,7 @@
3
3
  require 'uri'
4
4
  require 'redis_client'
5
5
  require 'redis_client/cluster'
6
+ require 'redis_client/cluster/errors'
6
7
  require 'redis_client/cluster/node_key'
7
8
  require 'redis_client/command_builder'
8
9
 
@@ -27,10 +28,10 @@ class RedisClient
27
28
  :VALID_SCHEMES, :VALID_NODES_KEYS, :MERGE_CONFIG_KEYS, :IGNORE_GENERIC_CONFIG_KEYS,
28
29
  :MAX_WORKERS, :SLOW_COMMAND_TIMEOUT, :MAX_STARTUP_SAMPLE
29
30
 
30
- InvalidClientConfigError = Class.new(::RedisClient::Error)
31
+ InvalidClientConfigError = Class.new(::RedisClient::Cluster::Error)
31
32
 
32
33
  attr_reader :command_builder, :client_config, :replica_affinity, :slow_command_timeout,
33
- :connect_with_original_config, :startup_nodes, :max_startup_sample
34
+ :connect_with_original_config, :startup_nodes, :max_startup_sample, :id
34
35
 
35
36
  def initialize( # rubocop:disable Metrics/ParameterLists
36
37
  nodes: DEFAULT_NODES,
@@ -59,6 +60,7 @@ class RedisClient
59
60
  @client_implementation = client_implementation
60
61
  @slow_command_timeout = slow_command_timeout
61
62
  @max_startup_sample = max_startup_sample
63
+ @id = client_config[:id]
62
64
  end
63
65
 
64
66
  def inspect
@@ -92,6 +94,18 @@ class RedisClient
92
94
  augment_client_config(config)
93
95
  end
94
96
 
97
+ def resolved?
98
+ true
99
+ end
100
+
101
+ def sentinel?
102
+ false
103
+ end
104
+
105
+ def server_url
106
+ nil
107
+ end
108
+
95
109
  private
96
110
 
97
111
  def merge_concurrency_option(option)
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: redis-cluster-client
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.11.5
4
+ version: 0.12.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Taishi Kasuga
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-10-13 00:00:00.000000000 Z
11
+ date: 2024-11-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: redis-client
@@ -77,7 +77,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
77
77
  - !ruby/object:Gem::Version
78
78
  version: '0'
79
79
  requirements: []
80
- rubygems_version: 3.5.16
80
+ rubygems_version: 3.5.22
81
81
  signing_key:
82
82
  specification_version: 4
83
83
  summary: A Redis cluster client for Ruby