redis-cluster-client 0.11.0 → 0.11.1

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: d885c147de46fd113d493e60bd340cac591ecc94b7eafcfddbd6044abe10f79a
4
- data.tar.gz: f13c1012ef977e8f9c3e38d928c5c3eb95ba2242761fa992033323c115b318f9
3
+ metadata.gz: cd66efdf343224572aeffdbfdd4cb96c8189bd45183c0a3934e6dd1964a35655
4
+ data.tar.gz: 66805c538066aabab413b767f0b9e8756c1eff8920f410a665939f18d69c0621
5
5
  SHA512:
6
- metadata.gz: 68c939431088425c7735c68b4af0949bc5918837d9f19878c62d71aa1fec6e31efee5de2868325447a9e4672c108299a86cc6d047447ef10e7987daa04ecb1e9
7
- data.tar.gz: f8ae784f6977151fd825c34878e7f6162175cd828d0b81a92da07e5af96a00a56467ffbed3822f24c198f2373900dc20ea69f8446e3468fc7ba54945baa44f65
6
+ metadata.gz: d325c2955718451c15084b58b0231d521065ebe1b8b549f79106f26132b50eb77ac90e6b0dcfb11868eb80b2d37a742f741b21d963c6ff92faacf4d88bc64830
7
+ data.tar.gz: 7e6c92442c2852b0533c19b4e60ff8e8e6532d55cbeebc9424e1b62081c6eb21af6a5717ae04fbb683083c271d3008934d0e17becc4b7abf232c005304c2b713
@@ -12,6 +12,8 @@ class RedisClient
12
12
  EMPTY_HASH = {}.freeze
13
13
  EMPTY_ARRAY = [].freeze
14
14
 
15
+ private_constant :EMPTY_STRING, :EMPTY_HASH, :EMPTY_ARRAY
16
+
15
17
  Detail = Struct.new(
16
18
  'RedisCommand',
17
19
  :first_key_position,
@@ -6,6 +6,8 @@ class RedisClient
6
6
  class Cluster
7
7
  ERR_ARG_NORMALIZATION = ->(arg) { Array[arg].flatten.reject { |e| e.nil? || (e.respond_to?(:empty?) && e.empty?) } }
8
8
 
9
+ private_constant :ERR_ARG_NORMALIZATION
10
+
9
11
  class InitialSetupError < ::RedisClient::Error
10
12
  def initialize(errors)
11
13
  msg = ERR_ARG_NORMALIZATION.call(errors).map(&:message).uniq.join(',')
@@ -30,7 +32,7 @@ class RedisClient
30
32
  def initialize(errors)
31
33
  @errors = {}
32
34
  if !errors.is_a?(Hash) || errors.empty?
33
- super('')
35
+ super(errors.to_s)
34
36
  return
35
37
  end
36
38
 
@@ -3,6 +3,7 @@
3
3
  class RedisClient
4
4
  class Cluster
5
5
  module KeySlotConverter
6
+ HASH_SLOTS = 16_384
6
7
  EMPTY_STRING = ''
7
8
  LEFT_BRACKET = '{'
8
9
  RIGHT_BRACKET = '}'
@@ -41,7 +42,8 @@ class RedisClient
41
42
  0x6e17, 0x7e36, 0x4e55, 0x5e74, 0x2e93, 0x3eb2, 0x0ed1, 0x1ef0
42
43
  ].freeze
43
44
 
44
- HASH_SLOTS = 16_384
45
+ private_constant :HASH_SLOTS, :EMPTY_STRING,
46
+ :LEFT_BRACKET, :RIGHT_BRACKET, :XMODEM_CRC16_LOOKUP
45
47
 
46
48
  module_function
47
49
 
@@ -8,6 +8,8 @@ class RedisClient
8
8
  EMPTY_HASH = {}.freeze
9
9
  EMPTY_ARRAY = [].freeze
10
10
 
11
+ private_constant :IGNORE_GENERIC_CONFIG_KEYS, :EMPTY_HASH, :EMPTY_ARRAY
12
+
11
13
  attr_reader :clients, :primary_clients, :replica_clients
