redis-cluster-client 0.0.0 → 0.0.3

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: ad96d02be39509b6df863c42e630940f053b6aea6cb478fe1fbec26fb5464e7c
4
- data.tar.gz: '09983aedc4498a48f0fa2d7882e0df4b0998cd3ffd9d2ef13940abe50606a3e9'
3
+ metadata.gz: 7a4668f3eaba9c3053d1464bdf1dc85524e2ba43fdbb2ffbbadc46fb4759d5e6
4
+ data.tar.gz: eba5280f698fe559a69cdeba28cef2c0fff87023fa78e7d098042d9cc6c036ac
5
5
  SHA512:
6
- metadata.gz: '098a5674c2aa837fc26287c599b8dfd3d849af73ba5267a63b0741411c60312b15a819dc8fe0acd48e5a0d1696f610bb1c7196461ac12c6a49000a13c702836b'
7
- data.tar.gz: 5134b1fd9f789166f29555abece7ef888c9a4c9e73a3ea04c0e06ed93a872ca908fa9a17a0f86746c7fe532d14a8a307f457115cf9b6f989c24d2b26938f38e4
6
+ metadata.gz: 989fd1b82d429ae6450a8c9e1555fc3635c1592c4d68744ad33df9cb26f5d222666fbeb077af99757c6305956936f44b8e38aef405c1a275c10e501559337934
7
+ data.tar.gz: c0e88ca74a4d0ff70116969ea0722d1659858587c1cfe8a404edf41658c0b9a87e0f3651db02fe88ad6d099b2948814fcc9415aa793394fb4e75ebc5eef0acec
@@ -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
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'redis_client'
4
+ require 'redis_client/config'
4
5
  require 'redis_client/cluster/errors'
5
6
 
6
7
  class RedisClient
@@ -9,14 +10,33 @@ class RedisClient
9
10
  include Enumerable
10
11
 
11
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
+
12
17
  ReloadNeeded = Class.new(::RedisClient::Error)
13
18
 
19
+ class Config < ::RedisClient::Config
20
+ def initialize(scale_read: false, **kwargs)
21
+ @scale_read = scale_read
22
+ super(**kwargs)
23
+ end
24
+
25
+ private
26
+
27
+ def build_connection_prelude
28
+ prelude = super.dup
29
+ prelude << ['READONLY'] if @scale_read
30
+ prelude.freeze
31
+ end
32
+ end
33
+
14
34
  class << self
15
35
  def load_info(options, **kwargs)
16
- tmp_nodes = ::RedisClient::Cluster::Node.new(options, **kwargs)
36
+ startup_nodes = ::RedisClient::Cluster::Node.new(options, **kwargs)
17
37
 
18
- errors = tmp_nodes.map do |tmp_node|
19
- reply = tmp_node.call('CLUSTER', 'NODES')
38
+ errors = startup_nodes.map do |n|
39
+ reply = n.call('CLUSTER', 'NODES')
20
40
  return parse_node_info(reply)
21
41
  rescue ::RedisClient::ConnectionError, ::RedisClient::CommandError => e
22
42
  e
@@ -24,12 +44,13 @@ class RedisClient
24
44
 
25
45
  raise ::RedisClient::Cluster::InitialSetupError, errors
26
46
  ensure
27
- tmp_nodes&.each(&:close)
47
+ startup_nodes&.each(&:close)
28
48
  end
29
49
 
30
50
  private
31
51
 
32
52
  # @see https://redis.io/commands/cluster-nodes/
53
+ # @see https://github.com/redis/redis/blob/78960ad57b8a5e6af743d789ed8fd767e37d42b8/src/cluster.c#L4660-L4683
33
54
  def parse_node_info(info) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/MethodLength
34
55
  rows = info.split("\n").map(&:split)
35
56
  rows.each { |arr| arr[2] = arr[2].split(',') }
@@ -37,7 +58,13 @@ class RedisClient
37
58
  rows.each do |arr|
38
59
  arr[1] = arr[1].split('@').first
39
60
  arr[2] = (arr[2] & %w[master slave]).first
40
- 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)
41
68
  end
