redis-cluster-client 0.0.5 → 0.0.8

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