redis-cluster-client 0.0.0 → 0.0.3
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 +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
|