42
69
 
43
70
  rows.map do |arr|
@@ -47,15 +74,15 @@ class RedisClient
47
74
  end
48
75
  end
49
76
 
50
- def initialize(options, node_info = [], pool = nil, with_replica: false, **kwargs)
77
+ def initialize(options, node_info: [], pool: nil, with_replica: false, **kwargs)
51
78
  @with_replica = with_replica
52
79
  @slots = build_slot_node_mappings(node_info)
53
80
  @replications = build_replication_mappings(node_info)
54
- @clients = build_clients(options, pool, **kwargs)
81
+ @clients = build_clients(options, pool: pool, **kwargs)
55
82
  end
56
83
 
57
84
  def inspect
58
- @clients.keys.sort.to_s
85
+ "#<#{self.class.name} #{node_keys.join(', ')}>"
59
86
  end
60
87
 
61
88
  def each(&block)
@@ -66,6 +93,14 @@ class RedisClient
66
93
  @clients.values.sample
67
94
  end
68
95
 
96
+ def node_keys
97
+ @clients.keys.sort
98
+ end
99
+
100
+ def primary_node_keys
101
+ @clients.filter_map { |k, _| primary?(k) ? k : nil }.sort
102
+ end
103
+
69
104
  def find_by(node_key)
70
105
  @clients.fetch(node_key)
71
106
  rescue KeyError
@@ -94,32 +129,34 @@ class RedisClient
94
129
  end.values
95
130
  end
96
131
 
97
- # TODO: impl
98
- def process_all(commands, &block)
99
- try_map { |_, client| client.process(commands, &block) }.values
100
- end
101
-
102
132
  def scale_reading_clients
103
- reading_clients = []
104
-
105
- @clients.each do |node_key, client|
106
- next unless replica_disabled? ? primary?(node_key) : replica?(node_key)
107
-
108
- reading_clients << client
133
+ clients = @clients.select do |node_key, _|
134
+ replica_disabled? ? primary?(node_key) : replica?(node_key)
109
135
  end
110
136
 
111
- reading_clients
137
+ clients.values.sort_by do |client|
138
+ ::RedisClient::Cluster::NodeKey.build_from_host_port(client.config.host, client.config.port)
139
+ end
112
140
  end
113
141
 
114
142
  def slot_exists?(slot)
143
+ slot = Integer(slot)
144
+ return false if slot < MIN_SLOT || slot > MAX_SLOT
145
+
115
146
  !@slots[slot].nil?
116
147
  end
117
148
 
118
149
  def find_node_key_of_primary(slot)
150
+ slot = Integer(slot)
151
+ return if slot < MIN_SLOT || slot > MAX_SLOT
152
+
119
153
  @slots[slot]
120
154
  end
121
155
 
122
156
  def find_node_key_of_replica(slot)
157
+ slot = Integer(slot)
158
+ return if slot < MIN_SLOT || slot > MAX_SLOT
159
+
123
160
  return @slots[slot] if replica_disabled? || @replications[@slots[slot]].size.zero?
124
161
 
125
162
  @replications[@slots[slot]].sample
@@ -143,18 +180,6 @@ class RedisClient
143
180
  !(@replications.nil? || @replications.size.zero?) && @replications[node_key].size.zero?
144
181
  end
145
182
 
146
- def build_clients(options, pool, **kwargs)
147
- options.filter_map do |node_key, option|
148
- next if replica_disabled? && replica?(node_key)
149
-
150
- config = ::RedisClient.config(**option)
151
- client = pool.nil? ? config.new_client(**kwargs) : config.new_pool(**pool, **kwargs)
152
- client.call('READONLY') if replica?(node_key) # FIXME: Send every pooled conns
153
-
154
- [node_key, client]
155
- end.to_h
156
- end
157
-
158
183
  def build_slot_node_mappings(node_info)
159
184
  slots = Array.new(SLOT_SIZE)
160
185
  node_info.each do |info|
@@ -175,6 +200,20 @@ class RedisClient
175
200
  end
176
201
  end
177
202
 