12
14
 
13
15
  def initialize(pool, concurrent_worker, **kwargs)
@@ -9,6 +9,8 @@ class RedisClient
9
9
  DUMMY_LATENCY_MSEC = 100 * 1000 * 1000
10
10
  MEASURE_ATTEMPT_COUNT = 10
11
11
 
12
+ private_constant :DUMMY_LATENCY_MSEC, :MEASURE_ATTEMPT_COUNT
13
+
12
14
  def clients_for_scanning(seed: nil) # rubocop:disable Lint/UnusedMethodArgument
13
15
  @clients_for_scanning
14
16
  end
@@ -23,6 +23,10 @@ class RedisClient
23
23
  ROLE_FLAGS = %w[master slave].freeze
24
24
  EMPTY_ARRAY = [].freeze
25
25
  EMPTY_HASH = {}.freeze
26
+ STATE_REFRESH_INTERVAL = (3..10).freeze
27
+
28
+ private_constant :USE_CHAR_ARRAY_SLOT, :SLOT_SIZE, :MIN_SLOT, :MAX_SLOT,
29
+ :DEAD_FLAGS, :ROLE_FLAGS, :EMPTY_ARRAY, :EMPTY_HASH
26
30
 
27
31
  ReloadNeeded = Class.new(::RedisClient::Error)
28
32
 
@@ -45,6 +49,8 @@ class RedisClient
45
49
  BASE = ''
46
50
  PADDING = '0'
47
51
 
52
+ private_constant :BASE, :PADDING
53
+
48
54
  def initialize(size, elements)
49
55
  @elements = elements
50
56
  @string = String.new(BASE, encoding: Encoding::BINARY, capacity: size)
@@ -98,6 +104,8 @@ class RedisClient
98
104
  @config = config
99
105
  @mutex = Mutex.new
100
106
  @last_reloaded_at = nil
107
+ @reload_times = 0
108
+ @random = Random.new
101
109
  end
102
110
 
103
111
  def inspect
@@ -414,15 +422,27 @@ class RedisClient
414
422
  # performed the reload.
415
423
  # Probably in the future we should add a circuit breaker to #reload itself, and stop trying if the cluster is
416
424
  # obviously not working.
417
- wait_start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
425
+ wait_start = obtain_current_time
418
426
  @mutex.synchronize do
419
427
  return if @last_reloaded_at && @last_reloaded_at > wait_start
420
428
 
429
+ if @last_reloaded_at && @reload_times > 1
430
+ # Mitigate load of servers by naive logic. Don't sleep with exponential backoff.
431
+ now = obtain_current_time
432
+ elapsed = @last_reloaded_at + @random.rand(STATE_REFRESH_INTERVAL) * 1_000_000
433
+ return if now < elapsed
434
+ end
435
+
421
436
  r = yield
422
- @last_reloaded_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
437
+ @last_reloaded_at = obtain_current_time
438
+ @reload_times += 1
423
439
  r
424
440
  end
425
441
  end
442
+
443
+ def obtain_current_time
444
+ Process.clock_gettime(Process::CLOCK_MONOTONIC, :microsecond)
445
+ end
426
446
  end
427
447
  end
428
448
  end
@@ -8,6 +8,8 @@ class RedisClient
8
8
  module NodeKey
9
9
  DELIMITER = ':'
10
10
 
11
+ private_constant :DELIMITER
12
+
11
13
  module_function
12
14
 
13
15
  def hashify(node_key)
@@ -9,6 +9,8 @@ class RedisClient
9
9
 
10
10
  EMPTY_STRING = ''
11
11
 
12
+ private_constant :EMPTY_STRING
13
+
12
14
  def initialize
13
15
  @cache = {}
14
16
  @mutex = Mutex.new
@@ -11,7 +11,7 @@ class RedisClient
11
11
  @asking = false
12
12
  end
13
13
 
14
- def watch(keys)
14
+ def watch(keys) # rubocop:disable Metrics/AbcSize
15
15
  slot = find_slot(keys)
