redis-cluster-client 0.1.0 → 0.3.1
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/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
|