203
+ def build_clients(options, pool: nil, **kwargs)
204
+ options.filter_map do |node_key, option|
205
+ next if replica_disabled? && replica?(node_key)
206
+
207
+ config = ::RedisClient::Cluster::Node::Config.new(
208
+ scale_read: replica?(node_key),
209
+ **option.merge(kwargs.reject { |k, _| IGNORE_GENERIC_CONFIG_KEYS.include?(k) })
210
+ )
211
+ client = pool.nil? ? config.new_client : config.new_pool(**pool)
212
+
213
+ [node_key, client]
214
+ end.to_h
215
+ end
216
+
178
217
  def try_map # rubocop:disable Metrics/MethodLength
179
218
  errors = {}
180
219
  results = {}
@@ -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,170 @@ 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
+ @replies = []
19
+ @size = 0
20
+ end
21
+
22
+ def call(*command, **kwargs)
23
+ node_key = @client.send(:find_node_key, *command, primary_only: true)
24
+ @grouped[node_key] += [[@size, :call, command, kwargs]]
25
+ @size += 1
26
+ end
27
+
28
+ def call_once(*command, **kwargs)
29
+ node_key = @client.send(:find_node_key, *command, primary_only: true)
30
+ @grouped[node_key] += [[@size, :call_once, command, kwargs]]
31
+ @size += 1
32
+ end
33
+
34
+ def blocking_call(timeout, *command, **kwargs)
35
+ node_key = @client.send(:find_node_key, *command, primary_only: true)
36
+ @grouped[node_key] += [[@size, :blocking_call, timeout, command, kwargs]]
37
+ @size += 1
38
+ end
39
+
40
+ def empty?
41
+ @size.zero?
42
+ end
43
+
44
+ # TODO: use concurrency
45
+ def execute # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
46
+ @grouped.each do |node_key, rows|
47
+ node_key = node_key.nil? ? @client.instance_variable_get(:@node).primary_node_keys.sample : node_key
48
+ replies = @client.send(:find_node, node_key).pipelined do |pipeline|
49
+ rows.each do |row|
50
+ case row[1]
51
+ when :call then pipeline.call(*row[2], **row[3])
52
+ when :call_once then pipeline.call_once(*row[2], **row[3])
53
+ when :blocking_call then pipeline.blocking_call(row[2], *row[3], **row[4])
54
+ else raise NotImplementedError, row[1]
55
+ end
56
+ end
57
+ end
58
+
59
+ raise ReplySizeError, "commands: #{rows.size}, replies: #{replies.size}" if rows.size != replies.size
60
+
61
+ rows.each_with_index { |row, idx| @replies[row.first] = replies[idx] }
62
+ end
63
+
64
+ @replies
65
+ end
66
+ end
67
+
68
+ class PubSub
69
+ def initialize(client)
70
+ @client = client
71
+ @pubsub = nil
72
+ end
73
+
74
+ def call(*command, **kwargs)
75
+ close
76
+ @pubsub = @client.send(:assign_node, *command).pubsub
77
+ @pubsub.call(*command, **kwargs)
78
+ end
79
+
80
+ def close
81
+ @pubsub&.close
82
+ @pubsub = nil
83
+ end
84
+
85
+ def next_event(timeout = nil)
86
+ @pubsub&.next_event(timeout)
87
+ end
88
+ end
89
+
90
+ ZERO_CURSOR_FOR_SCAN = '0'
91
+ CMD_SCAN = 'SCAN'
92
+ CMD_SSCAN = 'SSCAN'
93
+ CMD_HSCAN = 'HSCAN'
94
+ CMD_ZSCAN = 'ZSCAN'
95
+ CMD_ASKING = 'ASKING'
96
+ REPLY_OK = 'OK'
97
+ REPLY_MOVED = 'MOVED'
98
+ REPLY_ASK = 'ASK'
99
+
11
100
  def initialize(config, pool: nil, **kwargs)
12
101
  @config = config.dup
13
102
  @pool = pool
14
103
  @client_kwargs = kwargs
15
- @node = fetch_cluster_info!(@config, @pool, **@client_kwargs)
104
+ @node = fetch_cluster_info!(@config, pool: @pool, **@client_kwargs)
16
105
  @command = ::RedisClient::Cluster::Command.load(@node)