16
16
  raise ::RedisClient::Cluster::Transaction::ConsistencyError, "unsafe watch: #{keys.join(' ')}" if slot.nil?
17
17
 
@@ -32,7 +32,13 @@ class RedisClient
32
32
  c.call('UNWATCH')
33
33
  raise
34
34
  end
35
+ rescue ::RedisClient::CommandError => e
36
+ @router.renew_cluster_state if e.message.start_with?('CLUSTERDOWN Hash slot not served')
37
+ raise
35
38
  end
39
+ rescue ::RedisClient::ConnectionError
40
+ @router.renew_cluster_state
41
+ raise
36
42
  end
37
43
  end
38
44
  end
@@ -55,32 +55,48 @@ class RedisClient
55
55
  results = Array.new(commands.size)
56
56
  @pending_reads += size
57
57
  write_multi(commands)
58
+ redirection_indices = stale_cluster_state = first_exception = nil
58
59
 
59
- redirection_indices = nil
60
- first_exception = nil
61
60
  size.times do |index|
62
61
  timeout = timeouts && timeouts[index]
63
- result = read(timeout)
62
+ result = read(connection_timeout(timeout))
64
63
  @pending_reads -= 1
64
+
65
65
  if result.is_a?(::RedisClient::Error)
66
66
  result._set_command(commands[index])
67
+ result._set_config(config)
68
+
67
69
  if result.is_a?(::RedisClient::CommandError) && result.message.start_with?('MOVED', 'ASK')
68
70
  redirection_indices ||= []
69
71
  redirection_indices << index
70
72
  elsif exception
71
73
  first_exception ||= result
72
74
  end
75
+
76
+ stale_cluster_state = true if result.message.start_with?('CLUSTERDOWN Hash slot not served')
73
77
  end
78
+
74
79
  results[index] = result
75
80
  end
76
81
 
77
- raise first_exception if exception && first_exception
78
- return results if redirection_indices.nil?
82
+ if redirection_indices
83
+ err = ::RedisClient::Cluster::Pipeline::RedirectionNeeded.new
84
+ err.replies = results
85
+ err.indices = redirection_indices
86
+ err.first_exception = first_exception
87
+ raise err
88
+ end
89
+
90
+ if stale_cluster_state
91
+ err = ::RedisClient::Cluster::Pipeline::StaleClusterState.new
92
+ err.replies = results
93
+ err.first_exception = first_exception
94
+ raise err
95
+ end
96
+
97
+ raise first_exception if first_exception
79
98
 
80
- err = ::RedisClient::Cluster::Pipeline::RedirectionNeeded.new
81
- err.replies = results
82
- err.indices = redirection_indices
83
- raise err
99
+ results
84
100
  end
85
101
  end
86
102
 
@@ -94,8 +110,12 @@ class RedisClient
94
110
 
95
111
  ReplySizeError = Class.new(::RedisClient::Error)
96
112
 
113
+ class StaleClusterState < ::RedisClient::Error
114
+ attr_accessor :replies, :first_exception
115
+ end
116
+
97
117
  class RedirectionNeeded < ::RedisClient::Error
98
- attr_accessor :replies, :indices
118
+ attr_accessor :replies, :indices, :first_exception
99
119
  end
100
120
 
101
121
  def initialize(router, command_builder, concurrent_worker, exception:, seed: Random.new_seed)
@@ -162,14 +182,18 @@ class RedisClient
162
182
  end
163
183
  end
164
184
 
165
- all_replies = errors = required_redirections = nil
185
+ all_replies = errors = required_redirections = cluster_state_errors = nil
166
186
 
167
187
  work_group.each do |node_key, v|
168
188
  case v
169
189
  when ::RedisClient::Cluster::Pipeline::RedirectionNeeded
170
190
  required_redirections ||= {}
171
191
  required_redirections[node_key] = v
192
+ when ::RedisClient::Cluster::Pipeline::StaleClusterState
193
+ cluster_state_errors ||= {}
194
+ cluster_state_errors[node_key] = v
172
195
  when StandardError
