redis-cluster-client 0.0.5 → 0.0.8

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: 6fce5f4a15cfcab3d7cca37052e9f85ad45abcee8f4de9e458fc560a01382842
4
- data.tar.gz: 2da9fb7a3ae8836dd7606fbfd05e33f423529716493d3bb8fccd6d01bae10bc3
3
+ metadata.gz: d13300ba8c7f294d3e4c86932d13775ad22060c172900c01d521b1ed8c2278d6
4
+ data.tar.gz: d5b457978bb312a718ba57b48b56fac1ceb07a463889835c8190794007ecbf7d
5
5
  SHA512:
6
- metadata.gz: fd4b929261fe0a50f4b13fe4963550e7b8c1197ee4f813a464b309140b29d7cc75a95259f100481d2f7e5a958a98608aca980e8308dca125200c1acb4e506e06
7
- data.tar.gz: b41237f574ca12b3826e8d869f3ea3251a0acb41961fb009efe7bb5d2f07517b17cae80c5161da91603b4586a63beef11ba618b6386cfd684012cc7fa09752d1
6
+ metadata.gz: 37fb06428eed5a9cca5b42d27c034f057dd7d6f0c5d0c17e4b42c60a8fa3711127c5ed81f06e0565331f64aa762ccc30ee6674fb541e56a8b322ee50ab7a75ce
7
+ data.tar.gz: 06d46f93c8c7b7116378d2ef9e4380ccce3aac74c071b709cbacd677ec0b302302b52160c04b2c3523771f69446b96d84e8b4f729765073386c5e5f1f3f27f5b
@@ -68,12 +68,14 @@ class RedisClient
68
68
  @details.fetch(name).fetch(key)
69
69
  end
70
70
 
71
- def determine_first_key_position(command) # rubocop:disable Metrics/CyclomaticComplexity
71
+ def determine_first_key_position(command) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength
72
72
  case command&.flatten&.first.to_s.downcase
73
- when 'eval', 'evalsha', 'migrate', 'zinterstore', 'zunionstore' then 3
73
+ when 'eval', 'evalsha', 'zinterstore', 'zunionstore' then 3
74
74
  when 'object' then 2
75
75
  when 'memory'
76
76
  command[1].to_s.casecmp('usage').zero? ? 2 : 0
77
+ when 'migrate'
78
+ command[3] == '""' ? determine_optional_key_position(command, 'keys') : 3
77
79
  when 'xread', 'xreadgroup'
78
80
  determine_optional_key_position(command, 'streams')
79
81
  else
@@ -29,7 +29,7 @@ class RedisClient
29
29
  end
30
30
 
31
31
  # Raised when error occurs on any node of cluster.
32
- class CommandErrorCollection < ::RedisClient::Error
32
+ class ErrorCollection < ::RedisClient::Error
33
33
  attr_reader :errors
34
34
 
35
35
  def initialize(errors)
@@ -51,5 +51,15 @@ class RedisClient
51
51
  super("Cluster client doesn't know which node the #{command} command should be sent to.")
52
52
  end
53
53
  end
54
+
55
+ class NodeMightBeDown < ::RedisClient::Error
56
+ def initialize(_ = '')
57
+ super(
58
+ 'The client is trying to fetch the latest cluster state '\
59
+ 'because a subset of nodes might be down. '\
60
+ 'It might continue to raise errors for a while.'
61
+ )
62
+ end
63
+ end
54
64
  end
55
65
  end
@@ -43,6 +43,8 @@ class RedisClient
43
43
  module_function
44
44
 
45
45
  def convert(key)
46
+ return nil if key.nil?
47
+
46
48
  crc = 0
47
49
  key.each_byte do |b|
48
50
  crc = ((crc << 8) & 0xffff) ^ XMODEM_CRC16_LOOKUP[((crc >> 8) ^ b) & 0xff]
@@ -12,6 +12,7 @@ class RedisClient
12
12
  SLOT_SIZE = 16_384
13
13
  MIN_SLOT = 0
14
14
  MAX_SLOT = SLOT_SIZE - 1
15
+ MAX_STARTUP_SAMPLE = 37
15
16
  IGNORE_GENERIC_CONFIG_KEYS = %i[url host port path].freeze
16
17
 
17
18
  ReloadNeeded = Class.new(::RedisClient::Error)
@@ -32,19 +33,33 @@ class RedisClient
32
33
  end
