redis-cluster-client 0.0.1 → 0.0.2

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