196
+ cluster_state_errors ||= {} if v.is_a?(::RedisClient::ConnectionError)
173
197
  errors ||= {}
174
198
  errors[node_key] = v
175
199
  else
@@ -179,15 +203,25 @@ class RedisClient
179
203
  end
180
204
 
181
205
  work_group.close
206
+ @router.renew_cluster_state if cluster_state_errors
182
207
  raise ::RedisClient::Cluster::ErrorCollection, errors unless errors.nil?
183
208
 
184
209
  required_redirections&.each do |node_key, v|
210
+ raise v.first_exception if v.first_exception
211
+
185
212
  all_replies ||= Array.new(@size)
186
213
  pipeline = @pipelines[node_key]
187
214
  v.indices.each { |i| v.replies[i] = handle_redirection(v.replies[i], pipeline, i) }
188
215
  pipeline.outer_indices.each_with_index { |outer, inner| all_replies[outer] = v.replies[inner] }
189
216
  end
190
217
 
218
+ cluster_state_errors&.each do |node_key, v|
219
+ raise v.first_exception if v.first_exception
220
+
221
+ all_replies ||= Array.new(@size)
222
+ @pipelines[node_key].outer_indices.each_with_index { |outer, inner| all_replies[outer] = v.replies[inner] }
223
+ end
224
+
191
225
  all_replies
192
226
  end
193
227
 
@@ -24,6 +24,8 @@ class RedisClient
24
24
  def close
25
25
  @worker.exit if @worker&.alive?
26
26
  @client.close
27
+ rescue ::RedisClient::ConnectionError
28
+ # ignore
27
29
  end
28
30
 
29
31
  private
@@ -44,32 +46,40 @@ class RedisClient
44
46
 
45
47
  BUF_SIZE = Integer(ENV.fetch('REDIS_CLIENT_PUBSUB_BUF_SIZE', 1024))
46
48
 
49
+ private_constant :BUF_SIZE
50
+
47
51
  def initialize(router, command_builder)
48
52
  @router = router
49
53
  @command_builder = command_builder
50
54
  @queue = SizedQueue.new(BUF_SIZE)
51
55
  @state_dict = {}
56
+ @commands = []
52
57
  end
53
58
 
54
59
  def call(*args, **kwargs)
55
- _call(@command_builder.generate(args, kwargs))
60
+ command = @command_builder.generate(args, kwargs)
61
+ _call(command)
62
+ @commands << command
56
63
  nil
57
64
  end
58
65
 
59
66
  def call_v(command)
60
- _call(@command_builder.generate(command))
67
+ command = @command_builder.generate(command)
68
+ _call(command)
69
+ @commands << command
61
70
  nil
62
71
  end
63
72
 
64
73
  def close
65
74
  @state_dict.each_value(&:close)
66
75
  @state_dict.clear
76
+ @commands.clear
67
77
  @queue.clear
68
78
  @queue.close
69
79
  nil
70
80
  end
71
81
 
72
- def next_event(timeout = nil)
82
+ def next_event(timeout = nil) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity
73
83
  @state_dict.each_value(&:ensure_worker)
74
84
  max_duration = calc_max_duration(timeout)
75
85
  starting = obtain_current_time
@@ -78,6 +88,13 @@ class RedisClient
78
88
  break if max_duration > 0 && obtain_current_time - starting > max_duration
79
89
 
80
90
  case event = @queue.pop(true)
91
+ when ::RedisClient::CommandError
92
+ if event.message.start_with?('MOVED', 'CLUSTERDOWN Hash slot not served')
93
+ @router.renew_cluster_state
94
+ break start_over
95
+ end
96
+
97
+ raise event
81
98
  when StandardError then raise event
82
99
  when Array then break event
83
100
  end
@@ -97,13 +114,26 @@ class RedisClient
97
114
  end
98
115
  end
99
116
 
100
- def call_to_single_state(command)
117
+ def call_to_single_state(command, retry_count: 1)
101
118
  node_key = @router.find_node_key(command)
