redis-cluster-client 0.10.0 → 0.11.1

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: f88b4bd96fd24566377d49a45a06eaa5fc8029749462995e746074979ba25e70
4
- data.tar.gz: a5ecd1339399ba2dda3c004c1cf1bbb2e2510c2f963f0f69afcf71544a8b530c
3
+ metadata.gz: cd66efdf343224572aeffdbfdd4cb96c8189bd45183c0a3934e6dd1964a35655
4
+ data.tar.gz: 66805c538066aabab413b767f0b9e8756c1eff8920f410a665939f18d69c0621
5
5
  SHA512:
6
- metadata.gz: 81b1776f48d1b84866c21e0f00ee644df2f077e06536a6af2b305e50104296b7dabeb9732cce51e0aaca6ce5111a406b21b14db8c114d56e2b3f8f4b7701f2f7
7
- data.tar.gz: b2ecb424e00825a115d15808814ee58a7afc06205cf5ca1e4732ca7189a6b9d42d13597974fa8674ca6f1102aa13641d8b7b25883bcea9ede743e2df3dba4814
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
@@ -13,9 +13,6 @@ class RedisClient
13
13
  class Node
14
14
  include Enumerable
15
15
 
16
- # It affects to strike a balance between load and stability in initialization or changed states.
17
- MAX_STARTUP_SAMPLE = Integer(ENV.fetch('REDIS_CLIENT_MAX_STARTUP_SAMPLE', 3))
18
-
19
16
  # less memory consumption, but slow
20
17
  USE_CHAR_ARRAY_SLOT = Integer(ENV.fetch('REDIS_CLIENT_USE_CHAR_ARRAY_SLOT', 1)) == 1
21
18
 
@@ -26,6 +23,10 @@ class RedisClient
26
23
  ROLE_FLAGS = %w[master slave].freeze
27
24
  EMPTY_ARRAY = [].freeze
28
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
29
30
 
30
31
  ReloadNeeded = Class.new(::RedisClient::Error)
31
32
 
@@ -48,6 +49,8 @@ class RedisClient
48
49
  BASE = ''
49
50
  PADDING = '0'
50
51
 
52
+ private_constant :BASE, :PADDING
53
+
51
54
  def initialize(size, elements)
52
55
  @elements = elements
53
56
  @string = String.new(BASE, encoding: Encoding::BINARY, capacity: size)
@@ -101,6 +104,8 @@ class RedisClient
101
104
  @config = config
102
105
  @mutex = Mutex.new
103
106
  @last_reloaded_at = nil
107
+ @reload_times = 0
108
+ @random = Random.new
104
109
  end
105
110
 
106
111
  def inspect
@@ -197,7 +202,7 @@ class RedisClient
197
202
 
198
203
  def reload!
199
204
  with_reload_lock do
200
- with_startup_clients(MAX_STARTUP_SAMPLE) do |startup_clients|
205
+ with_startup_clients(@config.max_startup_sample) do |startup_clients|
201
206
  @node_info = refetch_node_info_list(startup_clients)
202
207
  @node_configs = @node_info.to_h do |node_info|
203
208
  [node_info.node_key, @config.client_config_for_node(node_info.node_key)]
@@ -417,15 +422,27 @@ class RedisClient
417
422
  # performed the reload.
418
423
  # Probably in the future we should add a circuit breaker to #reload itself, and stop trying if the cluster is
419
424
  # obviously not working.
420
- wait_start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
425
+ wait_start = obtain_current_time
421
426
  @mutex.synchronize do
422
427
  return if @last_reloaded_at && @last_reloaded_at > wait_start
423
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
+
424
436
  r = yield
425
- @last_reloaded_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
437
+ @last_reloaded_at = obtain_current_time
438
+ @reload_times += 1
426
439
  r
427
440
  end
428
441
  end
442
+
443
+ def obtain_current_time
444
+ Process.clock_gettime(Process::CLOCK_MONOTONIC, :microsecond)
445
+ end
429
446
  end
430
447
  end
431
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)
@@ -20,13 +20,19 @@ class RedisClient
20
20
  MAX_WORKERS = Integer(ENV.fetch('REDIS_CLIENT_MAX_THREADS', 5))
21
21
  # It's used with slow queries of fetching meta data like CLUSTER NODES, COMMAND and so on.
22
22
  SLOW_COMMAND_TIMEOUT = Float(ENV.fetch('REDIS_CLIENT_SLOW_COMMAND_TIMEOUT', -1))
23
+ # It affects to strike a balance between load and stability in initialization or changed states.
24
+ MAX_STARTUP_SAMPLE = Integer(ENV.fetch('REDIS_CLIENT_MAX_STARTUP_SAMPLE', 3))
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
23
29
 
24
30
  InvalidClientConfigError = Class.new(::RedisClient::Error)
25
31
 
26
32
  attr_reader :command_builder, :client_config, :replica_affinity, :slow_command_timeout,
27
- :connect_with_original_config, :startup_nodes
33
+ :connect_with_original_config, :startup_nodes, :max_startup_sample
28
34
 
29
- def initialize(
35
+ def initialize( # rubocop:disable Metrics/ParameterLists
30
36
  nodes: DEFAULT_NODES,
31
37
  replica: false,
32
38
  replica_affinity: :random,
@@ -36,6 +42,7 @@ class RedisClient
36
42
  client_implementation: ::RedisClient::Cluster, # for redis gem
37
43
  slow_command_timeout: SLOW_COMMAND_TIMEOUT,
38
44
  command_builder: ::RedisClient::CommandBuilder,
45
+ max_startup_sample: MAX_STARTUP_SAMPLE,
39
46
  **client_config
40
47
  )
41
48
 
@@ -51,6 +58,7 @@ class RedisClient
51
58
  @connect_with_original_config = connect_with_original_config
52
59
  @client_implementation = client_implementation
53
60
  @slow_command_timeout = slow_command_timeout
61
+ @max_startup_sample = max_startup_sample
54
62
  end
55
63
 
56
64
  def inspect
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.10.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-05-13 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.9
80
+ rubygems_version: 3.5.16
81
81
  signing_key:
82
82
  specification_version: 4
83
83
  summary: A Redis cluster client for Ruby