redis-cluster-client 0.11.0 → 0.13.5

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.
@@ -2,6 +2,7 @@
2
2
 
3
3
  require 'redis_client'
4
4
  require 'redis_client/cluster/errors'
5
+ require 'redis_client/cluster/noop_command_builder'
5
6
  require 'redis_client/connection_mixin'
6
7
  require 'redis_client/middlewares'
7
8
  require 'redis_client/pooled'
@@ -55,32 +56,48 @@ class RedisClient
55
56
  results = Array.new(commands.size)
56
57
  @pending_reads += size
57
58
  write_multi(commands)
59
+ redirection_indices = stale_cluster_state = first_exception = nil
58
60
 
59
- redirection_indices = nil
60
- first_exception = nil
61
61
  size.times do |index|
62
62
  timeout = timeouts && timeouts[index]
63
- result = read(timeout)
63
+ result = read(connection_timeout(timeout))
64
64
  @pending_reads -= 1
65
+
65
66
  if result.is_a?(::RedisClient::Error)
66
67
  result._set_command(commands[index])
68
+ result._set_config(config)
69
+
67
70
  if result.is_a?(::RedisClient::CommandError) && result.message.start_with?('MOVED', 'ASK')
68
71
  redirection_indices ||= []
69
72
  redirection_indices << index
70
73
  elsif exception
71
74
  first_exception ||= result
72
75
  end
76
+
77
+ stale_cluster_state = true if result.message.start_with?('CLUSTERDOWN')
73
78
  end
79
+
74
80
  results[index] = result
75
81
  end
76
82
 
77
- raise first_exception if exception && first_exception
78
- return results if redirection_indices.nil?
83
+ if redirection_indices
84
+ err = ::RedisClient::Cluster::Pipeline::RedirectionNeeded.new
85
+ err.replies = results
86
+ err.indices = redirection_indices
87
+ err.first_exception = first_exception
88
+ raise err
89
+ end
90
+
91
+ if stale_cluster_state
92
+ err = ::RedisClient::Cluster::Pipeline::StaleClusterState.new
93
+ err.replies = results
94
+ err.first_exception = first_exception
95
+ raise err
96
+ end
97
+
98
+ raise first_exception if first_exception
79
99
 
80
- err = ::RedisClient::Cluster::Pipeline::RedirectionNeeded.new
81
- err.replies = results
82
- err.indices = redirection_indices
83
- raise err
100
+ results
84
101
  end
85
102
  end
86
103
 
@@ -92,10 +109,14 @@ class RedisClient
92
109
  end
93
110
  end
94
111
 
95
- ReplySizeError = Class.new(::RedisClient::Error)
112
+ ReplySizeError = Class.new(::RedisClient::Cluster::Error)
96
113
 
97
- class RedirectionNeeded < ::RedisClient::Error
98
- attr_accessor :replies, :indices
114
+ class StaleClusterState < ::RedisClient::Cluster::Error
115
+ attr_accessor :replies, :first_exception
116
+ end
117
+
118
+ class RedirectionNeeded < ::RedisClient::Cluster::Error
119
+ attr_accessor :replies, :indices, :first_exception
99
120
  end
100
121
 
101
122
  def initialize(router, command_builder, concurrent_worker, exception:, seed: Random.new_seed)
@@ -162,14 +183,18 @@ class RedisClient
162
183
  end
163
184
  end
164
185
 
165
- all_replies = errors = required_redirections = nil
186
+ all_replies = errors = required_redirections = cluster_state_errors = nil
166
187
 
167
188
  work_group.each do |node_key, v|
168
189
  case v
169
190
  when ::RedisClient::Cluster::Pipeline::RedirectionNeeded
170
191
  required_redirections ||= {}
171
192
  required_redirections[node_key] = v
193
+ when ::RedisClient::Cluster::Pipeline::StaleClusterState
194
+ cluster_state_errors ||= {}
195
+ cluster_state_errors[node_key] = v
172
196
  when StandardError
197
+ cluster_state_errors ||= {} if v.is_a?(::RedisClient::ConnectionError)
173
198
  errors ||= {}
174
199
  errors[node_key] = v
175
200
  else
@@ -179,15 +204,25 @@ class RedisClient
179
204
  end
180
205
 
181
206
  work_group.close