102
- try_call(node_key, command)
119
+ @state_dict[node_key] ||= State.new(@router.find_node(node_key).pubsub, @queue)
120
+ @state_dict[node_key].call(command)
121
+ rescue ::RedisClient::ConnectionError
122
+ @state_dict[node_key].close
123
+ @state_dict.delete(node_key)
124
+ @router.renew_cluster_state
125
+ retry_count -= 1
126
+ retry_count >= 0 ? retry : raise
103
127
  end
104
128
 
105
129
  def call_to_all_states(command)
106
- @state_dict.each_value { |s| s.call(command) }
130
+ @state_dict.each do |node_key, state|
131
+ state.call(command)
132
+ rescue ::RedisClient::ConnectionError
133
+ @state_dict[node_key].close
134
+ @state_dict.delete(node_key)
135
+ @router.renew_cluster_state
136
+ end
107
137
  end
108
138
 
109
139
  def call_for_sharded_states(command)
@@ -114,24 +144,6 @@ class RedisClient
114
144
  end
115
145
  end
116
146
 
117
- def try_call(node_key, command, retry_count: 1)
118
- add_state(node_key).call(command)
119
- rescue ::RedisClient::CommandError => e
120
- raise if !e.message.start_with?('MOVED') || retry_count <= 0
121
-
122
- # for sharded pub/sub
123
- node_key = e.message.split[2]
124
- retry_count -= 1
125
- retry
126
- end
127
-
128
- def add_state(node_key)
129
- return @state_dict[node_key] if @state_dict.key?(node_key)
130
-
131
- state = State.new(@router.find_node(node_key).pubsub, @queue)
132
- @state_dict[node_key] = state
133
- end
134
-
135
147
  def obtain_current_time
136
148
  Process.clock_gettime(Process::CLOCK_MONOTONIC, :microsecond)
137
149
  end
@@ -139,6 +151,13 @@ class RedisClient
139
151
  def calc_max_duration(timeout)
140
152
  timeout.nil? || timeout < 0 ? 0 : timeout * 1_000_000
141
153
  end
154
+
155
+ def start_over
156
+ @state_dict.each_value(&:close)
157
+ @state_dict.clear
158
+ @commands.each { |command| _call(command) }
159
+ nil
160
+ end
142
161
  end
143
162
  end
144
163
  end
@@ -19,6 +19,8 @@ class RedisClient
19
19
  ZERO_CURSOR_FOR_SCAN = '0'
20
20
  TSF = ->(f, x) { f.nil? ? x : f.call(x) }.curry
21
21
 
22
+ private_constant :ZERO_CURSOR_FOR_SCAN, :TSF
23
+
22
24
  def initialize(config, concurrent_worker, pool: nil, **kwargs)
23
25
  @config = config.dup
24
26
  @original_config = config.dup if config.connect_with_original_config
@@ -27,7 +29,7 @@ class RedisClient
27
29
  @pool = pool
28
30
  @client_kwargs = kwargs
29
31
  @node = ::RedisClient::Cluster::Node.new(concurrent_worker, config: config, pool: pool, **kwargs)
30
- update_cluster_info!
32
+ renew_cluster_state
31
33
  @command = ::RedisClient::Cluster::Command.load(@node.replica_clients.shuffle, slow_command_timeout: config.slow_command_timeout)
32
34
  @command_builder = @config.command_builder
33
35
  end
@@ -66,15 +68,21 @@ class RedisClient
66
68
  rescue ::RedisClient::CircuitBreaker::OpenCircuitError
67
69
  raise
68
70
  rescue ::RedisClient::Cluster::Node::ReloadNeeded
69
- update_cluster_info!
71
+ renew_cluster_state
70
72
  raise ::RedisClient::Cluster::NodeMightBeDown
73
+ rescue ::RedisClient::ConnectionError
74
+ renew_cluster_state
75
+ raise
76
+ rescue ::RedisClient::CommandError => e
77
+ renew_cluster_state if e.message.start_with?('CLUSTERDOWN Hash slot not served')
78
+ raise
71
79
  rescue ::RedisClient::Cluster::ErrorCollection => e
