redis 3.3.5 → 4.1.4
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 +5 -5
- data/CHANGELOG.md +84 -2
- data/README.md +131 -76
- data/lib/redis.rb +912 -200
- data/lib/redis/client.rb +71 -29
- data/lib/redis/cluster.rb +291 -0
- data/lib/redis/cluster/command.rb +81 -0
- data/lib/redis/cluster/command_loader.rb +34 -0
- data/lib/redis/cluster/key_slot_converter.rb +72 -0
- data/lib/redis/cluster/node.rb +104 -0
- data/lib/redis/cluster/node_key.rb +31 -0
- data/lib/redis/cluster/node_loader.rb +37 -0
- data/lib/redis/cluster/option.rb +87 -0
- data/lib/redis/cluster/slot.rb +72 -0
- data/lib/redis/cluster/slot_loader.rb +50 -0
- data/lib/redis/connection.rb +3 -2
- data/lib/redis/connection/command_helper.rb +3 -8
- data/lib/redis/connection/hiredis.rb +3 -2
- data/lib/redis/connection/registry.rb +1 -0
- data/lib/redis/connection/ruby.rb +48 -32
- data/lib/redis/connection/synchrony.rb +13 -4
- data/lib/redis/distributed.rb +39 -15
- data/lib/redis/errors.rb +47 -0
- data/lib/redis/hash_ring.rb +21 -64
- data/lib/redis/pipeline.rb +54 -12
- data/lib/redis/subscribe.rb +1 -0
- data/lib/redis/version.rb +2 -1
- metadata +40 -198
- data/.gitignore +0 -16
- data/.travis.yml +0 -89
- data/.travis/Gemfile +0 -11
- data/.yardopts +0 -3
- data/Gemfile +0 -4
- data/Rakefile +0 -87
- data/benchmarking/logging.rb +0 -71
- data/benchmarking/pipeline.rb +0 -51
- data/benchmarking/speed.rb +0 -21
- data/benchmarking/suite.rb +0 -24
- data/benchmarking/worker.rb +0 -71
- data/examples/basic.rb +0 -15
- data/examples/consistency.rb +0 -114
- data/examples/dist_redis.rb +0 -43
- data/examples/incr-decr.rb +0 -17
- data/examples/list.rb +0 -26
- data/examples/pubsub.rb +0 -37
- data/examples/sentinel.rb +0 -41
- data/examples/sentinel/sentinel.conf +0 -9
- data/examples/sentinel/start +0 -49
- data/examples/sets.rb +0 -36
- data/examples/unicorn/config.ru +0 -3
- data/examples/unicorn/unicorn.rb +0 -20
- data/redis.gemspec +0 -44
- data/test/bitpos_test.rb +0 -69
- data/test/blocking_commands_test.rb +0 -42
- data/test/client_test.rb +0 -59
- data/test/command_map_test.rb +0 -30
- data/test/commands_on_hashes_test.rb +0 -21
- data/test/commands_on_hyper_log_log_test.rb +0 -21
- data/test/commands_on_lists_test.rb +0 -20
- data/test/commands_on_sets_test.rb +0 -77
- data/test/commands_on_sorted_sets_test.rb +0 -137
- data/test/commands_on_strings_test.rb +0 -101
- data/test/commands_on_value_types_test.rb +0 -133
- data/test/connection_handling_test.rb +0 -277
- data/test/connection_test.rb +0 -57
- data/test/db/.gitkeep +0 -0
- data/test/distributed_blocking_commands_test.rb +0 -46
- data/test/distributed_commands_on_hashes_test.rb +0 -10
- data/test/distributed_commands_on_hyper_log_log_test.rb +0 -33
- data/test/distributed_commands_on_lists_test.rb +0 -22
- data/test/distributed_commands_on_sets_test.rb +0 -83
- data/test/distributed_commands_on_sorted_sets_test.rb +0 -18
- data/test/distributed_commands_on_strings_test.rb +0 -59
- data/test/distributed_commands_on_value_types_test.rb +0 -95
- data/test/distributed_commands_requiring_clustering_test.rb +0 -164
- data/test/distributed_connection_handling_test.rb +0 -23
- data/test/distributed_internals_test.rb +0 -79
- data/test/distributed_key_tags_test.rb +0 -52
- data/test/distributed_persistence_control_commands_test.rb +0 -26
- data/test/distributed_publish_subscribe_test.rb +0 -92
- data/test/distributed_remote_server_control_commands_test.rb +0 -66
- data/test/distributed_scripting_test.rb +0 -102
- data/test/distributed_sorting_test.rb +0 -20
- data/test/distributed_test.rb +0 -58
- data/test/distributed_transactions_test.rb +0 -32
- data/test/encoding_test.rb +0 -18
- data/test/error_replies_test.rb +0 -59
- data/test/fork_safety_test.rb +0 -65
- data/test/helper.rb +0 -232
- data/test/helper_test.rb +0 -24
- data/test/internals_test.rb +0 -417
- data/test/lint/blocking_commands.rb +0 -150
- data/test/lint/hashes.rb +0 -162
- data/test/lint/hyper_log_log.rb +0 -60
- data/test/lint/lists.rb +0 -143
- data/test/lint/sets.rb +0 -140
- data/test/lint/sorted_sets.rb +0 -316
- data/test/lint/strings.rb +0 -260
- data/test/lint/value_types.rb +0 -122
- data/test/persistence_control_commands_test.rb +0 -26
- data/test/pipelining_commands_test.rb +0 -242
- data/test/publish_subscribe_test.rb +0 -282
- data/test/remote_server_control_commands_test.rb +0 -118
- data/test/scanning_test.rb +0 -413
- data/test/scripting_test.rb +0 -78
- data/test/sentinel_command_test.rb +0 -80
- data/test/sentinel_test.rb +0 -255
- data/test/sorting_test.rb +0 -59
- data/test/ssl_test.rb +0 -73
- data/test/support/connection/hiredis.rb +0 -1
- data/test/support/connection/ruby.rb +0 -1
- data/test/support/connection/synchrony.rb +0 -17
- data/test/support/redis_mock.rb +0 -130
- data/test/support/ssl/gen_certs.sh +0 -31
- data/test/support/ssl/trusted-ca.crt +0 -25
- data/test/support/ssl/trusted-ca.key +0 -27
- data/test/support/ssl/trusted-cert.crt +0 -81
- data/test/support/ssl/trusted-cert.key +0 -28
- data/test/support/ssl/untrusted-ca.crt +0 -26
- data/test/support/ssl/untrusted-ca.key +0 -27
- data/test/support/ssl/untrusted-cert.crt +0 -82
- data/test/support/ssl/untrusted-cert.key +0 -28
- data/test/support/wire/synchrony.rb +0 -24
- data/test/support/wire/thread.rb +0 -5
- data/test/synchrony_driver.rb +0 -88
- data/test/test.conf.erb +0 -9
- data/test/thread_safety_test.rb +0 -62
- data/test/transactions_test.rb +0 -264
- data/test/unknown_commands_test.rb +0 -14
- data/test/url_param_test.rb +0 -138
data/lib/redis/client.rb
CHANGED
@@ -1,4 +1,6 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "errors"
|
2
4
|
require "socket"
|
3
5
|
require "cgi"
|
4
6
|
|
@@ -18,12 +20,12 @@ class Redis
|
|
18
20
|
:id => nil,
|
19
21
|
:tcp_keepalive => 0,
|
20
22
|
:reconnect_attempts => 1,
|
23
|
+
:reconnect_delay => 0,
|
24
|
+
:reconnect_delay_max => 0.5,
|
21
25
|
:inherit_socket => false
|
22
26
|
}
|
23
27
|
|
24
|
-
|
25
|
-
Marshal.load(Marshal.dump(@options))
|
26
|
-
end
|
28
|
+
attr_reader :options
|
27
29
|
|
28
30
|
def scheme
|
29
31
|
@options[:scheme]
|
@@ -86,11 +88,14 @@ class Redis
|
|
86
88
|
|
87
89
|
@pending_reads = 0
|
88
90
|
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
91
|
+
@connector =
|
92
|
+
if options.include?(:sentinels)
|
93
|
+
Connector::Sentinel.new(@options)
|
94
|
+
elsif options.include?(:connector) && options[:connector].respond_to?(:new)
|
95
|
+
options.delete(:connector).new(@options)
|
96
|
+
else
|
97
|
+
Connector.new(@options)
|
98
|
+
end
|
94
99
|
end
|
95
100
|
|
96
101
|
def connect
|
@@ -152,9 +157,11 @@ class Redis
|
|
152
157
|
end
|
153
158
|
|
154
159
|
def call_pipeline(pipeline)
|
160
|
+
return [] if pipeline.futures.empty?
|
161
|
+
|
155
162
|
with_reconnect pipeline.with_reconnect? do
|
156
163
|
begin
|
157
|
-
pipeline.finish(call_pipelined(pipeline
|
164
|
+
pipeline.finish(call_pipelined(pipeline)).tap do
|
158
165
|
self.db = pipeline.db if pipeline.db
|
159
166
|
end
|
160
167
|
rescue ConnectionError => e
|
@@ -167,8 +174,8 @@ class Redis
|
|
167
174
|
end
|
168
175
|
end
|
169
176
|
|
170
|
-
def call_pipelined(
|
171
|
-
return [] if
|
177
|
+
def call_pipelined(pipeline)
|
178
|
+
return [] if pipeline.futures.empty?
|
172
179
|
|
173
180
|
# The method #ensure_connected (called from #process) reconnects once on
|
174
181
|
# I/O errors. To make an effort in making sure that commands are not
|
@@ -178,6 +185,8 @@ class Redis
|
|
178
185
|
# already successfully executed commands. To circumvent this, don't retry
|
179
186
|
# after the first reply has been read successfully.
|
180
187
|
|
188
|
+
commands = pipeline.commands
|
189
|
+
|
181
190
|
result = Array.new(commands.size)
|
182
191
|
reconnect = @reconnect
|
183
192
|
|
@@ -185,13 +194,14 @@ class Redis
|
|
185
194
|
exception = nil
|
186
195
|
|
187
196
|
process(commands) do
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
result[i
|
197
|
+
pipeline.timeouts.each_with_index do |timeout, i|
|
198
|
+
reply = if timeout
|
199
|
+
with_socket_timeout(timeout) { read }
|
200
|
+
else
|
201
|
+
read
|
202
|
+
end
|
203
|
+
result[i] = reply
|
204
|
+
@reconnect = false
|
195
205
|
exception = reply if exception.nil? && reply.is_a?(CommandError)
|
196
206
|
end
|
197
207
|
end
|
@@ -240,6 +250,7 @@ class Redis
|
|
240
250
|
def disconnect
|
241
251
|
connection.disconnect if connected?
|
242
252
|
end
|
253
|
+
alias_method :close, :disconnect
|
243
254
|
|
244
255
|
def reconnect
|
245
256
|
disconnect
|
@@ -274,12 +285,15 @@ class Redis
|
|
274
285
|
|
275
286
|
def with_socket_timeout(timeout)
|
276
287
|
connect unless connected?
|
288
|
+
original = @options[:read_timeout]
|
277
289
|
|
278
290
|
begin
|
279
291
|
connection.timeout = timeout
|
292
|
+
@options[:read_timeout] = timeout # for reconnection
|
280
293
|
yield
|
281
294
|
ensure
|
282
295
|
connection.timeout = self.timeout if connected?
|
296
|
+
@options[:read_timeout] = original
|
283
297
|
end
|
284
298
|
end
|
285
299
|
|
@@ -336,11 +350,15 @@ class Redis
|
|
336
350
|
@connection = @options[:driver].connect(@options)
|
337
351
|
@pending_reads = 0
|
338
352
|
rescue TimeoutError,
|
353
|
+
SocketError,
|
354
|
+
Errno::EADDRNOTAVAIL,
|
339
355
|
Errno::ECONNREFUSED,
|
340
356
|
Errno::EHOSTDOWN,
|
341
357
|
Errno::EHOSTUNREACH,
|
342
358
|
Errno::ENETUNREACH,
|
343
|
-
Errno::
|
359
|
+
Errno::ENOENT,
|
360
|
+
Errno::ETIMEDOUT,
|
361
|
+
Errno::EINVAL
|
344
362
|
|
345
363
|
raise CannotConnectError, "Error connecting to Redis on #{location} (#{$!.class})"
|
346
364
|
end
|
@@ -369,6 +387,10 @@ class Redis
|
|
369
387
|
disconnect
|
370
388
|
|
371
389
|
if attempts <= @options[:reconnect_attempts] && @reconnect
|
390
|
+
sleep_t = [(@options[:reconnect_delay] * 2**(attempts-1)),
|
391
|
+
@options[:reconnect_delay_max]].min
|
392
|
+
|
393
|
+
Kernel.sleep(sleep_t)
|
372
394
|
retry
|
373
395
|
else
|
374
396
|
raise
|
@@ -395,7 +417,8 @@ class Redis
|
|
395
417
|
options[key] = options[key.to_s] if options.has_key?(key.to_s)
|
396
418
|
end
|
397
419
|
|
398
|
-
url = options[:url]
|
420
|
+
url = options[:url]
|
421
|
+
url = defaults[:url] if url == nil
|
399
422
|
|
400
423
|
# Override defaults from URL if given
|
401
424
|
if url
|
@@ -445,6 +468,10 @@ class Redis
|
|
445
468
|
options[:read_timeout] = Float(options[:read_timeout])
|
446
469
|
options[:write_timeout] = Float(options[:write_timeout])
|
447
470
|
|
471
|
+
options[:reconnect_attempts] = options[:reconnect_attempts].to_i
|
472
|
+
options[:reconnect_delay] = options[:reconnect_delay].to_f
|
473
|
+
options[:reconnect_delay_max] = options[:reconnect_delay_max].to_f
|
474
|
+
|
448
475
|
options[:db] = options[:db].to_i
|
449
476
|
options[:driver] = _parse_driver(options[:driver]) || Connection.drivers.last
|
450
477
|
|
@@ -478,11 +505,16 @@ class Redis
|
|
478
505
|
|
479
506
|
if driver.kind_of?(String)
|
480
507
|
begin
|
481
|
-
|
482
|
-
|
483
|
-
|
484
|
-
|
508
|
+
require_relative "connection/#{driver}"
|
509
|
+
rescue LoadError, NameError => e
|
510
|
+
begin
|
511
|
+
require "connection/#{driver}"
|
512
|
+
rescue LoadError, NameError => e
|
513
|
+
raise RuntimeError, "Cannot load driver #{driver.inspect}: #{e.message}"
|
514
|
+
end
|
485
515
|
end
|
516
|
+
|
517
|
+
driver = Connection.const_get(driver.capitalize)
|
486
518
|
end
|
487
519
|
|
488
520
|
driver
|
@@ -504,7 +536,6 @@ class Redis
|
|
504
536
|
def initialize(options)
|
505
537
|
super(options)
|
506
538
|
|
507
|
-
@options[:password] = DEFAULTS.fetch(:password)
|
508
539
|
@options[:db] = DEFAULTS.fetch(:db)
|
509
540
|
|
510
541
|
@sentinels = @options.delete(:sentinels).dup
|
@@ -547,6 +578,7 @@ class Redis
|
|
547
578
|
client = Client.new(@options.merge({
|
548
579
|
:host => sentinel[:host],
|
549
580
|
:port => sentinel[:port],
|
581
|
+
password: sentinel[:password],
|
550
582
|
:reconnect_attempts => 0,
|
551
583
|
}))
|
552
584
|
|
@@ -578,9 +610,19 @@ class Redis
|
|
578
610
|
def resolve_slave
|
579
611
|
sentinel_detect do |client|
|
580
612
|
if reply = client.call(["sentinel", "slaves", @master])
|
581
|
-
|
582
|
-
|
583
|
-
{
|
613
|
+
slaves = reply.map { |s| s.each_slice(2).to_h }
|
614
|
+
slaves.each { |s| s['flags'] = s.fetch('flags').split(',') }
|
615
|
+
slaves.reject! { |s| s.fetch('flags').include?('s_down') }
|
616
|
+
|
617
|
+
if slaves.empty?
|
618
|
+
raise CannotConnectError, 'No slaves available.'
|
619
|
+
else
|
620
|
+
slave = slaves.sample
|
621
|
+
{
|
622
|
+
host: slave.fetch('ip'),
|
623
|
+
port: slave.fetch('port'),
|
624
|
+
}
|
625
|
+
end
|
584
626
|
end
|
585
627
|
end
|
586
628
|
end
|
@@ -0,0 +1,291 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'errors'
|
4
|
+
require_relative 'client'
|
5
|
+
require_relative 'cluster/command'
|
6
|
+
require_relative 'cluster/command_loader'
|
7
|
+
require_relative 'cluster/key_slot_converter'
|
8
|
+
require_relative 'cluster/node'
|
9
|
+
require_relative 'cluster/node_key'
|
10
|
+
require_relative 'cluster/node_loader'
|
11
|
+
require_relative 'cluster/option'
|
12
|
+
require_relative 'cluster/slot'
|
13
|
+
require_relative 'cluster/slot_loader'
|
14
|
+
|
15
|
+
class Redis
|
16
|
+
# Redis Cluster client
|
17
|
+
#
|
18
|
+
# @see https://github.com/antirez/redis-rb-cluster POC implementation
|
19
|
+
# @see https://redis.io/topics/cluster-spec Redis Cluster specification
|
20
|
+
# @see https://redis.io/topics/cluster-tutorial Redis Cluster tutorial
|
21
|
+
#
|
22
|
+
# Copyright (C) 2013 Salvatore Sanfilippo <antirez@gmail.com>
|
23
|
+
class Cluster
|
24
|
+
def initialize(options = {})
|
25
|
+
@option = Option.new(options)
|
26
|
+
@node, @slot = fetch_cluster_info!(@option)
|
27
|
+
@command = fetch_command_details(@node)
|
28
|
+
end
|
29
|
+
|
30
|
+
def id
|
31
|
+
@node.map(&:id).sort.join(' ')
|
32
|
+
end
|
33
|
+
|
34
|
+
# db feature is disabled in cluster mode
|
35
|
+
def db
|
36
|
+
0
|
37
|
+
end
|
38
|
+
|
39
|
+
# db feature is disabled in cluster mode
|
40
|
+
def db=(_db); end
|
41
|
+
|
42
|
+
def timeout
|
43
|
+
@node.first.timeout
|
44
|
+
end
|
45
|
+
|
46
|
+
def connected?
|
47
|
+
@node.any?(&:connected?)
|
48
|
+
end
|
49
|
+
|
50
|
+
def disconnect
|
51
|
+
@node.each(&:disconnect)
|
52
|
+
true
|
53
|
+
end
|
54
|
+
|
55
|
+
def connection_info
|
56
|
+
@node.sort_by(&:id).map do |client|
|
57
|
+
{
|
58
|
+
host: client.host,
|
59
|
+
port: client.port,
|
60
|
+
db: client.db,
|
61
|
+
id: client.id,
|
62
|
+
location: client.location
|
63
|
+
}
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def with_reconnect(val = true, &block)
|
68
|
+
try_send(@node.sample, :with_reconnect, val, &block)
|
69
|
+
end
|
70
|
+
|
71
|
+
def call(command, &block)
|
72
|
+
send_command(command, &block)
|
73
|
+
end
|
74
|
+
|
75
|
+
def call_loop(command, timeout = 0, &block)
|
76
|
+
node = assign_node(command)
|
77
|
+
try_send(node, :call_loop, command, timeout, &block)
|
78
|
+
end
|
79
|
+
|
80
|
+
def call_pipeline(pipeline)
|
81
|
+
node_keys, command_keys = extract_keys_in_pipeline(pipeline)
|
82
|
+
raise CrossSlotPipeliningError, command_keys if node_keys.size > 1
|
83
|
+
node = find_node(node_keys.first)
|
84
|
+
try_send(node, :call_pipeline, pipeline)
|
85
|
+
end
|
86
|
+
|
87
|
+
def call_with_timeout(command, timeout, &block)
|
88
|
+
node = assign_node(command)
|
89
|
+
try_send(node, :call_with_timeout, command, timeout, &block)
|
90
|
+
end
|
91
|
+
|
92
|
+
def call_without_timeout(command, &block)
|
93
|
+
call_with_timeout(command, 0, &block)
|
94
|
+
end
|
95
|
+
|
96
|
+
def process(commands, &block)
|
97
|
+
if commands.size == 1 &&
|
98
|
+
%w[unsubscribe punsubscribe].include?(commands.first.first.to_s.downcase) &&
|
99
|
+
commands.first.size == 1
|
100
|
+
|
101
|
+
# Node is indeterminate. We do just a best-effort try here.
|
102
|
+
@node.process_all(commands, &block)
|
103
|
+
else
|
104
|
+
node = assign_node(commands.first)
|
105
|
+
try_send(node, :process, commands, &block)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
private
|
110
|
+
|
111
|
+
def fetch_cluster_info!(option)
|
112
|
+
node = Node.new(option.per_node_key)
|
113
|
+
available_slots = SlotLoader.load(node)
|
114
|
+
node_flags = NodeLoader.load_flags(node)
|
115
|
+
option.update_node(available_slots.keys.map { |k| NodeKey.optionize(k) })
|
116
|
+
[Node.new(option.per_node_key, node_flags, option.use_replica?),
|
117
|
+
Slot.new(available_slots, node_flags, option.use_replica?)]
|
118
|
+
ensure
|
119
|
+
node&.each(&:disconnect)
|
120
|
+
end
|
121
|
+
|
122
|
+
def fetch_command_details(nodes)
|
123
|
+
details = CommandLoader.load(nodes)
|
124
|
+
Command.new(details)
|
125
|
+
end
|
126
|
+
|
127
|
+
def send_command(command, &block)
|
128
|
+
cmd = command.first.to_s.downcase
|
129
|
+
case cmd
|
130
|
+
when 'auth', 'bgrewriteaof', 'bgsave', 'quit', 'save'
|
131
|
+
@node.call_all(command, &block).first
|
132
|
+
when 'flushall', 'flushdb'
|
133
|
+
@node.call_master(command, &block).first
|
134
|
+
when 'wait' then @node.call_master(command, &block).reduce(:+)
|
135
|
+
when 'keys' then @node.call_slave(command, &block).flatten.sort
|
136
|
+
when 'dbsize' then @node.call_slave(command, &block).reduce(:+)
|
137
|
+
when 'lastsave' then @node.call_all(command, &block).sort
|
138
|
+
when 'role' then @node.call_all(command, &block)
|
139
|
+
when 'config' then send_config_command(command, &block)
|
140
|
+
when 'client' then send_client_command(command, &block)
|
141
|
+
when 'cluster' then send_cluster_command(command, &block)
|
142
|
+
when 'readonly', 'readwrite', 'shutdown'
|
143
|
+
raise OrchestrationCommandNotSupported, cmd
|
144
|
+
when 'memory' then send_memory_command(command, &block)
|
145
|
+
when 'script' then send_script_command(command, &block)
|
146
|
+
when 'pubsub' then send_pubsub_command(command, &block)
|
147
|
+
when 'discard', 'exec', 'multi', 'unwatch'
|
148
|
+
raise AmbiguousNodeError, cmd
|
149
|
+
else
|
150
|
+
node = assign_node(command)
|
151
|
+
try_send(node, :call, command, &block)
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
def send_config_command(command, &block)
|
156
|
+
case command[1].to_s.downcase
|
157
|
+
when 'resetstat', 'rewrite', 'set'
|
158
|
+
@node.call_all(command, &block).first
|
159
|
+
else assign_node(command).call(command, &block)
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
def send_memory_command(command, &block)
|
164
|
+
case command[1].to_s.downcase
|
165
|
+
when 'stats' then @node.call_all(command, &block)
|
166
|
+
when 'purge' then @node.call_all(command, &block).first
|
167
|
+
else assign_node(command).call(command, &block)
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
def send_client_command(command, &block)
|
172
|
+
case command[1].to_s.downcase
|
173
|
+
when 'list' then @node.call_all(command, &block).flatten
|
174
|
+
when 'pause', 'reply', 'setname'
|
175
|
+
@node.call_all(command, &block).first
|
176
|
+
else assign_node(command).call(command, &block)
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
def send_cluster_command(command, &block)
|
181
|
+
subcommand = command[1].to_s.downcase
|
182
|
+
case subcommand
|
183
|
+
when 'addslots', 'delslots', 'failover', 'forget', 'meet', 'replicate',
|
184
|
+
'reset', 'set-config-epoch', 'setslot'
|
185
|
+
raise OrchestrationCommandNotSupported, 'cluster', subcommand
|
186
|
+
when 'saveconfig' then @node.call_all(command, &block).first
|
187
|
+
else assign_node(command).call(command, &block)
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
def send_script_command(command, &block)
|
192
|
+
case command[1].to_s.downcase
|
193
|
+
when 'debug', 'kill'
|
194
|
+
@node.call_all(command, &block).first
|
195
|
+
when 'flush', 'load'
|
196
|
+
@node.call_master(command, &block).first
|
197
|
+
else assign_node(command).call(command, &block)
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
def send_pubsub_command(command, &block)
|
202
|
+
case command[1].to_s.downcase
|
203
|
+
when 'channels' then @node.call_all(command, &block).flatten.uniq.sort
|
204
|
+
when 'numsub'
|
205
|
+
@node.call_all(command, &block).reject(&:empty?).map { |e| Hash[*e] }
|
206
|
+
.reduce({}) { |a, e| a.merge(e) { |_, v1, v2| v1 + v2 } }
|
207
|
+
when 'numpat' then @node.call_all(command, &block).reduce(:+)
|
208
|
+
else assign_node(command).call(command, &block)
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
# @see https://redis.io/topics/cluster-spec#redirection-and-resharding
|
213
|
+
# Redirection and resharding
|
214
|
+
def try_send(node, method_name, *args, retry_count: 3, &block)
|
215
|
+
node.public_send(method_name, *args, &block)
|
216
|
+
rescue CommandError => err
|
217
|
+
if err.message.start_with?('MOVED')
|
218
|
+
raise if retry_count <= 0
|
219
|
+
node = assign_redirection_node(err.message)
|
220
|
+
retry_count -= 1
|
221
|
+
retry
|
222
|
+
elsif err.message.start_with?('ASK')
|
223
|
+
raise if retry_count <= 0
|
224
|
+
node = assign_asking_node(err.message)
|
225
|
+
node.call(%i[asking])
|
226
|
+
retry_count -= 1
|
227
|
+
retry
|
228
|
+
else
|
229
|
+
raise
|
230
|
+
end
|
231
|
+
rescue CannotConnectError
|
232
|
+
update_cluster_info!
|
233
|
+
raise
|
234
|
+
end
|
235
|
+
|
236
|
+
def assign_redirection_node(err_msg)
|
237
|
+
_, slot, node_key = err_msg.split(' ')
|
238
|
+
slot = slot.to_i
|
239
|
+
@slot.put(slot, node_key)
|
240
|
+
find_node(node_key)
|
241
|
+
end
|
242
|
+
|
243
|
+
def assign_asking_node(err_msg)
|
244
|
+
_, _, node_key = err_msg.split(' ')
|
245
|
+
find_node(node_key)
|
246
|
+
end
|
247
|
+
|
248
|
+
def assign_node(command)
|
249
|
+
node_key = find_node_key(command)
|
250
|
+
find_node(node_key)
|
251
|
+
end
|
252
|
+
|
253
|
+
def find_node_key(command)
|
254
|
+
key = @command.extract_first_key(command)
|
255
|
+
return if key.empty?
|
256
|
+
|
257
|
+
slot = KeySlotConverter.convert(key)
|
258
|
+
return unless @slot.exists?(slot)
|
259
|
+
|
260
|
+
if @command.should_send_to_master?(command)
|
261
|
+
@slot.find_node_key_of_master(slot)
|
262
|
+
else
|
263
|
+
@slot.find_node_key_of_slave(slot)
|
264
|
+
end
|
265
|
+
end
|
266
|
+
|
267
|
+
def find_node(node_key)
|
268
|
+
return @node.sample if node_key.nil?
|
269
|
+
@node.find_by(node_key)
|
270
|
+
rescue Node::ReloadNeeded
|
271
|
+
update_cluster_info!(node_key)
|
272
|
+
@node.find_by(node_key)
|
273
|
+
end
|
274
|
+
|
275
|
+
def update_cluster_info!(node_key = nil)
|
276
|
+
unless node_key.nil?
|
277
|
+
host, port = NodeKey.split(node_key)
|
278
|
+
@option.add_node(host, port)
|
279
|
+
end
|
280
|
+
|
281
|
+
@node.map(&:disconnect)
|
282
|
+
@node, @slot = fetch_cluster_info!(@option)
|
283
|
+
end
|
284
|
+
|
285
|
+
def extract_keys_in_pipeline(pipeline)
|
286
|
+
node_keys = pipeline.commands.map { |cmd| find_node_key(cmd) }.compact.uniq
|
287
|
+
command_keys = pipeline.commands.map { |cmd| @command.extract_first_key(cmd) }.reject(&:empty?)
|
288
|
+
[node_keys, command_keys]
|
289
|
+
end
|
290
|
+
end
|
291
|
+
end
|