17
106
  end
18
107
 
19
108
  def inspect
20
- @node.inspect
109
+ "#<#{self.class.name} #{@node.node_keys.join(', ')}>"
21
110
  end
22
111
 
23
- def call(*command, **kwargs, &block)
24
- send_command(:call, *command, **kwargs, &block)
112
+ def call(*command, **kwargs)
113
+ send_command(:call, *command, **kwargs)
25
114
  end
26
115
 
27
- def call_once(*command, **kwargs, &block)
28
- send_command(:call_once, *command, **kwargs, &block)
116
+ def call_once(*command, **kwargs)
117
+ send_command(:call_once, *command, **kwargs)
29
118
  end
30
119
 
31
- def blocking_call(timeout, *command, **kwargs, &block)
120
+ def blocking_call(timeout, *command, **kwargs)
32
121
  node = assign_node(*command)
33
- try_send(node, :blocking_call, timeout, *command, **kwargs, &block)
122
+ try_send(node, :blocking_call, timeout, *command, **kwargs)
34
123
  end
35
124
 
36
125
  def scan(*args, **kwargs, &block)
37
- _scan(:scan, *args, **kwargs, &block)
126
+ raise ArgumentError, 'block required' unless block
127
+
128
+ cursor = ZERO_CURSOR_FOR_SCAN
129
+ loop do
130
+ cursor, keys = _scan(CMD_SCAN, cursor, *args, **kwargs)
131
+ keys.each(&block)
132
+ break if cursor == ZERO_CURSOR_FOR_SCAN
133
+ end
38
134
  end
39
135
 
40
136
  def sscan(key, *args, **kwargs, &block)
41
- node = assign_node('SSCAN', key)
137
+ node = assign_node(CMD_SSCAN, key)
42
138
  try_send(node, :sscan, key, *args, **kwargs, &block)
43
139
  end
44
140
 
45
141
  def hscan(key, *args, **kwargs, &block)
46
- node = assign_node('HSCAN', key)
142
+ node = assign_node(CMD_HSCAN, key)
47
143
  try_send(node, :hscan, key, *args, **kwargs, &block)
48
144
  end
49
145
 
50
146
  def zscan(key, *args, **kwargs, &block)
51
- node = assign_node('ZSCAN', key)
147
+ node = assign_node(CMD_ZSCAN, key)
52
148
  try_send(node, :zscan, key, *args, **kwargs, &block)
53
149
  end
54
150
 
55
151
  def pipelined
56
- # TODO: impl
57
- end
152
+ pipeline = ::RedisClient::Cluster::Pipeline.new(self)
153
+ yield pipeline
154
+ return [] if pipeline.empty? == 0
58
155
 
59
- def multi
60
- # TODO: impl
156
+ pipeline.execute
61
157
  end
62
158
 
63
159
  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?)
160
+ ::RedisClient::Cluster::PubSub.new(self)
82
161
  end
83
162
 
84
163
  def close
85
164
  @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
165
+ nil
112
166
  end
113
167
 
114
168
  private
115
169
 
116
- def fetch_cluster_info!(config, pool, **kwargs)
170
+ def fetch_cluster_info!(config, pool: nil, **kwargs)
117
171
  node_info = ::RedisClient::Cluster::Node.load_info(config.per_node_key, **kwargs)
118
172
  node_addrs = node_info.map { |info| ::RedisClient::Cluster::NodeKey.hashify(info[:node_key]) }
119
173
  config.update_node(node_addrs)
120
- ::RedisClient::Cluster::Node.new(config.per_node_key, node_info, pool, with_replica: config.use_replica?, **kwargs)
174
+ ::RedisClient::Cluster::Node.new(config.per_node_key,
175
+ node_info: node_info, pool: pool, with_replica: config.use_replica?, **kwargs)
121
176
  end
122
177
 
123
178
  def send_command(method, *command, **kwargs, &block) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
@@ -130,7 +185,7 @@ class RedisClient
130
185
  when 'wait' then @node.call_primary(method, *command, **kwargs, &block).sum