72
80
  raise if e.errors.any?(::RedisClient::CircuitBreaker::OpenCircuitError)
73
81
 
74
- update_cluster_info! if e.errors.values.any? do |err|
82
+ renew_cluster_state if e.errors.values.any? do |err|
75
83
  next false if ::RedisClient::Cluster::ErrorIdentification.identifiable?(err) && @node.none? { |c| ::RedisClient::Cluster::ErrorIdentification.client_owns_error?(err, c) }
76
84
 
77
- err.message.start_with?('CLUSTERDOWN Hash slot not served')
85
+ err.message.start_with?('CLUSTERDOWN Hash slot not served') || err.is_a?(::RedisClient::ConnectionError)
78
86
  end
79
87
 
80
88
  raise
@@ -105,32 +113,29 @@ class RedisClient
105
113
  rescue ::RedisClient::CommandError => e
106
114
  raise unless ::RedisClient::Cluster::ErrorIdentification.client_owns_error?(e, node)
107
115
 
116
+ retry_count -= 1
108
117
  if e.message.start_with?('MOVED')
109
118
  node = assign_redirection_node(e.message)
110
- retry_count -= 1
111
119
  retry if retry_count >= 0
112
120
  elsif e.message.start_with?('ASK')
113
121
  node = assign_asking_node(e.message)
114
- retry_count -= 1
115
122
  if retry_count >= 0
116
123
  node.call('ASKING')
117
124
  retry
118
125
  end
119
126
  elsif e.message.start_with?('CLUSTERDOWN Hash slot not served')
120
- update_cluster_info!
121
- retry_count -= 1
127
+ renew_cluster_state
122
128
  retry if retry_count >= 0
123
129
  end
130
+
124
131
  raise
125
132
  rescue ::RedisClient::ConnectionError => e
126
133
  raise unless ::RedisClient::Cluster::ErrorIdentification.client_owns_error?(e, node)
127
134
 
128
- update_cluster_info!
129
-
130
- raise if retry_count <= 0
131
-
132
135
  retry_count -= 1
133
- retry
136
+ renew_cluster_state
137
+ retry if retry_count >= 0
138
+ raise
134
139
  end
135
140
 
136
141
  def scan(*command, seed: nil, **kwargs) # rubocop:disable Metrics/AbcSize
@@ -155,11 +160,16 @@ class RedisClient
155
160
  client_index += 1 if result_cursor == 0
156
161
 
157
162
  [((result_cursor << 8) + client_index).to_s, result_keys]
163
+ rescue ::RedisClient::ConnectionError
164
+ renew_cluster_state
165
+ raise
158
166
  end
159
167
 
160
168
  def assign_node(command)
161
- node_key = find_node_key(command)
162
- find_node(node_key)
169
+ handle_node_reload_error do
170
+ node_key = find_node_key(command)
171
+ @node.find_by(node_key)
172
+ end
163
173
  end
164
174
 
165
175
  def find_node_key_by_key(key, seed: nil, primary: false)
@@ -172,8 +182,10 @@ class RedisClient
172
182
  end
173
183
 
174
184
  def find_primary_node_by_slot(slot)
175
- node_key = @node.find_node_key_of_primary(slot)
176
- find_node(node_key)
185
+ handle_node_reload_error do
186
+ node_key = @node.find_node_key_of_primary(slot)
187
+ @node.find_by(node_key)
188
+ end
177
189
  end
178
190
 
179
191
  def find_node_key(command, seed: nil)
@@ -198,14 +210,8 @@ class RedisClient
198
210
  ::RedisClient::Cluster::KeySlotConverter.convert(key)
199
211
  end
200
212
 
201
- def find_node(node_key, retry_count: 3)
202
- @node.find_by(node_key)
203
- rescue ::RedisClient::Cluster::Node::ReloadNeeded
204
- raise ::RedisClient::Cluster::NodeMightBeDown if retry_count <= 0
205
-
206
- update_cluster_info!
207
- retry_count -= 1
208
- retry
213
+ def find_node(node_key)
214
+ handle_node_reload_error { @node.find_by(node_key) }
209
215
  end
