redis-cluster-client 0.0.1 → 0.0.2

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: 4339be83a679ef61e9d9a98e26cd2dd52b58b56eaa852a53c8f0f01ef1987396
4
- data.tar.gz: ac7ea102f9d5a711ee495a36c844443027b39d2c64e0d9177d419d00e1fa0de0
3
+ metadata.gz: 374dde2480377552fd2178d4940d0f9f9b4473c4308c89fa210e2b0ac8f36d6d
4
+ data.tar.gz: 5b22559b68cd287904a17f853a4cfd6a0fc0bf53418eeca3988987a585a55b71
5
5
  SHA512:
6
- metadata.gz: 268000fa3691afc08d969b35cf8ac90221e96ffb41db59c63d9737e9ebed3d978f84f36ba4ee91ceb4bb88c957c3d2ecead676bc7b6c7a4411598969213e269c
7
- data.tar.gz: 1a44f77bdd4202f327025d3fa0d0a3b65b577332cd714f787159c8ba41f20f5b3c8b058b5c48aab74f5d0a7b3ec4638f227b5f205ed07c8e32dba4c71220695b
6
+ metadata.gz: da95dc7179899376c736d6e5b75a04b9f8d91d0b97095b2478c9b96879f0f4ff01d75ffe86bf3efdebb2af0e13b6397a3afd41a1e6857fc41097f53ac8cdacf2
7
+ data.tar.gz: 6759c62ea25c92e8160540b5edb354a03253d7d43199ccbf0479d1b19070b90cb17877b376012186ff063316f28f16157d8fbd1e0bb3b67c3168ed92d6afa31a
@@ -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,8 @@ class RedisClient
10
10
  include Enumerable
11
11
 
12
12
  SLOT_SIZE = 16_384
13
+ MIN_SLOT = 0
14
+ MAX_SLOT = SLOT_SIZE - 1
13
15
  ReloadNeeded = Class.new(::RedisClient::Error)
14
16
 
15
17
  class Config < ::RedisClient::Config
@@ -18,6 +20,8 @@ class RedisClient
18
20
  super(**kwargs)
19
21
  end
20
22
 
23
+ private
24
+
21
25
  def build_connection_prelude
22
26
  prelude = super.dup
23
27
  prelude << ['READONLY'] if @scale_read
@@ -27,10 +31,10 @@ class RedisClient
27
31
 
28
32
  class << self
29
33
  def load_info(options, **kwargs)
30
- tmp_nodes = ::RedisClient::Cluster::Node.new(options, **kwargs)
34
+ startup_nodes = ::RedisClient::Cluster::Node.new(options, **kwargs)
31
35
 
32
- errors = tmp_nodes.map do |tmp_node|
33
- reply = tmp_node.call('CLUSTER', 'NODES')
36
+ errors = startup_nodes.map do |n|
37
+ reply = n.call('CLUSTER', 'NODES')
34
38
  return parse_node_info(reply)
35
39
  rescue ::RedisClient::ConnectionError, ::RedisClient::CommandError => e
36
40
  e
@@ -38,12 +42,13 @@ class RedisClient
38
42
 
39
43
  raise ::RedisClient::Cluster::InitialSetupError, errors
40
44
  ensure
41
- tmp_nodes&.each(&:close)
45
+ startup_nodes&.each(&:close)
42
46
  end
43
47
 
44
48
  private
45
49
 
46
50
  # @see https://redis.io/commands/cluster-nodes/
51
+ # @see https://github.com/redis/redis/blob/78960ad57b8a5e6af743d789ed8fd767e37d42b8/src/cluster.c#L4660-L4683
47
52
  def parse_node_info(info) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/MethodLength
48
53
  rows = info.split("\n").map(&:split)
49
54
  rows.each { |arr| arr[2] = arr[2].split(',') }
@@ -51,7 +56,13 @@ class RedisClient
51
56
  rows.each do |arr|
52
57
  arr[1] = arr[1].split('@').first
53
58
  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) } }
59
+ if arr[8].nil?
60
+ arr[8] = []
61
+ next
62
+ end
63
+
64
+ arr[8] = arr[8].split(',').map { |r| r.split('-').map { |s| Integer(s) } }
65
+ arr[8] = arr[8].map { |a| a.size == 1 ? a << a.first : a }.map(&:sort)
55
66
  end
56
67
 
57
68
  rows.map do |arr|
@@ -61,15 +72,15 @@ class RedisClient
61
72
  end
62
73
  end
63
74
 
64
- def initialize(options, node_info = [], pool = nil, with_replica: false, **kwargs)
75
+ def initialize(options, node_info: [], pool: nil, with_replica: false, **kwargs)
65
76
  @with_replica = with_replica
