redis-cluster-client 0.0.0 → 0.0.3
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 +71 -32
- data/lib/redis_client/cluster/node_key.rb +6 -1
- data/lib/redis_client/cluster.rb +130 -74
- data/lib/redis_client/cluster_config.rb +43 -33
- metadata +4 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7a4668f3eaba9c3053d1464bdf1dc85524e2ba43fdbb2ffbbadc46fb4759d5e6
|
4
|
+
data.tar.gz: eba5280f698fe559a69cdeba28cef2c0fff87023fa78e7d098042d9cc6c036ac
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 989fd1b82d429ae6450a8c9e1555fc3635c1592c4d68744ad33df9cb26f5d222666fbeb077af99757c6305956936f44b8e38aef405c1a275c10e501559337934
|
7
|
+
data.tar.gz: c0e88ca74a4d0ff70116969ea0722d1659858587c1cfe8a404edf41658c0b9a87e0f3651db02fe88ad6d099b2948814fcc9415aa793394fb4e75ebc5eef0acec
|
@@ -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
|
@@ -1,6 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'redis_client'
|
4
|
+
require 'redis_client/config'
|
4
5
|
require 'redis_client/cluster/errors'
|
5
6
|
|
6
7
|
class RedisClient
|
@@ -9,14 +10,33 @@ class RedisClient
|
|
9
10
|
include Enumerable
|
10
11
|
|
11
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
|
+
|
12
17
|
ReloadNeeded = Class.new(::RedisClient::Error)
|
13
18
|
|
19
|
+
class Config < ::RedisClient::Config
|
20
|
+
def initialize(scale_read: false, **kwargs)
|
21
|
+
@scale_read = scale_read
|
22
|
+
super(**kwargs)
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def build_connection_prelude
|
28
|
+
prelude = super.dup
|
29
|
+
prelude << ['READONLY'] if @scale_read
|
30
|
+
prelude.freeze
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
14
34
|
class << self
|
15
35
|
def load_info(options, **kwargs)
|
16
|
-
|
36
|
+
startup_nodes = ::RedisClient::Cluster::Node.new(options, **kwargs)
|
17
37
|
|
18
|
-
errors =
|
19
|
-
reply =
|
38
|
+
errors = startup_nodes.map do |n|
|
39
|
+
reply = n.call('CLUSTER', 'NODES')
|
20
40
|
return parse_node_info(reply)
|
21
41
|
rescue ::RedisClient::ConnectionError, ::RedisClient::CommandError => e
|
22
42
|
e
|
@@ -24,12 +44,13 @@ class RedisClient
|
|
24
44
|
|
25
45
|
raise ::RedisClient::Cluster::InitialSetupError, errors
|
26
46
|
ensure
|
27
|
-
|
47
|
+
startup_nodes&.each(&:close)
|
28
48
|
end
|
29
49
|
|
30
50
|
private
|
31
51
|
|
32
52
|
# @see https://redis.io/commands/cluster-nodes/
|
53
|
+
# @see https://github.com/redis/redis/blob/78960ad57b8a5e6af743d789ed8fd767e37d42b8/src/cluster.c#L4660-L4683
|
33
54
|
def parse_node_info(info) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/MethodLength
|
34
55
|
rows = info.split("\n").map(&:split)
|
35
56
|
rows.each { |arr| arr[2] = arr[2].split(',') }
|
@@ -37,7 +58,13 @@ class RedisClient
|
|
37
58
|
rows.each do |arr|
|
38
59
|
arr[1] = arr[1].split('@').first
|
39
60
|
arr[2] = (arr[2] & %w[master slave]).first
|
40
|
-
|
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)
|
41
68
|
end
|
42
69
|
|
43
70
|
rows.map do |arr|
|
@@ -47,15 +74,15 @@ class RedisClient
|
|
47
74
|
end
|
48
75
|
end
|
49
76
|
|
50
|
-
def initialize(options, node_info
|
77
|
+
def initialize(options, node_info: [], pool: nil, with_replica: false, **kwargs)
|
51
78
|
@with_replica = with_replica
|
52
79
|
@slots = build_slot_node_mappings(node_info)
|
53
80
|
@replications = build_replication_mappings(node_info)
|
54
|
-
@clients = build_clients(options, pool, **kwargs)
|
81
|
+
@clients = build_clients(options, pool: pool, **kwargs)
|
55
82
|
end
|
56
83
|
|
57
84
|
def inspect
|
58
|
-
|
85
|
+
"#<#{self.class.name} #{node_keys.join(', ')}>"
|
59
86
|
end
|
60
87
|
|
61
88
|
def each(&block)
|
@@ -66,6 +93,14 @@ class RedisClient
|
|
66
93
|
@clients.values.sample
|
67
94
|
end
|
68
95
|
|
96
|
+
def node_keys
|
97
|
+
@clients.keys.sort
|
98
|
+
end
|
99
|
+
|
100
|
+
def primary_node_keys
|
101
|
+
@clients.filter_map { |k, _| primary?(k) ? k : nil }.sort
|
102
|
+
end
|
103
|
+
|
69
104
|
def find_by(node_key)
|
70
105
|
@clients.fetch(node_key)
|
71
106
|
rescue KeyError
|
@@ -94,32 +129,34 @@ class RedisClient
|
|
94
129
|
end.values
|
95
130
|
end
|
96
131
|
|
97
|
-
# TODO: impl
|
98
|
-
def process_all(commands, &block)
|
99
|
-
try_map { |_, client| client.process(commands, &block) }.values
|
100
|
-
end
|
101
|
-
|
102
132
|
def scale_reading_clients
|
103
|
-
|
104
|
-
|
105
|
-
@clients.each do |node_key, client|
|
106
|
-
next unless replica_disabled? ? primary?(node_key) : replica?(node_key)
|
107
|
-
|
108
|
-
reading_clients << client
|
133
|
+
clients = @clients.select do |node_key, _|
|
134
|
+
replica_disabled? ? primary?(node_key) : replica?(node_key)
|
109
135
|
end
|
110
136
|
|
111
|
-
|
137
|
+
clients.values.sort_by do |client|
|
138
|
+
::RedisClient::Cluster::NodeKey.build_from_host_port(client.config.host, client.config.port)
|
139
|
+
end
|
112
140
|
end
|
113
141
|
|
114
142
|
def slot_exists?(slot)
|
143
|
+
slot = Integer(slot)
|
144
|
+
return false if slot < MIN_SLOT || slot > MAX_SLOT
|
145
|
+
|
115
146
|
!@slots[slot].nil?
|
116
147
|
end
|
117
148
|
|
118
149
|
def find_node_key_of_primary(slot)
|
150
|
+
slot = Integer(slot)
|
151
|
+
return if slot < MIN_SLOT || slot > MAX_SLOT
|
152
|
+
|
119
153
|
@slots[slot]
|
120
154
|
end
|
121
155
|
|
122
156
|
def find_node_key_of_replica(slot)
|
157
|
+
slot = Integer(slot)
|
158
|
+
return if slot < MIN_SLOT || slot > MAX_SLOT
|
159
|
+
|
123
160
|
return @slots[slot] if replica_disabled? || @replications[@slots[slot]].size.zero?
|
124
161
|
|
125
162
|
@replications[@slots[slot]].sample
|
@@ -143,18 +180,6 @@ class RedisClient
|
|
143
180
|
!(@replications.nil? || @replications.size.zero?) && @replications[node_key].size.zero?
|
144
181
|
end
|
145
182
|
|
146
|
-
def build_clients(options, pool, **kwargs)
|
147
|
-
options.filter_map do |node_key, option|
|
148
|
-
next if replica_disabled? && replica?(node_key)
|
149
|
-
|
150
|
-
config = ::RedisClient.config(**option)
|
151
|
-
client = pool.nil? ? config.new_client(**kwargs) : config.new_pool(**pool, **kwargs)
|
152
|
-
client.call('READONLY') if replica?(node_key) # FIXME: Send every pooled conns
|
153
|
-
|
154
|
-
[node_key, client]
|
155
|
-
end.to_h
|
156
|
-
end
|
157
|
-
|
158
183
|
def build_slot_node_mappings(node_info)
|
159
184
|
slots = Array.new(SLOT_SIZE)
|
160
185
|
node_info.each do |info|
|
@@ -175,6 +200,20 @@ class RedisClient
|
|
175
200
|
end
|
176
201
|
end
|
177
202
|
|
203
|
+
def build_clients(options, pool: nil, **kwargs)
|
204
|
+
options.filter_map do |node_key, option|
|
205
|
+
next if replica_disabled? && replica?(node_key)
|
206
|
+
|
207
|
+
config = ::RedisClient::Cluster::Node::Config.new(
|
208
|
+
scale_read: replica?(node_key),
|
209
|
+
**option.merge(kwargs.reject { |k, _| IGNORE_GENERIC_CONFIG_KEYS.include?(k) })
|
210
|
+
)
|
211
|
+
client = pool.nil? ? config.new_client : config.new_pool(**pool)
|
212
|
+
|
213
|
+
[node_key, client]
|
214
|
+
end.to_h
|
215
|
+
end
|
216
|
+
|
178
217
|
def try_map # rubocop:disable Metrics/MethodLength
|
179
218
|
errors = {}
|
180
219
|
results = {}
|
@@ -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,170 @@ 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
|
+
class PubSub
|
69
|
+
def initialize(client)
|
70
|
+
@client = client
|
71
|
+
@pubsub = nil
|
72
|
+
end
|
73
|
+
|
74
|
+
def call(*command, **kwargs)
|
75
|
+
close
|
76
|
+
@pubsub = @client.send(:assign_node, *command).pubsub
|
77
|
+
@pubsub.call(*command, **kwargs)
|
78
|
+
end
|
79
|
+
|
80
|
+
def close
|
81
|
+
@pubsub&.close
|
82
|
+
@pubsub = nil
|
83
|
+
end
|
84
|
+
|
85
|
+
def next_event(timeout = nil)
|
86
|
+
@pubsub&.next_event(timeout)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
ZERO_CURSOR_FOR_SCAN = '0'
|
91
|
+
CMD_SCAN = 'SCAN'
|
92
|
+
CMD_SSCAN = 'SSCAN'
|
93
|
+
CMD_HSCAN = 'HSCAN'
|
94
|
+
CMD_ZSCAN = 'ZSCAN'
|
95
|
+
CMD_ASKING = 'ASKING'
|
96
|
+
REPLY_OK = 'OK'
|
97
|
+
REPLY_MOVED = 'MOVED'
|
98
|
+
REPLY_ASK = 'ASK'
|
99
|
+
|
11
100
|
def initialize(config, pool: nil, **kwargs)
|
12
101
|
@config = config.dup
|
13
102
|
@pool = pool
|
14
103
|
@client_kwargs = kwargs
|
15
|
-
@node = fetch_cluster_info!(@config, @pool, **@client_kwargs)
|
104
|
+
@node = fetch_cluster_info!(@config, pool: @pool, **@client_kwargs)
|
16
105
|
@command = ::RedisClient::Cluster::Command.load(@node)
|
17
106
|
end
|
18
107
|
|
19
108
|
def inspect
|
20
|
-
@node.
|
109
|
+
"#<#{self.class.name} #{@node.node_keys.join(', ')}>"
|
21
110
|
end
|
22
111
|
|
23
|
-
def call(*command, **kwargs
|
24
|
-
send_command(:call, *command, **kwargs
|
112
|
+
def call(*command, **kwargs)
|
113
|
+
send_command(:call, *command, **kwargs)
|
25
114
|
end
|
26
115
|
|
27
|
-
def call_once(*command, **kwargs
|
28
|
-
send_command(:call_once, *command, **kwargs
|
116
|
+
def call_once(*command, **kwargs)
|
117
|
+
send_command(:call_once, *command, **kwargs)
|
29
118
|
end
|
30
119
|
|
31
|
-
def blocking_call(timeout, *command, **kwargs
|
120
|
+
def blocking_call(timeout, *command, **kwargs)
|
32
121
|
node = assign_node(*command)
|
33
|
-
try_send(node, :blocking_call, timeout, *command, **kwargs
|
122
|
+
try_send(node, :blocking_call, timeout, *command, **kwargs)
|
34
123
|
end
|
35
124
|
|
36
125
|
def scan(*args, **kwargs, &block)
|
37
|
-
|
126
|
+
raise ArgumentError, 'block required' unless block
|
127
|
+
|
128
|
+
cursor = ZERO_CURSOR_FOR_SCAN
|
129
|
+
loop do
|
130
|
+
cursor, keys = _scan(CMD_SCAN, cursor, *args, **kwargs)
|
131
|
+
keys.each(&block)
|
132
|
+
break if cursor == ZERO_CURSOR_FOR_SCAN
|
133
|
+
end
|
38
134
|
end
|
39
135
|
|
40
136
|
def sscan(key, *args, **kwargs, &block)
|
41
|
-
node = assign_node(
|
137
|
+
node = assign_node(CMD_SSCAN, key)
|
42
138
|
try_send(node, :sscan, key, *args, **kwargs, &block)
|
43
139
|
end
|
44
140
|
|
45
141
|
def hscan(key, *args, **kwargs, &block)
|
46
|
-
node = assign_node(
|
142
|
+
node = assign_node(CMD_HSCAN, key)
|
47
143
|
try_send(node, :hscan, key, *args, **kwargs, &block)
|
48
144
|
end
|
49
145
|
|
50
146
|
def zscan(key, *args, **kwargs, &block)
|
51
|
-
node = assign_node(
|
147
|
+
node = assign_node(CMD_ZSCAN, key)
|
52
148
|
try_send(node, :zscan, key, *args, **kwargs, &block)
|
53
149
|
end
|
54
150
|
|
55
151
|
def pipelined
|
56
|
-
|
57
|
-
|
152
|
+
pipeline = ::RedisClient::Cluster::Pipeline.new(self)
|
153
|
+
yield pipeline
|
154
|
+
return [] if pipeline.empty? == 0
|
58
155
|
|
59
|
-
|
60
|
-
# TODO: impl
|
156
|
+
pipeline.execute
|
61
157
|
end
|
62
158
|
|
63
159
|
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?)
|
160
|
+
::RedisClient::Cluster::PubSub.new(self)
|
82
161
|
end
|
83
162
|
|
84
163
|
def close
|
85
164
|
@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
|
165
|
+
nil
|
112
166
|
end
|
113
167
|
|
114
168
|
private
|
115
169
|
|
116
|
-
def fetch_cluster_info!(config, pool, **kwargs)
|
170
|
+
def fetch_cluster_info!(config, pool: nil, **kwargs)
|
117
171
|
node_info = ::RedisClient::Cluster::Node.load_info(config.per_node_key, **kwargs)
|
118
172
|
node_addrs = node_info.map { |info| ::RedisClient::Cluster::NodeKey.hashify(info[:node_key]) }
|
119
173
|
config.update_node(node_addrs)
|
120
|
-
::RedisClient::Cluster::Node.new(config.per_node_key,
|
174
|
+
::RedisClient::Cluster::Node.new(config.per_node_key,
|
175
|
+
node_info: node_info, pool: pool, with_replica: config.use_replica?, **kwargs)
|
121
176
|
end
|
122
177
|
|
123
178
|
def send_command(method, *command, **kwargs, &block) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
|
@@ -130,7 +185,7 @@ class RedisClient
|
|
130
185
|
when 'wait' then @node.call_primary(method, *command, **kwargs, &block).sum
|
131
186
|
when 'keys' then @node.call_replica(method, *command, **kwargs, &block).flatten.sort
|
132
187
|
when 'dbsize' then @node.call_replica(method, *command, **kwargs, &block).sum
|
133
|
-
when 'scan' then _scan(
|
188
|
+
when 'scan' then _scan(*command, **kwargs)
|
134
189
|
when 'lastsave' then @node.call_all(method, *command, **kwargs, &block).sort
|
135
190
|
when 'role' then @node.call_all(method, *command, **kwargs, &block)
|
136
191
|
when 'config' then send_config_command(method, *command, **kwargs, &block)
|
@@ -179,7 +234,7 @@ class RedisClient
|
|
179
234
|
case subcommand
|
180
235
|
when 'addslots', 'delslots', 'failover', 'forget', 'meet', 'replicate',
|
181
236
|
'reset', 'set-config-epoch', 'setslot'
|
182
|
-
raise ::RedisClient::Cluster::OrchestrationCommandNotSupported, 'cluster', subcommand
|
237
|
+
raise ::RedisClient::Cluster::OrchestrationCommandNotSupported, ['cluster', subcommand]
|
183
238
|
when 'saveconfig' then @node.call_all(method, *command, **kwargs, &block).first
|
184
239
|
else assign_node(*command).send(method, *command, **kwargs, &block)
|
185
240
|
end
|
@@ -195,7 +250,7 @@ class RedisClient
|
|
195
250
|
end
|
196
251
|
end
|
197
252
|
|
198
|
-
def send_pubsub_command(method, *command, **kwargs, &block) # rubocop:disable
|
253
|
+
def send_pubsub_command(method, *command, **kwargs, &block) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity
|
199
254
|
case command[1].to_s.downcase
|
200
255
|
when 'channels' then @node.call_all(method, *command, **kwargs, &block).flatten.uniq.sort
|
201
256
|
when 'numsub'
|
@@ -211,17 +266,17 @@ class RedisClient
|
|
211
266
|
def try_send(node, method, *args, retry_count: 3, **kwargs, &block) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
212
267
|
node.send(method, *args, **kwargs, &block)
|
213
268
|
rescue ::RedisClient::CommandError => e
|
214
|
-
if e.message.start_with?(
|
269
|
+
if e.message.start_with?(REPLY_MOVED)
|
215
270
|
raise if retry_count <= 0
|
216
271
|
|
217
272
|
node = assign_redirection_node(e.message)
|
218
273
|
retry_count -= 1
|
219
274
|
retry
|
220
|
-
elsif e.message.start_with?(
|
275
|
+
elsif e.message.start_with?(REPLY_ASK)
|
221
276
|
raise if retry_count <= 0
|
222
277
|
|
223
278
|
node = assign_asking_node(e.message)
|
224
|
-
node.call(
|
279
|
+
node.call(CMD_ASKING)
|
225
280
|
retry_count -= 1
|
226
281
|
retry
|
227
282
|
else
|
@@ -232,7 +287,8 @@ class RedisClient
|
|
232
287
|
raise
|
233
288
|
end
|
234
289
|
|
235
|
-
def _scan(
|
290
|
+
def _scan(*command, **kwargs) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
291
|
+
command[1] = ZERO_CURSOR_FOR_SCAN if command.size == 1
|
236
292
|
input_cursor = Integer(command[1])
|
237
293
|
|
238
294
|
client_index = input_cursor % 256
|
@@ -241,11 +297,11 @@ class RedisClient
|
|
241
297
|
clients = @node.scale_reading_clients
|
242
298
|
|
243
299
|
client = clients[client_index]
|
244
|
-
return [
|
300
|
+
return [ZERO_CURSOR_FOR_SCAN, []] unless client
|
245
301
|
|
246
302
|
command[1] = raw_cursor.to_s
|
247
303
|
|
248
|
-
result_cursor, result_keys = client.
|
304
|
+
result_cursor, result_keys = client.call(*command, **kwargs)
|
249
305
|
result_cursor = Integer(result_cursor)
|
250
306
|
|
251
307
|
client_index += 1 if result_cursor == 0
|
@@ -266,11 +322,11 @@ class RedisClient
|
|
266
322
|
end
|
267
323
|
|
268
324
|
def assign_node(*command)
|
269
|
-
node_key = find_node_key(command)
|
325
|
+
node_key = find_node_key(*command)
|
270
326
|
find_node(node_key)
|
271
327
|
end
|
272
328
|
|
273
|
-
def find_node_key(command, primary_only: false)
|
329
|
+
def find_node_key(*command, primary_only: false)
|
274
330
|
key = @command.extract_first_key(command)
|
275
331
|
return if key.empty?
|
276
332
|
|
@@ -300,7 +356,7 @@ class RedisClient
|
|
300
356
|
end
|
301
357
|
|
302
358
|
@node.each(&:close)
|
303
|
-
@node = fetch_cluster_info!(@config, @pool, **@client_kwargs)
|
359
|
+
@node = fetch_cluster_info!(@config, pool: @pool, **@client_kwargs)
|
304
360
|
end
|
305
361
|
end
|
306
362
|
end
|
@@ -2,32 +2,31 @@
|
|
2
2
|
|
3
3
|
require 'uri'
|
4
4
|
require 'redis_client'
|
5
|
-
require 'redis_client/config'
|
6
5
|
require 'redis_client/cluster'
|
7
6
|
require 'redis_client/cluster/node_key'
|
8
7
|
|
9
8
|
class RedisClient
|
10
9
|
class ClusterConfig
|
11
|
-
|
12
|
-
|
10
|
+
DEFAULT_HOST = '127.0.0.1'
|
11
|
+
DEFAULT_PORT = 6379
|
13
12
|
DEFAULT_SCHEME = 'redis'
|
14
13
|
SECURE_SCHEME = 'rediss'
|
14
|
+
DEFAULT_NODES = ["#{DEFAULT_SCHEME}://#{DEFAULT_HOST}:#{DEFAULT_PORT}"].freeze
|
15
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
|
+
|
16
19
|
InvalidClientConfigError = Class.new(::RedisClient::Error)
|
17
20
|
|
18
|
-
def initialize(nodes
|
19
|
-
@replica = replica
|
20
|
-
@fixed_hostname = fixed_hostname
|
21
|
-
@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
|
22
24
|
@node_configs = build_node_configs(nodes.dup)
|
23
|
-
|
24
|
-
add_common_node_config_if_needed(@client_config, @node_configs, :username)
|
25
|
-
add_common_node_config_if_needed(@client_config, @node_configs, :password)
|
26
|
-
super(**@client_config)
|
25
|
+
@client_config = merge_generic_config(client_config, @node_configs)
|
27
26
|
end
|
28
27
|
|
29
28
|
def inspect
|
30
|
-
per_node_key.
|
29
|
+
"#<#{self.class.name} #{per_node_key.values}>"
|
31
30
|
end
|
32
31
|
|
33
32
|
def new_pool(size: 5, timeout: 5, **kwargs)
|
@@ -42,7 +41,7 @@ class RedisClient
|
|
42
41
|
@node_configs.to_h do |config|
|
43
42
|
node_key = ::RedisClient::Cluster::NodeKey.build_from_host_port(config[:host], config[:port])
|
44
43
|
config = @client_config.merge(config)
|
45
|
-
config = config.merge(host: @fixed_hostname)
|
44
|
+
config = config.merge(host: @fixed_hostname) unless @fixed_hostname.empty?
|
46
45
|
[node_key, config]
|
47
46
|
end
|
48
47
|
end
|
@@ -66,9 +65,10 @@ class RedisClient
|
|
66
65
|
private
|
67
66
|
|
68
67
|
def build_node_configs(addrs)
|
69
|
-
|
68
|
+
configs = Array[addrs].flatten.filter_map { |addr| parse_node_addr(addr) }
|
69
|
+
raise InvalidClientConfigError, '`nodes` option is empty' if configs.size.zero?
|
70
70
|
|
71
|
-
|
71
|
+
configs
|
72
72
|
end
|
73
73
|
|
74
74
|
def parse_node_addr(addr)
|
@@ -78,41 +78,51 @@ class RedisClient
|
|
78
78
|
when Hash
|
79
79
|
parse_node_option(addr)
|
80
80
|
else
|
81
|
-
raise InvalidClientConfigError,
|
81
|
+
raise InvalidClientConfigError, "`nodes` option includes invalid type values: #{addr}"
|
82
82
|
end
|
83
83
|
end
|
84
84
|
|
85
|
-
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
|
+
|
86
88
|
uri = URI(addr)
|
87
|
-
|
89
|
+
scheme = uri.scheme || DEFAULT_SCHEME
|
90
|
+
raise InvalidClientConfigError, "`nodes` option includes a invalid uri scheme: #{addr}" unless VALID_SCHEMES.include?(scheme)
|
88
91
|
|
89
|
-
db = uri.path.split('/')[1]&.to_i
|
90
92
|
username = uri.user ? URI.decode_www_form_component(uri.user) : nil
|
91
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)
|
92
98
|
|
93
|
-
{
|
94
|
-
|
95
|
-
port: uri.port,
|
96
|
-
username: username,
|
97
|
-
password: password,
|
98
|
-
db: db,
|
99
|
-
ssl: uri.scheme == SECURE_SCHEME
|
100
|
-
}.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 }
|
101
101
|
rescue URI::InvalidURIError => e
|
102
|
-
raise InvalidClientConfigError, e.message
|
102
|
+
raise InvalidClientConfigError, "#{e.message}: #{addr}"
|
103
103
|
end
|
104
104
|
|
105
105
|
def parse_node_option(addr)
|
106
|
+
return if addr.empty?
|
107
|
+
|
106
108
|
addr = addr.transform_keys(&:to_sym)
|
107
|
-
|
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
|
108
113
|
|
109
|
-
|
114
|
+
def ensure_integer(value)
|
115
|
+
Integer(value)
|
116
|
+
rescue ArgumentError => e
|
117
|
+
raise InvalidClientConfigError, e.message
|
110
118
|
end
|
111
119
|
|
112
|
-
def
|
113
|
-
return client_config if
|
120
|
+
def merge_generic_config(client_config, node_configs)
|
121
|
+
return client_config if node_configs.size.zero?
|
114
122
|
|
115
|
-
|
123
|
+
cfg = node_configs.first
|
124
|
+
MERGE_CONFIG_KEYS.each { |k| client_config[k] = cfg[k] if cfg.key?(k) }
|
125
|
+
client_config
|
116
126
|
end
|
117
127
|
end
|
118
128
|
end
|
metadata
CHANGED
@@ -1,15 +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.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
|
-
- Taishi
|
8
|
-
- Kasuga
|
7
|
+
- Taishi Kasuga
|
9
8
|
autorequire:
|
10
9
|
bindir: bin
|
11
10
|
cert_chain: []
|
12
|
-
date: 2022-06-
|
11
|
+
date: 2022-06-17 00:00:00.000000000 Z
|
13
12
|
dependencies:
|
14
13
|
- !ruby/object:Gem::Dependency
|
15
14
|
name: redis-client
|
@@ -61,7 +60,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
61
60
|
- !ruby/object:Gem::Version
|
62
61
|
version: '0'
|
63
62
|
requirements: []
|
64
|
-
rubygems_version: 3.3.
|
63
|
+
rubygems_version: 3.3.15
|
65
64
|
signing_key:
|
66
65
|
specification_version: 4
|
67
66
|
summary: A Redis cluster client for Ruby
|