redis-cluster-client 0.11.5 → 0.12.0

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.
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