redis-cluster-client 0.0.1 → 0.0.4

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: 4339be83a679ef61e9d9a98e26cd2dd52b58b56eaa852a53c8f0f01ef1987396
4
- data.tar.gz: ac7ea102f9d5a711ee495a36c844443027b39d2c64e0d9177d419d00e1fa0de0
3
+ metadata.gz: 0561fdd5885f8c6c5c5c100af032b867224eff3ed91b546b6885f8bf3bd76663
4
+ data.tar.gz: 1ebe59416fb043af9da7a926cd338ac260ce1a46e5220b46f7a6744c8f1b8c2b
5
5
  SHA512:
6
- metadata.gz: 268000fa3691afc08d969b35cf8ac90221e96ffb41db59c63d9737e9ebed3d978f84f36ba4ee91ceb4bb88c957c3d2ecead676bc7b6c7a4411598969213e269c
7
- data.tar.gz: 1a44f77bdd4202f327025d3fa0d0a3b65b577332cd714f787159c8ba41f20f5b3c8b058b5c48aab74f5d0a7b3ec4638f227b5f205ed07c8e32dba4c71220695b
6
+ metadata.gz: 86661400532550e005beb98e740e4939ffcf93c1b66fb47930bc2a5819f5dbba60e940c8e0183d312a6ce7cb55d1cc0634081029eb8e5c1b8f68ea2862d9e0df
7
+ data.tar.gz: 3629f63ed379946bc8951c57ada535573130e8db504a2f5b7644ef9091408ac3223c5b0a260b13d6b84ab889b3e47c4571bbc10f5b21ceaf6758dc60c28a00b3
@@ -8,8 +8,9 @@ class RedisClient
8
8
  class Command
9
9
  class << self
10
10
  def load(nodes)
11
- errors = nodes.map do |node|
12
- details = fetch_command_details(node)
11
+ errors = nodes&.map do |node|
12
+ reply = node.call('COMMAND')
13
+ details = parse_command_details(reply)
13
14
  return ::RedisClient::Cluster::Command.new(details)
14
15
  rescue ::RedisClient::ConnectionError, ::RedisClient::CommandError => e
15
16
  e
@@ -20,9 +21,9 @@ class RedisClient
20
21
 
21
22
  private
22
23
 
23
- def fetch_command_details(node)
24
- node.call('COMMAND').to_h do |reply|
25
- [reply[0], { arity: reply[1], flags: reply[2], first: reply[3], last: reply[4], step: reply[5] }]
24
+ def parse_command_details(rows)
25
+ rows&.reject { |row| row[0].nil? }.to_h do |row|
26
+ [row[0].downcase, { arity: row[1], flags: row[2], first: row[3], last: row[4], step: row[5] }]
26
27
  end
27
28
  end
28
29
  end
@@ -35,7 +36,7 @@ class RedisClient
35
36
  i = determine_first_key_position(command)
36
37
  return '' if i == 0
37
38
 
38
- key = command[i].to_s
39
+ key = (command[i].is_a?(Array) ? command[i].flatten.first : command[i]).to_s
39
40
  hash_tag = extract_hash_tag(key)
40
41
  hash_tag.empty? ? key : hash_tag
41
42
  end
@@ -51,7 +52,7 @@ class RedisClient
51
52
  private
52
53
 
53
54
  def pick_details(details)