131
186
  when 'keys' then @node.call_replica(method, *command, **kwargs, &block).flatten.sort
132
187
  when 'dbsize' then @node.call_replica(method, *command, **kwargs, &block).sum
133
- when 'scan' then _scan(method, *command, **kwargs, &block)
188
+ when 'scan' then _scan(*command, **kwargs)
134
189
  when 'lastsave' then @node.call_all(method, *command, **kwargs, &block).sort
135
190
  when 'role' then @node.call_all(method, *command, **kwargs, &block)
136
191
  when 'config' then send_config_command(method, *command, **kwargs, &block)
@@ -179,7 +234,7 @@ class RedisClient
179
234
  case subcommand
180
235
  when 'addslots', 'delslots', 'failover', 'forget', 'meet', 'replicate',
181
236
  'reset', 'set-config-epoch', 'setslot'
182
- raise ::RedisClient::Cluster::OrchestrationCommandNotSupported, 'cluster', subcommand
237
+ raise ::RedisClient::Cluster::OrchestrationCommandNotSupported, ['cluster', subcommand]
183
238
  when 'saveconfig' then @node.call_all(method, *command, **kwargs, &block).first
184
239
  else assign_node(*command).send(method, *command, **kwargs, &block)
185
240
  end
@@ -195,7 +250,7 @@ class RedisClient
195
250
  end
196
251
  end
197
252
 
198
- def send_pubsub_command(method, *command, **kwargs, &block) # rubocop:disable Metircs/AbcSize, Metrics/CyclomaticComplexity
253
+ def send_pubsub_command(method, *command, **kwargs, &block) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity
199
254
  case command[1].to_s.downcase
200
255
  when 'channels' then @node.call_all(method, *command, **kwargs, &block).flatten.uniq.sort
201
256
  when 'numsub'
@@ -211,17 +266,17 @@ class RedisClient
211
266
  def try_send(node, method, *args, retry_count: 3, **kwargs, &block) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
212
267
  node.send(method, *args, **kwargs, &block)
213
268
  rescue ::RedisClient::CommandError => e
214
- if e.message.start_with?('MOVED')
269
+ if e.message.start_with?(REPLY_MOVED)
215
270
  raise if retry_count <= 0
216
271
 
217
272
  node = assign_redirection_node(e.message)
218
273
  retry_count -= 1
219
274
  retry
220
- elsif e.message.start_with?('ASK')
275
+ elsif e.message.start_with?(REPLY_ASK)
221
276
  raise if retry_count <= 0
222
277
 
223
278
  node = assign_asking_node(e.message)
224
- node.call('ASKING')
279
+ node.call(CMD_ASKING)
225
280
  retry_count -= 1
226
281
  retry
227
282
  else
@@ -232,7 +287,8 @@ class RedisClient
232
287
  raise
233
288
  end
234
289
 
235
- def _scan(method, *command, **kwargs, &block) # rubocop:disable Metrics/MethodLength
290
+ def _scan(*command, **kwargs) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
291
+ command[1] = ZERO_CURSOR_FOR_SCAN if command.size == 1
236
292
  input_cursor = Integer(command[1])
237
293
 
238
294
  client_index = input_cursor % 256
@@ -241,11 +297,11 @@ class RedisClient
241
297
  clients = @node.scale_reading_clients
242
298
 
243
299
  client = clients[client_index]
244
- return ['0', []] unless client
300
+ return [ZERO_CURSOR_FOR_SCAN, []] unless client
245
301
 
246
302
  command[1] = raw_cursor.to_s
247
303
 
248
- result_cursor, result_keys = client.send(method, *command, **kwargs, &block)
304
+ result_cursor, result_keys = client.call(*command, **kwargs)
249
305
  result_cursor = Integer(result_cursor)
250
306
 
251
307
  client_index += 1 if result_cursor == 0
@@ -266,11 +322,11 @@ class RedisClient
266
322
  end
267
323
 
268
324
  def assign_node(*command)
269
- node_key = find_node_key(command)
325
+ node_key = find_node_key(*command)
270
326
  find_node(node_key)