210
216
 
211
217
  def command_exists?(name)
@@ -216,35 +222,37 @@ class RedisClient
216
222
  _, slot, node_key = err_msg.split
217
223
  slot = slot.to_i
218
224
  @node.update_slot(slot, node_key)
219
- find_node(node_key)
225
+ handle_node_reload_error { @node.find_by(node_key) }
220
226
  end
221
227
 
222
228
  def assign_asking_node(err_msg)
223
229
  _, _, node_key = err_msg.split
224
- find_node(node_key)
230
+ handle_node_reload_error { @node.find_by(node_key) }
225
231
  end
226
232
 
227
233
  def node_keys
228
234
  @node.node_keys
229
235
  end
230
236
 
237
+ def renew_cluster_state
238
+ @node.reload!
239
+ end
240
+
231
241
  def close
232
242
  @node.each(&:close)
233
243
  end
234
244
 
235
245
  private
236
246
 
237
- def send_wait_command(method, command, args, retry_count: 3, &block) # rubocop:disable Metrics/AbcSize
247
+ def send_wait_command(method, command, args, retry_count: 1, &block) # rubocop:disable Metrics/AbcSize
238
248
  @node.call_primaries(method, command, args).select { |r| r.is_a?(Integer) }.sum.then(&TSF.call(block))
239
249
  rescue ::RedisClient::Cluster::ErrorCollection => e
240
250
  raise if e.errors.any?(::RedisClient::CircuitBreaker::OpenCircuitError)
241
251
  raise if retry_count <= 0
242
- raise if e.errors.values.none? do |err|
243
- err.message.include?('WAIT cannot be used with replica instances')
244
- end
252
+ raise if e.errors.values.none? { |err| err.message.include?('WAIT cannot be used with replica instances') }
245
253
 
246
- update_cluster_info!
247
254
  retry_count -= 1
255
+ renew_cluster_state
248
256
  retry
249
257
  end
250
258
 
@@ -273,7 +281,7 @@ class RedisClient
273
281
  end
274
282
  end
275
283
 
276
- def send_cluster_command(method, command, args, &block)
284
+ def send_cluster_command(method, command, args, &block) # rubocop:disable Metrics/AbcSize
277
285
  case subcommand = ::RedisClient::Cluster::NormalizedCmdName.instance.get_by_subcommand(command)
278
286
  when 'addslots', 'delslots', 'failover', 'forget', 'meet', 'replicate',
279
287
  'reset', 'set-config-epoch', 'setslot'
@@ -282,7 +290,10 @@ class RedisClient
282
290
  when 'getkeysinslot'
283
291
  raise ArgumentError, command.join(' ') if command.size != 4
284
292
 
285
- find_node(@node.find_node_key_of_replica(command[2])).public_send(method, *args, command, &block)
293
+ handle_node_reload_error do
294
+ node_key = @node.find_node_key_of_replica(command[2])
295
+ @node.find_by(node_key).public_send(method, *args, command, &block)
296
+ end
286
297
  else assign_node(command).public_send(method, *args, command, &block)
287
298
  end
288
299
  end
@@ -335,6 +346,8 @@ class RedisClient
335
346
  'del' => ['del', 1].freeze
336
347
  }.freeze
337
348
 
349
+ private_constant :MULTIPLE_KEYS_COMMAND_TO_SINGLE
350
+
338
351
  def send_multiple_keys_command(cmd, method, command, args, &block) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
339
352
  # This implementation is prioritized performance rather than readability or so.
340
353
  single_key_cmd, keys_step = MULTIPLE_KEYS_COMMAND_TO_SINGLE.fetch(cmd)
@@ -367,8 +380,14 @@ class RedisClient
367
380
  block_given? ? yield(result) : result
368
381
  end
369
382
 
