redis-cluster-client 0.0.12 → 0.3.0
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-cluster-client.rb +3 -0
- data/lib/redis_client/cluster/command.rb +4 -0
- data/lib/redis_client/cluster/node/latency_replica.rb +82 -0
- data/lib/redis_client/cluster/node/primary_only.rb +47 -0
- data/lib/redis_client/cluster/node/random_replica.rb +37 -0
- data/lib/redis_client/cluster/node/replica_mixin.rb +37 -0
- data/lib/redis_client/cluster/node.rb +84 -114
- data/lib/redis_client/cluster/pipeline.rb +51 -26
- data/lib/redis_client/cluster/pub_sub.rb +13 -4
- data/lib/redis_client/cluster/router.rb +101 -71
- data/lib/redis_client/cluster.rb +57 -17
- data/lib/redis_client/cluster_config.rb +21 -3
- metadata +9 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d9908afa1b60432aec9067825832fa16d71ce90075b70f7ecb8d4271bd021e36
|
|
4
|
+
data.tar.gz: 3d613f5604cef8d67fea9c7bf24b93ac1313000faaa86162dc471a00c92e3cfd
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 13bd2cff41f0dfc139d1618c8eb402ee1883b20ccf5fbcc78c79ff0f6ddd740860db138959f437c02b238764eec884cc720fb4093b339a7a31e472c2cdf044c9
|
|
7
|
+
data.tar.gz: a5ba95acecca5f555589bca85d6c1addf3585172387a1a95afde6d628fd5da0d24536506aa86eaaaf368606f71a28662b3c4999fe7d8f522f4ae21d75aaf1759
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'redis_client/cluster/node/replica_mixin'
|
|
4
|
+
|
|
5
|
+
class RedisClient
|
|
6
|
+
class Cluster
|
|
7
|
+
class Node
|
|
8
|
+
class LatencyReplica
|
|
9
|
+
include ::RedisClient::Cluster::Node::ReplicaMixin
|
|
10
|
+
|
|
11
|
+
attr_reader :replica_clients
|
|
12
|
+
|
|
13
|
+
DUMMY_LATENCY_SEC = 100.0
|
|
14
|
+
MEASURE_ATTEMPT_COUNT = 10
|
|
15
|
+
|
|
16
|
+
def initialize(replications, options, pool, **kwargs)
|
|
17
|
+
super
|
|
18
|
+
|
|
19
|
+
all_replica_clients = @clients.select { |k, _| @replica_node_keys.include?(k) }
|
|
20
|
+
latencies = measure_latencies(all_replica_clients)
|
|
21
|
+
@replications.each_value { |keys| keys.sort_by! { |k| latencies.fetch(k) } }
|
|
22
|
+
@replica_clients = select_replica_clients(@replications, @clients)
|
|
23
|
+
@clients_for_scanning = select_clients_for_scanning(@replications, @clients)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def clients_for_scanning(seed: nil) # rubocop:disable Lint/UnusedMethodArgument
|
|
27
|
+
@clients_for_scanning
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def find_node_key_of_replica(primary_node_key, seed: nil) # rubocop:disable Lint/UnusedMethodArgument
|
|
31
|
+
@replications.fetch(primary_node_key, EMPTY_ARRAY).first || primary_node_key
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def any_replica_node_key(seed: nil)
|
|
35
|
+
random = seed.nil? ? Random : Random.new(seed)
|
|
36
|
+
@replications.reject { |_, v| v.empty? }.values.sample(random: random).first
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def measure_latencies(clients) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
|
42
|
+
latencies = {}
|
|
43
|
+
|
|
44
|
+
clients.each_slice(::RedisClient::Cluster::Node::MAX_THREADS) do |chuncked_clients|
|
|
45
|
+
threads = chuncked_clients.map do |k, v|
|
|
46
|
+
Thread.new(k, v) do |node_key, client|
|
|
47
|
+
Thread.pass
|
|
48
|
+
min = DUMMY_LATENCY_SEC + 1.0
|
|
49
|
+
MEASURE_ATTEMPT_COUNT.times do
|
|
50
|
+
starting = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
51
|
+
client.send(:call_once, 'PING')
|
|
52
|
+
duration = Process.clock_gettime(Process::CLOCK_MONOTONIC) - starting
|
|
53
|
+
min = duration if duration < min
|
|
54
|
+
end
|
|
55
|
+
latencies[node_key] = min
|
|
56
|
+
rescue StandardError
|
|
57
|
+
latencies[node_key] = DUMMY_LATENCY_SEC
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
threads.each(&:join)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
latencies
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def select_replica_clients(replications, clients)
|
|
68
|
+
keys = replications.values.filter_map(&:first)
|
|
69
|
+
clients.select { |k, _| keys.include?(k) }
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def select_clients_for_scanning(replications, clients)
|
|
73
|
+
keys = replications.map do |primary_node_key, replica_node_keys|
|
|
74
|
+
replica_node_keys.empty? ? primary_node_key : replica_node_keys.first
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
clients.select { |k, _| keys.include?(k) }
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class RedisClient
|
|
4
|
+
class Cluster
|
|
5
|
+
class Node
|
|
6
|
+
class PrimaryOnly
|
|
7
|
+
attr_reader :clients
|
|
8
|
+
|
|
9
|
+
def initialize(replications, options, pool, **kwargs)
|
|
10
|
+
@primary_node_keys = replications.keys.sort
|
|
11
|
+
@clients = build_clients(@primary_node_keys, options, pool, **kwargs)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
alias primary_clients clients
|
|
15
|
+
alias replica_clients clients
|
|
16
|
+
|
|
17
|
+
def clients_for_scanning(seed: nil) # rubocop:disable Lint/UnusedMethodArgument
|
|
18
|
+
@clients
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def find_node_key_of_replica(primary_node_key, seed: nil) # rubocop:disable Lint/UnusedMethodArgument
|
|
22
|
+
primary_node_key
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def any_primary_node_key(seed: nil)
|
|
26
|
+
random = seed.nil? ? Random : Random.new(seed)
|
|
27
|
+
@primary_node_keys.sample(random: random)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
alias any_replica_node_key any_primary_node_key
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def build_clients(primary_node_keys, options, pool, **kwargs)
|
|
35
|
+
options.filter_map do |node_key, option|
|
|
36
|
+
next if !primary_node_keys.empty? && !primary_node_keys.include?(node_key)
|
|
37
|
+
|
|
38
|
+
option = option.merge(kwargs.reject { |k, _| ::RedisClient::Cluster::Node::IGNORE_GENERIC_CONFIG_KEYS.include?(k) })
|
|
39
|
+
config = ::RedisClient::Cluster::Node::Config.new(**option)
|
|
40
|
+
client = pool.nil? ? config.new_client : config.new_pool(**pool)
|
|
41
|
+
[node_key, client]
|
|
42
|
+
end.to_h
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'redis_client/cluster/node/replica_mixin'
|
|
4
|
+
|
|
5
|
+
class RedisClient
|
|
6
|
+
class Cluster
|
|
7
|
+
class Node
|
|
8
|
+
class RandomReplica
|
|
9
|
+
include ::RedisClient::Cluster::Node::ReplicaMixin
|
|
10
|
+
|
|
11
|
+
def replica_clients
|
|
12
|
+
keys = @replications.values.filter_map(&:sample)
|
|
13
|
+
@clients.select { |k, _| keys.include?(k) }
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def clients_for_scanning(seed: nil)
|
|
17
|
+
random = seed.nil? ? Random : Random.new(seed)
|
|
18
|
+
keys = @replications.map do |primary_node_key, replica_node_keys|
|
|
19
|
+
replica_node_keys.empty? ? primary_node_key : replica_node_keys.sample(random: random)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
clients.select { |k, _| keys.include?(k) }
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def find_node_key_of_replica(primary_node_key, seed: nil)
|
|
26
|
+
random = seed.nil? ? Random : Random.new(seed)
|
|
27
|
+
@replications.fetch(primary_node_key, EMPTY_ARRAY).sample(random: random) || primary_node_key
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def any_replica_node_key(seed: nil)
|
|
31
|
+
random = seed.nil? ? Random : Random.new(seed)
|
|
32
|
+
@replica_node_keys.sample(random: random)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class RedisClient
|
|
4
|
+
class Cluster
|
|
5
|
+
class Node
|
|
6
|
+
module ReplicaMixin
|
|
7
|
+
attr_reader :clients, :primary_clients
|
|
8
|
+
|
|
9
|
+
EMPTY_ARRAY = [].freeze
|
|
10
|
+
|
|
11
|
+
def initialize(replications, options, pool, **kwargs)
|
|
12
|
+
@replications = replications
|
|
13
|
+
@primary_node_keys = @replications.keys.sort
|
|
14
|
+
@replica_node_keys = @replications.values.flatten.sort
|
|
15
|
+
@clients = build_clients(@primary_node_keys, options, pool, **kwargs)
|
|
16
|
+
@primary_clients = @clients.select { |k, _| @primary_node_keys.include?(k) }
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def any_primary_node_key(seed: nil)
|
|
20
|
+
random = seed.nil? ? Random : Random.new(seed)
|
|
21
|
+
@primary_node_keys.sample(random: random)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def build_clients(primary_node_keys, options, pool, **kwargs)
|
|
27
|
+
options.filter_map do |node_key, option|
|
|
28
|
+
option = option.merge(kwargs.reject { |k, _| ::RedisClient::Cluster::Node::IGNORE_GENERIC_CONFIG_KEYS.include?(k) })
|
|
29
|
+
config = ::RedisClient::Cluster::Node::Config.new(scale_read: !primary_node_keys.include?(node_key), **option)
|
|
30
|
+
client = pool.nil? ? config.new_client : config.new_pool(**pool)
|
|
31
|
+
[node_key, client]
|
|
32
|
+
end.to_h
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
require 'redis_client'
|
|
4
4
|
require 'redis_client/config'
|
|
5
5
|
require 'redis_client/cluster/errors'
|
|
6
|
+
require 'redis_client/cluster/node/primary_only'
|
|
7
|
+
require 'redis_client/cluster/node/random_replica'
|
|
8
|
+
require 'redis_client/cluster/node/latency_replica'
|
|
6
9
|
|
|
7
10
|
class RedisClient
|
|
8
11
|
class Cluster
|
|
@@ -13,6 +16,7 @@ class RedisClient
|
|
|
13
16
|
MIN_SLOT = 0
|
|
14
17
|
MAX_SLOT = SLOT_SIZE - 1
|
|
15
18
|
MAX_STARTUP_SAMPLE = 37
|
|
19
|
+
MAX_THREADS = Integer(ENV.fetch('REDIS_CLIENT_MAX_THREADS', 5))
|
|
16
20
|
IGNORE_GENERIC_CONFIG_KEYS = %i[url host port path].freeze
|
|
17
21
|
|
|
18
22
|
ReloadNeeded = Class.new(::RedisClient::Error)
|
|
@@ -39,18 +43,22 @@ class RedisClient
|
|
|
39
43
|
errors = Array.new(startup_size)
|
|
40
44
|
startup_options = options.to_a.sample(MAX_STARTUP_SAMPLE).to_h
|
|
41
45
|
startup_nodes = ::RedisClient::Cluster::Node.new(startup_options, **kwargs)
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
Thread.
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
46
|
+
startup_nodes.each_slice(MAX_THREADS).with_index do |chuncked_startup_nodes, chuncked_idx|
|
|
47
|
+
threads = chuncked_startup_nodes.each_with_index.map do |raw_client, idx|
|
|
48
|
+
Thread.new(raw_client, (MAX_THREADS * chuncked_idx) + idx) do |cli, i|
|
|
49
|
+
Thread.pass
|
|
50
|
+
reply = cli.call('CLUSTER', 'NODES')
|
|
51
|
+
node_info_list[i] = parse_node_info(reply)
|
|
52
|
+
rescue StandardError => e
|
|
53
|
+
errors[i] = e
|
|
54
|
+
ensure
|
|
55
|
+
cli&.close
|
|
56
|
+
end
|
|
51
57
|
end
|
|
58
|
+
|
|
59
|
+
threads.each(&:join)
|
|
52
60
|
end
|
|
53
|
-
|
|
61
|
+
|
|
54
62
|
raise ::RedisClient::Cluster::InitialSetupError, errors if node_info_list.all?(&:nil?)
|
|
55
63
|
|
|
56
64
|
grouped = node_info_list.compact.group_by do |rows|
|
|
@@ -88,11 +96,18 @@ class RedisClient
|
|
|
88
96
|
end
|
|
89
97
|
end
|
|
90
98
|
|
|
91
|
-
def initialize(
|
|
92
|
-
|
|
99
|
+
def initialize( # rubocop:disable Metrics/ParameterLists
|
|
100
|
+
options,
|
|
101
|
+
node_info: [],
|
|
102
|
+
with_replica: false,
|
|
103
|
+
replica_affinity: :random,
|
|
104
|
+
pool: nil,
|
|
105
|
+
**kwargs
|
|
106
|
+
)
|
|
107
|
+
|
|
93
108
|
@slots = build_slot_node_mappings(node_info)
|
|
94
109
|
@replications = build_replication_mappings(node_info)
|
|
95
|
-
@
|
|
110
|
+
@topology = make_topology_class(with_replica, replica_affinity).new(@replications, options, pool, **kwargs)
|
|
96
111
|
@mutex = Mutex.new
|
|
97
112
|
end
|
|
98
113
|
|
|
@@ -101,87 +116,46 @@ class RedisClient
|
|
|
101
116
|
end
|
|
102
117
|
|
|
103
118
|
def each(&block)
|
|
104
|
-
@clients.each_value(&block)
|
|
119
|
+
@topology.clients.each_value(&block)
|
|
105
120
|
end
|
|
106
121
|
|
|
107
122
|
def sample
|
|
108
|
-
@clients.values.sample
|
|
123
|
+
@topology.clients.values.sample
|
|
109
124
|
end
|
|
110
125
|
|
|
111
126
|
def node_keys
|
|
112
|
-
@clients.keys.sort
|
|
113
|
-
end
|
|
114
|
-
|
|
115
|
-
def primary_node_keys
|
|
116
|
-
@clients.filter_map { |k, _| primary?(k) ? k : nil }.sort
|
|
117
|
-
end
|
|
118
|
-
|
|
119
|
-
def replica_node_keys
|
|
120
|
-
return primary_node_keys if replica_disabled?
|
|
121
|
-
|
|
122
|
-
@clients.filter_map { |k, _| replica?(k) ? k : nil }.sort
|
|
127
|
+
@topology.clients.keys.sort
|
|
123
128
|
end
|
|
124
129
|
|
|
125
130
|
def find_by(node_key)
|
|
126
|
-
raise ReloadNeeded if node_key.nil? || !@clients.key?(node_key)
|
|
131
|
+
raise ReloadNeeded if node_key.nil? || !@topology.clients.key?(node_key)
|
|
127
132
|
|
|
128
|
-
@clients.fetch(node_key)
|
|
133
|
+
@topology.clients.fetch(node_key)
|
|
129
134
|
end
|
|
130
135
|
|
|
131
|
-
def call_all(method,
|
|
132
|
-
|
|
133
|
-
client.send(method, *args, **kwargs, &block)
|
|
134
|
-
end
|
|
135
|
-
|
|
136
|
-
return results.values if errors.empty?
|
|
137
|
-
|
|
138
|
-
raise ::RedisClient::Cluster::ErrorCollection, errors
|
|
136
|
+
def call_all(method, command, args, &block)
|
|
137
|
+
call_multiple_nodes!(@topology.clients, method, command, args, &block)
|
|
139
138
|
end
|
|
140
139
|
|
|
141
|
-
def call_primaries(method,
|
|
142
|
-
|
|
143
|
-
next if replica?(node_key)
|
|
144
|
-
|
|
145
|
-
client.send(method, *args, **kwargs, &block)
|
|
146
|
-
end
|
|
147
|
-
|
|
148
|
-
return results.values if errors.empty?
|
|
149
|
-
|
|
150
|
-
raise ::RedisClient::Cluster::ErrorCollection, errors
|
|
140
|
+
def call_primaries(method, command, args, &block)
|
|
141
|
+
call_multiple_nodes!(@topology.primary_clients, method, command, args, &block)
|
|
151
142
|
end
|
|
152
143
|
|
|
153
|
-
def call_replicas(method,
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
replica_node_keys = @replications.values.map(&:sample)
|
|
157
|
-
results, errors = try_map do |node_key, client|
|
|
158
|
-
next if primary?(node_key) || !replica_node_keys.include?(node_key)
|
|
159
|
-
|
|
160
|
-
client.send(method, *args, **kwargs, &block)
|
|
161
|
-
end
|
|
162
|
-
|
|
163
|
-
return results.values if errors.empty?
|
|
164
|
-
|
|
165
|
-
raise ::RedisClient::Cluster::ErrorCollection, errors
|
|
144
|
+
def call_replicas(method, command, args, &block)
|
|
145
|
+
call_multiple_nodes!(@topology.replica_clients, method, command, args, &block)
|
|
166
146
|
end
|
|
167
147
|
|
|
168
|
-
def send_ping(method,
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
end
|
|
172
|
-
|
|
173
|
-
return results.values if errors.empty?
|
|
148
|
+
def send_ping(method, command, args, &block)
|
|
149
|
+
result_values, errors = call_multiple_nodes(@topology.clients, method, command, args, &block)
|
|
150
|
+
return result_values if errors.empty?
|
|
174
151
|
|
|
175
152
|
raise ReloadNeeded if errors.values.any?(::RedisClient::ConnectionError)
|
|
176
153
|
|
|
177
154
|
raise ::RedisClient::Cluster::ErrorCollection, errors
|
|
178
155
|
end
|
|
179
156
|
|
|
180
|
-
def
|
|
181
|
-
|
|
182
|
-
@clients.select { |k, _| keys.include?(k) }.values.sort_by do |client|
|
|
183
|
-
"#{client.config.host}-#{client.config.port}"
|
|
184
|
-
end
|
|
157
|
+
def clients_for_scanning(seed: nil)
|
|
158
|
+
@topology.clients_for_scanning(seed: seed).values.sort_by { |c| "#{c.config.host}-#{c.config.port}" }
|
|
185
159
|
end
|
|
186
160
|
|
|
187
161
|
def find_node_key_of_primary(slot)
|
|
@@ -193,41 +167,33 @@ class RedisClient
|
|
|
193
167
|
@slots[slot]
|
|
194
168
|
end
|
|
195
169
|
|
|
196
|
-
def find_node_key_of_replica(slot)
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
return if slot < MIN_SLOT || slot > MAX_SLOT
|
|
170
|
+
def find_node_key_of_replica(slot, seed: nil)
|
|
171
|
+
primary_node_key = find_node_key_of_primary(slot)
|
|
172
|
+
@topology.find_node_key_of_replica(primary_node_key, seed: seed)
|
|
173
|
+
end
|
|
201
174
|
|
|
202
|
-
|
|
175
|
+
def any_primary_node_key(seed: nil)
|
|
176
|
+
@topology.any_primary_node_key(seed: seed)
|
|
177
|
+
end
|
|
203
178
|
|
|
204
|
-
|
|
179
|
+
def any_replica_node_key(seed: nil)
|
|
180
|
+
@topology.any_replica_node_key(seed: seed)
|
|
205
181
|
end
|
|
206
182
|
|
|
207
183
|
def update_slot(slot, node_key)
|
|
208
184
|
@mutex.synchronize { @slots[slot] = node_key }
|
|
209
185
|
end
|
|
210
186
|
|
|
211
|
-
def replicated?(primary_node_key, replica_node_key)
|
|
212
|
-
return false if @replications.nil? || @replications.size.zero?
|
|
213
|
-
|
|
214
|
-
@replications.fetch(primary_node_key).include?(replica_node_key)
|
|
215
|
-
end
|
|
216
|
-
|
|
217
187
|
private
|
|
218
188
|
|
|
219
|
-
def
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
def replica?(node_key)
|
|
228
|
-
return false if @replications.nil? || @replications.size.zero?
|
|
229
|
-
|
|
230
|
-
!@replications.key?(node_key)
|
|
189
|
+
def make_topology_class(with_replica, replica_affinity)
|
|
190
|
+
if with_replica && replica_affinity == :random
|
|
191
|
+
::RedisClient::Cluster::Node::RandomReplica
|
|
192
|
+
elsif with_replica && replica_affinity == :latency
|
|
193
|
+
::RedisClient::Cluster::Node::LatencyReplica
|
|
194
|
+
else
|
|
195
|
+
::RedisClient::Cluster::Node::PrimaryOnly
|
|
196
|
+
end
|
|
231
197
|
end
|
|
232
198
|
|
|
233
199
|
def build_slot_node_mappings(node_info)
|
|
@@ -250,34 +216,38 @@ class RedisClient
|
|
|
250
216
|
end
|
|
251
217
|
end
|
|
252
218
|
|
|
253
|
-
def
|
|
254
|
-
|
|
255
|
-
|
|
219
|
+
def call_multiple_nodes(clients, method, command, args, &block)
|
|
220
|
+
results, errors = try_map(clients) do |_, client|
|
|
221
|
+
client.send(method, *args, command, &block)
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
[results.values, errors]
|
|
225
|
+
end
|
|
256
226
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
)
|
|
261
|
-
client = pool.nil? ? config.new_client : config.new_pool(**pool)
|
|
227
|
+
def call_multiple_nodes!(clients, method, command, args, &block)
|
|
228
|
+
result_values, errors = call_multiple_nodes(clients, method, command, args, &block)
|
|
229
|
+
return result_values if errors.empty?
|
|
262
230
|
|
|
263
|
-
|
|
264
|
-
end.to_h
|
|
231
|
+
raise ::RedisClient::Cluster::ErrorCollection, errors
|
|
265
232
|
end
|
|
266
233
|
|
|
267
|
-
def try_map # rubocop:disable Metrics/MethodLength
|
|
234
|
+
def try_map(clients) # rubocop:disable Metrics/MethodLength
|
|
268
235
|
results = {}
|
|
269
236
|
errors = {}
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
Thread.
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
237
|
+
clients.each_slice(MAX_THREADS) do |chuncked_clients|
|
|
238
|
+
threads = chuncked_clients.map do |k, v|
|
|
239
|
+
Thread.new(k, v) do |node_key, client|
|
|
240
|
+
Thread.pass
|
|
241
|
+
reply = yield(node_key, client)
|
|
242
|
+
results[node_key] = reply unless reply.nil?
|
|
243
|
+
rescue StandardError => e
|
|
244
|
+
errors[node_key] = e
|
|
245
|
+
end
|
|
277
246
|
end
|
|
247
|
+
|
|
248
|
+
threads.each(&:join)
|
|
278
249
|
end
|
|
279
250
|
|
|
280
|
-
threads.each(&:join)
|
|
281
251
|
[results, errors]
|
|
282
252
|
end
|
|
283
253
|
end
|
|
@@ -7,28 +7,55 @@ class RedisClient
|
|
|
7
7
|
class Cluster
|
|
8
8
|
class Pipeline
|
|
9
9
|
ReplySizeError = Class.new(::RedisClient::Error)
|
|
10
|
+
MAX_THREADS = Integer(ENV.fetch('REDIS_CLIENT_MAX_THREADS', 5))
|
|
10
11
|
|
|
11
|
-
def initialize(router)
|
|
12
|
+
def initialize(router, command_builder)
|
|
12
13
|
@router = router
|
|
14
|
+
@command_builder = command_builder
|
|
13
15
|
@grouped = Hash.new([].freeze)
|
|
14
16
|
@size = 0
|
|
17
|
+
@seed = Random.new_seed
|
|
15
18
|
end
|
|
16
19
|
|
|
17
|
-
def call(*
|
|
18
|
-
|
|
19
|
-
|
|
20
|
+
def call(*args, **kwargs, &block)
|
|
21
|
+
command = @command_builder.generate(args, kwargs)
|
|
22
|
+
node_key = @router.find_node_key(command, seed: @seed)
|
|
23
|
+
@grouped[node_key] += [[@size, :call_v, command, block]]
|
|
20
24
|
@size += 1
|
|
21
25
|
end
|
|
22
26
|
|
|
23
|
-
def
|
|
24
|
-
|
|
25
|
-
|
|
27
|
+
def call_v(args, &block)
|
|
28
|
+
command = @command_builder.generate(args)
|
|
29
|
+
node_key = @router.find_node_key(command, seed: @seed)
|
|
30
|
+
@grouped[node_key] += [[@size, :call_v, command, block]]
|
|
26
31
|
@size += 1
|
|
27
32
|
end
|
|
28
33
|
|
|
29
|
-
def
|
|
30
|
-
|
|
31
|
-
|
|
34
|
+
def call_once(*args, **kwargs, &block)
|
|
35
|
+
command = @command_builder.generate(args, kwargs)
|
|
36
|
+
node_key = @router.find_node_key(command, seed: @seed)
|
|
37
|
+
@grouped[node_key] += [[@size, :call_once_v, command, block]]
|
|
38
|
+
@size += 1
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def call_once_v(args, &block)
|
|
42
|
+
command = @command_builder.generate(args)
|
|
43
|
+
node_key = @router.find_node_key(command, seed: @seed)
|
|
44
|
+
@grouped[node_key] += [[@size, :call_once_v, command, block]]
|
|
45
|
+
@size += 1
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def blocking_call(timeout, *args, **kwargs, &block)
|
|
49
|
+
command = @command_builder.generate(args, kwargs)
|
|
50
|
+
node_key = @router.find_node_key(command, seed: @seed)
|
|
51
|
+
@grouped[node_key] += [[@size, :blocking_call_v, timeout, command, block]]
|
|
52
|
+
@size += 1
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def blocking_call_v(timeout, args, &block)
|
|
56
|
+
command = @command_builder.generate(args)
|
|
57
|
+
node_key = @router.find_node_key(command, seed: @seed)
|
|
58
|
+
@grouped[node_key] += [[@size, :blocking_call_v, timeout, command, block]]
|
|
32
59
|
@size += 1
|
|
33
60
|
end
|
|
34
61
|
|
|
@@ -40,29 +67,27 @@ class RedisClient
|
|
|
40
67
|
def execute # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
41
68
|
all_replies = Array.new(@size)
|
|
42
69
|
errors = {}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
Thread.
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
when :call_once then pipeline.call_once(*row[2], **row[3])
|
|
51
|
-
when :blocking_call then pipeline.blocking_call(row[2], *row[3], **row[4])
|
|
52
|
-
else raise NotImplementedError, row[1]
|
|
70
|
+
@grouped.each_slice(MAX_THREADS) do |chuncked_grouped|
|
|
71
|
+
threads = chuncked_grouped.map do |k, v|
|
|
72
|
+
Thread.new(@router, k, v) do |router, node_key, rows|
|
|
73
|
+
Thread.pass
|
|
74
|
+
replies = router.find_node(node_key).pipelined do |pipeline|
|
|
75
|
+
rows.each do |(_size, *row, block)|
|
|
76
|
+
pipeline.send(*row, &block)
|
|
53
77
|
end
|
|
54
78
|
end
|
|
55
|
-
end
|
|
56
79
|
|
|
57
|
-
|
|
80
|
+
raise ReplySizeError, "commands: #{rows.size}, replies: #{replies.size}" if rows.size != replies.size
|
|
58
81
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
82
|
+
rows.each_with_index { |row, idx| all_replies[row.first] = replies[idx] }
|
|
83
|
+
rescue StandardError => e
|
|
84
|
+
errors[node_key] = e
|
|
85
|
+
end
|
|
62
86
|
end
|
|
87
|
+
|
|
88
|
+
threads.each(&:join)
|
|
63
89
|
end
|
|
64
90
|
|
|
65
|
-
threads.each(&:join)
|
|
66
91
|
return all_replies if errors.empty?
|
|
67
92
|
|
|
68
93
|
raise ::RedisClient::Cluster::ErrorCollection, errors
|
|
@@ -3,15 +3,24 @@
|
|
|
3
3
|
class RedisClient
|
|
4
4
|
class Cluster
|
|
5
5
|
class PubSub
|
|
6
|
-
def initialize(router)
|
|
6
|
+
def initialize(router, command_builder)
|
|
7
7
|
@router = router
|
|
8
|
+
@command_builder = command_builder
|
|
8
9
|
@pubsub = nil
|
|
9
10
|
end
|
|
10
11
|
|
|
11
|
-
def call(*
|
|
12
|
+
def call(*args, **kwargs)
|
|
12
13
|
close
|
|
13
|
-
|
|
14
|
-
@pubsub.
|
|
14
|
+
command = @command_builder.generate(args, kwargs)
|
|
15
|
+
@pubsub = @router.assign_node(command).pubsub
|
|
16
|
+
@pubsub.call_v(command)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def call_v(command)
|
|
20
|
+
close
|
|
21
|
+
command = @command_builder.generate(command)
|
|
22
|
+
@pubsub = @router.assign_node(command).pubsub
|
|
23
|
+
@pubsub.call_v(command)
|
|
15
24
|
end
|
|
16
25
|
|
|
17
26
|
def close
|
|
@@ -21,37 +21,36 @@ class RedisClient
|
|
|
21
21
|
@node = fetch_cluster_info(@config, pool: @pool, **@client_kwargs)
|
|
22
22
|
@command = ::RedisClient::Cluster::Command.load(@node)
|
|
23
23
|
@mutex = Mutex.new
|
|
24
|
+
@command_builder = @config.command_builder
|
|
24
25
|
end
|
|
25
26
|
|
|
26
|
-
def send_command(method, *args,
|
|
27
|
-
command = method == :blocking_call && args.size > 1 ? args[1..] : args
|
|
28
|
-
|
|
27
|
+
def send_command(method, command, *args, &block) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
29
28
|
cmd = command.first.to_s.downcase
|
|
30
29
|
case cmd
|
|
31
30
|
when 'acl', 'auth', 'bgrewriteaof', 'bgsave', 'quit', 'save'
|
|
32
|
-
@node.call_all(method,
|
|
31
|
+
@node.call_all(method, command, args, &block).first
|
|
33
32
|
when 'flushall', 'flushdb'
|
|
34
|
-
@node.call_primaries(method,
|
|
35
|
-
when 'ping' then @node.send_ping(method,
|
|
36
|
-
when 'wait' then send_wait_command(method,
|
|
37
|
-
when 'keys' then @node.call_replicas(method,
|
|
38
|
-
when 'dbsize' then @node.call_replicas(method,
|
|
39
|
-
when 'scan' then scan(
|
|
40
|
-
when 'lastsave' then @node.call_all(method,
|
|
41
|
-
when 'role' then @node.call_all(method,
|
|
42
|
-
when 'config' then send_config_command(method,
|
|
43
|
-
when 'client' then send_client_command(method,
|
|
44
|
-
when 'cluster' then send_cluster_command(method,
|
|
33
|
+
@node.call_primaries(method, command, args, &block).first
|
|
34
|
+
when 'ping' then @node.send_ping(method, command, args, &block).first
|
|
35
|
+
when 'wait' then send_wait_command(method, command, args, &block)
|
|
36
|
+
when 'keys' then @node.call_replicas(method, command, args, &block).flatten.sort_by(&:to_s)
|
|
37
|
+
when 'dbsize' then @node.call_replicas(method, command, args, &block).select { |e| e.is_a?(Integer) }.sum
|
|
38
|
+
when 'scan' then scan(command, seed: 1)
|
|
39
|
+
when 'lastsave' then @node.call_all(method, command, args, &block).sort_by(&:to_i)
|
|
40
|
+
when 'role' then @node.call_all(method, command, args, &block)
|
|
41
|
+
when 'config' then send_config_command(method, command, args, &block)
|
|
42
|
+
when 'client' then send_client_command(method, command, args, &block)
|
|
43
|
+
when 'cluster' then send_cluster_command(method, command, args, &block)
|
|
45
44
|
when 'readonly', 'readwrite', 'shutdown'
|
|
46
45
|
raise ::RedisClient::Cluster::OrchestrationCommandNotSupported, cmd
|
|
47
|
-
when 'memory' then send_memory_command(method,
|
|
48
|
-
when 'script' then send_script_command(method,
|
|
49
|
-
when 'pubsub' then send_pubsub_command(method,
|
|
46
|
+
when 'memory' then send_memory_command(method, command, args, &block)
|
|
47
|
+
when 'script' then send_script_command(method, command, args, &block)
|
|
48
|
+
when 'pubsub' then send_pubsub_command(method, command, args, &block)
|
|
50
49
|
when 'discard', 'exec', 'multi', 'unwatch'
|
|
51
50
|
raise ::RedisClient::Cluster::AmbiguousNodeError, cmd
|
|
52
51
|
else
|
|
53
|
-
node = assign_node(
|
|
54
|
-
try_send(node, method,
|
|
52
|
+
node = assign_node(command)
|
|
53
|
+
try_send(node, method, command, args, &block)
|
|
55
54
|
end
|
|
56
55
|
rescue ::RedisClient::Cluster::Node::ReloadNeeded
|
|
57
56
|
update_cluster_info!
|
|
@@ -65,7 +64,37 @@ class RedisClient
|
|
|
65
64
|
|
|
66
65
|
# @see https://redis.io/topics/cluster-spec#redirection-and-resharding
|
|
67
66
|
# Redirection and resharding
|
|
68
|
-
def try_send(node, method,
|
|
67
|
+
def try_send(node, method, command, args, retry_count: 3, &block) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
68
|
+
node.send(method, *args, command, &block)
|
|
69
|
+
rescue ::RedisClient::CommandError => e
|
|
70
|
+
raise if retry_count <= 0
|
|
71
|
+
|
|
72
|
+
if e.message.start_with?('MOVED')
|
|
73
|
+
node = assign_redirection_node(e.message)
|
|
74
|
+
retry_count -= 1
|
|
75
|
+
retry
|
|
76
|
+
elsif e.message.start_with?('ASK')
|
|
77
|
+
node = assign_asking_node(e.message)
|
|
78
|
+
node.call('ASKING')
|
|
79
|
+
retry_count -= 1
|
|
80
|
+
retry
|
|
81
|
+
elsif e.message.start_with?('CLUSTERDOWN Hash slot not served')
|
|
82
|
+
update_cluster_info!
|
|
83
|
+
retry_count -= 1
|
|
84
|
+
retry
|
|
85
|
+
else
|
|
86
|
+
raise
|
|
87
|
+
end
|
|
88
|
+
rescue ::RedisClient::ConnectionError => e
|
|
89
|
+
raise if method == :blocking_call_v || (method == :blocking_call && e.is_a?(RedisClient::ReadTimeoutError))
|
|
90
|
+
raise if retry_count <= 0
|
|
91
|
+
|
|
92
|
+
update_cluster_info!
|
|
93
|
+
retry_count -= 1
|
|
94
|
+
retry
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def try_delegate(node, method, *args, retry_count: 3, **kwargs, &block) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
|
69
98
|
node.send(method, *args, **kwargs, &block)
|
|
70
99
|
rescue ::RedisClient::CommandError => e
|
|
71
100
|
raise if retry_count <= 0
|
|
@@ -94,21 +123,23 @@ class RedisClient
|
|
|
94
123
|
retry
|
|
95
124
|
end
|
|
96
125
|
|
|
97
|
-
def scan(*command, **kwargs) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
|
126
|
+
def scan(*command, seed: nil, **kwargs) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
|
127
|
+
command = @command_builder.generate(command, kwargs)
|
|
128
|
+
|
|
98
129
|
command[1] = ZERO_CURSOR_FOR_SCAN if command.size == 1
|
|
99
130
|
input_cursor = Integer(command[1])
|
|
100
131
|
|
|
101
132
|
client_index = input_cursor % 256
|
|
102
133
|
raw_cursor = input_cursor >> 8
|
|
103
134
|
|
|
104
|
-
clients = @node.
|
|
135
|
+
clients = @node.clients_for_scanning(seed: seed)
|
|
105
136
|
|
|
106
137
|
client = clients[client_index]
|
|
107
138
|
return [ZERO_CURSOR_FOR_SCAN, []] unless client
|
|
108
139
|
|
|
109
140
|
command[1] = raw_cursor.to_s
|
|
110
141
|
|
|
111
|
-
result_cursor, result_keys = client.
|
|
142
|
+
result_cursor, result_keys = client.call_v(command)
|
|
112
143
|
result_cursor = Integer(result_cursor)
|
|
113
144
|
|
|
114
145
|
client_index += 1 if result_cursor == 0
|
|
@@ -116,19 +147,19 @@ class RedisClient
|
|
|
116
147
|
[((result_cursor << 8) + client_index).to_s, result_keys]
|
|
117
148
|
end
|
|
118
149
|
|
|
119
|
-
def assign_node(
|
|
120
|
-
node_key = find_node_key(
|
|
150
|
+
def assign_node(command)
|
|
151
|
+
node_key = find_node_key(command)
|
|
121
152
|
find_node(node_key)
|
|
122
153
|
end
|
|
123
154
|
|
|
124
|
-
def find_node_key(
|
|
155
|
+
def find_node_key(command, seed: nil)
|
|
125
156
|
key = @command.extract_first_key(command)
|
|
126
157
|
slot = key.empty? ? nil : ::RedisClient::Cluster::KeySlotConverter.convert(key)
|
|
127
158
|
|
|
128
|
-
if @command.should_send_to_primary?(command)
|
|
129
|
-
@node.find_node_key_of_primary(slot) || @node.
|
|
159
|
+
if @command.should_send_to_primary?(command)
|
|
160
|
+
@node.find_node_key_of_primary(slot) || @node.any_primary_node_key(seed: seed)
|
|
130
161
|
else
|
|
131
|
-
@node.find_node_key_of_replica(slot) || @node.
|
|
162
|
+
@node.find_node_key_of_replica(slot, seed: seed) || @node.any_replica_node_key(seed: seed)
|
|
132
163
|
end
|
|
133
164
|
end
|
|
134
165
|
|
|
@@ -142,10 +173,14 @@ class RedisClient
|
|
|
142
173
|
retry
|
|
143
174
|
end
|
|
144
175
|
|
|
176
|
+
def command_exists?(name)
|
|
177
|
+
@command.exists?(name)
|
|
178
|
+
end
|
|
179
|
+
|
|
145
180
|
private
|
|
146
181
|
|
|
147
|
-
def send_wait_command(method,
|
|
148
|
-
@node.call_primaries(method,
|
|
182
|
+
def send_wait_command(method, command, args, retry_count: 3, &block)
|
|
183
|
+
@node.call_primaries(method, command, args, &block).select { |r| r.is_a?(Integer) }.sum
|
|
149
184
|
rescue ::RedisClient::Cluster::ErrorCollection => e
|
|
150
185
|
raise if retry_count <= 0
|
|
151
186
|
raise if e.errors.values.none? do |err|
|
|
@@ -157,76 +192,65 @@ class RedisClient
|
|
|
157
192
|
retry
|
|
158
193
|
end
|
|
159
194
|
|
|
160
|
-
def send_config_command(method,
|
|
161
|
-
command = method == :blocking_call && args.size > 1 ? args[1..] : args
|
|
162
|
-
|
|
195
|
+
def send_config_command(method, command, args, &block)
|
|
163
196
|
case command[1].to_s.downcase
|
|
164
197
|
when 'resetstat', 'rewrite', 'set'
|
|
165
|
-
@node.call_all(method,
|
|
166
|
-
else assign_node(
|
|
198
|
+
@node.call_all(method, command, args, &block).first
|
|
199
|
+
else assign_node(command).send(method, *args, command, &block)
|
|
167
200
|
end
|
|
168
201
|
end
|
|
169
202
|
|
|
170
|
-
def send_memory_command(method,
|
|
171
|
-
command = method == :blocking_call && args.size > 1 ? args[1..] : args
|
|
172
|
-
|
|
203
|
+
def send_memory_command(method, command, args, &block)
|
|
173
204
|
case command[1].to_s.downcase
|
|
174
|
-
when 'stats' then @node.call_all(method,
|
|
175
|
-
when 'purge' then @node.call_all(method,
|
|
176
|
-
else assign_node(
|
|
205
|
+
when 'stats' then @node.call_all(method, command, args, &block)
|
|
206
|
+
when 'purge' then @node.call_all(method, command, args, &block).first
|
|
207
|
+
else assign_node(command).send(method, *args, command, &block)
|
|
177
208
|
end
|
|
178
209
|
end
|
|
179
210
|
|
|
180
|
-
def send_client_command(method,
|
|
181
|
-
command = method == :blocking_call && args.size > 1 ? args[1..] : args
|
|
182
|
-
|
|
211
|
+
def send_client_command(method, command, args, &block)
|
|
183
212
|
case command[1].to_s.downcase
|
|
184
|
-
when 'list' then @node.call_all(method,
|
|
213
|
+
when 'list' then @node.call_all(method, command, args, &block).flatten
|
|
185
214
|
when 'pause', 'reply', 'setname'
|
|
186
|
-
@node.call_all(method,
|
|
187
|
-
else assign_node(
|
|
215
|
+
@node.call_all(method, command, args, &block).first
|
|
216
|
+
else assign_node(command).send(method, *args, command, &block)
|
|
188
217
|
end
|
|
189
218
|
end
|
|
190
219
|
|
|
191
|
-
def send_cluster_command(method,
|
|
192
|
-
command = method == :blocking_call && args.size > 1 ? args[1..] : args
|
|
220
|
+
def send_cluster_command(method, command, args, &block) # rubocop:disable Metrics/MethodLength
|
|
193
221
|
subcommand = command[1].to_s.downcase
|
|
194
222
|
|
|
195
223
|
case subcommand
|
|
196
224
|
when 'addslots', 'delslots', 'failover', 'forget', 'meet', 'replicate',
|
|
197
225
|
'reset', 'set-config-epoch', 'setslot'
|
|
198
226
|
raise ::RedisClient::Cluster::OrchestrationCommandNotSupported, ['cluster', subcommand]
|
|
199
|
-
when 'saveconfig' then @node.call_all(method,
|
|
227
|
+
when 'saveconfig' then @node.call_all(method, command, args, &block).first
|
|
200
228
|
when 'getkeysinslot'
|
|
201
229
|
raise ArgumentError, command.join(' ') if command.size != 4
|
|
202
230
|
|
|
203
|
-
find_node(@node.find_node_key_of_replica(command[2])).send(method, *args,
|
|
204
|
-
else assign_node(
|
|
231
|
+
find_node(@node.find_node_key_of_replica(command[2])).send(method, *args, command, &block)
|
|
232
|
+
else assign_node(command).send(method, *args, command, &block)
|
|
205
233
|
end
|
|
206
234
|
end
|
|
207
235
|
|
|
208
|
-
def send_script_command(method,
|
|
209
|
-
command = method == :blocking_call && args.size > 1 ? args[1..] : args
|
|
210
|
-
|
|
236
|
+
def send_script_command(method, command, args, &block)
|
|
211
237
|
case command[1].to_s.downcase
|
|
212
238
|
when 'debug', 'kill'
|
|
213
|
-
@node.call_all(method,
|
|
239
|
+
@node.call_all(method, command, args, &block).first
|
|
214
240
|
when 'flush', 'load'
|
|
215
|
-
@node.call_primaries(method,
|
|
216
|
-
else assign_node(
|
|
241
|
+
@node.call_primaries(method, command, args, &block).first
|
|
242
|
+
else assign_node(command).send(method, *args, command, &block)
|
|
217
243
|
end
|
|
218
244
|
end
|
|
219
245
|
|
|
220
|
-
def send_pubsub_command(method,
|
|
221
|
-
command = method == :blocking_call && args.size > 1 ? args[1..] : args
|
|
222
|
-
|
|
246
|
+
def send_pubsub_command(method, command, args, &block) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
223
247
|
case command[1].to_s.downcase
|
|
224
|
-
when 'channels' then @node.call_all(method,
|
|
248
|
+
when 'channels' then @node.call_all(method, command, args, &block).flatten.uniq.sort_by(&:to_s)
|
|
225
249
|
when 'numsub'
|
|
226
|
-
@node.call_all(method,
|
|
250
|
+
@node.call_all(method, command, args, &block).reject(&:empty?).map { |e| Hash[*e] }
|
|
227
251
|
.reduce({}) { |a, e| a.merge(e) { |_, v1, v2| v1 + v2 } }
|
|
228
|
-
when 'numpat' then @node.call_all(method,
|
|
229
|
-
else assign_node(
|
|
252
|
+
when 'numpat' then @node.call_all(method, command, args, &block).select { |e| e.is_a?(Integer) }.sum
|
|
253
|
+
else assign_node(command).send(method, *args, command, &block)
|
|
230
254
|
end
|
|
231
255
|
end
|
|
232
256
|
|
|
@@ -242,18 +266,24 @@ class RedisClient
|
|
|
242
266
|
find_node(node_key)
|
|
243
267
|
end
|
|
244
268
|
|
|
245
|
-
def fetch_cluster_info(config, pool: nil, **kwargs)
|
|
269
|
+
def fetch_cluster_info(config, pool: nil, **kwargs) # rubocop:disable Metrics/MethodLength
|
|
246
270
|
node_info = ::RedisClient::Cluster::Node.load_info(config.per_node_key, **kwargs)
|
|
247
271
|
node_addrs = node_info.map { |info| ::RedisClient::Cluster::NodeKey.hashify(info[:node_key]) }
|
|
248
272
|
config.update_node(node_addrs)
|
|
249
|
-
::RedisClient::Cluster::Node.new(
|
|
250
|
-
|
|
273
|
+
::RedisClient::Cluster::Node.new(
|
|
274
|
+
config.per_node_key,
|
|
275
|
+
node_info: node_info,
|
|
276
|
+
pool: pool,
|
|
277
|
+
with_replica: config.use_replica?,
|
|
278
|
+
replica_affinity: config.replica_affinity,
|
|
279
|
+
**kwargs
|
|
280
|
+
)
|
|
251
281
|
end
|
|
252
282
|
|
|
253
283
|
def update_cluster_info!
|
|
254
284
|
@mutex.synchronize do
|
|
255
285
|
begin
|
|
256
|
-
@node.
|
|
286
|
+
@node.each(&:close)
|
|
257
287
|
rescue ::RedisClient::Cluster::ErrorCollection
|
|
258
288
|
# ignore
|
|
259
289
|
end
|
data/lib/redis_client/cluster.rb
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require 'redis_client'
|
|
4
3
|
require 'redis_client/cluster/pipeline'
|
|
5
4
|
require 'redis_client/cluster/pub_sub'
|
|
6
5
|
require 'redis_client/cluster/router'
|
|
@@ -9,54 +8,77 @@ class RedisClient
|
|
|
9
8
|
class Cluster
|
|
10
9
|
ZERO_CURSOR_FOR_SCAN = '0'
|
|
11
10
|
|
|
11
|
+
attr_reader :config
|
|
12
|
+
|
|
12
13
|
def initialize(config, pool: nil, **kwargs)
|
|
14
|
+
@config = config
|
|
13
15
|
@router = ::RedisClient::Cluster::Router.new(config, pool: pool, **kwargs)
|
|
16
|
+
@command_builder = config.command_builder
|
|
14
17
|
end
|
|
15
18
|
|
|
16
19
|
def inspect
|
|
17
20
|
"#<#{self.class.name} #{@router.node.node_keys.join(', ')}>"
|
|
18
21
|
end
|
|
19
22
|
|
|
20
|
-
def call(*
|
|
21
|
-
@
|
|
23
|
+
def call(*args, **kwargs, &block)
|
|
24
|
+
command = @command_builder.generate(args, kwargs)
|
|
25
|
+
@router.send_command(:call_v, command, &block)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def call_v(command, &block)
|
|
29
|
+
command = @command_builder.generate(command)
|
|
30
|
+
@router.send_command(:call_v, command, &block)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def call_once(*args, **kwargs, &block)
|
|
34
|
+
command = @command_builder.generate(args, kwargs)
|
|
35
|
+
@router.send_command(:call_once_v, command, &block)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def call_once_v(command, &block)
|
|
39
|
+
command = @command_builder.generate(command)
|
|
40
|
+
@router.send_command(:call_once_v, command, &block)
|
|
22
41
|
end
|
|
23
42
|
|
|
24
|
-
def
|
|
25
|
-
@
|
|
43
|
+
def blocking_call(timeout, *args, **kwargs, &block)
|
|
44
|
+
command = @command_builder.generate(args, kwargs)
|
|
45
|
+
@router.send_command(:blocking_call_v, command, timeout, &block)
|
|
26
46
|
end
|
|
27
47
|
|
|
28
|
-
def
|
|
29
|
-
@
|
|
48
|
+
def blocking_call_v(timeout, command, &block)
|
|
49
|
+
command = @command_builder.generate(command)
|
|
50
|
+
@router.send_command(:blocking_call_v, command, timeout, &block)
|
|
30
51
|
end
|
|
31
52
|
|
|
32
53
|
def scan(*args, **kwargs, &block)
|
|
33
54
|
raise ArgumentError, 'block required' unless block
|
|
34
55
|
|
|
56
|
+
seed = Random.new_seed
|
|
35
57
|
cursor = ZERO_CURSOR_FOR_SCAN
|
|
36
58
|
loop do
|
|
37
|
-
cursor, keys = @router.scan('SCAN', cursor, *args, **kwargs)
|
|
59
|
+
cursor, keys = @router.scan('SCAN', cursor, *args, seed: seed, **kwargs)
|
|
38
60
|
keys.each(&block)
|
|
39
61
|
break if cursor == ZERO_CURSOR_FOR_SCAN
|
|
40
62
|
end
|
|
41
63
|
end
|
|
42
64
|
|
|
43
65
|
def sscan(key, *args, **kwargs, &block)
|
|
44
|
-
node = @router.assign_node('SSCAN', key)
|
|
45
|
-
@router.
|
|
66
|
+
node = @router.assign_node(['SSCAN', key])
|
|
67
|
+
@router.try_delegate(node, :sscan, key, *args, **kwargs, &block)
|
|
46
68
|
end
|
|
47
69
|
|
|
48
70
|
def hscan(key, *args, **kwargs, &block)
|
|
49
|
-
node = @router.assign_node('HSCAN', key)
|
|
50
|
-
@router.
|
|
71
|
+
node = @router.assign_node(['HSCAN', key])
|
|
72
|
+
@router.try_delegate(node, :hscan, key, *args, **kwargs, &block)
|
|
51
73
|
end
|
|
52
74
|
|
|
53
75
|
def zscan(key, *args, **kwargs, &block)
|
|
54
|
-
node = @router.assign_node('ZSCAN', key)
|
|
55
|
-
@router.
|
|
76
|
+
node = @router.assign_node(['ZSCAN', key])
|
|
77
|
+
@router.try_delegate(node, :zscan, key, *args, **kwargs, &block)
|
|
56
78
|
end
|
|
57
79
|
|
|
58
80
|
def pipelined
|
|
59
|
-
pipeline = ::RedisClient::Cluster::Pipeline.new(@router)
|
|
81
|
+
pipeline = ::RedisClient::Cluster::Pipeline.new(@router, @command_builder)
|
|
60
82
|
yield pipeline
|
|
61
83
|
return [] if pipeline.empty? == 0
|
|
62
84
|
|
|
@@ -64,12 +86,30 @@ class RedisClient
|
|
|
64
86
|
end
|
|
65
87
|
|
|
66
88
|
def pubsub
|
|
67
|
-
::RedisClient::Cluster::PubSub.new(@router)
|
|
89
|
+
::RedisClient::Cluster::PubSub.new(@router, @command_builder)
|
|
68
90
|
end
|
|
69
91
|
|
|
70
92
|
def close
|
|
71
|
-
@router.node.
|
|
93
|
+
@router.node.each(&:close)
|
|
72
94
|
nil
|
|
73
95
|
end
|
|
96
|
+
|
|
97
|
+
private
|
|
98
|
+
|
|
99
|
+
def method_missing(name, *args, **kwargs, &block)
|
|
100
|
+
if @router.command_exists?(name)
|
|
101
|
+
args.unshift(name)
|
|
102
|
+
command = @command_builder.generate(args, kwargs)
|
|
103
|
+
return @router.send_command(:call_v, command, &block)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
super
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def respond_to_missing?(name, include_private = false)
|
|
110
|
+
return true if @router.command_exists?(name)
|
|
111
|
+
|
|
112
|
+
super
|
|
113
|
+
end
|
|
74
114
|
end
|
|
75
115
|
end
|
|
@@ -4,6 +4,7 @@ require 'uri'
|
|
|
4
4
|
require 'redis_client'
|
|
5
5
|
require 'redis_client/cluster'
|
|
6
6
|
require 'redis_client/cluster/node_key'
|
|
7
|
+
require 'redis_client/command_builder'
|
|
7
8
|
|
|
8
9
|
class RedisClient
|
|
9
10
|
class ClusterConfig
|
|
@@ -19,12 +20,25 @@ class RedisClient
|
|
|
19
20
|
|
|
20
21
|
InvalidClientConfigError = Class.new(::RedisClient::Error)
|
|
21
22
|
|
|
22
|
-
|
|
23
|
+
attr_reader :command_builder, :client_config, :replica_affinity
|
|
24
|
+
|
|
25
|
+
def initialize( # rubocop:disable Metrics/ParameterLists
|
|
26
|
+
nodes: DEFAULT_NODES,
|
|
27
|
+
replica: false,
|
|
28
|
+
replica_affinity: :random,
|
|
29
|
+
fixed_hostname: '',
|
|
30
|
+
client_implementation: Cluster,
|
|
31
|
+
**client_config
|
|
32
|
+
)
|
|
33
|
+
|
|
23
34
|
@replica = true & replica
|
|
35
|
+
@replica_affinity = replica_affinity.to_s.to_sym
|
|
24
36
|
@fixed_hostname = fixed_hostname.to_s
|
|
25
37
|
@node_configs = build_node_configs(nodes.dup)
|
|
26
38
|
client_config = client_config.reject { |k, _| IGNORE_GENERIC_CONFIG_KEYS.include?(k) }
|
|
39
|
+
@command_builder = client_config.fetch(:command_builder, ::RedisClient::CommandBuilder)
|
|
27
40
|
@client_config = merge_generic_config(client_config, @node_configs)
|
|
41
|
+
@client_implementation = client_implementation
|
|
28
42
|
@mutex = Mutex.new
|
|
29
43
|
end
|
|
30
44
|
|
|
@@ -32,12 +46,16 @@ class RedisClient
|
|
|
32
46
|
"#<#{self.class.name} #{per_node_key.values}>"
|
|
33
47
|
end
|
|
34
48
|
|
|
49
|
+
def read_timeout
|
|
50
|
+
@client_config[:read_timeout] || @client_config[:timeout] || RedisClient::Config::DEFAULT_TIMEOUT
|
|
51
|
+
end
|
|
52
|
+
|
|
35
53
|
def new_pool(size: 5, timeout: 5, **kwargs)
|
|
36
|
-
|
|
54
|
+
@client_implementation.new(self, pool: { size: size, timeout: timeout }, **kwargs)
|
|
37
55
|
end
|
|
38
56
|
|
|
39
57
|
def new_client(**kwargs)
|
|
40
|
-
|
|
58
|
+
@client_implementation.new(self, **kwargs)
|
|
41
59
|
end
|
|
42
60
|
|
|
43
61
|
def per_node_key
|
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.3.0
|
|
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-
|
|
11
|
+
date: 2022-09-05 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: redis-client
|
|
@@ -16,14 +16,14 @@ dependencies:
|
|
|
16
16
|
requirements:
|
|
17
17
|
- - "~>"
|
|
18
18
|
- !ruby/object:Gem::Version
|
|
19
|
-
version: '0.
|
|
19
|
+
version: '0.6'
|
|
20
20
|
type: :runtime
|
|
21
21
|
prerelease: false
|
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
|
23
23
|
requirements:
|
|
24
24
|
- - "~>"
|
|
25
25
|
- !ruby/object:Gem::Version
|
|
26
|
-
version: '0.
|
|
26
|
+
version: '0.6'
|
|
27
27
|
description:
|
|
28
28
|
email:
|
|
29
29
|
- proxy0721@gmail.com
|
|
@@ -31,11 +31,16 @@ executables: []
|
|
|
31
31
|
extensions: []
|
|
32
32
|
extra_rdoc_files: []
|
|
33
33
|
files:
|
|
34
|
+
- lib/redis-cluster-client.rb
|
|
34
35
|
- lib/redis_client/cluster.rb
|
|
35
36
|
- lib/redis_client/cluster/command.rb
|
|
36
37
|
- lib/redis_client/cluster/errors.rb
|
|
37
38
|
- lib/redis_client/cluster/key_slot_converter.rb
|
|
38
39
|
- lib/redis_client/cluster/node.rb
|
|
40
|
+
- lib/redis_client/cluster/node/latency_replica.rb
|
|
41
|
+
- lib/redis_client/cluster/node/primary_only.rb
|
|
42
|
+
- lib/redis_client/cluster/node/random_replica.rb
|
|
43
|
+
- lib/redis_client/cluster/node/replica_mixin.rb
|
|
39
44
|
- lib/redis_client/cluster/node_key.rb
|
|
40
45
|
- lib/redis_client/cluster/pipeline.rb
|
|
41
46
|
- lib/redis_client/cluster/pub_sub.rb
|