182
- raise ::RedisClient::Cluster::ErrorCollection, errors unless errors.nil?
207
+ @router.renew_cluster_state if cluster_state_errors
208
+ raise ::RedisClient::Cluster::ErrorCollection.with_errors(errors).with_config(@router.config) unless errors.nil?
183
209
 
184
210
  required_redirections&.each do |node_key, v|
211
+ raise v.first_exception if v.first_exception
212
+
185
213
  all_replies ||= Array.new(@size)
186
214
  pipeline = @pipelines[node_key]
187
215
  v.indices.each { |i| v.replies[i] = handle_redirection(v.replies[i], pipeline, i) }
188
216
  pipeline.outer_indices.each_with_index { |outer, inner| all_replies[outer] = v.replies[inner] }
189
217
  end
190
218
 
219
+ cluster_state_errors&.each do |node_key, v|
220
+ raise v.first_exception if v.first_exception
221
+
222
+ all_replies ||= Array.new(@size)
223
+ @pipelines[node_key].outer_indices.each_with_index { |outer, inner| all_replies[outer] = v.replies[inner] }
224
+ end
225
+
191
226
  all_replies
192
227
  end
193
228
 
@@ -195,7 +230,7 @@ class RedisClient
195
230
 
196
231
  def append_pipeline(node_key)
197
232
  @pipelines ||= {}
198
- @pipelines[node_key] ||= ::RedisClient::Cluster::Pipeline::Extended.new(@command_builder)
233
+ @pipelines[node_key] ||= ::RedisClient::Cluster::Pipeline::Extended.new(::RedisClient::Cluster::NoopCommandBuilder)
199
234
  @pipelines[node_key].add_outer_index(@size)
200
235
  @size += 1
201
236
  @pipelines[node_key]
@@ -248,14 +283,14 @@ class RedisClient
248
283
  args = timeout.nil? ? [] : [timeout]
249
284
 
250
285
  if block.nil?
251
- @router.try_send(node, method, command, args)
286
+ @router.send_command_to_node(node, method, command, args)
252
287
  else
253
- @router.try_send(node, method, command, args, &block)
288
+ @router.send_command_to_node(node, method, command, args, &block)
254
289
  end
255
290
  end
256
291
 
257
292
  def try_asking(node)
258
- node.call('ASKING') == 'OK'
293
+ node.call('asking') == 'OK'
259
294
  rescue StandardError
260
295
  false
261
296
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'redis_client'
4
- require 'redis_client/cluster/normalized_cmd_name'
4
+ require 'redis_client/cluster/errors'
5
5
 
6
6
  class RedisClient
7
7
  class Cluster
@@ -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
@@ -32,11 +34,15 @@ class RedisClient
32
34
  # Ruby VM allocates 1 MB memory as a stack for a thread.
33
35
  # It is a fixed size but we can modify the size with some environment variables.
34
36
  # So it consumes memory 1 MB multiplied a number of workers.
35
- Thread.new(client, queue) do |pubsub, q|
37
+ Thread.new(client, queue, nil) do |pubsub, q, prev_err|
36
38
  loop do
37
39
  q << pubsub.next_event
40
+ prev_err = nil
38
41
  rescue StandardError => e
42
+ next sleep 0.005 if e.instance_of?(prev_err.class) && e.message == prev_err&.message
43
+
39
44
  q << e
45
+ prev_err = e
40
46
  end
41
47
  end
42
48
  end
@@ -44,32 +50,40 @@ class RedisClient
44
50
 
45
51
  BUF_SIZE = Integer(ENV.fetch('REDIS_CLIENT_PUBSUB_BUF_SIZE', 1024))
46
52
 
53
+ private_constant :BUF_SIZE
54
+
47
55
  def initialize(router, command_builder)
48
56
  @router = router
49
57
  @command_builder = command_builder
50
58
  @queue = SizedQueue.new(BUF_SIZE)
51
59
  @state_dict = {}
60
+ @commands = []
52
61
  end
53
62
 
54
63
  def call(*args, **kwargs)
55
- _call(@command_builder.generate(args, kwargs))
64
+ command = @command_builder.generate(args, kwargs)
65
+ _call(command)
66
+ @commands << command
56
67
  nil
57
68
  end
58
69
 
59
70
  def call_v(command)
60
- _call(@command_builder.generate(command))
71
+ command = @command_builder.generate(command)
72
+ _call(command)
73
+ @commands << command
61
74
  nil
62
75
  end
