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 +4 -4
- data/lib/redis_client/cluster/command.rb +15 -13
- data/lib/redis_client/cluster/errors.rb +16 -17
- data/lib/redis_client/cluster/node.rb +44 -18
- data/lib/redis_client/cluster/node_key.rb +6 -1
- data/lib/redis_client/cluster.rb +97 -54
- data/lib/redis_client/cluster_config.rb +43 -29
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 374dde2480377552fd2178d4940d0f9f9b4473c4308c89fa210e2b0ac8f36d6d
|
4
|
+
data.tar.gz: 5b22559b68cd287904a17f853a4cfd6a0fc0bf53418eeca3988987a585a55b71
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
12
|
-
|
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
|
24
|
-
|
25
|
-
[
|
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
|
65
|
-
return
|
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
|
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
|
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
|
-
|
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
|
20
|
-
str =
|
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-
|
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
|
-
|
34
|
-
|
35
|
-
|
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
|
-
|
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
|
-
|
34
|
+
startup_nodes = ::RedisClient::Cluster::Node.new(options, **kwargs)
|
31
35
|
|
32
|
-
errors =
|
33
|
-
reply =
|
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
|
-
|
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
|
-
|
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
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
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
|
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
|
|
data/lib/redis_client/cluster.rb
CHANGED
@@ -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.
|
79
|
+
"#<#{self.class.name} #{@node.node_keys.join(', ')}>"
|
21
80
|
end
|
22
81
|
|
23
|
-
def call(*command, **kwargs
|
24
|
-
send_command(:call, *command, **kwargs
|
82
|
+
def call(*command, **kwargs)
|
83
|
+
send_command(:call, *command, **kwargs)
|
25
84
|
end
|
26
85
|
|
27
|
-
def call_once(*command, **kwargs
|
28
|
-
send_command(:call_once, *command, **kwargs
|
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
|
90
|
+
def blocking_call(timeout, *command, **kwargs)
|
32
91
|
node = assign_node(*command)
|
33
|
-
try_send(node, :blocking_call, timeout, *command, **kwargs
|
92
|
+
try_send(node, :blocking_call, timeout, *command, **kwargs)
|
34
93
|
end
|
35
94
|
|
36
95
|
def scan(*args, **kwargs, &block)
|
37
|
-
|
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
|
121
|
+
def mset
|
56
122
|
# TODO: impl
|
57
123
|
end
|
58
124
|
|
59
|
-
def
|
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
|
-
|
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,
|
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(
|
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
|
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(
|
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 [
|
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.
|
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
|
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
|
-
|
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.
|
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)
|
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
|
-
|
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
|
-
|
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,
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
114
|
+
def ensure_integer(value)
|
115
|
+
Integer(value)
|
116
|
+
rescue ArgumentError => e
|
117
|
+
raise InvalidClientConfigError, e.message
|
106
118
|
end
|
107
119
|
|
108
|
-
def
|
109
|
-
return client_config if
|
120
|
+
def merge_generic_config(client_config, node_configs)
|
121
|
+
return client_config if node_configs.size.zero?
|
110
122
|
|
111
|
-
|
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.
|
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-
|
11
|
+
date: 2022-06-16 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: redis-client
|