redis-cluster-client 0.0.5 → 0.0.8
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
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d13300ba8c7f294d3e4c86932d13775ad22060c172900c01d521b1ed8c2278d6
|
4
|
+
data.tar.gz: d5b457978bb312a718ba57b48b56fac1ceb07a463889835c8190794007ecbf7d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 37fb06428eed5a9cca5b42d27c034f057dd7d6f0c5d0c17e4b42c60a8fa3711127c5ed81f06e0565331f64aa762ccc30ee6674fb541e56a8b322ee50ab7a75ce
|
7
|
+
data.tar.gz: 06d46f93c8c7b7116378d2ef9e4380ccce3aac74c071b709cbacd677ec0b302302b52160c04b2c3523771f69446b96d84e8b4f729765073386c5e5f1f3f27f5b
|
@@ -68,12 +68,14 @@ class RedisClient
|
|
68
68
|
@details.fetch(name).fetch(key)
|
69
69
|
end
|
70
70
|
|
71
|
-
def determine_first_key_position(command) # rubocop:disable Metrics/CyclomaticComplexity
|
71
|
+
def determine_first_key_position(command) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength
|
72
72
|
case command&.flatten&.first.to_s.downcase
|
73
|
-
when 'eval', 'evalsha', '
|
73
|
+
when 'eval', 'evalsha', 'zinterstore', 'zunionstore' then 3
|
74
74
|
when 'object' then 2
|
75
75
|
when 'memory'
|
76
76
|
command[1].to_s.casecmp('usage').zero? ? 2 : 0
|
77
|
+
when 'migrate'
|
78
|
+
command[3] == '""' ? determine_optional_key_position(command, 'keys') : 3
|
77
79
|
when 'xread', 'xreadgroup'
|
78
80
|
determine_optional_key_position(command, 'streams')
|
79
81
|
else
|
@@ -29,7 +29,7 @@ class RedisClient
|
|
29
29
|
end
|
30
30
|
|
31
31
|
# Raised when error occurs on any node of cluster.
|
32
|
-
class
|
32
|
+
class ErrorCollection < ::RedisClient::Error
|
33
33
|
attr_reader :errors
|
34
34
|
|
35
35
|
def initialize(errors)
|
@@ -51,5 +51,15 @@ class RedisClient
|
|
51
51
|
super("Cluster client doesn't know which node the #{command} command should be sent to.")
|
52
52
|
end
|
53
53
|
end
|
54
|
+
|
55
|
+
class NodeMightBeDown < ::RedisClient::Error
|
56
|
+
def initialize(_ = '')
|
57
|
+
super(
|
58
|
+
'The client is trying to fetch the latest cluster state '\
|
59
|
+
'because a subset of nodes might be down. '\
|
60
|
+
'It might continue to raise errors for a while.'
|
61
|
+
)
|
62
|
+
end
|
63
|
+
end
|
54
64
|
end
|
55
65
|
end
|
@@ -12,6 +12,7 @@ class RedisClient
|
|
12
12
|
SLOT_SIZE = 16_384
|
13
13
|
MIN_SLOT = 0
|
14
14
|
MAX_SLOT = SLOT_SIZE - 1
|
15
|
+
MAX_STARTUP_SAMPLE = 37
|
15
16
|
IGNORE_GENERIC_CONFIG_KEYS = %i[url host port path].freeze
|
16
17
|
|
17
18
|
ReloadNeeded = Class.new(::RedisClient::Error)
|
@@ -32,19 +33,33 @@ class RedisClient
|
|
32
33
|
end
|
33
34
|
|
34
35
|
class << self
|
35
|
-
def load_info(options, **kwargs)
|
36
|
-
|
37
|
-
|
38
|
-
errors =
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
36
|
+
def load_info(options, **kwargs) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
37
|
+
startup_size = options.size > MAX_STARTUP_SAMPLE ? MAX_STARTUP_SAMPLE : options.size
|
38
|
+
node_info_list = Array.new(startup_size)
|
39
|
+
errors = Array.new(startup_size)
|
40
|
+
startup_options = options.to_a.sample(MAX_STARTUP_SAMPLE).to_h
|
41
|
+
startup_nodes = ::RedisClient::Cluster::Node.new(startup_options, **kwargs)
|
42
|
+
threads = startup_nodes.each_with_index.map do |raw_client, idx|
|
43
|
+
Thread.new(raw_client, idx) do |cli, i|
|
44
|
+
Thread.pass
|
45
|
+
reply = cli.call('CLUSTER', 'NODES')
|
46
|
+
node_info_list[i] = parse_node_info(reply)
|
47
|
+
rescue StandardError => e
|
48
|
+
errors[i] = e
|
49
|
+
ensure
|
50
|
+
cli&.close
|
51
|
+
end
|
52
|
+
end
|
53
|
+
threads.each(&:join)
|
54
|
+
raise ::RedisClient::Cluster::InitialSetupError, errors if node_info_list.all?(&:nil?)
|
55
|
+
|
56
|
+
grouped = node_info_list.compact.group_by do |rows|
|
57
|
+
rows.sort_by { |row| row[:id] }
|
58
|
+
.map { |r| "#{r[:id]}#{r[:node_key]}#{r[:role]}#{r[:primary_id]}#{r[:config_epoch]}" }
|
59
|
+
.join
|
43
60
|
end
|
44
61
|
|
45
|
-
|
46
|
-
ensure
|
47
|
-
startup_nodes&.each(&:close)
|
62
|
+
grouped.max_by { |_, v| v.size }[1].first
|
48
63
|
end
|
49
64
|
|
50
65
|
private
|
@@ -109,32 +124,58 @@ class RedisClient
|
|
109
124
|
end
|
110
125
|
|
111
126
|
def find_by(node_key)
|
127
|
+
raise ReloadNeeded if node_key.nil? || !@clients.key?(node_key)
|
128
|
+
|
112
129
|
@clients.fetch(node_key)
|
113
|
-
rescue KeyError
|
114
|
-
raise ReloadNeeded
|
115
130
|
end
|
116
131
|
|
117
132
|
def call_all(method, *command, **kwargs, &block)
|
118
|
-
try_map
|
133
|
+
results, errors = try_map do |_, client|
|
134
|
+
client.send(method, *command, **kwargs, &block)
|
135
|
+
end
|
136
|
+
|
137
|
+
return results.values if errors.empty?
|
138
|
+
|
139
|
+
raise ::RedisClient::Cluster::ErrorCollection, errors
|
119
140
|
end
|
120
141
|
|
121
|
-
def
|
122
|
-
try_map do |node_key, client|
|
142
|
+
def call_primaries(method, *command, **kwargs, &block)
|
143
|
+
results, errors = try_map do |node_key, client|
|
123
144
|
next if replica?(node_key)
|
124
145
|
|
125
146
|
client.send(method, *command, **kwargs, &block)
|
126
|
-
end
|
147
|
+
end
|
148
|
+
|
149
|
+
return results.values if errors.empty?
|
150
|
+
|
151
|
+
raise ::RedisClient::Cluster::ErrorCollection, errors
|
127
152
|
end
|
128
153
|
|
129
|
-
def
|
130
|
-
return
|
154
|
+
def call_replicas(method, *command, **kwargs, &block)
|
155
|
+
return call_primaries(method, *command, **kwargs, &block) if replica_disabled?
|
131
156
|
|
132
157
|
replica_node_keys = @replications.values.map(&:sample)
|
133
|
-
try_map do |node_key, client|
|
158
|
+
results, errors = try_map do |node_key, client|
|
134
159
|
next if primary?(node_key) || !replica_node_keys.include?(node_key)
|
135
160
|
|
136
161
|
client.send(method, *command, **kwargs, &block)
|
137
|
-
end
|
162
|
+
end
|
163
|
+
|
164
|
+
return results.values if errors.empty?
|
165
|
+
|
166
|
+
raise ::RedisClient::Cluster::ErrorCollection, errors
|
167
|
+
end
|
168
|
+
|
169
|
+
def send_ping(method, *command, **kwargs, &block)
|
170
|
+
results, errors = try_map do |_, client|
|
171
|
+
client.send(method, *command, **kwargs, &block)
|
172
|
+
end
|
173
|
+
|
174
|
+
return results.values if errors.empty?
|
175
|
+
|
176
|
+
raise ReloadNeeded if errors.values.any?(::RedisClient::ConnectionError)
|
177
|
+
|
178
|
+
raise ::RedisClient::Cluster::ErrorCollection, errors
|
138
179
|
end
|
139
180
|
|
140
181
|
def scale_reading_clients
|
@@ -144,14 +185,9 @@ class RedisClient
|
|
144
185
|
end
|
145
186
|
end
|
146
187
|
|
147
|
-
def slot_exists?(slot)
|
148
|
-
slot = Integer(slot)
|
149
|
-
return false if slot < MIN_SLOT || slot > MAX_SLOT
|
150
|
-
|
151
|
-
!@slots[slot].nil?
|
152
|
-
end
|
153
|
-
|
154
188
|
def find_node_key_of_primary(slot)
|
189
|
+
return if slot.nil?
|
190
|
+
|
155
191
|
slot = Integer(slot)
|
156
192
|
return if slot < MIN_SLOT || slot > MAX_SLOT
|
157
193
|
|
@@ -159,6 +195,8 @@ class RedisClient
|
|
159
195
|
end
|
160
196
|
|
161
197
|
def find_node_key_of_replica(slot)
|
198
|
+
return if slot.nil?
|
199
|
+
|
162
200
|
slot = Integer(slot)
|
163
201
|
return if slot < MIN_SLOT || slot > MAX_SLOT
|
164
202
|
|
@@ -171,6 +209,12 @@ class RedisClient
|
|
171
209
|
@mutex.synchronize { @slots[slot] = node_key }
|
172
210
|
end
|
173
211
|
|
212
|
+
def replicated?(primary_node_key, replica_node_key)
|
213
|
+
return false if @replications.nil? || @replications.size.zero?
|
214
|
+
|
215
|
+
@replications.fetch(primary_node_key).include?(replica_node_key)
|
216
|
+
end
|
217
|
+
|
174
218
|
private
|
175
219
|
|
176
220
|
def replica_disabled?
|
@@ -182,7 +226,9 @@ class RedisClient
|
|
182
226
|
end
|
183
227
|
|
184
228
|
def replica?(node_key)
|
185
|
-
|
229
|
+
return false if @replications.nil? || @replications.size.zero?
|
230
|
+
|
231
|
+
!@replications.key?(node_key)
|
186
232
|
end
|
187
233
|
|
188
234
|
def build_slot_node_mappings(node_info)
|
@@ -196,11 +242,12 @@ class RedisClient
|
|
196
242
|
slots
|
197
243
|
end
|
198
244
|
|
199
|
-
def build_replication_mappings(node_info)
|
245
|
+
def build_replication_mappings(node_info) # rubocop:disable Metrics/AbcSize
|
200
246
|
dict = node_info.to_h { |info| [info[:id], info] }
|
201
247
|
node_info.each_with_object(Hash.new { |h, k| h[k] = [] }) do |info, acc|
|
202
248
|
primary_info = dict[info[:primary_id]]
|
203
249
|
acc[primary_info[:node_key]] << info[:node_key] unless primary_info.nil?
|
250
|
+
acc[info[:node_key]] if info[:role] == 'master' # for the primary which have no replicas
|
204
251
|
end
|
205
252
|
end
|
206
253
|
|
@@ -219,22 +266,20 @@ class RedisClient
|
|
219
266
|
end
|
220
267
|
|
221
268
|
def try_map # rubocop:disable Metrics/MethodLength
|
222
|
-
errors = {}
|
223
269
|
results = {}
|
270
|
+
errors = {}
|
224
271
|
threads = @clients.map do |k, v|
|
225
272
|
Thread.new(k, v) do |node_key, client|
|
226
273
|
Thread.pass
|
227
274
|
reply = yield(node_key, client)
|
228
275
|
results[node_key] = reply unless reply.nil?
|
229
|
-
rescue
|
276
|
+
rescue StandardError => e
|
230
277
|
errors[node_key] = e
|
231
278
|
end
|
232
279
|
end
|
233
280
|
|
234
281
|
threads.each(&:join)
|
235
|
-
|
236
|
-
|
237
|
-
raise ::RedisClient::Cluster::CommandErrorCollection, errors
|
282
|
+
[results, errors]
|
238
283
|
end
|
239
284
|
end
|
240
285
|
end
|
data/lib/redis_client/cluster.rb
CHANGED
@@ -40,8 +40,10 @@ class RedisClient
|
|
40
40
|
@size.zero?
|
41
41
|
end
|
42
42
|
|
43
|
-
|
43
|
+
# TODO: https://github.com/redis-rb/redis-cluster-client/issues/37 handle redirections
|
44
|
+
def execute # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
44
45
|
all_replies = Array.new(@size)
|
46
|
+
errors = {}
|
45
47
|
threads = @grouped.map do |k, v|
|
46
48
|
Thread.new(@client, k, v) do |client, node_key, rows|
|
47
49
|
Thread.pass
|
@@ -59,11 +61,15 @@ class RedisClient
|
|
59
61
|
raise ReplySizeError, "commands: #{rows.size}, replies: #{replies.size}" if rows.size != replies.size
|
60
62
|
|
61
63
|
rows.each_with_index { |row, idx| all_replies[row.first] = replies[idx] }
|
64
|
+
rescue StandardError => e
|
65
|
+
errors[node_key] = e
|
62
66
|
end
|
63
67
|
end
|
64
68
|
|
65
69
|
threads.each(&:join)
|
66
|
-
all_replies
|
70
|
+
return all_replies if errors.empty?
|
71
|
+
|
72
|
+
raise ::RedisClient::Cluster::ErrorCollection, errors
|
67
73
|
end
|
68
74
|
end
|
69
75
|
|
@@ -164,8 +170,10 @@ class RedisClient
|
|
164
170
|
end
|
165
171
|
|
166
172
|
def close
|
167
|
-
@node.
|
173
|
+
@node.call_all(:close)
|
168
174
|
nil
|
175
|
+
rescue StandardError
|
176
|
+
# ignore
|
169
177
|
end
|
170
178
|
|
171
179
|
private
|
@@ -184,10 +192,11 @@ class RedisClient
|
|
184
192
|
when 'acl', 'auth', 'bgrewriteaof', 'bgsave', 'quit', 'save'
|
185
193
|
@node.call_all(method, *command, **kwargs, &block).first
|
186
194
|
when 'flushall', 'flushdb'
|
187
|
-
@node.
|
188
|
-
when '
|
189
|
-
when '
|
190
|
-
when '
|
195
|
+
@node.call_primaries(method, *command, **kwargs, &block).first
|
196
|
+
when 'ping' then @node.send_ping(method, *command, **kwargs, &block).first
|
197
|
+
when 'wait' then send_wait_command(method, *command, **kwargs, &block)
|
198
|
+
when 'keys' then @node.call_replicas(method, *command, **kwargs, &block).flatten.sort
|
199
|
+
when 'dbsize' then @node.call_replicas(method, *command, **kwargs, &block).sum
|
191
200
|
when 'scan' then _scan(*command, **kwargs)
|
192
201
|
when 'lastsave' then @node.call_all(method, *command, **kwargs, &block).sort
|
193
202
|
when 'role' then @node.call_all(method, *command, **kwargs, &block)
|
@@ -205,6 +214,22 @@ class RedisClient
|
|
205
214
|
node = assign_node(*command)
|
206
215
|
try_send(node, method, *command, **kwargs, &block)
|
207
216
|
end
|
217
|
+
rescue RedisClient::Cluster::Node::ReloadNeeded
|
218
|
+
update_cluster_info!
|
219
|
+
raise ::RedisClient::Cluster::NodeMightBeDown
|
220
|
+
end
|
221
|
+
|
222
|
+
def send_wait_command(method, *command, retry_count: 3, **kwargs, &block)
|
223
|
+
@node.call_primaries(method, *command, **kwargs, &block).sum
|
224
|
+
rescue RedisClient::Cluster::ErrorCollection => e
|
225
|
+
raise if retry_count <= 0
|
226
|
+
raise if e.errors.values.none? do |err|
|
227
|
+
err.message.include?('WAIT cannot be used with replica instances')
|
228
|
+
end
|
229
|
+
|
230
|
+
update_cluster_info!
|
231
|
+
retry_count -= 1
|
232
|
+
retry
|
208
233
|
end
|
209
234
|
|
210
235
|
def send_config_command(method, *command, **kwargs, &block)
|
@@ -232,13 +257,17 @@ class RedisClient
|
|
232
257
|
end
|
233
258
|
end
|
234
259
|
|
235
|
-
def send_cluster_command(method, *command, **kwargs, &block)
|
260
|
+
def send_cluster_command(method, *command, **kwargs, &block) # rubocop:disable Metrics/MethodLength
|
236
261
|
subcommand = command[1].to_s.downcase
|
237
262
|
case subcommand
|
238
263
|
when 'addslots', 'delslots', 'failover', 'forget', 'meet', 'replicate',
|
239
264
|
'reset', 'set-config-epoch', 'setslot'
|
240
265
|
raise ::RedisClient::Cluster::OrchestrationCommandNotSupported, ['cluster', subcommand]
|
241
266
|
when 'saveconfig' then @node.call_all(method, *command, **kwargs, &block).first
|
267
|
+
when 'getkeysinslot'
|
268
|
+
raise ArgumentError, command.join(' ') if command.size != 4
|
269
|
+
|
270
|
+
find_node(@node.find_node_key_of_replica(command[2])).send(method, *command, **kwargs, &block)
|
242
271
|
else assign_node(*command).send(method, *command, **kwargs, &block)
|
243
272
|
end
|
244
273
|
end
|
@@ -248,7 +277,7 @@ class RedisClient
|
|
248
277
|
when 'debug', 'kill'
|
249
278
|
@node.call_all(method, *command, **kwargs, &block).first
|
250
279
|
when 'flush', 'load'
|
251
|
-
@node.
|
280
|
+
@node.call_primaries(method, *command, **kwargs, &block).first
|
252
281
|
else assign_node(*command).send(method, *command, **kwargs, &block)
|
253
282
|
end
|
254
283
|
end
|
@@ -266,18 +295,16 @@ class RedisClient
|
|
266
295
|
|
267
296
|
# @see https://redis.io/topics/cluster-spec#redirection-and-resharding
|
268
297
|
# Redirection and resharding
|
269
|
-
def try_send(node, method, *args, retry_count: 3, **kwargs, &block) # rubocop:disable Metrics/
|
298
|
+
def try_send(node, method, *args, retry_count: 3, **kwargs, &block) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
270
299
|
node.send(method, *args, **kwargs, &block)
|
271
300
|
rescue ::RedisClient::CommandError => e
|
272
|
-
if
|
273
|
-
raise if retry_count <= 0
|
301
|
+
raise if retry_count <= 0
|
274
302
|
|
303
|
+
if e.message.start_with?(REPLY_MOVED)
|
275
304
|
node = assign_redirection_node(e.message)
|
276
305
|
retry_count -= 1
|
277
306
|
retry
|
278
307
|
elsif e.message.start_with?(REPLY_ASK)
|
279
|
-
raise if retry_count <= 0
|
280
|
-
|
281
308
|
node = assign_asking_node(e.message)
|
282
309
|
node.call(CMD_ASKING)
|
283
310
|
retry_count -= 1
|
@@ -286,8 +313,11 @@ class RedisClient
|
|
286
313
|
raise
|
287
314
|
end
|
288
315
|
rescue ::RedisClient::ConnectionError
|
316
|
+
raise if retry_count <= 0
|
317
|
+
|
289
318
|
update_cluster_info!
|
290
|
-
|
319
|
+
retry_count -= 1
|
320
|
+
retry
|
291
321
|
end
|
292
322
|
|
293
323
|
def _scan(*command, **kwargs) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
@@ -329,41 +359,30 @@ class RedisClient
|
|
329
359
|
find_node(node_key)
|
330
360
|
end
|
331
361
|
|
332
|
-
def find_node_key(*command, primary_only: false)
|
362
|
+
def find_node_key(*command, primary_only: false)
|
333
363
|
key = @command.extract_first_key(command)
|
334
|
-
|
335
|
-
return @node.primary_node_keys.sample if @command.should_send_to_primary?(command) || primary_only
|
336
|
-
|
337
|
-
return @node.replica_node_keys.sample
|
338
|
-
end
|
339
|
-
|
340
|
-
slot = ::RedisClient::Cluster::KeySlotConverter.convert(key)
|
341
|
-
return unless @node.slot_exists?(slot)
|
364
|
+
slot = key.empty? ? nil : ::RedisClient::Cluster::KeySlotConverter.convert(key)
|
342
365
|
|
343
366
|
if @command.should_send_to_primary?(command) || primary_only
|
344
|
-
@node.find_node_key_of_primary(slot)
|
367
|
+
@node.find_node_key_of_primary(slot) || @node.primary_node_keys.sample
|
345
368
|
else
|
346
|
-
@node.find_node_key_of_replica(slot)
|
369
|
+
@node.find_node_key_of_replica(slot) || @node.replica_node_keys.sample
|
347
370
|
end
|
348
371
|
end
|
349
372
|
|
350
|
-
def find_node(node_key)
|
351
|
-
return @node.sample if node_key.nil?
|
352
|
-
|
373
|
+
def find_node(node_key, retry_count: 3)
|
353
374
|
@node.find_by(node_key)
|
354
375
|
rescue ::RedisClient::Cluster::Node::ReloadNeeded
|
355
|
-
|
356
|
-
|
376
|
+
raise ::RedieClient::Cluster::NodeMightBeDown if retry_count <= 0
|
377
|
+
|
378
|
+
update_cluster_info!
|
379
|
+
retry_count -= 1
|
380
|
+
retry
|
357
381
|
end
|
358
382
|
|
359
|
-
def update_cluster_info!
|
383
|
+
def update_cluster_info!
|
360
384
|
@mutex.synchronize do
|
361
|
-
|
362
|
-
host, port = ::RedisClient::Cluster::NodeKey.split(node_key)
|
363
|
-
@config.add_node(host, port)
|
364
|
-
end
|
365
|
-
|
366
|
-
@node.each(&:close)
|
385
|
+
close
|
367
386
|
@node = fetch_cluster_info!(@config, pool: @pool, **@client_kwargs)
|
368
387
|
end
|
369
388
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: redis-cluster-client
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.8
|
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-06-
|
11
|
+
date: 2022-06-21 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: redis-client
|