redis-cluster-client 0.0.0 → 0.0.3

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