redis-cluster-client 0.3.3 → 0.3.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1eaec04db89e1afa8a1d0a27688194d5941a94edb00aef979b1f368d01b4dd41
4
- data.tar.gz: 36571e86def197fc3633d4c026958fbe635576d5f9aec3ab19b32759957a8f14
3
+ metadata.gz: a697c2bd33f615cb435ee48c11c301bba56cad4eac139f30db70a183d888d891
4
+ data.tar.gz: 52200e9400bd7bd7fde2019c52b6cf1e26c6c019cbf1ed12a25c2167cd7ac129
5
5
  SHA512:
6
- metadata.gz: 2da9f21f30cc49f9df64cf11ed5ee3c0385bc6310bbb7e8cf3478552272f716ab68928aee05bb90b24d39253040f170fd38df78687afb78ea612f310f6a96b2e
7
- data.tar.gz: 4f7828b8072f01aaa5aa56264d7fda6f8d4ff17bce7a88a5adb2c05bd5eab8a74fe137b3c59ce1bba086a9793a05d490252d0df94228e35c6c10a2b0d869d797
6
+ metadata.gz: 05dc86fd1aa3cbf3cd3f0fa43d9d1c8463fcbe9e0ce1bcfe094f6095fd92d878a4bb21e6d6fa62203e91f3dc642e5869552f0b83d579cfa158b791501e1cb600
7
+ data.tar.gz: 8f80c5072d4d1bdbeb3e2cb4c9f92493537a6d9002edd71426598844ba28a497171c055135bb19a1d89a325c194026195100f9a0aea89c0beac3d8e9424f7f58
@@ -2,10 +2,13 @@
2
2
 
3
3
  require 'redis_client'
4
4
  require 'redis_client/cluster/errors'
5
+ require 'redis_client/cluster/normalized_cmd_name'
5
6
 
6
7
  class RedisClient
7
8
  class Cluster
8
9
  class Command
10
+ EMPTY_STRING = ''
11
+
9
12
  class << self
10
13
  def load(nodes) # rubocop:disable Metrics/MethodLength
11
14
  errors = []
@@ -29,7 +32,10 @@ class RedisClient
29
32
 
30
33
  def parse_command_details(rows)
31
34
  rows&.reject { |row| row[0].nil? }.to_h do |row|
32
- [row[0].downcase, { arity: row[1], flags: row[2], first: row[3], last: row[4], step: row[5] }]
35
+ [
36
+ ::RedisClient::Cluster::NormalizedCmdName.instance.get_by_name(row[0]),
37
+ { arity: row[1], flags: row[2], first: row[3], last: row[4], step: row[5] }
38
+ ]
33
39
  end
34
40
  end
35
41
  end
@@ -40,7 +46,7 @@ class RedisClient
40
46
 
41
47
  def extract_first_key(command)
42
48
  i = determine_first_key_position(command)
43
- return '' if i == 0
49
+ return EMPTY_STRING if i == 0
44
50
 
45
51
  key = (command[i].is_a?(Array) ? command[i].flatten.first : command[i]).to_s
46
52
  hash_tag = extract_hash_tag(key)
@@ -56,7 +62,8 @@ class RedisClient
56
62
  end
57
63
 
58
64
  def exists?(name)
59
- @details.key?(name.to_s.downcase)
65
+ key = ::RedisClient::Cluster::NormalizedCmdName.instance.get_by_name(name)
66
+ @details.key?(key)
60
67
  end
61
68
 
62
69
  private
@@ -72,14 +79,14 @@ class RedisClient
72
79
  end
73
80
 
74
81
  def dig_details(command, key)
75
- name = command&.flatten&.first.to_s.downcase # OPTIMIZE: prevent allocation for string
82
+ name = ::RedisClient::Cluster::NormalizedCmdName.instance.get_by_command(command)
76
83
  return if name.empty? || !@details.key?(name)
77
84
 
78
85
  @details.fetch(name).fetch(key)
79
86
  end
80
87
 
81
88
  def determine_first_key_position(command) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength
82
- case command&.flatten&.first.to_s.downcase # OPTIMIZE: prevent allocation for string
89
+ case ::RedisClient::Cluster::NormalizedCmdName.instance.get_by_command(command)
83
90
  when 'eval', 'evalsha', 'zinterstore', 'zunionstore' then 3
84
91
  when 'object' then 2
85
92
  when 'memory'
@@ -104,7 +111,7 @@ class RedisClient
104
111
  s = key.index('{')
105
112
  e = key.index('}', s.to_i + 1)
106
113
 
107
- return '' if s.nil? || e.nil?
114
+ return EMPTY_STRING if s.nil? || e.nil?
108
115
 