63
76
 
64
77
  def close
65
78
  @state_dict.each_value(&:close)
66
79
  @state_dict.clear
80
+ @commands.clear
67
81
  @queue.clear
68
82
  @queue.close
69
83
  nil
70
84
  end
71
85
 
72
- def next_event(timeout = nil)
86
+ def next_event(timeout = nil) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity
73
87
  @state_dict.each_value(&:ensure_worker)
74
88
  max_duration = calc_max_duration(timeout)
75
89
  starting = obtain_current_time
@@ -78,6 +92,11 @@ class RedisClient
78
92
  break if max_duration > 0 && obtain_current_time - starting > max_duration
79
93
 
80
94
  case event = @queue.pop(true)
95
+ when ::RedisClient::CommandError
96
+ raise event unless event.message.start_with?('MOVED', 'CLUSTERDOWN')
97
+
98
+ break start_over
99
+ when ::RedisClient::ConnectionError then break start_over
81
100
  when StandardError then raise event
82
101
  when Array then break event
83
102
  end
@@ -88,22 +107,39 @@ class RedisClient
88
107
 
89
108
  private
90
109
 
91
- def _call(command)
92
- case ::RedisClient::Cluster::NormalizedCmdName.instance.get_by_command(command)
93
- when 'subscribe', 'psubscribe', 'ssubscribe' then call_to_single_state(command)
94
- when 'unsubscribe', 'punsubscribe' then call_to_all_states(command)
95
- when 'sunsubscribe' then call_for_sharded_states(command)
96
- else call_to_single_state(command)
110
+ def _call(command) # rubocop:disable Metrics/AbcSize
111
+ if command.first.casecmp('subscribe').zero?
112
+ call_to_single_state(command)
113
+ elsif command.first.casecmp('psubscribe').zero?
114
+ call_to_single_state(command)
115
+ elsif command.first.casecmp('ssubscribe').zero?
116
+ call_to_single_state(command)
117
+ elsif command.first.casecmp('unsubscribe').zero?
118
+ call_to_all_states(command)
119
+ elsif command.first.casecmp('punsubscribe').zero?
120
+ call_to_all_states(command)
121
+ elsif command.first.casecmp('sunsubscribe').zero?
122
+ call_for_sharded_states(command)
123
+ else
124
+ call_to_single_state(command)
97
125
  end
98
126
  end
99
127
 
100
128
  def call_to_single_state(command)
101
129
  node_key = @router.find_node_key(command)
102
- try_call(node_key, command)
130
+
131
+ handle_connection_error(node_key) do
132
+ @state_dict[node_key] ||= State.new(@router.find_node(node_key).pubsub, @queue)
133
+ @state_dict[node_key].call(command)
134
+ end
103
135
  end
104
136
 
105
137
  def call_to_all_states(command)
106
- @state_dict.each_value { |s| s.call(command) }
138
+ @state_dict.each do |node_key, state|
139
+ handle_connection_error(node_key, ignore: true) do
140
+ state.call(command)
141
+ end
142
+ end
107
143
  end
108
144
 
109
145
  def call_for_sharded_states(command)
@@ -114,24 +150,6 @@ class RedisClient
114
150
  end
115
151
  end
116
152
 
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
153
  def obtain_current_time
136
154
  Process.clock_gettime(Process::CLOCK_MONOTONIC, :microsecond)
137
155
  end
@@ -139,6 +157,27 @@ class RedisClient
139
157
  def calc_max_duration(timeout)
140
158
  timeout.nil? || timeout < 0 ? 0 : timeout * 1_000_000
141
159
  end
160
+
161
+ def handle_connection_error(node_key, ignore: false)
162
+ yield
163
+ rescue ::RedisClient::ConnectionError
164
+ @state_dict[node_key]&.close
165
+ @state_dict.delete(node_key)
166
+ @router.renew_cluster_state
167
+ raise unless ignore
168
+ end
169
+
170
+ def start_over
171
+ loop do
172
+ @router.renew_cluster_state
173
+ @state_dict.each_value(&:close)
174
+ @state_dict.clear
175
+ @commands.each { |command| _call(command) }
176
+ break
177
+ rescue ::RedisClient::ConnectionError, ::RedisClient::Cluster::NodeMightBeDown
178
+ sleep 1.0
179
+ end
180
+ end
142
181
  end
143
182
  end
144
183
  end