redis-cluster-client 0.1.0 → 0.3.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/redis-cluster-client.rb +3 -0
- data/lib/redis_client/cluster/node/latency_replica.rb +84 -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 +96 -82
- data/lib/redis_client/cluster.rb +41 -18
- data/lib/redis_client/cluster_config.rb +18 -4
- 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: 619becbe41f6cb5c829de9df021a399ac966b1976284e06e7792c0b570061e32
|
4
|
+
data.tar.gz: 5b6ddc1c8410bcf8ab03538bf5dd77d0425d4bf34f170ea026ff4d1c1be2ce8e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 658a78bdd61f7e902b2385c73f23f3fa692a73b77a073afc0cb3d0616726a99cb65884ec4ab7cbae2bdd4574444f1de2f639105eaa4f40e1c07c186d7589dff5
|
7
|
+
data.tar.gz: 33b9d88cc33fec6f5a556ae3beb494c9875796088b93a47b647efe02db6f41540d49f3120d5d7114c8757aa78e29b173abd20a8b2a2341861e953a0ab4b0f67a
|
@@ -0,0 +1,84 @@
|
|
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
|
+
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
|
+
|
49
|
+
min = DUMMY_LATENCY_NSEC
|
50
|
+
MEASURE_ATTEMPT_COUNT.times do
|
51
|
+
starting = Process.clock_gettime(Process::CLOCK_MONOTONIC, :nanosecond)
|
52
|
+
client.send(:call_once, 'PING')
|
53
|
+
duration = Process.clock_gettime(Process::CLOCK_MONOTONIC, :nanosecond) - starting
|
54
|
+
min = duration if duration < min
|
55
|
+
end
|
56
|
+
|
57
|
+
latencies[node_key] = min
|
58
|
+
rescue StandardError
|
59
|
+
latencies[node_key] = DUMMY_LATENCY_NSEC
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
threads.each(&:join)
|
64
|
+
end
|
65
|
+
|
66
|
+
latencies
|
67
|
+
end
|
68
|
+
|
69
|
+
def select_replica_clients(replications, clients)
|
70
|
+
keys = replications.values.filter_map(&:first)
|
71
|
+
clients.select { |k, _| keys.include?(k) }
|
72
|
+
end
|
73
|
+
|
74
|
+
def select_clients_for_scanning(replications, clients)
|
75
|
+
keys = replications.map do |primary_node_key, replica_node_keys|
|
76
|
+
replica_node_keys.empty? ? primary_node_key : replica_node_keys.first
|
77
|
+
end
|
78
|
+
|
79
|
+
clients.select { |k, _| keys.include?(k) }
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
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
|
@@ -20,40 +20,37 @@ class RedisClient
|
|
20
20
|
@client_kwargs = kwargs
|
21
21
|
@node = fetch_cluster_info(@config, pool: @pool, **@client_kwargs)
|
22
22
|
@command = ::RedisClient::Cluster::Command.load(@node)
|
23
|
-
@command_builder = @config.command_builder
|
24
23
|
@mutex = Mutex.new
|
24
|
+
@command_builder = @config.command_builder
|
25
25
|
end
|
26
26
|
|
27
|
-
def send_command(method, *args,
|
28
|
-
command = method == :blocking_call && args.size > 1 ? args[1..] : args
|
29
|
-
command = @command_builder.generate!(command, kwargs)
|
30
|
-
|
27
|
+
def send_command(method, command, *args, &block) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
31
28
|
cmd = command.first.to_s.downcase
|
32
29
|
case cmd
|
33
30
|
when 'acl', 'auth', 'bgrewriteaof', 'bgsave', 'quit', 'save'
|
34
|
-
@node.call_all(method,
|
31
|
+
@node.call_all(method, command, args, &block).first
|
35
32
|
when 'flushall', 'flushdb'
|
36
|
-
@node.call_primaries(method,
|
37
|
-
when 'ping' then @node.send_ping(method,
|
38
|
-
when 'wait' then send_wait_command(method,
|
39
|
-
when 'keys' then @node.call_replicas(method,
|
40
|
-
when 'dbsize' then @node.call_replicas(method,
|
41
|
-
when 'scan' then scan(
|
42
|
-
when 'lastsave' then @node.call_all(method,
|
43
|
-
when 'role' then @node.call_all(method,
|
44
|
-
when 'config' then send_config_command(method,
|
45
|
-
when 'client' then send_client_command(method,
|
46
|
-
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)
|
47
44
|
when 'readonly', 'readwrite', 'shutdown'
|
48
45
|
raise ::RedisClient::Cluster::OrchestrationCommandNotSupported, cmd
|
49
|
-
when 'memory' then send_memory_command(method,
|
50
|
-
when 'script' then send_script_command(method,
|
51
|
-
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)
|
52
49
|
when 'discard', 'exec', 'multi', 'unwatch'
|
53
50
|
raise ::RedisClient::Cluster::AmbiguousNodeError, cmd
|
54
51
|
else
|
55
|
-
node = assign_node(
|
56
|
-
try_send(node, method,
|
52
|
+
node = assign_node(command)
|
53
|
+
try_send(node, method, command, args, &block)
|
57
54
|
end
|
58
55
|
rescue ::RedisClient::Cluster::Node::ReloadNeeded
|
59
56
|
update_cluster_info!
|
@@ -67,7 +64,37 @@ class RedisClient
|
|
67
64
|
|
68
65
|
# @see https://redis.io/topics/cluster-spec#redirection-and-resharding
|
69
66
|
# Redirection and resharding
|
70
|
-
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
|
71
98
|
node.send(method, *args, **kwargs, &block)
|
72
99
|
rescue ::RedisClient::CommandError => e
|
73
100
|
raise if retry_count <= 0
|
@@ -96,8 +123,8 @@ class RedisClient
|
|
96
123
|
retry
|
97
124
|
end
|
98
125
|
|
99
|
-
def scan(*command, **kwargs) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
100
|
-
command = @command_builder.generate
|
126
|
+
def scan(*command, seed: nil, **kwargs) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
127
|
+
command = @command_builder.generate(command, kwargs)
|
101
128
|
|
102
129
|
command[1] = ZERO_CURSOR_FOR_SCAN if command.size == 1
|
103
130
|
input_cursor = Integer(command[1])
|
@@ -105,14 +132,14 @@ class RedisClient
|
|
105
132
|
client_index = input_cursor % 256
|
106
133
|
raw_cursor = input_cursor >> 8
|
107
134
|
|
108
|
-
clients = @node.
|
135
|
+
clients = @node.clients_for_scanning(seed: seed)
|
109
136
|
|
110
137
|
client = clients[client_index]
|
111
138
|
return [ZERO_CURSOR_FOR_SCAN, []] unless client
|
112
139
|
|
113
140
|
command[1] = raw_cursor.to_s
|
114
141
|
|
115
|
-
result_cursor, result_keys = client.
|
142
|
+
result_cursor, result_keys = client.call_v(command)
|
116
143
|
result_cursor = Integer(result_cursor)
|
117
144
|
|
118
145
|
client_index += 1 if result_cursor == 0
|
@@ -120,21 +147,19 @@ class RedisClient
|
|
120
147
|
[((result_cursor << 8) + client_index).to_s, result_keys]
|
121
148
|
end
|
122
149
|
|
123
|
-
def assign_node(
|
124
|
-
node_key = find_node_key(
|
150
|
+
def assign_node(command)
|
151
|
+
node_key = find_node_key(command)
|
125
152
|
find_node(node_key)
|
126
153
|
end
|
127
154
|
|
128
|
-
def find_node_key(
|
129
|
-
command = @command_builder.generate!(command, kwargs)
|
130
|
-
|
155
|
+
def find_node_key(command, seed: nil)
|
131
156
|
key = @command.extract_first_key(command)
|
132
157
|
slot = key.empty? ? nil : ::RedisClient::Cluster::KeySlotConverter.convert(key)
|
133
158
|
|
134
|
-
if @command.should_send_to_primary?(command)
|
135
|
-
@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)
|
136
161
|
else
|
137
|
-
@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)
|
138
163
|
end
|
139
164
|
end
|
140
165
|
|
@@ -154,8 +179,8 @@ class RedisClient
|
|
154
179
|
|
155
180
|
private
|
156
181
|
|
157
|
-
def send_wait_command(method,
|
158
|
-
@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
|
159
184
|
rescue ::RedisClient::Cluster::ErrorCollection => e
|
160
185
|
raise if retry_count <= 0
|
161
186
|
raise if e.errors.values.none? do |err|
|
@@ -167,82 +192,65 @@ class RedisClient
|
|
167
192
|
retry
|
168
193
|
end
|
169
194
|
|
170
|
-
def send_config_command(method,
|
171
|
-
command = method == :blocking_call && args.size > 1 ? args[1..] : args
|
172
|
-
command = @command_builder.generate!(command, kwargs)
|
173
|
-
|
195
|
+
def send_config_command(method, command, args, &block)
|
174
196
|
case command[1].to_s.downcase
|
175
197
|
when 'resetstat', 'rewrite', 'set'
|
176
|
-
@node.call_all(method,
|
177
|
-
else assign_node(
|
198
|
+
@node.call_all(method, command, args, &block).first
|
199
|
+
else assign_node(command).send(method, *args, command, &block)
|
178
200
|
end
|
179
201
|
end
|
180
202
|
|
181
|
-
def send_memory_command(method,
|
182
|
-
command = method == :blocking_call && args.size > 1 ? args[1..] : args
|
183
|
-
command = @command_builder.generate!(command, kwargs)
|
184
|
-
|
203
|
+
def send_memory_command(method, command, args, &block)
|
185
204
|
case command[1].to_s.downcase
|
186
|
-
when 'stats' then @node.call_all(method,
|
187
|
-
when 'purge' then @node.call_all(method,
|
188
|
-
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)
|
189
208
|
end
|
190
209
|
end
|
191
210
|
|
192
|
-
def send_client_command(method,
|
193
|
-
command = method == :blocking_call && args.size > 1 ? args[1..] : args
|
194
|
-
command = @command_builder.generate!(command, kwargs)
|
195
|
-
|
211
|
+
def send_client_command(method, command, args, &block)
|
196
212
|
case command[1].to_s.downcase
|
197
|
-
when 'list' then @node.call_all(method,
|
213
|
+
when 'list' then @node.call_all(method, command, args, &block).flatten
|
198
214
|
when 'pause', 'reply', 'setname'
|
199
|
-
@node.call_all(method,
|
200
|
-
else assign_node(
|
215
|
+
@node.call_all(method, command, args, &block).first
|
216
|
+
else assign_node(command).send(method, *args, command, &block)
|
201
217
|
end
|
202
218
|
end
|
203
219
|
|
204
|
-
def send_cluster_command(method,
|
205
|
-
command = method == :blocking_call && args.size > 1 ? args[1..] : args
|
206
|
-
command = @command_builder.generate!(command, kwargs)
|
220
|
+
def send_cluster_command(method, command, args, &block) # rubocop:disable Metrics/MethodLength
|
207
221
|
subcommand = command[1].to_s.downcase
|
208
222
|
|
209
223
|
case subcommand
|
210
224
|
when 'addslots', 'delslots', 'failover', 'forget', 'meet', 'replicate',
|
211
225
|
'reset', 'set-config-epoch', 'setslot'
|
212
226
|
raise ::RedisClient::Cluster::OrchestrationCommandNotSupported, ['cluster', subcommand]
|
213
|
-
when 'saveconfig' then @node.call_all(method,
|
227
|
+
when 'saveconfig' then @node.call_all(method, command, args, &block).first
|
214
228
|
when 'getkeysinslot'
|
215
229
|
raise ArgumentError, command.join(' ') if command.size != 4
|
216
230
|
|
217
|
-
find_node(@node.find_node_key_of_replica(command[2])).send(method, *args,
|
218
|
-
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)
|
219
233
|
end
|
220
234
|
end
|
221
235
|
|
222
|
-
def send_script_command(method,
|
223
|
-
command = method == :blocking_call && args.size > 1 ? args[1..] : args
|
224
|
-
command = @command_builder.generate!(command, kwargs)
|
225
|
-
|
236
|
+
def send_script_command(method, command, args, &block)
|
226
237
|
case command[1].to_s.downcase
|
227
238
|
when 'debug', 'kill'
|
228
|
-
@node.call_all(method,
|
239
|
+
@node.call_all(method, command, args, &block).first
|
229
240
|
when 'flush', 'load'
|
230
|
-
@node.call_primaries(method,
|
231
|
-
else assign_node(
|
241
|
+
@node.call_primaries(method, command, args, &block).first
|
242
|
+
else assign_node(command).send(method, *args, command, &block)
|
232
243
|
end
|
233
244
|
end
|
234
245
|
|
235
|
-
def send_pubsub_command(method,
|
236
|
-
command = method == :blocking_call && args.size > 1 ? args[1..] : args
|
237
|
-
command = @command_builder.generate!(command, kwargs)
|
238
|
-
|
246
|
+
def send_pubsub_command(method, command, args, &block) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
239
247
|
case command[1].to_s.downcase
|
240
|
-
when 'channels' then @node.call_all(method,
|
248
|
+
when 'channels' then @node.call_all(method, command, args, &block).flatten.uniq.sort_by(&:to_s)
|
241
249
|
when 'numsub'
|
242
|
-
@node.call_all(method,
|
250
|
+
@node.call_all(method, command, args, &block).reject(&:empty?).map { |e| Hash[*e] }
|
243
251
|
.reduce({}) { |a, e| a.merge(e) { |_, v1, v2| v1 + v2 } }
|
244
|
-
when 'numpat' then @node.call_all(method,
|
245
|
-
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)
|
246
254
|
end
|
247
255
|
end
|
248
256
|
|
@@ -258,18 +266,24 @@ class RedisClient
|
|
258
266
|
find_node(node_key)
|
259
267
|
end
|
260
268
|
|
261
|
-
def fetch_cluster_info(config, pool: nil, **kwargs)
|
269
|
+
def fetch_cluster_info(config, pool: nil, **kwargs) # rubocop:disable Metrics/MethodLength
|
262
270
|
node_info = ::RedisClient::Cluster::Node.load_info(config.per_node_key, **kwargs)
|
263
271
|
node_addrs = node_info.map { |info| ::RedisClient::Cluster::NodeKey.hashify(info[:node_key]) }
|
264
272
|
config.update_node(node_addrs)
|
265
|
-
::RedisClient::Cluster::Node.new(
|
266
|
-
|
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
|
+
)
|
267
281
|
end
|
268
282
|
|
269
283
|
def update_cluster_info!
|
270
284
|
@mutex.synchronize do
|
271
285
|
begin
|
272
|
-
@node.
|
286
|
+
@node.each(&:close)
|
273
287
|
rescue ::RedisClient::Cluster::ErrorCollection
|
274
288
|
# ignore
|
275
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,11 +86,11 @@ 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
|
74
96
|
|
@@ -77,7 +99,8 @@ class RedisClient
|
|
77
99
|
def method_missing(name, *args, **kwargs, &block)
|
78
100
|
if @router.command_exists?(name)
|
79
101
|
args.unshift(name)
|
80
|
-
|
102
|
+
command = @command_builder.generate(args, kwargs)
|
103
|
+
return @router.send_command(:call_v, command, &block)
|
81
104
|
end
|
82
105
|
|
83
106
|
super
|
@@ -20,15 +20,25 @@ class RedisClient
|
|
20
20
|
|
21
21
|
InvalidClientConfigError = Class.new(::RedisClient::Error)
|
22
22
|
|
23
|
-
attr_reader :command_builder
|
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, 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) }
|
30
39
|
@command_builder = client_config.fetch(:command_builder, ::RedisClient::CommandBuilder)
|
31
40
|
@client_config = merge_generic_config(client_config, @node_configs)
|
41
|
+
@client_implementation = client_implementation
|
32
42
|
@mutex = Mutex.new
|
33
43
|
end
|
34
44
|
|
@@ -36,12 +46,16 @@ class RedisClient
|
|
36
46
|
"#<#{self.class.name} #{per_node_key.values}>"
|
37
47
|
end
|
38
48
|
|
49
|
+
def read_timeout
|
50
|
+
@client_config[:read_timeout] || @client_config[:timeout] || RedisClient::Config::DEFAULT_TIMEOUT
|
51
|
+
end
|
52
|
+
|
39
53
|
def new_pool(size: 5, timeout: 5, **kwargs)
|
40
|
-
|
54
|
+
@client_implementation.new(self, pool: { size: size, timeout: timeout }, **kwargs)
|
41
55
|
end
|
42
56
|
|
43
57
|
def new_client(**kwargs)
|
44
|
-
|
58
|
+
@client_implementation.new(self, **kwargs)
|
45
59
|
end
|
46
60
|
|
47
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.1
|
4
|
+
version: 0.3.1
|
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
|