109
116
  key[s + 1..e - 1]
110
117
  end
@@ -40,12 +40,12 @@ class RedisClient
40
40
  private
41
41
 
42
42
  def measure_latencies(clients) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
43
- latencies = {}
44
-
43
+ latencies = nil
45
44
  clients.each_slice(::RedisClient::Cluster::Node::MAX_THREADS) do |chuncked_clients|
46
45
  threads = chuncked_clients.map do |k, v|
47
46
  Thread.new(k, v) do |node_key, client|
48
47
  Thread.pass
48
+ Thread.current.thread_variable_set(:node_key, node_key)
49
49
 
50
50
  min = DUMMY_LATENCY_NSEC
51
51
  MEASURE_ATTEMPT_COUNT.times do
@@ -55,13 +55,17 @@ class RedisClient
55
55
  min = duration if duration < min
56
56
  end
57
57
 
58
- latencies[node_key] = min
58
+ Thread.current.thread_variable_set(:latency, min)
59
59
  rescue StandardError
60
- latencies[node_key] = DUMMY_LATENCY_NSEC
60
+ Thread.current.thread_variable_set(:latency, DUMMY_LATENCY_NSEC)
61
61
  end
62
62
  end
63
63
 
64
- threads.each(&:join)
64
+ threads.each do |t|
65
+ t.join
66
+ latencies ||= {}
67
+ latencies[t.thread_variable_get(:node_key)] = t.thread_variable_get(:latency)
68
+ end
65
69
  end
66
70
 
67
71
  latencies
@@ -39,27 +39,36 @@ class RedisClient
39
39
  class << self
40
40
  def load_info(options, **kwargs) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
41
41
  startup_size = options.size > MAX_STARTUP_SAMPLE ? MAX_STARTUP_SAMPLE : options.size
42
- node_info_list = Array.new(startup_size)
43
- errors = Array.new(startup_size)
42
+ node_info_list = errors = nil
44
43
  startup_options = options.to_a.sample(MAX_STARTUP_SAMPLE).to_h
45
44
  startup_nodes = ::RedisClient::Cluster::Node.new(startup_options, **kwargs)
46
45
  startup_nodes.each_slice(MAX_THREADS).with_index do |chuncked_startup_nodes, chuncked_idx|
47
46
  threads = chuncked_startup_nodes.each_with_index.map do |raw_client, idx|
48
47
  Thread.new(raw_client, (MAX_THREADS * chuncked_idx) + idx) do |cli, i|
49
48
  Thread.pass
49
+ Thread.current.thread_variable_set(:index, i)
50
50
  reply = cli.call('CLUSTER', 'NODES')
51
- node_info_list[i] = parse_node_info(reply)
51
+ Thread.current.thread_variable_set(:info, parse_node_info(reply))
52
52
  rescue StandardError => e
53
- errors[i] = e
53
+ Thread.current.thread_variable_set(:error, e)
54
54
  ensure
55
55
  cli&.close
56
56
  end
57
57
  end
58
58
 
59
- threads.each(&:join)
59
+ threads.each do |t|
60
+ t.join
61
+ if t.thread_variable?(:info)
62
+ node_info_list ||= Array.new(startup_size)
63
+ node_info_list[t.thread_variable_get(:index)] = t.thread_variable_get(:info)
64
+ elsif t.thread_variable?(:error)
65
+ errors ||= Array.new(startup_size)
66
+ errors[t.thread_variable_get(:index)] = t.thread_variable_get(:error)
67
+ end
68
+ end
60
69
  end
61
70
 
62
- raise ::RedisClient::Cluster::InitialSetupError, errors if node_info_list.all?(&:nil?)
71
+ raise ::RedisClient::Cluster::InitialSetupError, errors if node_info_list.nil?
63
72
 
64
73
  grouped = node_info_list.compact.group_by do |rows|
65
74
  rows.sort_by { |row| row[:id] }
@@ -147,7 +156,7 @@ class RedisClient
147
156
 
148
157
  def send_ping(method, command, args, &block)
149
158
  result_values, errors = call_multiple_nodes(@topology.clients, method, command, args, &block)
150
- return result_values if errors.empty?
159
+ return result_values if errors.nil? || errors.empty?
151
160
 
152
161
  raise ReloadNeeded if errors.values.any?(::RedisClient::ConnectionError)
153
162
 
@@ -228,26 +237,35 @@ class RedisClient
228
237
 
229
238
  def call_multiple_nodes!(clients, method, command, args, &block)
230
239
  result_values, errors = call_multiple_nodes(clients, method, command, args, &block)
