redis-cluster-client 0.0.1 → 0.0.4
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 +66 -31
- data/lib/redis_client/cluster/node_key.rb +6 -1
- data/lib/redis_client/cluster.rb +147 -80
- data/lib/redis_client/cluster_config.rb +48 -31
- 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: 0561fdd5885f8c6c5c5c100af032b867224eff3ed91b546b6885f8bf3bd76663
|
|
4
|
+
data.tar.gz: 1ebe59416fb043af9da7a926cd338ac260ce1a46e5220b46f7a6744c8f1b8c2b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 86661400532550e005beb98e740e4939ffcf93c1b66fb47930bc2a5819f5dbba60e940c8e0183d312a6ce7cb55d1cc0634081029eb8e5c1b8f68ea2862d9e0df
|
|
7
|
+
data.tar.gz: 3629f63ed379946bc8951c57ada535573130e8db504a2f5b7644ef9091408ac3223c5b0a260b13d6b84ab889b3e47c4571bbc10f5b21ceaf6758dc60c28a00b3
|
|
@@ -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,10 @@ class RedisClient
|
|
|
10
10
|
include Enumerable
|
|
11
11
|
|
|
12
12
|
SLOT_SIZE = 16_384
|
|
13
|
+
MIN_SLOT = 0
|
|
14
|
+
MAX_SLOT = SLOT_SIZE - 1
|
|
15
|
+
IGNORE_GENERIC_CONFIG_KEYS = %i[url host port path].freeze
|
|
16
|
+
|
|
13
17
|
ReloadNeeded = Class.new(::RedisClient::Error)
|
|
14
18
|
|
|
15
19
|
class Config < ::RedisClient::Config
|
|
@@ -18,6 +22,8 @@ class RedisClient
|
|
|
18
22
|
super(**kwargs)
|
|
19
23
|
end
|
|
20
24
|
|
|
25
|
+
private
|
|
26
|
+
|
|
21
27
|
def build_connection_prelude
|
|
22
28
|
prelude = super.dup
|
|
23
29
|
prelude << ['READONLY'] if @scale_read
|
|
@@ -27,10 +33,10 @@ class RedisClient
|
|
|
27
33
|
|
|
28
34
|
class << self
|
|
29
35
|
def load_info(options, **kwargs)
|
|
30
|
-
|
|
36
|
+
startup_nodes = ::RedisClient::Cluster::Node.new(options, **kwargs)
|
|
31
37
|
|
|
32
|
-
errors =
|
|
33
|
-
reply =
|
|
38
|
+
errors = startup_nodes.map do |n|
|
|
39
|
+
reply = n.call('CLUSTER', 'NODES')
|
|
34
40
|
return parse_node_info(reply)
|
|
35
41
|
rescue ::RedisClient::ConnectionError, ::RedisClient::CommandError => e
|
|
36
42
|
e
|
|
@@ -38,12 +44,13 @@ class RedisClient
|
|
|
38
44
|
|
|
39
45
|
raise ::RedisClient::Cluster::InitialSetupError, errors
|
|
40
46
|
ensure
|
|
41
|
-
|
|
47
|
+
startup_nodes&.each(&:close)
|
|
42
48
|
end
|
|
43
49
|
|
|
44
50
|
private
|
|
45
51
|
|
|
46
52
|
# @see https://redis.io/commands/cluster-nodes/
|
|
53
|
+
# @see https://github.com/redis/redis/blob/78960ad57b8a5e6af743d789ed8fd767e37d42b8/src/cluster.c#L4660-L4683
|
|
47
54
|
def parse_node_info(info) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/MethodLength
|
|
48
55
|
rows = info.split("\n").map(&:split)
|
|
49
56
|
rows.each { |arr| arr[2] = arr[2].split(',') }
|
|
@@ -51,7 +58,13 @@ class RedisClient
|
|
|
51
58
|
rows.each do |arr|
|
|
52
59
|
arr[1] = arr[1].split('@').first
|
|
53
60
|
arr[2] = (arr[2] & %w[master slave]).first
|
|
54
|
-
|
|
61
|
+
if arr[8].nil?
|
|
62
|
+
arr[8] = []
|
|
63
|
+
next
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
arr[8] = arr[8].split(',').map { |r| r.split('-').map { |s| Integer(s) } }
|
|
67
|
+
arr[8] = arr[8].map { |a| a.size == 1 ? a << a.first : a }.map(&:sort)
|
|
55
68
|
end
|
|
56
69
|
|
|
57
70
|
rows.map do |arr|
|
|
@@ -61,15 +74,16 @@ class RedisClient
|
|
|
61
74
|
end
|
|
62
75
|
end
|
|
63
76
|
|
|
64
|
-
def initialize(options, node_info
|
|
77
|
+
def initialize(options, node_info: [], pool: nil, with_replica: false, **kwargs)
|
|
65
78
|
@with_replica = with_replica
|
|
66
79
|
@slots = build_slot_node_mappings(node_info)
|
|
67
80
|
@replications = build_replication_mappings(node_info)
|
|
68
|
-
@clients = build_clients(options, pool, **kwargs)
|
|
81
|
+
@clients = build_clients(options, pool: pool, **kwargs)
|
|
82
|
+
@mutex = Mutex.new
|
|
69
83
|
end
|
|
70
84
|
|
|
71
85
|
def inspect
|
|
72
|
-
|
|
86
|
+
"#<#{self.class.name} #{node_keys.join(', ')}>"
|
|
73
87
|
end
|
|
74
88
|
|
|
75
89
|
def each(&block)
|
|
@@ -80,6 +94,20 @@ class RedisClient
|
|
|
80
94
|
@clients.values.sample
|
|
81
95
|
end
|
|
82
96
|
|
|
97
|
+
def node_keys
|
|
98
|
+
@clients.keys.sort
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def primary_node_keys
|
|
102
|
+
@clients.filter_map { |k, _| primary?(k) ? k : nil }.sort
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def replica_node_keys
|
|
106
|
+
return primary_node_keys if replica_disabled?
|
|
107
|
+
|
|
108
|
+
@clients.filter_map { |k, _| replica?(k) ? k : nil }.sort
|
|
109
|
+
end
|
|
110
|
+
|
|
83
111
|
def find_by(node_key)
|
|
84
112
|
@clients.fetch(node_key)
|
|
85
113
|
rescue KeyError
|
|
@@ -108,39 +136,41 @@ class RedisClient
|
|
|
108
136
|
end.values
|
|
109
137
|
end
|
|
110
138
|
|
|
111
|
-
# TODO: impl
|
|
112
|
-
def process_all(commands, &block)
|
|
113
|
-
try_map { |_, client| client.process(commands, &block) }.values
|
|
114
|
-
end
|
|
115
|
-
|
|
116
139
|
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
|
|
140
|
+
clients = @clients.select do |node_key, _|
|
|
141
|
+
replica_disabled? ? primary?(node_key) : replica?(node_key)
|
|
123
142
|
end
|
|
124
143
|
|
|
125
|
-
|
|
144
|
+
clients.values.sort_by do |client|
|
|
145
|
+
::RedisClient::Cluster::NodeKey.build_from_host_port(client.config.host, client.config.port)
|
|
146
|
+
end
|
|
126
147
|
end
|
|
127
148
|
|
|
128
149
|
def slot_exists?(slot)
|
|
150
|
+
slot = Integer(slot)
|
|
151
|
+
return false if slot < MIN_SLOT || slot > MAX_SLOT
|
|
152
|
+
|
|
129
153
|
!@slots[slot].nil?
|
|
130
154
|
end
|
|
131
155
|
|
|
132
156
|
def find_node_key_of_primary(slot)
|
|
157
|
+
slot = Integer(slot)
|
|
158
|
+
return if slot < MIN_SLOT || slot > MAX_SLOT
|
|
159
|
+
|
|
133
160
|
@slots[slot]
|
|
134
161
|
end
|
|
135
162
|
|
|
136
163
|
def find_node_key_of_replica(slot)
|
|
164
|
+
slot = Integer(slot)
|
|
165
|
+
return if slot < MIN_SLOT || slot > MAX_SLOT
|
|
166
|
+
|
|
137
167
|
return @slots[slot] if replica_disabled? || @replications[@slots[slot]].size.zero?
|
|
138
168
|
|
|
139
169
|
@replications[@slots[slot]].sample
|
|
140
170
|
end
|
|
141
171
|
|
|
142
172
|
def update_slot(slot, node_key)
|
|
143
|
-
@slots[slot] = node_key
|
|
173
|
+
@mutex.synchronize { @slots[slot] = node_key }
|
|
144
174
|
end
|
|
145
175
|
|
|
146
176
|
private
|
|
@@ -177,12 +207,15 @@ class RedisClient
|
|
|
177
207
|
end
|
|
178
208
|
end
|
|
179
209
|
|
|
180
|
-
def build_clients(options, pool, **kwargs)
|
|
210
|
+
def build_clients(options, pool: nil, **kwargs)
|
|
181
211
|
options.filter_map do |node_key, option|
|
|
182
212
|
next if replica_disabled? && replica?(node_key)
|
|
183
213
|
|
|
184
|
-
config = ::RedisClient::Cluster::Node::Config.new(
|
|
185
|
-
|
|
214
|
+
config = ::RedisClient::Cluster::Node::Config.new(
|
|
215
|
+
scale_read: replica?(node_key),
|
|
216
|
+
**option.merge(kwargs.reject { |k, _| IGNORE_GENERIC_CONFIG_KEYS.include?(k) })
|
|
217
|
+
)
|
|
218
|
+
client = pool.nil? ? config.new_client : config.new_pool(**pool)
|
|
186
219
|
|
|
187
220
|
[node_key, client]
|
|
188
221
|
end.to_h
|
|
@@ -191,15 +224,17 @@ class RedisClient
|
|
|
191
224
|
def try_map # rubocop:disable Metrics/MethodLength
|
|
192
225
|
errors = {}
|
|
193
226
|
results = {}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
227
|
+
threads = @clients.map do |k, v|
|
|
228
|
+
Thread.new(k, v) do |node_key, client|
|
|
229
|
+
Thread.pass
|
|
230
|
+
reply = yield(node_key, client)
|
|
231
|
+
results[node_key] = reply unless reply.nil?
|
|
232
|
+
rescue ::RedisClient::CommandError => e
|
|
233
|
+
errors[node_key] = e
|
|
234
|
+
end
|
|
201
235
|
end
|
|
202
236
|
|
|
237
|
+
threads.each(&:join)
|
|
203
238
|
return results if errors.empty?
|
|
204
239
|
|
|
205
240
|
raise ::RedisClient::Cluster::CommandErrorCollection, errors
|
|
@@ -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)..]]
|
|
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,116 +9,175 @@ 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
|
+
@size = 0
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def call(*command, **kwargs)
|
|
22
|
+
node_key = @client.send(:find_node_key, *command, primary_only: true)
|
|
23
|
+
@grouped[node_key] += [[@size, :call, command, kwargs]]
|
|
24
|
+
@size += 1
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def call_once(*command, **kwargs)
|
|
28
|
+
node_key = @client.send(:find_node_key, *command, primary_only: true)
|
|
29
|
+
@grouped[node_key] += [[@size, :call_once, command, kwargs]]
|
|
30
|
+
@size += 1
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def blocking_call(timeout, *command, **kwargs)
|
|
34
|
+
node_key = @client.send(:find_node_key, *command, primary_only: true)
|
|
35
|
+
@grouped[node_key] += [[@size, :blocking_call, timeout, command, kwargs]]
|
|
36
|
+
@size += 1
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def empty?
|
|
40
|
+
@size.zero?
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def execute # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
44
|
+
all_replies = []
|
|
45
|
+
threads = @grouped.map do |k, v|
|
|
46
|
+
Thread.new(@client, k, v) do |client, node_key, rows|
|
|
47
|
+
Thread.pass
|
|
48
|
+
|
|
49
|
+
node_key = node_key.nil? ? client.instance_variable_get(:@node).primary_node_keys.sample : node_key
|
|
50
|
+
replies = client.send(:find_node, node_key).pipelined do |pipeline|
|
|
51
|
+
rows.each do |row|
|
|
52
|
+
case row[1]
|
|
53
|
+
when :call then pipeline.call(*row[2], **row[3])
|
|
54
|
+
when :call_once then pipeline.call_once(*row[2], **row[3])
|
|
55
|
+
when :blocking_call then pipeline.blocking_call(row[2], *row[3], **row[4])
|
|
56
|
+
else raise NotImplementedError, row[1]
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
raise ReplySizeError, "commands: #{rows.size}, replies: #{replies.size}" if rows.size != replies.size
|
|
62
|
+
|
|
63
|
+
rows.each_with_index { |row, idx| all_replies[row.first] = replies[idx] }
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
threads.each(&:join)
|
|
68
|
+
all_replies
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
class PubSub
|
|
73
|
+
def initialize(client)
|
|
74
|
+
@client = client
|
|
75
|
+
@pubsub = nil
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def call(*command, **kwargs)
|
|
79
|
+
close
|
|
80
|
+
@pubsub = @client.send(:assign_node, *command).pubsub
|
|
81
|
+
@pubsub.call(*command, **kwargs)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def close
|
|
85
|
+
@pubsub&.close
|
|
86
|
+
@pubsub = nil
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def next_event(timeout = nil)
|
|
90
|
+
@pubsub&.next_event(timeout)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
ZERO_CURSOR_FOR_SCAN = '0'
|
|
95
|
+
CMD_SCAN = 'SCAN'
|
|
96
|
+
CMD_SSCAN = 'SSCAN'
|
|
97
|
+
CMD_HSCAN = 'HSCAN'
|
|
98
|
+
CMD_ZSCAN = 'ZSCAN'
|
|
99
|
+
CMD_ASKING = 'ASKING'
|
|
100
|
+
REPLY_OK = 'OK'
|
|
101
|
+
REPLY_MOVED = 'MOVED'
|
|
102
|
+
REPLY_ASK = 'ASK'
|
|
103
|
+
|
|
11
104
|
def initialize(config, pool: nil, **kwargs)
|
|
12
105
|
@config = config.dup
|
|
13
106
|
@pool = pool
|
|
14
107
|
@client_kwargs = kwargs
|
|
15
|
-
@node = fetch_cluster_info!(@config, @pool, **@client_kwargs)
|
|
108
|
+
@node = fetch_cluster_info!(@config, pool: @pool, **@client_kwargs)
|
|
16
109
|
@command = ::RedisClient::Cluster::Command.load(@node)
|
|
110
|
+
@mutex = Mutex.new
|
|
17
111
|
end
|
|
18
112
|
|
|
19
113
|
def inspect
|
|
20
|
-
@node.
|
|
114
|
+
"#<#{self.class.name} #{@node.node_keys.join(', ')}>"
|
|
21
115
|
end
|
|
22
116
|
|
|
23
|
-
def call(*command, **kwargs
|
|
24
|
-
send_command(:call, *command, **kwargs
|
|
117
|
+
def call(*command, **kwargs)
|
|
118
|
+
send_command(:call, *command, **kwargs)
|
|
25
119
|
end
|
|
26
120
|
|
|
27
|
-
def call_once(*command, **kwargs
|
|
28
|
-
send_command(:call_once, *command, **kwargs
|
|
121
|
+
def call_once(*command, **kwargs)
|
|
122
|
+
send_command(:call_once, *command, **kwargs)
|
|
29
123
|
end
|
|
30
124
|
|
|
31
|
-
def blocking_call(timeout, *command, **kwargs
|
|
125
|
+
def blocking_call(timeout, *command, **kwargs)
|
|
32
126
|
node = assign_node(*command)
|
|
33
|
-
try_send(node, :blocking_call, timeout, *command, **kwargs
|
|
127
|
+
try_send(node, :blocking_call, timeout, *command, **kwargs)
|
|
34
128
|
end
|
|
35
129
|
|
|
36
130
|
def scan(*args, **kwargs, &block)
|
|
37
|
-
|
|
131
|
+
raise ArgumentError, 'block required' unless block
|
|
132
|
+
|
|
133
|
+
cursor = ZERO_CURSOR_FOR_SCAN
|
|
134
|
+
loop do
|
|
135
|
+
cursor, keys = _scan(CMD_SCAN, cursor, *args, **kwargs)
|
|
136
|
+
keys.each(&block)
|
|
137
|
+
break if cursor == ZERO_CURSOR_FOR_SCAN
|
|
138
|
+
end
|
|
38
139
|
end
|
|
39
140
|
|
|
40
141
|
def sscan(key, *args, **kwargs, &block)
|
|
41
|
-
node = assign_node(
|
|
142
|
+
node = assign_node(CMD_SSCAN, key)
|
|
42
143
|
try_send(node, :sscan, key, *args, **kwargs, &block)
|
|
43
144
|
end
|
|
44
145
|
|
|
45
146
|
def hscan(key, *args, **kwargs, &block)
|
|
46
|
-
node = assign_node(
|
|
147
|
+
node = assign_node(CMD_HSCAN, key)
|
|
47
148
|
try_send(node, :hscan, key, *args, **kwargs, &block)
|
|
48
149
|
end
|
|
49
150
|
|
|
50
151
|
def zscan(key, *args, **kwargs, &block)
|
|
51
|
-
node = assign_node(
|
|
152
|
+
node = assign_node(CMD_ZSCAN, key)
|
|
52
153
|
try_send(node, :zscan, key, *args, **kwargs, &block)
|
|
53
154
|
end
|
|
54
155
|
|
|
55
156
|
def pipelined
|
|
56
|
-
|
|
57
|
-
|
|
157
|
+
pipeline = ::RedisClient::Cluster::Pipeline.new(self)
|
|
158
|
+
yield pipeline
|
|
159
|
+
return [] if pipeline.empty? == 0
|
|
58
160
|
|
|
59
|
-
|
|
60
|
-
# TODO: impl
|
|
161
|
+
pipeline.execute
|
|
61
162
|
end
|
|
62
163
|
|
|
63
164
|
def pubsub
|
|
64
|
-
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
def size
|
|
68
|
-
# TODO: impl
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
def with(options = {})
|
|
72
|
-
# TODO: impl
|
|
73
|
-
end
|
|
74
|
-
alias then with
|
|
75
|
-
|
|
76
|
-
def id
|
|
77
|
-
@node.flat_map(&:id).sort.join(' ')
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
def connected?
|
|
81
|
-
@node.any?(&:connected?)
|
|
165
|
+
::RedisClient::Cluster::PubSub.new(self)
|
|
82
166
|
end
|
|
83
167
|
|
|
84
168
|
def close
|
|
85
169
|
@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
|
|
170
|
+
nil
|
|
112
171
|
end
|
|
113
172
|
|
|
114
173
|
private
|
|
115
174
|
|
|
116
|
-
def fetch_cluster_info!(config, pool, **kwargs)
|
|
175
|
+
def fetch_cluster_info!(config, pool: nil, **kwargs)
|
|
117
176
|
node_info = ::RedisClient::Cluster::Node.load_info(config.per_node_key, **kwargs)
|
|
118
177
|
node_addrs = node_info.map { |info| ::RedisClient::Cluster::NodeKey.hashify(info[:node_key]) }
|
|
119
178
|
config.update_node(node_addrs)
|
|
120
|
-
::RedisClient::Cluster::Node.new(config.per_node_key,
|
|
179
|
+
::RedisClient::Cluster::Node.new(config.per_node_key,
|
|
180
|
+
node_info: node_info, pool: pool, with_replica: config.use_replica?, **kwargs)
|
|
121
181
|
end
|
|
122
182
|
|
|
123
183
|
def send_command(method, *command, **kwargs, &block) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
|
|
@@ -130,7 +190,7 @@ class RedisClient
|
|
|
130
190
|
when 'wait' then @node.call_primary(method, *command, **kwargs, &block).sum
|
|
131
191
|
when 'keys' then @node.call_replica(method, *command, **kwargs, &block).flatten.sort
|
|
132
192
|
when 'dbsize' then @node.call_replica(method, *command, **kwargs, &block).sum
|
|
133
|
-
when 'scan' then _scan(
|
|
193
|
+
when 'scan' then _scan(*command, **kwargs)
|
|
134
194
|
when 'lastsave' then @node.call_all(method, *command, **kwargs, &block).sort
|
|
135
195
|
when 'role' then @node.call_all(method, *command, **kwargs, &block)
|
|
136
196
|
when 'config' then send_config_command(method, *command, **kwargs, &block)
|
|
@@ -179,7 +239,7 @@ class RedisClient
|
|
|
179
239
|
case subcommand
|
|
180
240
|
when 'addslots', 'delslots', 'failover', 'forget', 'meet', 'replicate',
|
|
181
241
|
'reset', 'set-config-epoch', 'setslot'
|
|
182
|
-
raise ::RedisClient::Cluster::OrchestrationCommandNotSupported, 'cluster', subcommand
|
|
242
|
+
raise ::RedisClient::Cluster::OrchestrationCommandNotSupported, ['cluster', subcommand]
|
|
183
243
|
when 'saveconfig' then @node.call_all(method, *command, **kwargs, &block).first
|
|
184
244
|
else assign_node(*command).send(method, *command, **kwargs, &block)
|
|
185
245
|
end
|
|
@@ -195,7 +255,7 @@ class RedisClient
|
|
|
195
255
|
end
|
|
196
256
|
end
|
|
197
257
|
|
|
198
|
-
def send_pubsub_command(method, *command, **kwargs, &block) # rubocop:disable
|
|
258
|
+
def send_pubsub_command(method, *command, **kwargs, &block) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity
|
|
199
259
|
case command[1].to_s.downcase
|
|
200
260
|
when 'channels' then @node.call_all(method, *command, **kwargs, &block).flatten.uniq.sort
|
|
201
261
|
when 'numsub'
|
|
@@ -211,17 +271,17 @@ class RedisClient
|
|
|
211
271
|
def try_send(node, method, *args, retry_count: 3, **kwargs, &block) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
212
272
|
node.send(method, *args, **kwargs, &block)
|
|
213
273
|
rescue ::RedisClient::CommandError => e
|
|
214
|
-
if e.message.start_with?(
|
|
274
|
+
if e.message.start_with?(REPLY_MOVED)
|
|
215
275
|
raise if retry_count <= 0
|
|
216
276
|
|
|
217
277
|
node = assign_redirection_node(e.message)
|
|
218
278
|
retry_count -= 1
|
|
219
279
|
retry
|
|
220
|
-
elsif e.message.start_with?(
|
|
280
|
+
elsif e.message.start_with?(REPLY_ASK)
|
|
221
281
|
raise if retry_count <= 0
|
|
222
282
|
|
|
223
283
|
node = assign_asking_node(e.message)
|
|
224
|
-
node.call(
|
|
284
|
+
node.call(CMD_ASKING)
|
|
225
285
|
retry_count -= 1
|
|
226
286
|
retry
|
|
227
287
|
else
|
|
@@ -232,7 +292,8 @@ class RedisClient
|
|
|
232
292
|
raise
|
|
233
293
|
end
|
|
234
294
|
|
|
235
|
-
def _scan(
|
|
295
|
+
def _scan(*command, **kwargs) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
|
296
|
+
command[1] = ZERO_CURSOR_FOR_SCAN if command.size == 1
|
|
236
297
|
input_cursor = Integer(command[1])
|
|
237
298
|
|
|
238
299
|
client_index = input_cursor % 256
|
|
@@ -241,11 +302,11 @@ class RedisClient
|
|
|
241
302
|
clients = @node.scale_reading_clients
|
|
242
303
|
|
|
243
304
|
client = clients[client_index]
|
|
244
|
-
return [
|
|
305
|
+
return [ZERO_CURSOR_FOR_SCAN, []] unless client
|
|
245
306
|
|
|
246
307
|
command[1] = raw_cursor.to_s
|
|
247
308
|
|
|
248
|
-
result_cursor, result_keys = client.
|
|
309
|
+
result_cursor, result_keys = client.call(*command, **kwargs)
|
|
249
310
|
result_cursor = Integer(result_cursor)
|
|
250
311
|
|
|
251
312
|
client_index += 1 if result_cursor == 0
|
|
@@ -266,13 +327,17 @@ class RedisClient
|
|
|
266
327
|
end
|
|
267
328
|
|
|
268
329
|
def assign_node(*command)
|
|
269
|
-
node_key = find_node_key(command)
|
|
330
|
+
node_key = find_node_key(*command)
|
|
270
331
|
find_node(node_key)
|
|
271
332
|
end
|
|
272
333
|
|
|
273
|
-
def find_node_key(command, primary_only: false)
|
|
334
|
+
def find_node_key(*command, primary_only: false) # rubocop:disable Metrics/MethodLength
|
|
274
335
|
key = @command.extract_first_key(command)
|
|
275
|
-
|
|
336
|
+
if key.empty?
|
|
337
|
+
return @node.primary_node_keys.sample if @command.should_send_to_primary?(command) || primary_only
|
|
338
|
+
|
|
339
|
+
return @node.replica_node_keys.sample
|
|
340
|
+
end
|
|
276
341
|
|
|
277
342
|
slot = ::RedisClient::Cluster::KeySlotConverter.convert(key)
|
|
278
343
|
return unless @node.slot_exists?(slot)
|
|
@@ -294,13 +359,15 @@ class RedisClient
|
|
|
294
359
|
end
|
|
295
360
|
|
|
296
361
|
def update_cluster_info!(node_key = nil)
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
362
|
+
@mutex.synchronize do
|
|
363
|
+
unless node_key.nil?
|
|
364
|
+
host, port = ::RedisClient::Cluster::NodeKey.split(node_key)
|
|
365
|
+
@config.add_node(host, port)
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
@node.each(&:close)
|
|
369
|
+
@node = fetch_cluster_info!(@config, pool: @pool, **@client_kwargs)
|
|
300
370
|
end
|
|
301
|
-
|
|
302
|
-
@node.each(&:close)
|
|
303
|
-
@node = fetch_cluster_info!(@config, @pool, **@client_kwargs)
|
|
304
371
|
end
|
|
305
372
|
end
|
|
306
373
|
end
|
|
@@ -7,23 +7,29 @@ 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
|
+
IGNORE_GENERIC_CONFIG_KEYS = %i[url host port path].freeze
|
|
19
|
+
|
|
13
20
|
InvalidClientConfigError = Class.new(::RedisClient::Error)
|
|
14
21
|
|
|
15
|
-
def initialize(nodes
|
|
16
|
-
@replica = replica
|
|
17
|
-
@fixed_hostname = fixed_hostname
|
|
18
|
-
@client_config = client_config.dup
|
|
22
|
+
def initialize(nodes: DEFAULT_NODES, replica: false, fixed_hostname: '', **client_config)
|
|
23
|
+
@replica = true & replica
|
|
24
|
+
@fixed_hostname = fixed_hostname.to_s
|
|
19
25
|
@node_configs = build_node_configs(nodes.dup)
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
26
|
+
client_config = client_config.reject { |k, _| IGNORE_GENERIC_CONFIG_KEYS.include?(k) }
|
|
27
|
+
@client_config = merge_generic_config(client_config, @node_configs)
|
|
28
|
+
@mutex = Mutex.new
|
|
23
29
|
end
|
|
24
30
|
|
|
25
31
|
def inspect
|
|
26
|
-
per_node_key.
|
|
32
|
+
"#<#{self.class.name} #{per_node_key.values}>"
|
|
27
33
|
end
|
|
28
34
|
|
|
29
35
|
def new_pool(size: 5, timeout: 5, **kwargs)
|
|
@@ -38,7 +44,7 @@ class RedisClient
|
|
|
38
44
|
@node_configs.to_h do |config|
|
|
39
45
|
node_key = ::RedisClient::Cluster::NodeKey.build_from_host_port(config[:host], config[:port])
|
|
40
46
|
config = @client_config.merge(config)
|
|
41
|
-
config = config.merge(host: @fixed_hostname)
|
|
47
|
+
config = config.merge(host: @fixed_hostname) unless @fixed_hostname.empty?
|
|
42
48
|
[node_key, config]
|
|
43
49
|
end
|
|
44
50
|
end
|
|
@@ -48,11 +54,11 @@ class RedisClient
|
|
|
48
54
|
end
|
|
49
55
|
|
|
50
56
|
def update_node(addrs)
|
|
51
|
-
@node_configs = build_node_configs(addrs)
|
|
57
|
+
@mutex.synchronize { @node_configs = build_node_configs(addrs) }
|
|
52
58
|
end
|
|
53
59
|
|
|
54
60
|
def add_node(host, port)
|
|
55
|
-
@node_configs << { host: host, port: port }
|
|
61
|
+
@mutex.synchronize { @node_configs << { host: host, port: port } }
|
|
56
62
|
end
|
|
57
63
|
|
|
58
64
|
def dup
|
|
@@ -62,9 +68,10 @@ class RedisClient
|
|
|
62
68
|
private
|
|
63
69
|
|
|
64
70
|
def build_node_configs(addrs)
|
|
65
|
-
|
|
71
|
+
configs = Array[addrs].flatten.filter_map { |addr| parse_node_addr(addr) }
|
|
72
|
+
raise InvalidClientConfigError, '`nodes` option is empty' if configs.size.zero?
|
|
66
73
|
|
|
67
|
-
|
|
74
|
+
configs
|
|
68
75
|
end
|
|
69
76
|
|
|
70
77
|
def parse_node_addr(addr)
|
|
@@ -74,41 +81,51 @@ class RedisClient
|
|
|
74
81
|
when Hash
|
|
75
82
|
parse_node_option(addr)
|
|
76
83
|
else
|
|
77
|
-
raise InvalidClientConfigError,
|
|
84
|
+
raise InvalidClientConfigError, "`nodes` option includes invalid type values: #{addr}"
|
|
78
85
|
end
|
|
79
86
|
end
|
|
80
87
|
|
|
81
|
-
def parse_node_url(addr) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
|
|
88
|
+
def parse_node_url(addr) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/MethodLength
|
|
89
|
+
return if addr.empty?
|
|
90
|
+
|
|
82
91
|
uri = URI(addr)
|
|
83
|
-
|
|
92
|
+
scheme = uri.scheme || DEFAULT_SCHEME
|
|
93
|
+
raise InvalidClientConfigError, "`nodes` option includes a invalid uri scheme: #{addr}" unless VALID_SCHEMES.include?(scheme)
|
|
84
94
|
|
|
85
|
-
db = uri.path.split('/')[1]&.to_i
|
|
86
95
|
username = uri.user ? URI.decode_www_form_component(uri.user) : nil
|
|
87
96
|
password = uri.password ? URI.decode_www_form_component(uri.password) : nil
|
|
97
|
+
host = uri.host || DEFAULT_HOST
|
|
98
|
+
port = uri.port || DEFAULT_PORT
|
|
99
|
+
db = uri.path.index('/').nil? ? uri.path : uri.path.split('/')[1]
|
|
100
|
+
db = db.nil? || db.empty? ? db : ensure_integer(db)
|
|
88
101
|
|
|
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 == '' }
|
|
102
|
+
{ ssl: scheme == SECURE_SCHEME, username: username, password: password, host: host, port: port, db: db }
|
|
103
|
+
.reject { |_, v| v.nil? || v == '' || v == false }
|
|
97
104
|
rescue URI::InvalidURIError => e
|
|
98
|
-
raise InvalidClientConfigError, e.message
|
|
105
|
+
raise InvalidClientConfigError, "#{e.message}: #{addr}"
|
|
99
106
|
end
|
|
100
107
|
|
|
101
108
|
def parse_node_option(addr)
|
|
109
|
+
return if addr.empty?
|
|
110
|
+
|
|
102
111
|
addr = addr.transform_keys(&:to_sym)
|
|
103
|
-
|
|
112
|
+
addr[:host] ||= DEFAULT_HOST
|
|
113
|
+
addr[:port] = ensure_integer(addr[:port] || DEFAULT_PORT)
|
|
114
|
+
addr.select { |k, _| VALID_NODES_KEYS.include?(k) }
|
|
115
|
+
end
|
|
104
116
|
|
|
105
|
-
|
|
117
|
+
def ensure_integer(value)
|
|
118
|
+
Integer(value)
|
|
119
|
+
rescue ArgumentError => e
|
|
120
|
+
raise InvalidClientConfigError, e.message
|
|
106
121
|
end
|
|
107
122
|
|
|
108
|
-
def
|
|
109
|
-
return client_config if
|
|
123
|
+
def merge_generic_config(client_config, node_configs)
|
|
124
|
+
return client_config if node_configs.size.zero?
|
|
110
125
|
|
|
111
|
-
|
|
126
|
+
cfg = node_configs.first
|
|
127
|
+
MERGE_CONFIG_KEYS.each { |k| client_config[k] = cfg[k] if cfg.key?(k) }
|
|
128
|
+
client_config
|
|
112
129
|
end
|
|
113
130
|
end
|
|
114
131
|
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.4
|
|
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-17 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: redis-client
|