271
327
  end
272
328
 
273
- def find_node_key(command, primary_only: false)
329
+ def find_node_key(*command, primary_only: false)
274
330
  key = @command.extract_first_key(command)
275
331
  return if key.empty?
276
332
 
@@ -300,7 +356,7 @@ class RedisClient
300
356
  end
301
357
 
302
358
  @node.each(&:close)
303
- @node = fetch_cluster_info!(@config, @pool, **@client_kwargs)
359
+ @node = fetch_cluster_info!(@config, pool: @pool, **@client_kwargs)
304
360
  end
305
361
  end
306
362
  end
@@ -2,32 +2,31 @@
2
2
 
3
3
  require 'uri'
4
4
  require 'redis_client'
5
- require 'redis_client/config'
6
5
  require 'redis_client/cluster'
7
6
  require 'redis_client/cluster/node_key'
8
7
 
9
8
  class RedisClient
10
9
  class ClusterConfig
11
- include ::RedisClient::Config::Common
12
-
10
+ DEFAULT_HOST = '127.0.0.1'
11
+ DEFAULT_PORT = 6379
13
12
  DEFAULT_SCHEME = 'redis'
14
13
  SECURE_SCHEME = 'rediss'
14
+ DEFAULT_NODES = ["#{DEFAULT_SCHEME}://#{DEFAULT_HOST}:#{DEFAULT_PORT}"].freeze
15
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
+
16
19
  InvalidClientConfigError = Class.new(::RedisClient::Error)
17
20
 
18
- def initialize(nodes:, replica: false, fixed_hostname: nil, **client_config)
19
- @replica = replica
20
- @fixed_hostname = fixed_hostname
21
- @client_config = client_config.dup
21
+ def initialize(nodes: DEFAULT_NODES, replica: false, fixed_hostname: '', **client_config)
22
+ @replica = true & replica
23
+ @fixed_hostname = fixed_hostname.to_s
22
24
  @node_configs = build_node_configs(nodes.dup)
23
- add_common_node_config_if_needed(@client_config, @node_configs, :ssl)
24
- add_common_node_config_if_needed(@client_config, @node_configs, :username)
25
- add_common_node_config_if_needed(@client_config, @node_configs, :password)
26
- super(**@client_config)
25
+ @client_config = merge_generic_config(client_config, @node_configs)
27
26
  end
28
27
 
29
28
  def inspect
30
- per_node_key.to_s
29
+ "#<#{self.class.name} #{per_node_key.values}>"
31
30
  end
32
31
 
33
32
  def new_pool(size: 5, timeout: 5, **kwargs)
@@ -42,7 +41,7 @@ class RedisClient
42
41
  @node_configs.to_h do |config|
43
42
  node_key = ::RedisClient::Cluster::NodeKey.build_from_host_port(config[:host], config[:port])
44
43
  config = @client_config.merge(config)
45
- config = config.merge(host: @fixed_hostname) if @fixed_hostname && !@fixed_hostname.empty?
44
+ config = config.merge(host: @fixed_hostname) unless @fixed_hostname.empty?
46
45
  [node_key, config]
47
46
  end
48
47
  end
@@ -66,9 +65,10 @@ class RedisClient
66
65
  private
67
66
 
68
67
  def build_node_configs(addrs)
69
- raise InvalidClientConfigError, 'Redis option of `cluster` must be an Array' unless addrs.is_a?(Array)
68
+ configs = Array[addrs].flatten.filter_map { |addr| parse_node_addr(addr) }
69
+ raise InvalidClientConfigError, '`nodes` option is empty' if configs.size.zero?
70
70
 
71
- addrs.map { |addr| parse_node_addr(addr) }
71
+ configs
72
72
  end
73
73
 
74
74
  def parse_node_addr(addr)
@@ -78,41 +78,51 @@ class RedisClient
78
78
  when Hash
79
79
  parse_node_option(addr)
80
80
  else
81
- raise InvalidClientConfigError, 'Redis option of `cluster` must includes String or Hash'
81
+ raise InvalidClientConfigError, "`nodes` option includes invalid type values: #{addr}"
82
82
  end
