redis-cluster-client 0.3.3 → 0.3.5

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