33
34
 
34
35
  class << self
35
- def load_info(options, **kwargs)
36
- startup_nodes = ::RedisClient::Cluster::Node.new(options, **kwargs)
37
-
38
- errors = startup_nodes.map do |n|
39
- reply = n.call('CLUSTER', 'NODES')
40
- return parse_node_info(reply)
41
- rescue ::RedisClient::ConnectionError, ::RedisClient::CommandError => e
42
- e
36
+ def load_info(options, **kwargs) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
37
+ startup_size = options.size > MAX_STARTUP_SAMPLE ? MAX_STARTUP_SAMPLE : options.size
38
+ node_info_list = Array.new(startup_size)
39
+ errors = Array.new(startup_size)
40
+ startup_options = options.to_a.sample(MAX_STARTUP_SAMPLE).to_h
41
+ startup_nodes = ::RedisClient::Cluster::Node.new(startup_options, **kwargs)
42
+ threads = startup_nodes.each_with_index.map do |raw_client, idx|
43
+ Thread.new(raw_client, idx) do |cli, i|
44
+ Thread.pass
45
+ reply = cli.call('CLUSTER', 'NODES')
46
+ node_info_list[i] = parse_node_info(reply)
47
+ rescue StandardError => e
48
+ errors[i] = e
49
+ ensure
50
+ cli&.close
51
+ end
52
+ end
53
+ threads.each(&:join)
54
+ raise ::RedisClient::Cluster::InitialSetupError, errors if node_info_list.all?(&:nil?)
55
+
56
+ grouped = node_info_list.compact.group_by do |rows|
57
+ rows.sort_by { |row| row[:id] }
58
+ .map { |r| "#{r[:id]}#{r[:node_key]}#{r[:role]}#{r[:primary_id]}#{r[:config_epoch]}" }
59
+ .join
43
60
  end
44
61
 
45
- raise ::RedisClient::Cluster::InitialSetupError, errors
46
- ensure
47
- startup_nodes&.each(&:close)
62
+ grouped.max_by { |_, v| v.size }[1].first
48
63
  end
49
64
 
50
65
  private
@@ -109,32 +124,58 @@ class RedisClient
109
124
  end
110
125
 
111
126
  def find_by(node_key)
127
+ raise ReloadNeeded if node_key.nil? || !@clients.key?(node_key)
128
+
112
129
  @clients.fetch(node_key)
113
- rescue KeyError
114
- raise ReloadNeeded
115
130
  end
116
131
 
117
132
  def call_all(method, *command, **kwargs, &block)
118
- try_map { |_, client| client.send(method, *command, **kwargs, &block) }.values
133
+ results, errors = try_map do |_, client|
134
+ client.send(method, *command, **kwargs, &block)
135
+ end
136
+
137
+ return results.values if errors.empty?
138
+
139
+ raise ::RedisClient::Cluster::ErrorCollection, errors
119
140
  end
120
141
 
121
- def call_primary(method, *command, **kwargs, &block)
122
- try_map do |node_key, client|
142
+ def call_primaries(method, *command, **kwargs, &block)
143
+ results, errors = try_map do |node_key, client|
123
144
  next if replica?(node_key)
124
145
 
125
146
  client.send(method, *command, **kwargs, &block)
126
- end.values
147
+ end
148
+
149
+ return results.values if errors.empty?
150
+
151
+ raise ::RedisClient::Cluster::ErrorCollection, errors
127
152
  end
128
153
 
129
- def call_replica(method, *command, **kwargs, &block)
130
- return call_primary(method, *command, **kwargs, &block) if replica_disabled?
154
+ def call_replicas(method, *command, **kwargs, &block)
155
+ return call_primaries(method, *command, **kwargs, &block) if replica_disabled?
131
156
 
132
157
  replica_node_keys = @replications.values.map(&:sample)
133
- try_map do |node_key, client|
158
+ results, errors = try_map do |node_key, client|
134
159
  next if primary?(node_key) || !replica_node_keys.include?(node_key)
135
160
 
136
161
  client.send(method, *command, **kwargs, &block)