231
- return result_values if errors.empty?
240
+ return result_values if errors.nil? || errors.empty?
232
241
 
233
242
  raise ::RedisClient::Cluster::ErrorCollection, errors
234
243
  end
235
244
 
236
- def try_map(clients) # rubocop:disable Metrics/MethodLength
237
- results = {}
238
- errors = {}
245
+ def try_map(clients) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
246
+ results = errors = nil
239
247
  clients.each_slice(MAX_THREADS) do |chuncked_clients|
240
248
  threads = chuncked_clients.map do |k, v|
241
249
  Thread.new(k, v) do |node_key, client|
242
250
  Thread.pass
251
+ Thread.current.thread_variable_set(:node_key, node_key)
243
252
  reply = yield(node_key, client)
244
- results[node_key] = reply unless reply.nil?
253
+ Thread.current.thread_variable_set(:result, reply)
245
254
  rescue StandardError => e
246
- errors[node_key] = e
255
+ Thread.current.thread_variable_set(:error, e)
247
256
  end
248
257
  end
249
258
 
250
- threads.each(&:join)
259
+ threads.each do |t|
260
+ t.join
261
+ if t.thread_variable?(:result)
262
+ results ||= {}
263
+ results[t.thread_variable_get(:node_key)] = t.thread_variable_get(:result)
264
+ elsif t.thread_variable?(:error)
265
+ errors ||= {}
266
+ errors[t.thread_variable_get(:node_key)] = t.thread_variable_get(:error)
267
+ end
268
+ end
251
269
  end
252
270
 
253
271
  [results, errors]
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'singleton'
4
+
5
+ class RedisClient
6
+ class Cluster
7
+ class NormalizedCmdName
8
+ include Singleton
9
+
10
+ EMPTY_STRING = ''
11
+
12
+ def initialize
13
+ @cache = {}
14
+ @mutex = Mutex.new
15
+ end
16
+
17
+ def get_by_command(command)
18
+ get(command, index: 0)
19
+ end
20
+
21
+ def get_by_subcommand(command)
22
+ get(command, index: 1)
23
+ end
24
+
25
+ def get_by_name(name)
26
+ get(name, index: 0)
27
+ end
28
+
29
+ def clear
30
+ @mutex.synchronize { @cache.clear }
31
+ end
32
+
33
+ private
34
+
35
+ def get(command, index:)
36
+ name = extract_name(command, index: index)
37
+ return EMPTY_STRING if name.nil? || name.empty?
38
+
39
+ normalize(name)
40
+ end
41
+
42
+ def extract_name(command, index:)
43
+ case command
44
+ when String, Symbol then index.zero? ? command : nil
45
+ when Array then extract_name_from_array(command, index: index)
46
+ end
47
+ end
48
+
49
+ def extract_name_from_array(command, index:)
50
+ return if command.size - 1 < index
51
+
52
+ case e = command[index]
53
+ when String, Symbol then e
54
+ when Array then e[index]
55
+ end
56
+ end
57
+
58
+ def normalize(name)
59
+ return @cache[name] if @cache.key?(name)
60
+ return name.to_s.downcase if @mutex.locked?
61
+
62
+ @mutex.synchronize { @cache[name] = name.to_s.downcase }
63
+ @cache[name]
64
+ end
65
+ end
66
+ end
67
+ end
@@ -20,37 +20,37 @@ class RedisClient
20
20
  def call(*args, **kwargs, &block)
21
21
  command = @command_builder.generate(args, kwargs)
22
22
  node_key = @router.find_node_key(command, seed: @seed)
23
- add_line(node_key, [@size, :call_v, command, block])
23
+ add_row(node_key, [@size, :call_v, command, block])
24
24
  end
25
25
 
26
26
  def call_v(args, &block)
27
27
  command = @command_builder.generate(args)
28
28
  node_key = @router.find_node_key(command, seed: @seed)
29
- add_line(node_key, [@size, :call_v, command, block])
29
+ add_row(node_key, [@size, :call_v, command, block])
30
30
  end
31
31
 
32
32
  def call_once(*args, **kwargs, &block)
33
33
  command = @command_builder.generate(args, kwargs)
34
34
  node_key = @router.find_node_key(command, seed: @seed)
35
- add_line(node_key, [@size, :call_once_v, command, block])
35
+ add_row(node_key, [@size, :call_once_v, command, block])
36
36
  end
37
37
 
38
38
  def call_once_v(args, &block)
39
39
  command = @command_builder.generate(args)
40
40
  node_key = @router.find_node_key(command, seed: @seed)
41
- add_line(node_key, [@size, :call_once_v, command, block])
41
+ add_row(node_key, [@size, :call_once_v, command, block])
42
42
  end