66
77
  @slots = build_slot_node_mappings(node_info)
67
78
  @replications = build_replication_mappings(node_info)
68
- @clients = build_clients(options, pool, **kwargs)
79
+ @clients = build_clients(options, pool: pool, **kwargs)
69
80
  end
70
81
 
71
82
  def inspect
72
- @clients.keys.sort.to_s
83
+ "#<#{self.class.name} #{node_keys.join(', ')}>"
73
84
  end
74
85
 
75
86
  def each(&block)
@@ -80,6 +91,14 @@ class RedisClient
80
91
  @clients.values.sample
81
92
  end
82
93
 
94
+ def node_keys
95
+ @clients.keys.sort
96
+ end
97
+
98
+ def primary_node_keys
99
+ @clients.filter_map { |k, _| primary?(k) ? k : nil }.sort
100
+ end
101
+
83
102
  def find_by(node_key)
84
103
  @clients.fetch(node_key)
85
104
  rescue KeyError
@@ -114,26 +133,33 @@ class RedisClient
114
133
  end
115
134
 
116
135
  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
136
+ clients = @clients.select do |node_key, _|
137
+ replica_disabled? ? primary?(node_key) : replica?(node_key)
123
138
  end
124
139
 
125
- reading_clients
140
+ clients.values.sort_by do |client|
141
+ ::RedisClient::Cluster::NodeKey.build_from_host_port(client.config.host, client.config.port)
142
+ end
126
143
  end
127
144
 
128
145
  def slot_exists?(slot)
146
+ slot = Integer(slot)
147
+ return false if slot < MIN_SLOT || slot > MAX_SLOT
148
+
129
149
  !@slots[slot].nil?
130
150
  end
131
151
 
132
152
  def find_node_key_of_primary(slot)
153
+ slot = Integer(slot)
154
+ return if slot < MIN_SLOT || slot > MAX_SLOT
155
+
133
156
  @slots[slot]
134
157
  end
135
158
 
136
159
  def find_node_key_of_replica(slot)
160
+ slot = Integer(slot)
161
+ return if slot < MIN_SLOT || slot > MAX_SLOT
162
+
137
163
  return @slots[slot] if replica_disabled? || @replications[@slots[slot]].size.zero?
138
164
 
139
165
  @replications[@slots[slot]].sample
@@ -177,12 +203,12 @@ class RedisClient
177
203
  end
178
204
  end
179
205
 
180
- def build_clients(options, pool, **kwargs)
206
+ def build_clients(options, pool: nil, **kwargs)
181
207
  options.filter_map do |node_key, option|
182
208
  next if replica_disabled? && replica?(node_key)
183
209
 
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)
210
+ config = ::RedisClient::Cluster::Node::Config.new(scale_read: replica?(node_key), **option.merge(kwargs))
211
+ client = pool.nil? ? config.new_client : config.new_pool(**pool)
186
212
 
187
213
  [node_key, client]
188
214
  end.to_h
@@ -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, node_key.size - 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,33 +9,98 @@ 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
+ ZERO_CURSOR_FOR_SCAN = '0'
69
+
11
70
  def initialize(config, pool: nil, **kwargs)
12
71
  @config = config.dup
13
72
  @pool = pool
14
73
  @client_kwargs = kwargs
15
- @node = fetch_cluster_info!(@config, @pool, **@client_kwargs)
74
+ @node = fetch_cluster_info!(@config, pool: @pool, **@client_kwargs)
16
75
  @command = ::RedisClient::Cluster::Command.load(@node)
17
76
  end
18
77
 
19
78
  def inspect
20
- @node.inspect
79
+ "#<#{self.class.name} #{@node.node_keys.join(', ')}>"
21
80
  end
22
81
 
23
- def call(*command, **kwargs, &block)
24
- send_command(:call, *command, **kwargs, &block)
82
+ def call(*command, **kwargs)
83
+ send_command(:call, *command, **kwargs)
25
84
  end
26
85
 
27
- def call_once(*command, **kwargs, &block)
28
- send_command(:call_once, *command, **kwargs, &block)
86
+ def call_once(*command, **kwargs)
87
+ send_command(:call_once, *command, **kwargs)
29
88
  end
30
89
 
31
- def blocking_call(timeout, *command, **kwargs, &block)
90
+ def blocking_call(timeout, *command, **kwargs)
32
91
  node = assign_node(*command)
33
- try_send(node, :blocking_call, timeout, *command, **kwargs, &block)
92
+ try_send(node, :blocking_call, timeout, *command, **kwargs)
34
93
  end