137
- end.values
162
+ end
163
+
164
+ return results.values if errors.empty?
165
+
166
+ raise ::RedisClient::Cluster::ErrorCollection, errors
167
+ end
168
+
169
+ def send_ping(method, *command, **kwargs, &block)
170
+ results, errors = try_map do |_, client|
171
+ client.send(method, *command, **kwargs, &block)
172
+ end
173
+
174
+ return results.values if errors.empty?
175
+
176
+ raise ReloadNeeded if errors.values.any?(::RedisClient::ConnectionError)
177
+
178
+ raise ::RedisClient::Cluster::ErrorCollection, errors
138
179
  end
139
180
 
140
181
  def scale_reading_clients
@@ -144,14 +185,9 @@ class RedisClient
144
185
  end
145
186
  end
146
187
 
147
- def slot_exists?(slot)
148
- slot = Integer(slot)
149
- return false if slot < MIN_SLOT || slot > MAX_SLOT
150
-
151
- !@slots[slot].nil?
152
- end
153
-
154
188
  def find_node_key_of_primary(slot)
189
+ return if slot.nil?
190
+
155
191
  slot = Integer(slot)
156
192
  return if slot < MIN_SLOT || slot > MAX_SLOT
157
193
 
@@ -159,6 +195,8 @@ class RedisClient
159
195
  end
160
196
 
161
197
  def find_node_key_of_replica(slot)
198
+ return if slot.nil?
199
+
162
200
  slot = Integer(slot)
163
201
  return if slot < MIN_SLOT || slot > MAX_SLOT
164
202
 
@@ -171,6 +209,12 @@ class RedisClient
171
209
  @mutex.synchronize { @slots[slot] = node_key }
172
210
  end
173
211
 
212
+ def replicated?(primary_node_key, replica_node_key)
213
+ return false if @replications.nil? || @replications.size.zero?
214
+
215
+ @replications.fetch(primary_node_key).include?(replica_node_key)
216
+ end
217
+
174
218
  private
175
219
 
176
220
  def replica_disabled?
@@ -182,7 +226,9 @@ class RedisClient
182
226
  end
183
227
 
184
228
  def replica?(node_key)
185
- !(@replications.nil? || @replications.size.zero?) && !@replications.key?(node_key)
229
+ return false if @replications.nil? || @replications.size.zero?
230
+
231
+ !@replications.key?(node_key)
186
232
  end
187
233
 
188
234
  def build_slot_node_mappings(node_info)
@@ -196,11 +242,12 @@ class RedisClient
196
242
  slots
197
243
  end
198
244
 
199
- def build_replication_mappings(node_info)
245
+ def build_replication_mappings(node_info) # rubocop:disable Metrics/AbcSize
200
246
  dict = node_info.to_h { |info| [info[:id], info] }
201
247
  node_info.each_with_object(Hash.new { |h, k| h[k] = [] }) do |info, acc|
202
248
  primary_info = dict[info[:primary_id]]
203
249
  acc[primary_info[:node_key]] << info[:node_key] unless primary_info.nil?
250
+ acc[info[:node_key]] if info[:role] == 'master' # for the primary which have no replicas
204
251
  end
205
252
  end
206
253
 
@@ -219,22 +266,20 @@ class RedisClient
219
266
  end
220
267
 
221
268
  def try_map # rubocop:disable Metrics/MethodLength
222
- errors = {}
223
269
  results = {}
270
+ errors = {}
224
271
  threads = @clients.map do |k, v|
225
272
  Thread.new(k, v) do |node_key, client|
226
273
  Thread.pass
227
274
  reply = yield(node_key, client)
228
275
  results[node_key] = reply unless reply.nil?
229
- rescue ::RedisClient::CommandError => e
276
+ rescue StandardError => e
230
277
  errors[node_key] = e
231
278
  end
232
279
  end
233
280
 
234
281
  threads.each(&:join)
235
- return results if errors.empty?
236
-
237
- raise ::RedisClient::Cluster::CommandErrorCollection, errors
282
+ [results, errors]
238
283
  end
239
284
  end
240
285
  end
@@ -40,8 +40,10 @@ class RedisClient
40
40
  @size.zero?
41
41
  end
42
42
 
43
- def execute # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
43
+ # TODO: https://github.com/redis-rb/redis-cluster-client/issues/37 handle redirections
44
+ def execute # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
44
45
  all_replies = Array.new(@size)
46
+ errors = {}
45
47
  threads = @grouped.map do |k, v|
46
48
  Thread.new(@client, k, v) do |client, node_key, rows|
47
49
  Thread.pass
