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