43
43
 
44
44
  def blocking_call(timeout, *args, **kwargs, &block)
45
45
  command = @command_builder.generate(args, kwargs)
46
46
  node_key = @router.find_node_key(command, seed: @seed)
47
- add_line(node_key, [@size, :blocking_call_v, timeout, command, block])
47
+ add_row(node_key, [@size, :blocking_call_v, timeout, command, block])
48
48
  end
49
49
 
50
50
  def blocking_call_v(timeout, args, &block)
51
51
  command = @command_builder.generate(args)
52
52
  node_key = @router.find_node_key(command, seed: @seed)
53
- add_line(node_key, [@size, :blocking_call_v, timeout, command, block])
53
+ add_row(node_key, [@size, :blocking_call_v, timeout, command, block])
54
54
  end
55
55
 
56
56
  def empty?
@@ -59,41 +59,57 @@ class RedisClient
59
59
 
60
60
  # TODO: https://github.com/redis-rb/redis-cluster-client/issues/37 handle redirections
61
61
  def execute # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
62
- all_replies = Array.new(@size)
63
- errors = {}
62
+ all_replies = errors = nil
64
63
  @grouped.each_slice(MAX_THREADS) do |chuncked_grouped|
65
64
  threads = chuncked_grouped.map do |k, v|
66
65
  Thread.new(@router, k, v) do |router, node_key, rows|
67
66
  Thread.pass
68
- replies = router.find_node(node_key).pipelined do |pipeline|
69
- rows.each do |(_size, *row, block)|
70
- pipeline.send(*row, &block)
71
- end
72
- end
73
-
67
+ replies = do_pipelining(router.find_node(node_key), rows)
74
68
  raise ReplySizeError, "commands: #{rows.size}, replies: #{replies.size}" if rows.size != replies.size
75
69
 
76
- rows.each_with_index { |row, idx| all_replies[row.first] = replies[idx] }
70
+ Thread.current.thread_variable_set(:rows, rows)
71
+ Thread.current.thread_variable_set(:replies, replies)
77
72
  rescue StandardError => e
78
- errors[node_key] = e
73
+ Thread.current.thread_variable_set(:node_key, node_key)
74
+ Thread.current.thread_variable_set(:error, e)
79
75
  end
80
76
  end
81
77
 
82
- threads.each(&:join)
78
+ threads.each do |t|
79
+ t.join
80
+ if t.thread_variable?(:replies)
81
+ all_replies ||= Array.new(@size)
82
+ t.thread_variable_get(:rows).each_with_index { |r, i| all_replies[r.first] = t.thread_variable_get(:replies)[i] }
83
+ elsif t.thread_variable?(:error)
84
+ errors ||= {}
85
+ errors[t.thread_variable_get(:node_key)] = t.thread_variable_get(:error)
86
+ end
87
+ end
83
88
  end
84
89
 
85
- return all_replies if errors.empty?
90
+ return all_replies if errors.nil?
86
91
 
87
92
  raise ::RedisClient::Cluster::ErrorCollection, errors
88
93
  end
89
94
 
90
95
  private
91
96
 
92
- def add_line(node_key, line)
97
+ def add_row(node_key, row)
93
98
  @grouped[node_key] = [] unless @grouped.key?(node_key)
94
- @grouped[node_key] << line
99
+ @grouped[node_key] << row
95
100
  @size += 1
96
101
  end
102
+
103
+ def do_pipelining(node, rows)
104
+ node.pipelined do |pipeline|
105
+ rows.each do |row|
106
+ case row.size
107
+ when 4 then pipeline.send(row[1], row[2], &row[3])
108
+ when 5 then pipeline.send(row[1], row[2], row[3], &row[4])
109
+ end
110
+ end
111
+ end
112
+ end
97
113
  end
98
114
  end
99
115
  end
@@ -6,6 +6,7 @@ require 'redis_client/cluster/errors'
6
6
  require 'redis_client/cluster/key_slot_converter'
7
7
  require 'redis_client/cluster/node'
8
8
  require 'redis_client/cluster/node_key'
9
+ require 'redis_client/cluster/normalized_cmd_name'
9
10
 
10
11
  class RedisClient
11
12
  class Cluster
@@ -25,7 +26,7 @@ class RedisClient
25
26
  end
26
27
 
27
28
  def send_command(method, command, *args, &block) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
28
- cmd = command.first.to_s.downcase
29
+ cmd = ::RedisClient::Cluster::NormalizedCmdName.instance.get_by_command(command)
29
30
  case cmd