370
- def update_cluster_info!
371
- @node.reload!
383
+ def handle_node_reload_error(retry_count: 1)
384
+ yield
385
+ rescue ::RedisClient::Cluster::Node::ReloadNeeded
386
+ raise ::RedisClient::Cluster::NodeMightBeDown if retry_count <= 0
387
+
388
+ retry_count -= 1
389
+ renew_cluster_state
390
+ retry
372
391
  end
373
392
  end
374
393
  end
@@ -7,9 +7,12 @@ class RedisClient
7
7
  class Cluster
8
8
  class Transaction
9
9
  ConsistencyError = Class.new(::RedisClient::Error)
10
+
10
11
  MAX_REDIRECTION = 2
11
12
  EMPTY_ARRAY = [].freeze
12
13
 
14
+ private_constant :MAX_REDIRECTION, :EMPTY_ARRAY
15
+
13
16
  def initialize(router, command_builder, node: nil, slot: nil, asking: false)
14
17
  @router = router
15
18
  @command_builder = command_builder
@@ -119,7 +122,7 @@ class RedisClient
119
122
  end
120
123
  end
121
124
 
122
- def send_pipeline(client, redirect:)
125
+ def send_pipeline(client, redirect:) # rubocop:disable Metrics/AbcSize
123
126
  replies = client.ensure_connected_cluster_scoped(retryable: @retryable) do |connection|
124
127
  commands = @pipeline._commands
125
128
  client.middlewares.call_pipelined(commands, client.config) do
@@ -135,6 +138,9 @@ class RedisClient
135
138
  return if replies.last.nil?
136
139
 
137
140
  coerce_results!(replies.last)
141
+ rescue ::RedisClient::ConnectionError
142
+ @router.renew_cluster_state if @watching_slot.nil?
143
+ raise
138
144
  end
139
145
 
140
146
  def coerce_results!(results, offset: 1)
@@ -164,6 +170,9 @@ class RedisClient
164
170
  elsif err.message.start_with?('ASK')
165
171
  node = @router.assign_asking_node(err.message)
166
172
  try_asking(node) ? send_transaction(node, redirect: redirect - 1) : err
173
+ elsif err.message.start_with?('CLUSTERDOWN Hash slot not served')
174
+ @router.renew_cluster_state if @watching_slot.nil?
175
+ raise err
167
176
  else
168
177
  raise err
169
178
  end
@@ -11,6 +11,8 @@ class RedisClient
11
11
  class Cluster
12
12
  ZERO_CURSOR_FOR_SCAN = '0'
13
13
 
14
+ private_constant :ZERO_CURSOR_FOR_SCAN
15
+
14
16
  attr_reader :config
15
17
 
16
18
  def initialize(config, pool: nil, concurrency: nil, **kwargs)
@@ -23,6 +23,10 @@ class RedisClient
23
23
  # It affects to strike a balance between load and stability in initialization or changed states.
24
24
  MAX_STARTUP_SAMPLE = Integer(ENV.fetch('REDIS_CLIENT_MAX_STARTUP_SAMPLE', 3))
25
25
 
26
+ private_constant :DEFAULT_HOST, :DEFAULT_PORT, :DEFAULT_SCHEME, :SECURE_SCHEME, :DEFAULT_NODES,
27
+ :VALID_SCHEMES, :VALID_NODES_KEYS, :MERGE_CONFIG_KEYS, :IGNORE_GENERIC_CONFIG_KEYS,
28
+ :MAX_WORKERS, :SLOW_COMMAND_TIMEOUT, :MAX_STARTUP_SAMPLE
29
+
26
30
  InvalidClientConfigError = Class.new(::RedisClient::Error)
27
31
 
28
32
  attr_reader :command_builder, :client_config, :replica_affinity, :slow_command_timeout,
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.0
4
+ version: 0.11.1
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-08-19 00:00:00.000000000 Z
11
+ date: 2024-09-26 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.11
80
+ rubygems_version: 3.5.16
81
81
  signing_key:
82
82
  specification_version: 4
83
83
  summary: A Redis cluster client for Ruby