redis-cluster-client 0.2.0 → 0.3.2
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/node/latency_replica.rb +85 -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 +80 -110
- data/lib/redis_client/cluster/pipeline.rb +24 -19
- data/lib/redis_client/cluster/router.rb +18 -12
- data/lib/redis_client/cluster.rb +2 -1
- data/lib/redis_client/cluster_config.rb +22 -6
- metadata +6 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7fa6c9386e1ffd7571fdc8a29bea08960387d958149b972e1cf27d28eac98da0
|
4
|
+
data.tar.gz: e532422c94e7416df967f772112c77861f4501d5dcd73c2d9893237586077677
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ef29a0346608779f6528559e52e348df690baf44db0cd8c9e1fe94e44bf9d282eca81101a1246cb0bff418ec398b2b207eb47563e2b976b6523e0fe6583e55fb
|
7
|
+
data.tar.gz: 7e4059d96f02c078989c7bae6cc4790f1580d0fc721c545a86d694ddae7eb90541573c67bb66a93bc6732b45b1f1ac18a3370b0d3d20df41760be9eb78255f5c
|
@@ -0,0 +1,85 @@
|
|
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_NSEC = 100 * 1000 * 1000 * 1000
|
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
|
+
@existed_replicas = @replications.reject { |_, v| v.empty? }.values
|
25
|
+
end
|
26
|
+
|
27
|
+
def clients_for_scanning(seed: nil) # rubocop:disable Lint/UnusedMethodArgument
|
28
|
+
@clients_for_scanning
|
29
|
+
end
|
30
|
+
|
31
|
+
def find_node_key_of_replica(primary_node_key, seed: nil) # rubocop:disable Lint/UnusedMethodArgument
|
32
|
+
@replications.fetch(primary_node_key, EMPTY_ARRAY).first || primary_node_key
|
33
|
+
end
|
34
|
+
|
35
|
+
def any_replica_node_key(seed: nil)
|
36
|
+
random = seed.nil? ? Random : Random.new(seed)
|
37
|
+
@existed_replicas.sample(random: random).first
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def measure_latencies(clients) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
43
|
+
latencies = {}
|
44
|
+
|
45
|
+
clients.each_slice(::RedisClient::Cluster::Node::MAX_THREADS) do |chuncked_clients|
|
46
|
+
threads = chuncked_clients.map do |k, v|
|
47
|
+
Thread.new(k, v) do |node_key, client|
|
48
|
+
Thread.pass
|
49
|
+
|
50
|
+
min = DUMMY_LATENCY_NSEC
|
51
|
+
MEASURE_ATTEMPT_COUNT.times do
|
52
|
+
starting = Process.clock_gettime(Process::CLOCK_MONOTONIC, :nanosecond)
|
53
|
+
client.send(:call_once, 'PING')
|
54
|
+
duration = Process.clock_gettime(Process::CLOCK_MONOTONIC, :nanosecond) - starting
|
55
|
+
min = duration if duration < min
|
56
|
+
end
|
57
|
+
|
58
|
+
latencies[node_key] = min
|
59
|
+
rescue StandardError
|
60
|
+
latencies[node_key] = DUMMY_LATENCY_NSEC
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
threads.each(&:join)
|
65
|
+
end
|
66
|
+
|
67
|
+
latencies
|
68
|
+
end
|
69
|
+
|
70
|
+
def select_replica_clients(replications, clients)
|
71
|
+
keys = replications.values.filter_map(&:first)
|
72
|
+
clients.select { |k, _| keys.include?(k) }
|
73
|
+
end
|
74
|
+
|
75
|
+
def select_clients_for_scanning(replications, clients)
|
76
|
+
keys = replications.map do |primary_node_key, replica_node_keys|
|
77
|
+
replica_node_keys.empty? ? primary_node_key : replica_node_keys.first
|
78
|
+
end
|
79
|
+
|
80
|
+
clients.select { |k, _| keys.include?(k) }
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
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
136
|
def call_all(method, command, args, &block)
|
132
|
-
|
133
|
-
client.send(method, *args, command, &block)
|
134
|
-
end
|
135
|
-
|
136
|
-
return results.values if errors.empty?
|
137
|
-
|
138
|
-
raise ::RedisClient::Cluster::ErrorCollection, errors
|
137
|
+
call_multiple_nodes!(@topology.clients, method, command, args, &block)
|
139
138
|
end
|
140
139
|
|
141
140
|
def call_primaries(method, command, args, &block)
|
142
|
-
|
143
|
-
next if replica?(node_key)
|
144
|
-
|
145
|
-
client.send(method, *args, command, &block)
|
146
|
-
end
|
147
|
-
|
148
|
-
return results.values if errors.empty?
|
149
|
-
|
150
|
-
raise ::RedisClient::Cluster::ErrorCollection, errors
|
141
|
+
call_multiple_nodes!(@topology.primary_clients, method, command, args, &block)
|
151
142
|
end
|
152
143
|
|
153
144
|
def call_replicas(method, command, args, &block)
|
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, command, &block)
|
161
|
-
end
|
162
|
-
|
163
|
-
return results.values if errors.empty?
|
164
|
-
|
165
|
-
raise ::RedisClient::Cluster::ErrorCollection, errors
|
145
|
+
call_multiple_nodes!(@topology.replica_clients, method, command, args, &block)
|
166
146
|
end
|
167
147
|
|
168
148
|
def send_ping(method, command, args, &block)
|
169
|
-
|
170
|
-
|
171
|
-
end
|
172
|
-
|
173
|
-
return results.values if errors.empty?
|
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,52 +7,54 @@ 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
12
|
def initialize(router, command_builder)
|
12
13
|
@router = router
|
13
14
|
@command_builder = command_builder
|
14
15
|
@grouped = Hash.new([].freeze)
|
15
16
|
@size = 0
|
17
|
+
@seed = Random.new_seed
|
16
18
|
end
|
17
19
|
|
18
20
|
def call(*args, **kwargs, &block)
|
19
21
|
command = @command_builder.generate(args, kwargs)
|
20
|
-
node_key = @router.find_node_key(command,
|
22
|
+
node_key = @router.find_node_key(command, seed: @seed)
|
21
23
|
@grouped[node_key] += [[@size, :call_v, command, block]]
|
22
24
|
@size += 1
|
23
25
|
end
|
24
26
|
|
25
27
|
def call_v(args, &block)
|
26
28
|
command = @command_builder.generate(args)
|
27
|
-
node_key = @router.find_node_key(command,
|
29
|
+
node_key = @router.find_node_key(command, seed: @seed)
|
28
30
|
@grouped[node_key] += [[@size, :call_v, command, block]]
|
29
31
|
@size += 1
|
30
32
|
end
|
31
33
|
|
32
34
|
def call_once(*args, **kwargs, &block)
|
33
35
|
command = @command_builder.generate(args, kwargs)
|
34
|
-
node_key = @router.find_node_key(command,
|
36
|
+
node_key = @router.find_node_key(command, seed: @seed)
|
35
37
|
@grouped[node_key] += [[@size, :call_once_v, command, block]]
|
36
38
|
@size += 1
|
37
39
|
end
|
38
40
|
|
39
41
|
def call_once_v(args, &block)
|
40
42
|
command = @command_builder.generate(args)
|
41
|
-
node_key = @router.find_node_key(command,
|
43
|
+
node_key = @router.find_node_key(command, seed: @seed)
|
42
44
|
@grouped[node_key] += [[@size, :call_once_v, command, block]]
|
43
45
|
@size += 1
|
44
46
|
end
|
45
47
|
|
46
48
|
def blocking_call(timeout, *args, **kwargs, &block)
|
47
49
|
command = @command_builder.generate(args, kwargs)
|
48
|
-
node_key = @router.find_node_key(command,
|
50
|
+
node_key = @router.find_node_key(command, seed: @seed)
|
49
51
|
@grouped[node_key] += [[@size, :blocking_call_v, timeout, command, block]]
|
50
52
|
@size += 1
|
51
53
|
end
|
52
54
|
|
53
55
|
def blocking_call_v(timeout, args, &block)
|
54
56
|
command = @command_builder.generate(args)
|
55
|
-
node_key = @router.find_node_key(command,
|
57
|
+
node_key = @router.find_node_key(command, seed: @seed)
|
56
58
|
@grouped[node_key] += [[@size, :blocking_call_v, timeout, command, block]]
|
57
59
|
@size += 1
|
58
60
|
end
|
@@ -62,27 +64,30 @@ class RedisClient
|
|
62
64
|
end
|
63
65
|
|
64
66
|
# TODO: https://github.com/redis-rb/redis-cluster-client/issues/37 handle redirections
|
65
|
-
def execute # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
|
67
|
+
def execute # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
66
68
|
all_replies = Array.new(@size)
|
67
69
|
errors = {}
|
68
|
-
|
69
|
-
|
70
|
-
Thread.
|
71
|
-
|
72
|
-
|
73
|
-
|
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)
|
77
|
+
end
|
74
78
|
end
|
75
|
-
end
|
76
79
|
|
77
|
-
|
80
|
+
raise ReplySizeError, "commands: #{rows.size}, replies: #{replies.size}" if rows.size != replies.size
|
78
81
|
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
+
rows.each_with_index { |row, idx| all_replies[row.first] = replies[idx] }
|
83
|
+
rescue StandardError => e
|
84
|
+
errors[node_key] = e
|
85
|
+
end
|
82
86
|
end
|
87
|
+
|
88
|
+
threads.each(&:join)
|
83
89
|
end
|
84
90
|
|
85
|
-
threads.each(&:join)
|
86
91
|
return all_replies if errors.empty?
|
87
92
|
|
88
93
|
raise ::RedisClient::Cluster::ErrorCollection, errors
|
@@ -35,7 +35,7 @@ class RedisClient
|
|
35
35
|
when 'wait' then send_wait_command(method, command, args, &block)
|
36
36
|
when 'keys' then @node.call_replicas(method, command, args, &block).flatten.sort_by(&:to_s)
|
37
37
|
when 'dbsize' then @node.call_replicas(method, command, args, &block).select { |e| e.is_a?(Integer) }.sum
|
38
|
-
when 'scan' then scan(command)
|
38
|
+
when 'scan' then scan(command, seed: 1)
|
39
39
|
when 'lastsave' then @node.call_all(method, command, args, &block).sort_by(&:to_i)
|
40
40
|
when 'role' then @node.call_all(method, command, args, &block)
|
41
41
|
when 'config' then send_config_command(method, command, args, &block)
|
@@ -123,7 +123,7 @@ class RedisClient
|
|
123
123
|
retry
|
124
124
|
end
|
125
125
|
|
126
|
-
def scan(*command, **kwargs) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
126
|
+
def scan(*command, seed: nil, **kwargs) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
127
127
|
command = @command_builder.generate(command, kwargs)
|
128
128
|
|
129
129
|
command[1] = ZERO_CURSOR_FOR_SCAN if command.size == 1
|
@@ -132,7 +132,7 @@ class RedisClient
|
|
132
132
|
client_index = input_cursor % 256
|
133
133
|
raw_cursor = input_cursor >> 8
|
134
134
|
|
135
|
-
clients = @node.
|
135
|
+
clients = @node.clients_for_scanning(seed: seed)
|
136
136
|
|
137
137
|
client = clients[client_index]
|
138
138
|
return [ZERO_CURSOR_FOR_SCAN, []] unless client
|
@@ -147,19 +147,19 @@ class RedisClient
|
|
147
147
|
[((result_cursor << 8) + client_index).to_s, result_keys]
|
148
148
|
end
|
149
149
|
|
150
|
-
def assign_node(command
|
151
|
-
node_key = find_node_key(command
|
150
|
+
def assign_node(command)
|
151
|
+
node_key = find_node_key(command)
|
152
152
|
find_node(node_key)
|
153
153
|
end
|
154
154
|
|
155
|
-
def find_node_key(command,
|
155
|
+
def find_node_key(command, seed: nil)
|
156
156
|
key = @command.extract_first_key(command)
|
157
157
|
slot = key.empty? ? nil : ::RedisClient::Cluster::KeySlotConverter.convert(key)
|
158
158
|
|
159
|
-
if @command.should_send_to_primary?(command)
|
160
|
-
@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)
|
161
161
|
else
|
162
|
-
@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)
|
163
163
|
end
|
164
164
|
end
|
165
165
|
|
@@ -266,12 +266,18 @@ class RedisClient
|
|
266
266
|
find_node(node_key)
|
267
267
|
end
|
268
268
|
|
269
|
-
def fetch_cluster_info(config, pool: nil, **kwargs)
|
269
|
+
def fetch_cluster_info(config, pool: nil, **kwargs) # rubocop:disable Metrics/MethodLength
|
270
270
|
node_info = ::RedisClient::Cluster::Node.load_info(config.per_node_key, **kwargs)
|
271
271
|
node_addrs = node_info.map { |info| ::RedisClient::Cluster::NodeKey.hashify(info[:node_key]) }
|
272
272
|
config.update_node(node_addrs)
|
273
|
-
::RedisClient::Cluster::Node.new(
|
274
|
-
|
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
|
+
)
|
275
281
|
end
|
276
282
|
|
277
283
|
def update_cluster_info!
|
data/lib/redis_client/cluster.rb
CHANGED
@@ -53,9 +53,10 @@ class RedisClient
|
|
53
53
|
def scan(*args, **kwargs, &block)
|
54
54
|
raise ArgumentError, 'block required' unless block
|
55
55
|
|
56
|
+
seed = Random.new_seed
|
56
57
|
cursor = ZERO_CURSOR_FOR_SCAN
|
57
58
|
loop do
|
58
|
-
cursor, keys = @router.scan('SCAN', cursor, *args, **kwargs)
|
59
|
+
cursor, keys = @router.scan('SCAN', cursor, *args, seed: seed, **kwargs)
|
59
60
|
keys.each(&block)
|
60
61
|
break if cursor == ZERO_CURSOR_FOR_SCAN
|
61
62
|
end
|
@@ -20,10 +20,19 @@ class RedisClient
|
|
20
20
|
|
21
21
|
InvalidClientConfigError = Class.new(::RedisClient::Error)
|
22
22
|
|
23
|
-
attr_reader :command_builder, :client_config
|
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
|
+
)
|
24
33
|
|
25
|
-
def initialize(nodes: DEFAULT_NODES, replica: false, client_implementation: Cluster, fixed_hostname: '', **client_config)
|
26
34
|
@replica = true & replica
|
35
|
+
@replica_affinity = replica_affinity.to_s.to_sym
|
27
36
|
@fixed_hostname = fixed_hostname.to_s
|
28
37
|
@node_configs = build_node_configs(nodes.dup)
|
29
38
|
client_config = client_config.reject { |k, _| IGNORE_GENERIC_CONFIG_KEYS.include?(k) }
|
@@ -33,6 +42,17 @@ class RedisClient
|
|
33
42
|
@mutex = Mutex.new
|
34
43
|
end
|
35
44
|
|
45
|
+
def dup
|
46
|
+
self.class.new(
|
47
|
+
nodes: @node_configs,
|
48
|
+
replica: @replica,
|
49
|
+
replica_affinity: @replica_affinity,
|
50
|
+
fixed_hostname: @fixed_hostname,
|
51
|
+
client_implementation: @client_implementation,
|
52
|
+
**@client_config
|
53
|
+
)
|
54
|
+
end
|
55
|
+
|
36
56
|
def inspect
|
37
57
|
"#<#{self.class.name} #{per_node_key.values}>"
|
38
58
|
end
|
@@ -70,10 +90,6 @@ class RedisClient
|
|
70
90
|
@mutex.synchronize { @node_configs << { host: host, port: port } }
|
71
91
|
end
|
72
92
|
|
73
|
-
def dup
|
74
|
-
self.class.new(nodes: @node_configs, replica: @replica, fixed_hostname: @fixed_hostname, **@client_config)
|
75
|
-
end
|
76
|
-
|
77
93
|
private
|
78
94
|
|
79
95
|
def build_node_configs(addrs)
|
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.2
|
4
|
+
version: 0.3.2
|
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-07 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: redis-client
|
@@ -37,6 +37,10 @@ files:
|
|
37
37
|
- lib/redis_client/cluster/errors.rb
|
38
38
|
- lib/redis_client/cluster/key_slot_converter.rb
|
39
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
|
40
44
|
- lib/redis_client/cluster/node_key.rb
|
41
45
|
- lib/redis_client/cluster/pipeline.rb
|
42
46
|
- lib/redis_client/cluster/pub_sub.rb
|