30
31
  when 'acl', 'auth', 'bgrewriteaof', 'bgsave', 'quit', 'save'
31
32
  @node.call_all(method, command, args, &block).first
@@ -65,7 +66,12 @@ class RedisClient
65
66
  # @see https://redis.io/topics/cluster-spec#redirection-and-resharding
66
67
  # Redirection and resharding
67
68
  def try_send(node, method, command, args, retry_count: 3, &block) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
68
- node.send(method, *args, command, &block)
69
+ if args.empty?
70
+ # prevent memory allocation for variable-length args
71
+ node.send(method, command, &block)
72
+ else
73
+ node.send(method, *args, command, &block)
74
+ end
69
75
  rescue ::RedisClient::CommandError => e
70
76
  raise if retry_count <= 0
71
77
 
@@ -193,7 +199,7 @@ class RedisClient
193
199
  end
194
200
 
195
201
  def send_config_command(method, command, args, &block)
196
- case command[1].to_s.downcase
202
+ case ::RedisClient::Cluster::NormalizedCmdName.instance.get_by_subcommand(command)
197
203
  when 'resetstat', 'rewrite', 'set'
198
204
  @node.call_all(method, command, args, &block).first
199
205
  else assign_node(command).send(method, *args, command, &block)
@@ -201,7 +207,7 @@ class RedisClient
201
207
  end
202
208
 
203
209
  def send_memory_command(method, command, args, &block)
204
- case command[1].to_s.downcase
210
+ case ::RedisClient::Cluster::NormalizedCmdName.instance.get_by_subcommand(command)
205
211
  when 'stats' then @node.call_all(method, command, args, &block)
206
212
  when 'purge' then @node.call_all(method, command, args, &block).first
207
213
  else assign_node(command).send(method, *args, command, &block)
@@ -209,7 +215,7 @@ class RedisClient
209
215
  end
210
216
 
211
217
  def send_client_command(method, command, args, &block)
212
- case command[1].to_s.downcase
218
+ case ::RedisClient::Cluster::NormalizedCmdName.instance.get_by_subcommand(command)
213
219
  when 'list' then @node.call_all(method, command, args, &block).flatten
214
220
  when 'pause', 'reply', 'setname'
215
221
  @node.call_all(method, command, args, &block).first
@@ -217,10 +223,8 @@ class RedisClient
217
223
  end
218
224
  end
219
225
 
220
- def send_cluster_command(method, command, args, &block) # rubocop:disable Metrics/MethodLength
221
- subcommand = command[1].to_s.downcase
222
-
223
- case subcommand
226
+ def send_cluster_command(method, command, args, &block)
227
+ case subcommand = ::RedisClient::Cluster::NormalizedCmdName.instance.get_by_subcommand(command)
224
228
  when 'addslots', 'delslots', 'failover', 'forget', 'meet', 'replicate',
225
229
  'reset', 'set-config-epoch', 'setslot'
226
230
  raise ::RedisClient::Cluster::OrchestrationCommandNotSupported, ['cluster', subcommand]
@@ -234,7 +238,7 @@ class RedisClient
234
238
  end
235
239
 
236
240
  def send_script_command(method, command, args, &block)
237
- case command[1].to_s.downcase
241
+ case ::RedisClient::Cluster::NormalizedCmdName.instance.get_by_subcommand(command)
238
242
  when 'debug', 'kill'
239
243
  @node.call_all(method, command, args, &block).first
240
244
  when 'flush', 'load'
@@ -244,7 +248,7 @@ class RedisClient
244
248
  end
245
249
 
246
250
  def send_pubsub_command(method, command, args, &block) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
247
- case command[1].to_s.downcase
251
+ case ::RedisClient::Cluster::NormalizedCmdName.instance.get_by_subcommand(command)
248
252
  when 'channels' then @node.call_all(method, command, args, &block).flatten.uniq.sort_by(&:to_s)
249
253
  when 'numsub'
250
254
  @node.call_all(method, command, args, &block).reject(&:empty?).map { |e| Hash[*e] }
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.3.3
4
+ version: 0.3.5
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-09-18 00:00:00.000000000 Z
11
+ date: 2022-09-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: redis-client
@@ -42,6 +42,7 @@ files:
42
42
  - lib/redis_client/cluster/node/random_replica.rb
43
43
  - lib/redis_client/cluster/node/replica_mixin.rb
44
44
  - lib/redis_client/cluster/node_key.rb
45
+ - lib/redis_client/cluster/normalized_cmd_name.rb
45
46
  - lib/redis_client/cluster/pipeline.rb
46
47
  - lib/redis_client/cluster/pub_sub.rb
47
48
  - lib/redis_client/cluster/router.rb