35
94
 
36
95
  def scan(*args, **kwargs, &block)
37
- _scan(:scan, *args, **kwargs, &block)
96
+ raise ArgumentError, 'block required' unless block
97
+
98
+ cursor = ZERO_CURSOR_FOR_SCAN
99
+ loop do
100
+ cursor, keys = _scan('SCAN', cursor, *args, **kwargs)
101
+ keys.each(&block)
102
+ break if cursor == ZERO_CURSOR_FOR_SCAN
103
+ end
38
104
  end
39
105
 
40
106
  def sscan(key, *args, **kwargs, &block)
@@ -52,14 +118,22 @@ class RedisClient
52
118
  try_send(node, :zscan, key, *args, **kwargs, &block)
53
119
  end
54
120
 
55
- def pipelined
121
+ def mset
56
122
  # TODO: impl
57
123
  end
58
124
 
59
- def multi
125
+ def mget
60
126
  # TODO: impl
61
127
  end
62
128
 
129
+ def pipelined
130
+ pipeline = ::RedisClient::Cluster::Pipeline.new(self)
131
+ yield pipeline
132
+ return [] if pipeline.empty? == 0
133
+
134
+ pipeline.execute
135
+ end
136
+
63
137
  def pubsub
64
138
  # TODO: impl
65
139
  end
@@ -73,51 +147,19 @@ class RedisClient
73
147
  end
74
148
  alias then with
75
149
 
76
- def id
77
- @node.flat_map(&:id).sort.join(' ')
78
- end
79
-
80
- def connected?
81
- @node.any?(&:connected?)
82
- end
83
-
84
150
  def close
85
151
  @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
152
+ nil
112
153
  end
113
154
 
114
155
  private
115
156
 
116
- def fetch_cluster_info!(config, pool, **kwargs)
157
+ def fetch_cluster_info!(config, pool: nil, **kwargs)
117
158
  node_info = ::RedisClient::Cluster::Node.load_info(config.per_node_key, **kwargs)
118
159
  node_addrs = node_info.map { |info| ::RedisClient::Cluster::NodeKey.hashify(info[:node_key]) }
119
160
  config.update_node(node_addrs)
120
- ::RedisClient::Cluster::Node.new(config.per_node_key, node_info, pool, with_replica: config.use_replica?, **kwargs)
161
+ ::RedisClient::Cluster::Node.new(config.per_node_key,
162
+ node_info: node_info, pool: pool, with_replica: config.use_replica?, **kwargs)
121
163
  end
122
164
 
123
165
  def send_command(method, *command, **kwargs, &block) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
@@ -130,7 +172,7 @@ class RedisClient
130
172
  when 'wait' then @node.call_primary(method, *command, **kwargs, &block).sum
131
173
  when 'keys' then @node.call_replica(method, *command, **kwargs, &block).flatten.sort
132
174
  when 'dbsize' then @node.call_replica(method, *command, **kwargs, &block).sum
133
- when 'scan' then _scan(method, *command, **kwargs, &block)
175
+ when 'scan' then _scan(*command, **kwargs)
134
176
  when 'lastsave' then @node.call_all(method, *command, **kwargs, &block).sort
135
177
  when 'role' then @node.call_all(method, *command, **kwargs, &block)
136
178
  when 'config' then send_config_command(method, *command, **kwargs, &block)
@@ -179,7 +221,7 @@ class RedisClient
179
221
  case subcommand
180
222
  when 'addslots', 'delslots', 'failover', 'forget', 'meet', 'replicate',
181
223
  'reset', 'set-config-epoch', 'setslot'
182
- raise ::RedisClient::Cluster::OrchestrationCommandNotSupported, 'cluster', subcommand
224
+ raise ::RedisClient::Cluster::OrchestrationCommandNotSupported, ['cluster', subcommand]
183
225
  when 'saveconfig' then @node.call_all(method, *command, **kwargs, &block).first
184
226
  else assign_node(*command).send(method, *command, **kwargs, &block)
185
227
  end
@@ -195,7 +237,7 @@ class RedisClient
195
237
  end
196
238
  end
197
239
 
198
- def send_pubsub_command(method, *command, **kwargs, &block) # rubocop:disable Metircs/AbcSize, Metrics/CyclomaticComplexity
240
+ def send_pubsub_command(method, *command, **kwargs, &block) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity
199
241
  case command[1].to_s.downcase
200
242
  when 'channels' then @node.call_all(method, *command, **kwargs, &block).flatten.uniq.sort
201
243
  when 'numsub'
@@ -232,7 +274,8 @@ class RedisClient
232
274
  raise