@@ -59,11 +61,15 @@ class RedisClient
59
61
  raise ReplySizeError, "commands: #{rows.size}, replies: #{replies.size}" if rows.size != replies.size
60
62
 
61
63
  rows.each_with_index { |row, idx| all_replies[row.first] = replies[idx] }
64
+ rescue StandardError => e
65
+ errors[node_key] = e
62
66
  end
63
67
  end
64
68
 
65
69
  threads.each(&:join)
66
- all_replies
70
+ return all_replies if errors.empty?
71
+
72
+ raise ::RedisClient::Cluster::ErrorCollection, errors
67
73
  end
68
74
  end
69
75
 
@@ -164,8 +170,10 @@ class RedisClient
164
170
  end
165
171
 
166
172
  def close
167
- @node.each(&:close)
173
+ @node.call_all(:close)
168
174
  nil
175
+ rescue StandardError
176
+ # ignore
169
177
  end
170
178
 
171
179
  private
@@ -184,10 +192,11 @@ class RedisClient
184
192
  when 'acl', 'auth', 'bgrewriteaof', 'bgsave', 'quit', 'save'
185
193
  @node.call_all(method, *command, **kwargs, &block).first
186
194
  when 'flushall', 'flushdb'
187
- @node.call_primary(method, *command, **kwargs, &block).first
188
- when 'wait' then @node.call_primary(method, *command, **kwargs, &block).sum
189
- when 'keys' then @node.call_replica(method, *command, **kwargs, &block).flatten.sort
190
- when 'dbsize' then @node.call_replica(method, *command, **kwargs, &block).sum
195
+ @node.call_primaries(method, *command, **kwargs, &block).first
196
+ when 'ping' then @node.send_ping(method, *command, **kwargs, &block).first
197
+ when 'wait' then send_wait_command(method, *command, **kwargs, &block)
198
+ when 'keys' then @node.call_replicas(method, *command, **kwargs, &block).flatten.sort
199
+ when 'dbsize' then @node.call_replicas(method, *command, **kwargs, &block).sum
191
200
  when 'scan' then _scan(*command, **kwargs)
192
201
  when 'lastsave' then @node.call_all(method, *command, **kwargs, &block).sort
193
202
  when 'role' then @node.call_all(method, *command, **kwargs, &block)
@@ -205,6 +214,22 @@ class RedisClient
205
214
  node = assign_node(*command)
206
215
  try_send(node, method, *command, **kwargs, &block)
207
216
  end
217
+ rescue RedisClient::Cluster::Node::ReloadNeeded
218
+ update_cluster_info!
219
+ raise ::RedisClient::Cluster::NodeMightBeDown
220
+ end
221
+
222
+ def send_wait_command(method, *command, retry_count: 3, **kwargs, &block)
223
+ @node.call_primaries(method, *command, **kwargs, &block).sum
224
+ rescue RedisClient::Cluster::ErrorCollection => e
225
+ raise if retry_count <= 0
226
+ raise if e.errors.values.none? do |err|
227
+ err.message.include?('WAIT cannot be used with replica instances')
228
+ end
229
+
230
+ update_cluster_info!
231
+ retry_count -= 1
232
+ retry
208
233
  end
209
234
 
210
235
  def send_config_command(method, *command, **kwargs, &block)
@@ -232,13 +257,17 @@ class RedisClient
232
257
  end
233
258
  end
234
259
 
235
- def send_cluster_command(method, *command, **kwargs, &block)
260
+ def send_cluster_command(method, *command, **kwargs, &block) # rubocop:disable Metrics/MethodLength
236
261
  subcommand = command[1].to_s.downcase
237
262
  case subcommand
238
263
  when 'addslots', 'delslots', 'failover', 'forget', 'meet', 'replicate',
239
264
  'reset', 'set-config-epoch', 'setslot'
240
265
  raise ::RedisClient::Cluster::OrchestrationCommandNotSupported, ['cluster', subcommand]
241
266
  when 'saveconfig' then @node.call_all(method, *command, **kwargs, &block).first
267
+ when 'getkeysinslot'
268
+ raise ArgumentError, command.join(' ') if command.size != 4
269
+
270
+ find_node(@node.find_node_key_of_replica(command[2])).send(method, *command, **kwargs, &block)
242
271
  else assign_node(*command).send(method, *command, **kwargs, &block)
243
272
  end
244
273
  end