54
- details.transform_values do |detail|
55
+ (details || {}).transform_values do |detail|
55
56
  {
56
57
  first_key_position: detail[:first],
57
58
  write: detail[:flags].include?('write'),
@@ -61,14 +62,14 @@ class RedisClient
61
62
  end
62
63
 
63
64
  def dig_details(command, key)
64
- name = command.first.to_s
65
- return unless @details.key?(name)
65
+ name = command&.flatten&.first.to_s.downcase
66
+ return if name.empty? || !@details.key?(name)
66
67
 
67
68
  @details.fetch(name).fetch(key)
68
69
  end
69
70
 
70
- def determine_first_key_position(command)
71
- case command.first.to_s.downcase
71
+ def determine_first_key_position(command) # rubocop:disable Metrics/CyclomaticComplexity
72
+ case command&.flatten&.first.to_s.downcase
72
73
  when 'eval', 'evalsha', 'migrate', 'zinterstore', 'zunionstore' then 3
73
74
  when 'object' then 2
74
75
  when 'memory'
@@ -80,13 +81,14 @@ class RedisClient
80
81
  end
81
82
  end
82
83
 
83
- def determine_optional_key_position(command, option_name)
84
- idx = command.map(&:to_s).map(&:downcase).index(option_name)
84
+ def determine_optional_key_position(command, option_name) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
85
+ idx = command&.flatten&.map(&:to_s)&.map(&:downcase)&.index(option_name&.downcase)
85
86
  idx.nil? ? 0 : idx + 1
86
87
  end
87
88
 
88
89
  # @see https://redis.io/topics/cluster-spec#keys-hash-tags Keys hash tags
89
90
  def extract_hash_tag(key)
91
+ key = key.to_s
90
92
  s = key.index('{')
91
93
  e = key.index('}', s.to_i + 1)
92
94
 
@@ -4,22 +4,24 @@ require 'redis_client'
4
4
 
5
5
  class RedisClient
6
6
  class Cluster
7
+ ERR_ARG_NORMALIZATION = ->(arg) { Array[arg].flatten.reject { |e| e.nil? || (e.respond_to?(:empty?) && e.empty?) } }
8
+
7
9
  # Raised when client connected to redis as cluster mode
8
10
  # and failed to fetch cluster state information by commands.
9
11
  class InitialSetupError < ::RedisClient::Error
10
- # @param errors [Array<Redis::BaseError>]
11
12
  def initialize(errors)
12
- super("Redis client could not fetch cluster information: #{errors.map(&:message).uniq.join(',')}")
13
+ msg = ERR_ARG_NORMALIZATION.call(errors).map(&:message).uniq.join(',')
14
+ super("Redis client could not fetch cluster information: #{msg}")
13
15
  end
14
16
  end
15
17
 
16
18
  # Raised when client connected to redis as cluster mode
17
19
  # and some cluster subcommands were called.
18
20
  class OrchestrationCommandNotSupported < ::RedisClient::Error
19
- def initialize(command, subcommand = '')
20
- str = [command, subcommand].map(&:to_s).reject(&:empty?).join(' ').upcase
21
+ def initialize(command)
22
+ str = ERR_ARG_NORMALIZATION.call(command).map(&:to_s).join(' ').upcase
21
23
  msg = "#{str} command should be used with care "\
22
- 'only by applications orchestrating Redis Cluster, like redis-trib, '\
24
+ 'only by applications orchestrating Redis Cluster, like redis-cli, '\
23
25
  'and the command if used out of the right context can leave the cluster '\
24
26
  'in a wrong state or cause data loss.'
25
27
  super(msg)
@@ -30,11 +32,16 @@ class RedisClient
30
32
  class CommandErrorCollection < ::RedisClient::Error
31
33
  attr_reader :errors
32
34
 
33
- # @param errors [Hash{String => Redis::CommandError}]
34
- # @param error_message [String]
35
- def initialize(errors, error_message = 'Command errors were replied on any node')
35
+ def initialize(errors)
36
+ @errors = {}
37
+ if !errors.is_a?(Hash) || errors.empty?
38
+ super('')
39
+ return
40
+ end
41
+
36
42
  @errors = errors
37
- super(error_message)
43
+ messages = @errors.map { |node_key, error| "#{node_key}: #{error.message}" }
44
+ super("Command errors were replied on any node: #{messages.join(', ')}")
38
45
  end
39
46
  end
40
47
 
@@ -44,13 +51,5 @@ class RedisClient
44
51
  super("Cluster client doesn't know which node the #{command} command should be sent to.")
45
52
  end
46
53
  end
47
-
48
- # Raised when commands in pipelining include cross slot keys.
49
- class CrossSlotPipeliningError < ::RedisClient::Error
50
- def initialize(keys)
51
- super("Cluster client couldn't send pipelining to single node. "\
52
- "The commands include cross slot keys. #{keys}")
53
- end
54
- end
55
54
  end
56
55
  end
@@ -10,6 +10,10 @@ class RedisClient
10
10
  include Enumerable
11
11
 
12
12
  SLOT_SIZE = 16_384
13
+ MIN_SLOT = 0
14
+ MAX_SLOT = SLOT_SIZE - 1
15
+ IGNORE_GENERIC_CONFIG_KEYS = %i[url host port path].freeze
16
+
13
17
  ReloadNeeded = Class.new(::RedisClient::Error)
14
18
 
15
19
  class Config < ::RedisClient::Config
@@ -18,6 +22,8 @@ class RedisClient
18
22
  super(**kwargs)
19
23
  end
20
24
 
25
+ private
26
+
21
27
  def build_connection_prelude
22
28
  prelude = super.dup
23
29
  prelude << ['READONLY'] if @scale_read
@@ -27,10 +33,10 @@ class RedisClient
27
33
 
28
34
  class << self
29
35
  def load_info(options, **kwargs)
30
- tmp_nodes = ::RedisClient::Cluster::Node.new(options, **kwargs)
36
+ startup_nodes = ::RedisClient::Cluster::Node.new(options, **kwargs)
31
37
 
32
- errors = tmp_nodes.map do |tmp_node|
33
- reply = tmp_node.call('CLUSTER', 'NODES')
38
+ errors = startup_nodes.map do |n|
39
+ reply = n.call('CLUSTER', 'NODES')
34
40
  return parse_node_info(reply)
35
41
  rescue ::RedisClient::ConnectionError, ::RedisClient::CommandError => e
36
42
  e
@@ -38,12 +44,13 @@ class RedisClient
38
44
 
39
45
  raise ::RedisClient::Cluster::InitialSetupError, errors
40
46
  ensure
41
- tmp_nodes&.each(&:close)
47
+ startup_nodes&.each(&:close)
42
48
  end
43
49
 
44
50
  private
45
51
 
46
52
  # @see https://redis.io/commands/cluster-nodes/
53
+ # @see https://github.com/redis/redis/blob/78960ad57b8a5e6af743d789ed8fd767e37d42b8/src/cluster.c#L4660-L4683
47
54
  def parse_node_info(info) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/MethodLength
48
55
  rows = info.split("\n").map(&:split)
49
56
  rows.each { |arr| arr[2] = arr[2].split(',') }
@@ -51,7 +58,13 @@ class RedisClient
51
58
  rows.each do |arr|
52
59
  arr[1] = arr[1].split('@').first
53
60
  arr[2] = (arr[2] & %w[master slave]).first
54
- arr[8] = arr[8].nil? ? [] : arr[8].split(',').map { |r| r.split('-').map { |s| Integer(s) } }
61
+ if arr[8].nil?
62
+ arr[8] = []
63
+ next
64
+ end
65
+
66
+ arr[8] = arr[8].split(',').map { |r| r.split('-').map { |s| Integer(s) } }
67
+ arr[8] = arr[8].map { |a| a.size == 1 ? a << a.first : a }.map(&:sort)
55
68
  end
56
69
 
57
70
  rows.map do |arr|
@@ -61,15 +74,16 @@ class RedisClient
61
74
  end
62
75
  end
63
76
 
64
- def initialize(options, node_info = [], pool = nil, with_replica: false, **kwargs)
77
+ def initialize(options, node_info: [], pool: nil, with_replica: false, **kwargs)
65
78
  @with_replica = with_replica
66
79
  @slots = build_slot_node_mappings(node_info)
67
80
  @replications = build_replication_mappings(node_info)
68
- @clients = build_clients(options, pool, **kwargs)
81
+ @clients = build_clients(options, pool: pool, **kwargs)
82
+ @mutex = Mutex.new
69
83
  end
70
84
 
71
85
  def inspect
72
- @clients.keys.sort.to_s
86
+ "#<#{self.class.name} #{node_keys.join(', ')}>"
73
87
  end
74
88
 
75
89
  def each(&block)
@@ -80,6 +94,20 @@ class RedisClient
80
94
  @clients.values.sample
81
95
  end
82
96
 
97
+ def node_keys
98
+ @clients.keys.sort
99
+ end
100
+
101
+ def primary_node_keys
102
+ @clients.filter_map { |k, _| primary?(k) ? k : nil }.sort
103
+ end
104
+
105
+ def replica_node_keys
106
+ return primary_node_keys if replica_disabled?
107
+
108
+ @clients.filter_map { |k, _| replica?(k) ? k : nil }.sort
109
+ end
110
+
83
111
  def find_by(node_key)
84
112
  @clients.fetch(node_key)
85
113
  rescue KeyError
@@ -108,39 +136,41 @@ class RedisClient
108
136
  end.values
109
137
  end
110
138
 
111
- # TODO: impl
112
- def process_all(commands, &block)
113
- try_map { |_, client| client.process(commands, &block) }.values
114
- end
115
-
116
139
  def scale_reading_clients
117
- reading_clients = []
118
-
119
- @clients.each do |node_key, client|
120
- next unless replica_disabled? ? primary?(node_key) : replica?(node_key)
121
-
122
- reading_clients << client
140
+ clients = @clients.select do |node_key, _|
141
+ replica_disabled? ? primary?(node_key) : replica?(node_key)
123
142
  end
124
143
 
125
- reading_clients
144
+ clients.values.sort_by do |client|
145
+ ::RedisClient::Cluster::NodeKey.build_from_host_port(client.config.host, client.config.port)
146
+ end
126
147
  end
127
148
 
128
149
  def slot_exists?(slot)
150
+ slot = Integer(slot)
151
+ return false if slot < MIN_SLOT || slot > MAX_SLOT
152
+
129
153
  !@slots[slot].nil?
130
154
  end
131
155
 
132
156
  def find_node_key_of_primary(slot)
157
+ slot = Integer(slot)
158
+ return if slot < MIN_SLOT || slot > MAX_SLOT
159
+
133
160
  @slots[slot]
134
161
  end
135
162
 
136
163
  def find_node_key_of_replica(slot)
164
+ slot = Integer(slot)
165
+ return if slot < MIN_SLOT || slot > MAX_SLOT
166
+
137
167
  return @slots[slot] if replica_disabled? || @replications[@slots[slot]].size.zero?
138
168
 
139
169
  @replications[@slots[slot]].sample
140
170
  end
141
171
 
142
172
  def update_slot(slot, node_key)
143
- @slots[slot] = node_key
173
+ @mutex.synchronize { @slots[slot] = node_key }
144
174
  end
145
175
 
146
176
  private
@@ -177,12 +207,15 @@ class RedisClient
177
207
  end
178
208
  end
179
209
 
180
- def build_clients(options, pool, **kwargs)
210
+ def build_clients(options, pool: nil, **kwargs)
181
211
  options.filter_map do |node_key, option|
182
212
  next if replica_disabled? && replica?(node_key)
183
213
 
184
- config = ::RedisClient::Cluster::Node::Config.new(scale_read: replica?(node_key), **option)
185
- client = pool.nil? ? config.new_client(**kwargs) : config.new_pool(**pool, **kwargs)
214
+ config = ::RedisClient::Cluster::Node::Config.new(
215
+ scale_read: replica?(node_key),
216
+ **option.merge(kwargs.reject { |k, _| IGNORE_GENERIC_CONFIG_KEYS.include?(k) })
217
+ )
218
+ client = pool.nil? ? config.new_client : config.new_pool(**pool)
186
219
 
187
220
  [node_key, client]
188
221
  end.to_h
@@ -191,15 +224,17 @@ class RedisClient
191
224
  def try_map # rubocop:disable Metrics/MethodLength
192
225
  errors = {}
193
226
  results = {}
194
-
195
- @clients.each do |node_key, client|
196
- reply = yield(node_key, client)
197
- results[node_key] = reply unless reply.nil?
198
- rescue ::RedisClient::CommandError => e
199
- errors[node_key] = e
200
- next
227
+ threads = @clients.map do |k, v|
228
+ Thread.new(k, v) do |node_key, client|
229
+ Thread.pass
230
+ reply = yield(node_key, client)
231
+ results[node_key] = reply unless reply.nil?
232
+ rescue ::RedisClient::CommandError => e
233
+ errors[node_key] = e
234
+ end
201
235
  end
202
236
 
237
+ threads.each(&:join)
203
238
  return results if errors.empty?
204
239
 
205
240
  raise ::RedisClient::Cluster::CommandErrorCollection, errors
@@ -16,10 +16,15 @@ class RedisClient
16
16
  end
17
17
 
18
18
  def split(node_key)
19
- node_key.split(DELIMITER)
19
+ pos = node_key&.rindex(DELIMITER, -1)
20
+ return [node_key, nil] if pos.nil?
21
+
22
+ [node_key[0, pos], node_key[(pos + 1)..]]
20
23
  end
21
24
 
22
25
  def build_from_uri(uri)
26
+ return '' if uri.nil?
27
+
23
28
  "#{uri.host}#{DELIMITER}#{uri.port}"
24
29
  end
25
30
 
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'redis_client'
3
4
  require 'redis_client/cluster/command'
4
5
  require 'redis_client/cluster/errors'
5
6
  require 'redis_client/cluster/key_slot_converter'
@@ -8,116 +9,175 @@ require 'redis_client/cluster/node_key'
8
9
 
9
10
  class RedisClient
10
11
  class Cluster
12
+ class Pipeline
13
+ ReplySizeError = Class.new(::RedisClient::Error)
14
+
15
+ def initialize(client)
16
+ @client = client
17
+ @grouped = Hash.new([].freeze)
18
+ @size = 0
19
+ end
20
+
21
+ def call(*command, **kwargs)
22
+ node_key = @client.send(:find_node_key, *command, primary_only: true)
23
+ @grouped[node_key] += [[@size, :call, command, kwargs]]
24
+ @size += 1
25
+ end
26
+
27
+ def call_once(*command, **kwargs)
28
+ node_key = @client.send(:find_node_key, *command, primary_only: true)
29
+ @grouped[node_key] += [[@size, :call_once, command, kwargs]]
30
+ @size += 1
31
+ end
32
+
33
+ def blocking_call(timeout, *command, **kwargs)
34
+ node_key = @client.send(:find_node_key, *command, primary_only: true)
35
+ @grouped[node_key] += [[@size, :blocking_call, timeout, command, kwargs]]
36
+ @size += 1
37
+ end
38
+
39
+ def empty?
40
+ @size.zero?
41
+ end
42
+
43
+ def execute # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
44
+ all_replies = []
45
+ threads = @grouped.map do |k, v|
46
+ Thread.new(@client, k, v) do |client, node_key, rows|
47
+ Thread.pass
48
+
49
+ node_key = node_key.nil? ? client.instance_variable_get(:@node).primary_node_keys.sample : node_key
50
+ replies = client.send(:find_node, node_key).pipelined do |pipeline|
51
+ rows.each do |row|
52
+ case row[1]
53
+ when :call then pipeline.call(*row[2], **row[3])
54
+ when :call_once then pipeline.call_once(*row[2], **row[3])
55
+ when :blocking_call then pipeline.blocking_call(row[2], *row[3], **row[4])
56
+ else raise NotImplementedError, row[1]
57
+ end
58
+ end
59
+ end
60
+
61
+ raise ReplySizeError, "commands: #{rows.size}, replies: #{replies.size}" if rows.size != replies.size
62
+
63
+ rows.each_with_index { |row, idx| all_replies[row.first] = replies[idx] }
64
+ end
65
+ end
66
+
67
+ threads.each(&:join)
68
+ all_replies
69
+ end
70
+ end
71
+
72
+ class PubSub
73
+ def initialize(client)
74
+ @client = client
75
+ @pubsub = nil
76
+ end
77
+
78
+ def call(*command, **kwargs)
79
+ close
80
+ @pubsub = @client.send(:assign_node, *command).pubsub
81
+ @pubsub.call(*command, **kwargs)
82
+ end
83
+
84
+ def close
85
+ @pubsub&.close
86
+ @pubsub = nil
87
+ end
88
+
89
+ def next_event(timeout = nil)
90
+ @pubsub&.next_event(timeout)
91
+ end
92
+ end
93
+
94
+ ZERO_CURSOR_FOR_SCAN = '0'
95
+ CMD_SCAN = 'SCAN'
96
+ CMD_SSCAN = 'SSCAN'
97
+ CMD_HSCAN = 'HSCAN'
98
+ CMD_ZSCAN = 'ZSCAN'
99
+ CMD_ASKING = 'ASKING'
100
+ REPLY_OK = 'OK'
101
+ REPLY_MOVED = 'MOVED'
102
+ REPLY_ASK = 'ASK'
103
+
11
104
  def initialize(config, pool: nil, **kwargs)
12
105
  @config = config.dup
13
106
  @pool = pool
14
107
  @client_kwargs = kwargs
15
- @node = fetch_cluster_info!(@config, @pool, **@client_kwargs)
108
+ @node = fetch_cluster_info!(@config, pool: @pool, **@client_kwargs)
16
109
  @command = ::RedisClient::Cluster::Command.load(@node)
110
+ @mutex = Mutex.new
17
111
  end
18
112
 
19
113
  def inspect
20
- @node.inspect
114
+ "#<#{self.class.name} #{@node.node_keys.join(', ')}>"
21
115
  end
22
116
 
23
- def call(*command, **kwargs, &block)
24
- send_command(:call, *command, **kwargs, &block)
117
+ def call(*command, **kwargs)
118
+ send_command(:call, *command, **kwargs)
25
119
  end
26
120
 
27
- def call_once(*command, **kwargs, &block)
28
- send_command(:call_once, *command, **kwargs, &block)
121
+ def call_once(*command, **kwargs)
122
+ send_command(:call_once, *command, **kwargs)
29
123
  end
30
124
 
31
- def blocking_call(timeout, *command, **kwargs, &block)
125
+ def blocking_call(timeout, *command, **kwargs)
32
126
  node = assign_node(*command)
33
- try_send(node, :blocking_call, timeout, *command, **kwargs, &block)
127
+ try_send(node, :blocking_call, timeout, *command, **kwargs)
34
128
  end
35
129
 
36
130
  def scan(*args, **kwargs, &block)
37
- _scan(:scan, *args, **kwargs, &block)
131
+ raise ArgumentError, 'block required' unless block
132
+
133
+ cursor = ZERO_CURSOR_FOR_SCAN
134
+ loop do
135
+ cursor, keys = _scan(CMD_SCAN, cursor, *args, **kwargs)
136
+ keys.each(&block)
137
+ break if cursor == ZERO_CURSOR_FOR_SCAN
138
+ end
38
139
  end
39
140
 
40
141
  def sscan(key, *args, **kwargs, &block)
41
- node = assign_node('SSCAN', key)
142
+ node = assign_node(CMD_SSCAN, key)
42
143
  try_send(node, :sscan, key, *args, **kwargs, &block)
43
144
  end
44
145
 
45
146
  def hscan(key, *args, **kwargs, &block)
46
- node = assign_node('HSCAN', key)
147
+ node = assign_node(CMD_HSCAN, key)
47
148
  try_send(node, :hscan, key, *args, **kwargs, &block)
48
149
  end
49
150
 
50
151
  def zscan(key, *args, **kwargs, &block)
51
- node = assign_node('ZSCAN', key)
152
+ node = assign_node(CMD_ZSCAN, key)
52
153
  try_send(node, :zscan, key, *args, **kwargs, &block)
53
154
  end
54
155
 
55
156
  def pipelined
56
- # TODO: impl
57
- end
157
+ pipeline = ::RedisClient::Cluster::Pipeline.new(self)
158
+ yield pipeline
159
+ return [] if pipeline.empty? == 0
58
160
 
59
- def multi
60
- # TODO: impl
161
+ pipeline.execute
61
162
  end
62
163
 
63
164
  def pubsub
64
- # TODO: impl
65
- end
66
-
67
- def size
68
- # TODO: impl
69
- end
70
-
71
- def with(options = {})
72
- # TODO: impl
73
- end
74
- alias then with
75
-
76
- def id
77
- @node.flat_map(&:id).sort.join(' ')
78
- end
79
-
80
- def connected?
81
- @node.any?(&:connected?)
165
+ ::RedisClient::Cluster::PubSub.new(self)
82
166
  end
83
167
 
84
168
  def close
85
169
  @node.each(&:close)
86
- true
87
- end
88
-
89
- # TODO: remove
90
- def call_pipeline(pipeline)
91
- node_keys = pipeline.commands.filter_map { |cmd| find_node_key(cmd, primary_only: true) }.uniq
92
- if node_keys.size > 1
93
- raise(CrossSlotPipeliningError,
94
- pipeline.commands.map { |cmd| @command.extract_first_key(cmd) }.reject(&:empty?).uniq)
95
- end
96
-
97
- try_send(find_node(node_keys.first), :call_pipeline, pipeline)
98
- end
99
-
100
- # TODO: remove
101
- def process(commands, &block)
102
- if commands.size == 1 &&
103
- %w[unsubscribe punsubscribe].include?(commands.first.first.to_s.downcase) &&
104
- commands.first.size == 1
105
-
106
- # Node is indeterminate. We do just a best-effort try here.
107
- @node.process_all(commands, &block)
108
- else
109
- node = assign_node(commands.first)
110
- try_send(node, :process, commands, &block)
111
- end
170
+ nil
112
171
  end
113
172
 
114
173
  private
115
174
 
116
- def fetch_cluster_info!(config, pool, **kwargs)
175
+ def fetch_cluster_info!(config, pool: nil, **kwargs)
117
176
  node_info = ::RedisClient::Cluster::Node.load_info(config.per_node_key, **kwargs)
118
177
  node_addrs = node_info.map { |info| ::RedisClient::Cluster::NodeKey.hashify(info[:node_key]) }
119
178
  config.update_node(node_addrs)
120
- ::RedisClient::Cluster::Node.new(config.per_node_key, node_info, pool, with_replica: config.use_replica?, **kwargs)
179
+ ::RedisClient::Cluster::Node.new(config.per_node_key,
180
+ node_info: node_info, pool: pool, with_replica: config.use_replica?, **kwargs)
121
181
  end
122
182
 
123
183
  def send_command(method, *command, **kwargs, &block) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
@@ -130,7 +190,7 @@ class RedisClient
130
190
  when 'wait' then @node.call_primary(method, *command, **kwargs, &block).sum
131
191
  when 'keys' then @node.call_replica(method, *command, **kwargs, &block).flatten.sort
132
192
  when 'dbsize' then @node.call_replica(method, *command, **kwargs, &block).sum
133
- when 'scan' then _scan(method, *command, **kwargs, &block)
193
+ when 'scan' then _scan(*command, **kwargs)
134
194
  when 'lastsave' then @node.call_all(method, *command, **kwargs, &block).sort
135
195
  when 'role' then @node.call_all(method, *command, **kwargs, &block)
136
196
  when 'config' then send_config_command(method, *command, **kwargs, &block)
@@ -179,7 +239,7 @@ class RedisClient
179
239
  case subcommand
180
240
  when 'addslots', 'delslots', 'failover', 'forget', 'meet', 'replicate',
181
241
  'reset', 'set-config-epoch', 'setslot'
182
- raise ::RedisClient::Cluster::OrchestrationCommandNotSupported, 'cluster', subcommand
242
+ raise ::RedisClient::Cluster::OrchestrationCommandNotSupported, ['cluster', subcommand]
183
243
  when 'saveconfig' then @node.call_all(method, *command, **kwargs, &block).first
184
244
  else assign_node(*command).send(method, *command, **kwargs, &block)
185
245
  end
@@ -195,7 +255,7 @@ class RedisClient
195
255
  end
196
256
  end
197
257
 
198
- def send_pubsub_command(method, *command, **kwargs, &block) # rubocop:disable Metircs/AbcSize, Metrics/CyclomaticComplexity
258
+ def send_pubsub_command(method, *command, **kwargs, &block) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity
199
259
  case command[1].to_s.downcase
200
260
  when 'channels' then @node.call_all(method, *command, **kwargs, &block).flatten.uniq.sort
201
261
  when 'numsub'
@@ -211,17 +271,17 @@ class RedisClient
211
271
  def try_send(node, method, *args, retry_count: 3, **kwargs, &block) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
212
272
  node.send(method, *args, **kwargs, &block)
213
273
  rescue ::RedisClient::CommandError => e
214
- if e.message.start_with?('MOVED')
274
+ if e.message.start_with?(REPLY_MOVED)
215
275
  raise if retry_count <= 0
216
276
 
217
277
  node = assign_redirection_node(e.message)
218
278
  retry_count -= 1
219
279
  retry
220
- elsif e.message.start_with?('ASK')
280
+ elsif e.message.start_with?(REPLY_ASK)
221
281
  raise if retry_count <= 0
222
282
 
223
283
  node = assign_asking_node(e.message)
224
- node.call('ASKING')
284
+ node.call(CMD_ASKING)
225
285
  retry_count -= 1
226
286
  retry
227
287
  else
@@ -232,7 +292,8 @@ class RedisClient
232
292
  raise
233
293
  end
234
294
 
235
- def _scan(method, *command, **kwargs, &block) # rubocop:disable Metrics/MethodLength
295
+ def _scan(*command, **kwargs) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
296
+ command[1] = ZERO_CURSOR_FOR_SCAN if command.size == 1
236
297
  input_cursor = Integer(command[1])
237
298
 
238
299
  client_index = input_cursor % 256
@@ -241,11 +302,11 @@ class RedisClient
241
302
  clients = @node.scale_reading_clients
242
303
 
243
304
  client = clients[client_index]
244
- return ['0', []] unless client
305
+ return [ZERO_CURSOR_FOR_SCAN, []] unless client
245
306
 
246
307
  command[1] = raw_cursor.to_s
247
308
 
248
- result_cursor, result_keys = client.send(method, *command, **kwargs, &block)
309
+ result_cursor, result_keys = client.call(*command, **kwargs)
249
310
  result_cursor = Integer(result_cursor)
250
311
 
251
312
  client_index += 1 if result_cursor == 0
@@ -266,13 +327,17 @@ class RedisClient
266
327
  end
267
328
 
268
329
  def assign_node(*command)
269
- node_key = find_node_key(command)
330
+ node_key = find_node_key(*command)
270
331
  find_node(node_key)
271
332
  end
272
333
 
273
- def find_node_key(command, primary_only: false)
334
+ def find_node_key(*command, primary_only: false) # rubocop:disable Metrics/MethodLength
274
335
  key = @command.extract_first_key(command)
275
- return if key.empty?
336
+ if key.empty?
337
+ return @node.primary_node_keys.sample if @command.should_send_to_primary?(command) || primary_only
338
+
339
+ return @node.replica_node_keys.sample
340
+ end
276
341
 
277
342
  slot = ::RedisClient::Cluster::KeySlotConverter.convert(key)
278
343
  return unless @node.slot_exists?(slot)
@@ -294,13 +359,15 @@ class RedisClient
294
359
  end
295
360
 
296
361
  def update_cluster_info!(node_key = nil)
297
- unless node_key.nil?
298
- host, port = ::RedisClient::Cluster::NodeKey.split(node_key)
299
- @config.add_node(host, port)
362
+ @mutex.synchronize do
363
+ unless node_key.nil?
364
+ host, port = ::RedisClient::Cluster::NodeKey.split(node_key)
365
+ @config.add_node(host, port)
366
+ end
367
+
368
+ @node.each(&:close)
369
+ @node = fetch_cluster_info!(@config, pool: @pool, **@client_kwargs)
300
370
  end
301
-
302
- @node.each(&:close)
303
- @node = fetch_cluster_info!(@config, @pool, **@client_kwargs)
304
371
  end
305
372
  end
306
373
  end
@@ -7,23 +7,29 @@ require 'redis_client/cluster/node_key'
7
7
 
8
8
  class RedisClient
9
9
  class ClusterConfig
10
+ DEFAULT_HOST = '127.0.0.1'
11
+ DEFAULT_PORT = 6379
10
12
  DEFAULT_SCHEME = 'redis'
11
13
  SECURE_SCHEME = 'rediss'
14
+ DEFAULT_NODES = ["#{DEFAULT_SCHEME}://#{DEFAULT_HOST}:#{DEFAULT_PORT}"].freeze
12
15
  VALID_SCHEMES = [DEFAULT_SCHEME, SECURE_SCHEME].freeze
16
+ VALID_NODES_KEYS = %i[ssl username password host port db].freeze
17
+ MERGE_CONFIG_KEYS = %i[ssl username password].freeze
18
+ IGNORE_GENERIC_CONFIG_KEYS = %i[url host port path].freeze
19
+
13
20
  InvalidClientConfigError = Class.new(::RedisClient::Error)
14
21
 
15
- def initialize(nodes:, replica: false, fixed_hostname: nil, **client_config)
16
- @replica = replica
17
- @fixed_hostname = fixed_hostname
18
- @client_config = client_config.dup
22
+ def initialize(nodes: DEFAULT_NODES, replica: false, fixed_hostname: '', **client_config)
23
+ @replica = true & replica
24
+ @fixed_hostname = fixed_hostname.to_s
19
25
  @node_configs = build_node_configs(nodes.dup)
20
- add_common_node_config_if_needed(@client_config, @node_configs, :ssl)
21
- add_common_node_config_if_needed(@client_config, @node_configs, :username)
22
- add_common_node_config_if_needed(@client_config, @node_configs, :password)
26
+ client_config = client_config.reject { |k, _| IGNORE_GENERIC_CONFIG_KEYS.include?(k) }
27
+ @client_config = merge_generic_config(client_config, @node_configs)
28
+ @mutex = Mutex.new
23
29
  end
24
30
 
25
31
  def inspect
26
- per_node_key.to_s
32
+ "#<#{self.class.name} #{per_node_key.values}>"
27
33
  end
28
34
 
29
35
  def new_pool(size: 5, timeout: 5, **kwargs)
@@ -38,7 +44,7 @@ class RedisClient
38
44
  @node_configs.to_h do |config|
39
45
  node_key = ::RedisClient::Cluster::NodeKey.build_from_host_port(config[:host], config[:port])
40
46
  config = @client_config.merge(config)
41
- config = config.merge(host: @fixed_hostname) if @fixed_hostname && !@fixed_hostname.empty?
47
+ config = config.merge(host: @fixed_hostname) unless @fixed_hostname.empty?
42
48
  [node_key, config]
43
49
  end
44
50
  end
@@ -48,11 +54,11 @@ class RedisClient
48
54
  end
49
55
 
50
56
  def update_node(addrs)
51
- @node_configs = build_node_configs(addrs)
57
+ @mutex.synchronize { @node_configs = build_node_configs(addrs) }
52
58
  end
53
59
 
54
60
  def add_node(host, port)
55
- @node_configs << { host: host, port: port }
61
+ @mutex.synchronize { @node_configs << { host: host, port: port } }
56
62
  end
57
63
 
58
64
  def dup
@@ -62,9 +68,10 @@ class RedisClient
62
68
  private
63
69
 
64
70
  def build_node_configs(addrs)
65
- raise InvalidClientConfigError, 'Redis option of `cluster` must be an Array' unless addrs.is_a?(Array)
71
+ configs = Array[addrs].flatten.filter_map { |addr| parse_node_addr(addr) }
72
+ raise InvalidClientConfigError, '`nodes` option is empty' if configs.size.zero?
66
73
 
67
- addrs.map { |addr| parse_node_addr(addr) }
74
+ configs
68
75
  end
69
76
 
70
77
  def parse_node_addr(addr)
@@ -74,41 +81,51 @@ class RedisClient
74
81
  when Hash
75
82
  parse_node_option(addr)
76
83
  else
77
- raise InvalidClientConfigError, 'Redis option of `cluster` must includes String or Hash'
84
+ raise InvalidClientConfigError, "`nodes` option includes invalid type values: #{addr}"
78
85
  end
79
86
  end
80
87
 
81
- def parse_node_url(addr) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
88
+ def parse_node_url(addr) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/MethodLength
89
+ return if addr.empty?
90
+
82
91
  uri = URI(addr)
83
- raise InvalidClientConfigError, "Invalid uri scheme #{addr}" unless VALID_SCHEMES.include?(uri.scheme)
92
+ scheme = uri.scheme || DEFAULT_SCHEME
93
+ raise InvalidClientConfigError, "`nodes` option includes a invalid uri scheme: #{addr}" unless VALID_SCHEMES.include?(scheme)
84
94
 
85
- db = uri.path.split('/')[1]&.to_i
86
95
  username = uri.user ? URI.decode_www_form_component(uri.user) : nil
87
96
  password = uri.password ? URI.decode_www_form_component(uri.password) : nil
97
+ host = uri.host || DEFAULT_HOST
98
+ port = uri.port || DEFAULT_PORT
99
+ db = uri.path.index('/').nil? ? uri.path : uri.path.split('/')[1]
100
+ db = db.nil? || db.empty? ? db : ensure_integer(db)
88
101
 
89
- {
90
- host: uri.host,
91
- port: uri.port,
92
- username: username,
93
- password: password,
94
- db: db,
95
- ssl: uri.scheme == SECURE_SCHEME
96
- }.reject { |_, v| v.nil? || v == '' }
102
+ { ssl: scheme == SECURE_SCHEME, username: username, password: password, host: host, port: port, db: db }
103
+ .reject { |_, v| v.nil? || v == '' || v == false }
97
104
  rescue URI::InvalidURIError => e
98
- raise InvalidClientConfigError, e.message
105
+ raise InvalidClientConfigError, "#{e.message}: #{addr}"
99
106
  end
100
107
 
101
108
  def parse_node_option(addr)
109
+ return if addr.empty?
110
+
102
111
  addr = addr.transform_keys(&:to_sym)
103
- raise InvalidClientConfigError, 'Redis option of `cluster` must includes `:host` and `:port` keys' if addr.values_at(:host, :port).any?(&:nil?)
112
+ addr[:host] ||= DEFAULT_HOST
113
+ addr[:port] = ensure_integer(addr[:port] || DEFAULT_PORT)
114
+ addr.select { |k, _| VALID_NODES_KEYS.include?(k) }
115
+ end
104
116
 
105
- addr
117
+ def ensure_integer(value)
118
+ Integer(value)
119
+ rescue ArgumentError => e
120
+ raise InvalidClientConfigError, e.message
106
121
  end
107
122
 
108
- def add_common_node_config_if_needed(client_config, node_configs, key)
109
- return client_config if client_config[key].nil? && node_configs.first[key].nil?
123
+ def merge_generic_config(client_config, node_configs)
124
+ return client_config if node_configs.size.zero?
110
125
 
111
- client_config[key] ||= node_configs.first[key]
126
+ cfg = node_configs.first
127
+ MERGE_CONFIG_KEYS.each { |k| client_config[k] = cfg[k] if cfg.key?(k) }
128
+ client_config
112
129
  end
113
130
  end
114
131
  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.1
4
+ version: 0.0.4
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-12 00:00:00.000000000 Z
11
+ date: 2022-06-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: redis-client