233
275
  end
234
276
 
235
- def _scan(method, *command, **kwargs, &block) # rubocop:disable Metrics/MethodLength
277
+ def _scan(*command, **kwargs) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
278
+ command[1] = ZERO_CURSOR_FOR_SCAN if command.size == 1
236
279
  input_cursor = Integer(command[1])
237
280
 
238
281
  client_index = input_cursor % 256
@@ -241,11 +284,11 @@ class RedisClient
241
284
  clients = @node.scale_reading_clients
242
285
 
243
286
  client = clients[client_index]
244
- return ['0', []] unless client
287
+ return [ZERO_CURSOR_FOR_SCAN, []] unless client
245
288
 
246
289
  command[1] = raw_cursor.to_s
247
290
 
248
- result_cursor, result_keys = client.send(method, *command, **kwargs, &block)
291
+ result_cursor, result_keys = client.call(*command, **kwargs)
249
292
  result_cursor = Integer(result_cursor)
250
293
 
251
294
  client_index += 1 if result_cursor == 0
@@ -300,7 +343,7 @@ class RedisClient
300
343
  end
301
344
 
302
345
  @node.each(&:close)
303
- @node = fetch_cluster_info!(@config, @pool, **@client_kwargs)
346
+ @node = fetch_cluster_info!(@config, pool: @pool, **@client_kwargs)
304
347
  end
305
348
  end
306
349
  end
@@ -7,23 +7,26 @@ 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
+
13
19
  InvalidClientConfigError = Class.new(::RedisClient::Error)
14
20
 
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
21
+ def initialize(nodes: DEFAULT_NODES, replica: false, fixed_hostname: '', **client_config)
22
+ @replica = true & replica
23
+ @fixed_hostname = fixed_hostname.to_s
19
24
  @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)
25
+ @client_config = merge_generic_config(client_config, @node_configs)
23
26
  end
24
27
 
25
28
  def inspect
26
- per_node_key.to_s
29
+ "#<#{self.class.name} #{per_node_key.values}>"
27
30
  end
28
31
 
29
32
  def new_pool(size: 5, timeout: 5, **kwargs)
@@ -38,7 +41,7 @@ class RedisClient
38
41
  @node_configs.to_h do |config|
39
42
  node_key = ::RedisClient::Cluster::NodeKey.build_from_host_port(config[:host], config[:port])
40
43
  config = @client_config.merge(config)
41
- config = config.merge(host: @fixed_hostname) if @fixed_hostname && !@fixed_hostname.empty?
44
+ config = config.merge(host: @fixed_hostname) unless @fixed_hostname.empty?
42
45
  [node_key, config]
43
46
  end
44
47
  end
@@ -62,9 +65,10 @@ class RedisClient
62
65
  private
63
66
 
64
67
  def build_node_configs(addrs)
65
- 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?
66
70
 
67
- addrs.map { |addr| parse_node_addr(addr) }
71
+ configs
68
72
  end
69
73
 
70
74
  def parse_node_addr(addr)
@@ -74,41 +78,51 @@ class RedisClient
74
78
  when Hash
75
79
  parse_node_option(addr)
76
80
  else
77
- raise InvalidClientConfigError, 'Redis option of `cluster` must includes String or Hash'
81
+ raise InvalidClientConfigError, "`nodes` option includes invalid type values: #{addr}"
78
82
  end
79
83
  end
80
84
 
81
- 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
+
82
88
  uri = URI(addr)
83
- 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)
84
91
 
85
- db = uri.path.split('/')[1]&.to_i
86
92
  username = uri.user ? URI.decode_www_form_component(uri.user) : nil
87
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)
88
98
 
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 == '' }
99
+ { ssl: scheme == SECURE_SCHEME, username: username, password: password, host: host, port: port, db: db }
100
+ .reject { |_, v| v.nil? || v == '' || v == false }
97
101
  rescue URI::InvalidURIError => e
98
- raise InvalidClientConfigError, e.message
102
+ raise InvalidClientConfigError, "#{e.message}: #{addr}"
99
103
  end
100
104
 
101
105
  def parse_node_option(addr)
106
+ return if addr.empty?
107
+
102
108
  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?)
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
104
113
 
105
- addr
114
+ def ensure_integer(value)
115
+ Integer(value)
116
+ rescue ArgumentError => e
117
+ raise InvalidClientConfigError, e.message
106
118
  end
107
119
 
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?
120
+ def merge_generic_config(client_config, node_configs)
121
+ return client_config if node_configs.size.zero?
110
122
 
111
- 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
112
126
  end
113
127
  end
114
128
  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.2
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-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: redis-client