redis-cluster-client 0.0.1 → 0.0.4
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 +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
|