@@ -248,7 +277,7 @@ class RedisClient
248
277
  when 'debug', 'kill'
249
278
  @node.call_all(method, *command, **kwargs, &block).first
250
279
  when 'flush', 'load'
251
- @node.call_primary(method, *command, **kwargs, &block).first
280
+ @node.call_primaries(method, *command, **kwargs, &block).first
252
281
  else assign_node(*command).send(method, *command, **kwargs, &block)
253
282
  end
254
283
  end
@@ -266,18 +295,16 @@ class RedisClient
266
295
 
267
296
  # @see https://redis.io/topics/cluster-spec#redirection-and-resharding
268
297
  # Redirection and resharding
269
- def try_send(node, method, *args, retry_count: 3, **kwargs, &block) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
298
+ def try_send(node, method, *args, retry_count: 3, **kwargs, &block) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
270
299
  node.send(method, *args, **kwargs, &block)
271
300
  rescue ::RedisClient::CommandError => e
272
- if e.message.start_with?(REPLY_MOVED)
273
- raise if retry_count <= 0
301
+ raise if retry_count <= 0
274
302
 
303
+ if e.message.start_with?(REPLY_MOVED)
275
304
  node = assign_redirection_node(e.message)
276
305
  retry_count -= 1
277
306
  retry
278
307
  elsif e.message.start_with?(REPLY_ASK)
279
- raise if retry_count <= 0
280
-
281
308
  node = assign_asking_node(e.message)
282
309
  node.call(CMD_ASKING)
283
310
  retry_count -= 1
@@ -286,8 +313,11 @@ class RedisClient
286
313
  raise
287
314
  end
288
315
  rescue ::RedisClient::ConnectionError
316
+ raise if retry_count <= 0
317
+
289
318
  update_cluster_info!
290
- raise
319
+ retry_count -= 1
320
+ retry
291
321
  end
292
322
 
293
323
  def _scan(*command, **kwargs) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
@@ -329,41 +359,30 @@ class RedisClient
329
359
  find_node(node_key)
330
360
  end
331
361
 
332
- def find_node_key(*command, primary_only: false) # rubocop:disable Metrics/MethodLength
362
+ def find_node_key(*command, primary_only: false)
333
363
  key = @command.extract_first_key(command)
334
- if key.empty?
335
- return @node.primary_node_keys.sample if @command.should_send_to_primary?(command) || primary_only
336
-
337
- return @node.replica_node_keys.sample
338
- end
339
-
340
- slot = ::RedisClient::Cluster::KeySlotConverter.convert(key)
341
- return unless @node.slot_exists?(slot)
364
+ slot = key.empty? ? nil : ::RedisClient::Cluster::KeySlotConverter.convert(key)
342
365
 
343
366
  if @command.should_send_to_primary?(command) || primary_only
344
- @node.find_node_key_of_primary(slot)
367
+ @node.find_node_key_of_primary(slot) || @node.primary_node_keys.sample
345
368
  else
346
- @node.find_node_key_of_replica(slot)
369
+ @node.find_node_key_of_replica(slot) || @node.replica_node_keys.sample
347
370
  end
348
371
  end
349
372
 
350
- def find_node(node_key)
351
- return @node.sample if node_key.nil?
352
-
373
+ def find_node(node_key, retry_count: 3)
353
374
  @node.find_by(node_key)
354
375
  rescue ::RedisClient::Cluster::Node::ReloadNeeded
355
- update_cluster_info!(node_key)
356
- @node.find_by(node_key)
376
+ raise ::RedieClient::Cluster::NodeMightBeDown if retry_count <= 0
377
+
378
+ update_cluster_info!
379
+ retry_count -= 1
380
+ retry
357
381
  end
358
382
 
359
- def update_cluster_info!(node_key = nil)
383
+ def update_cluster_info!
360
384
  @mutex.synchronize do
361
- unless node_key.nil?
362
- host, port = ::RedisClient::Cluster::NodeKey.split(node_key)
363
- @config.add_node(host, port)
364
- end
365
-
366
- @node.each(&:close)
385
+ close
367
386
  @node = fetch_cluster_info!(@config, pool: @pool, **@client_kwargs)
368
387
  end
369
388
  end
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.0.5
4
+ version: 0.0.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - Taishi Kasuga
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-06-18 00:00:00.000000000 Z
11
+ date: 2022-06-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: redis-client