83
83
  end
84
84
 
85
- def parse_node_url(addr) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
85
+ def parse_node_url(addr) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/MethodLength
86
+ return if addr.empty?
87
+
86
88
  uri = URI(addr)
87
- raise InvalidClientConfigError, "Invalid uri scheme #{addr}" unless VALID_SCHEMES.include?(uri.scheme)
89
+ scheme = uri.scheme || DEFAULT_SCHEME
90
+ raise InvalidClientConfigError, "`nodes` option includes a invalid uri scheme: #{addr}" unless VALID_SCHEMES.include?(scheme)
88
91
 
89
- db = uri.path.split('/')[1]&.to_i
90
92
  username = uri.user ? URI.decode_www_form_component(uri.user) : nil
91
93
  password = uri.password ? URI.decode_www_form_component(uri.password) : nil
94
+ host = uri.host || DEFAULT_HOST
95
+ port = uri.port || DEFAULT_PORT
96
+ db = uri.path.index('/').nil? ? uri.path : uri.path.split('/')[1]
97
+ db = db.nil? || db.empty? ? db : ensure_integer(db)
92
98
 
93
- {
94
- host: uri.host,
95
- port: uri.port,
96
- username: username,
97
- password: password,
98
- db: db,
99
- ssl: uri.scheme == SECURE_SCHEME
100
- }.reject { |_, v| v.nil? || v == '' }
99
+ { ssl: scheme == SECURE_SCHEME, username: username, password: password, host: host, port: port, db: db }
100
+ .reject { |_, v| v.nil? || v == '' || v == false }
101
101
  rescue URI::InvalidURIError => e
102
- raise InvalidClientConfigError, e.message
102
+ raise InvalidClientConfigError, "#{e.message}: #{addr}"
103
103
  end
104
104
 
105
105
  def parse_node_option(addr)
106
+ return if addr.empty?
107
+
106
108
  addr = addr.transform_keys(&:to_sym)
107
- raise InvalidClientConfigError, 'Redis option of `cluster` must includes `:host` and `:port` keys' if addr.values_at(:host, :port).any?(&:nil?)
109
+ addr[:host] ||= DEFAULT_HOST
110
+ addr[:port] = ensure_integer(addr[:port] || DEFAULT_PORT)
111
+ addr.select { |k, _| VALID_NODES_KEYS.include?(k) }
112
+ end
108
113
 
109
- addr
114
+ def ensure_integer(value)
115
+ Integer(value)
116
+ rescue ArgumentError => e
117
+ raise InvalidClientConfigError, e.message
110
118
  end
111
119
 
112
- def add_common_node_config_if_needed(client_config, node_configs, key)
113
- return client_config if client_config[key].nil? && node_configs.first[key].nil?
120
+ def merge_generic_config(client_config, node_configs)
121
+ return client_config if node_configs.size.zero?
114
122
 
115
- client_config[key] ||= node_configs.first[key]
123
+ cfg = node_configs.first
124
+ MERGE_CONFIG_KEYS.each { |k| client_config[k] = cfg[k] if cfg.key?(k) }
125
+ client_config
116
126
  end
117
127
  end
118
128
  end
metadata CHANGED
@@ -1,15 +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.0
4
+ version: 0.0.3
5
5
  platform: ruby
6
6
  authors:
7
- - Taishi
8
- - Kasuga
7
+ - Taishi Kasuga
9
8
  autorequire:
10
9
  bindir: bin
11
10
  cert_chain: []
12
- date: 2022-06-11 00:00:00.000000000 Z
11
+ date: 2022-06-17 00:00:00.000000000 Z
13
12
  dependencies:
14
13
  - !ruby/object:Gem::Dependency
15
14
  name: redis-client
@@ -61,7 +60,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
61
60
  - !ruby/object:Gem::Version
62
61
  version: '0'
63
62
  requirements: []
64
- rubygems_version: 3.3.13
63
+ rubygems_version: 3.3.15
65
64
  signing_key:
66
65
  specification_version: 4
67
66
  summary: A Redis cluster client for Ruby