redis 3.3.5 → 4.1.0
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/CHANGELOG.md +54 -2
- data/README.md +77 -76
- data/lib/redis.rb +779 -63
- data/lib/redis/client.rb +41 -20
- data/lib/redis/cluster.rb +286 -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 +35 -0
- data/lib/redis/cluster/node_loader.rb +37 -0
- data/lib/redis/cluster/option.rb +77 -0
- data/lib/redis/cluster/slot.rb +69 -0
- data/lib/redis/cluster/slot_loader.rb +49 -0
- data/lib/redis/connection.rb +2 -2
- data/lib/redis/connection/command_helper.rb +2 -8
- data/lib/redis/connection/hiredis.rb +2 -2
- data/lib/redis/connection/ruby.rb +13 -30
- data/lib/redis/connection/synchrony.rb +12 -4
- data/lib/redis/distributed.rb +32 -12
- data/lib/redis/errors.rb +46 -0
- data/lib/redis/hash_ring.rb +20 -64
- data/lib/redis/pipeline.rb +9 -7
- data/lib/redis/version.rb +1 -1
- metadata +53 -196
- 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/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/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,4 @@
|
|
1
|
-
|
1
|
+
require_relative "errors"
|
2
2
|
require "socket"
|
3
3
|
require "cgi"
|
4
4
|
|
@@ -18,12 +18,12 @@ class Redis
|
|
18
18
|
:id => nil,
|
19
19
|
:tcp_keepalive => 0,
|
20
20
|
:reconnect_attempts => 1,
|
21
|
+
:reconnect_delay => 0,
|
22
|
+
:reconnect_delay_max => 0.5,
|
21
23
|
:inherit_socket => false
|
22
24
|
}
|
23
25
|
|
24
|
-
|
25
|
-
Marshal.load(Marshal.dump(@options))
|
26
|
-
end
|
26
|
+
attr_reader :options
|
27
27
|
|
28
28
|
def scheme
|
29
29
|
@options[:scheme]
|
@@ -86,11 +86,14 @@ class Redis
|
|
86
86
|
|
87
87
|
@pending_reads = 0
|
88
88
|
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
89
|
+
@connector =
|
90
|
+
if options.include?(:sentinels)
|
91
|
+
Connector::Sentinel.new(@options)
|
92
|
+
elsif options.include?(:connector) && options[:connector].respond_to?(:new)
|
93
|
+
options.delete(:connector).new(@options)
|
94
|
+
else
|
95
|
+
Connector.new(@options)
|
96
|
+
end
|
94
97
|
end
|
95
98
|
|
96
99
|
def connect
|
@@ -152,9 +155,12 @@ class Redis
|
|
152
155
|
end
|
153
156
|
|
154
157
|
def call_pipeline(pipeline)
|
158
|
+
commands = pipeline.commands
|
159
|
+
return [] if commands.empty?
|
160
|
+
|
155
161
|
with_reconnect pipeline.with_reconnect? do
|
156
162
|
begin
|
157
|
-
pipeline.finish(call_pipelined(
|
163
|
+
pipeline.finish(call_pipelined(commands)).tap do
|
158
164
|
self.db = pipeline.db if pipeline.db
|
159
165
|
end
|
160
166
|
rescue ConnectionError => e
|
@@ -185,13 +191,10 @@ class Redis
|
|
185
191
|
exception = nil
|
186
192
|
|
187
193
|
process(commands) do
|
188
|
-
|
189
|
-
|
190
|
-
@reconnect = false
|
191
|
-
|
192
|
-
(commands.size - 1).times do |i|
|
194
|
+
commands.size.times do |i|
|
193
195
|
reply = read
|
194
|
-
result[i
|
196
|
+
result[i] = reply
|
197
|
+
@reconnect = false
|
195
198
|
exception = reply if exception.nil? && reply.is_a?(CommandError)
|
196
199
|
end
|
197
200
|
end
|
@@ -274,12 +277,15 @@ class Redis
|
|
274
277
|
|
275
278
|
def with_socket_timeout(timeout)
|
276
279
|
connect unless connected?
|
280
|
+
original = @options[:read_timeout]
|
277
281
|
|
278
282
|
begin
|
279
283
|
connection.timeout = timeout
|
284
|
+
@options[:read_timeout] = timeout # for reconnection
|
280
285
|
yield
|
281
286
|
ensure
|
282
287
|
connection.timeout = self.timeout if connected?
|
288
|
+
@options[:read_timeout] = original
|
283
289
|
end
|
284
290
|
end
|
285
291
|
|
@@ -336,10 +342,12 @@ class Redis
|
|
336
342
|
@connection = @options[:driver].connect(@options)
|
337
343
|
@pending_reads = 0
|
338
344
|
rescue TimeoutError,
|
345
|
+
SocketError,
|
339
346
|
Errno::ECONNREFUSED,
|
340
347
|
Errno::EHOSTDOWN,
|
341
348
|
Errno::EHOSTUNREACH,
|
342
349
|
Errno::ENETUNREACH,
|
350
|
+
Errno::ENOENT,
|
343
351
|
Errno::ETIMEDOUT
|
344
352
|
|
345
353
|
raise CannotConnectError, "Error connecting to Redis on #{location} (#{$!.class})"
|
@@ -369,6 +377,10 @@ class Redis
|
|
369
377
|
disconnect
|
370
378
|
|
371
379
|
if attempts <= @options[:reconnect_attempts] && @reconnect
|
380
|
+
sleep_t = [(@options[:reconnect_delay] * 2**(attempts-1)),
|
381
|
+
@options[:reconnect_delay_max]].min
|
382
|
+
|
383
|
+
Kernel.sleep(sleep_t)
|
372
384
|
retry
|
373
385
|
else
|
374
386
|
raise
|
@@ -445,6 +457,10 @@ class Redis
|
|
445
457
|
options[:read_timeout] = Float(options[:read_timeout])
|
446
458
|
options[:write_timeout] = Float(options[:write_timeout])
|
447
459
|
|
460
|
+
options[:reconnect_attempts] = options[:reconnect_attempts].to_i
|
461
|
+
options[:reconnect_delay] = options[:reconnect_delay].to_f
|
462
|
+
options[:reconnect_delay_max] = options[:reconnect_delay_max].to_f
|
463
|
+
|
448
464
|
options[:db] = options[:db].to_i
|
449
465
|
options[:driver] = _parse_driver(options[:driver]) || Connection.drivers.last
|
450
466
|
|
@@ -478,11 +494,16 @@ class Redis
|
|
478
494
|
|
479
495
|
if driver.kind_of?(String)
|
480
496
|
begin
|
481
|
-
|
482
|
-
|
483
|
-
|
484
|
-
|
497
|
+
require_relative "connection/#{driver}"
|
498
|
+
rescue LoadError, NameError => e
|
499
|
+
begin
|
500
|
+
require "connection/#{driver}"
|
501
|
+
rescue LoadError, NameError => e
|
502
|
+
raise RuntimeError, "Cannot load driver #{driver.inspect}: #{e.message}"
|
503
|
+
end
|
485
504
|
end
|
505
|
+
|
506
|
+
driver = Connection.const_get(driver.capitalize)
|
486
507
|
end
|
487
508
|
|
488
509
|
driver
|
@@ -0,0 +1,286 @@
|
|
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
|
+
available_node_urls = NodeKey.to_node_urls(available_slots.keys, secure: option.secure?)
|
116
|
+
option.update_node(available_node_urls)
|
117
|
+
[Node.new(option.per_node_key, node_flags, option.use_replica?),
|
118
|
+
Slot.new(available_slots, node_flags, option.use_replica?)]
|
119
|
+
ensure
|
120
|
+
node.map(&:disconnect)
|
121
|
+
end
|
122
|
+
|
123
|
+
def fetch_command_details(nodes)
|
124
|
+
details = CommandLoader.load(nodes)
|
125
|
+
Command.new(details)
|
126
|
+
end
|
127
|
+
|
128
|
+
def send_command(command, &block)
|
129
|
+
cmd = command.first.to_s.downcase
|
130
|
+
case cmd
|
131
|
+
when 'auth', 'bgrewriteaof', 'bgsave', 'quit', 'save'
|
132
|
+
@node.call_all(command, &block).first
|
133
|
+
when 'flushall', 'flushdb'
|
134
|
+
@node.call_master(command, &block).first
|
135
|
+
when 'wait' then @node.call_master(command, &block).reduce(:+)
|
136
|
+
when 'keys' then @node.call_slave(command, &block).flatten.sort
|
137
|
+
when 'dbsize' then @node.call_slave(command, &block).reduce(:+)
|
138
|
+
when 'lastsave' then @node.call_all(command, &block).sort
|
139
|
+
when 'role' then @node.call_all(command, &block)
|
140
|
+
when 'config' then send_config_command(command, &block)
|
141
|
+
when 'client' then send_client_command(command, &block)
|
142
|
+
when 'cluster' then send_cluster_command(command, &block)
|
143
|
+
when 'readonly', 'readwrite', 'shutdown'
|
144
|
+
raise OrchestrationCommandNotSupported, cmd
|
145
|
+
when 'memory' then send_memory_command(command, &block)
|
146
|
+
when 'script' then send_script_command(command, &block)
|
147
|
+
when 'pubsub' then send_pubsub_command(command, &block)
|
148
|
+
when 'discard', 'exec', 'multi', 'unwatch'
|
149
|
+
raise AmbiguousNodeError, cmd
|
150
|
+
else
|
151
|
+
node = assign_node(command)
|
152
|
+
try_send(node, :call, command, &block)
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
def send_config_command(command, &block)
|
157
|
+
case command[1].to_s.downcase
|
158
|
+
when 'resetstat', 'rewrite', 'set'
|
159
|
+
@node.call_all(command, &block).first
|
160
|
+
else assign_node(command).call(command, &block)
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
def send_memory_command(command, &block)
|
165
|
+
case command[1].to_s.downcase
|
166
|
+
when 'stats' then @node.call_all(command, &block)
|
167
|
+
when 'purge' then @node.call_all(command, &block).first
|
168
|
+
else assign_node(command).call(command, &block)
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
def send_client_command(command, &block)
|
173
|
+
case command[1].to_s.downcase
|
174
|
+
when 'list' then @node.call_all(command, &block).flatten
|
175
|
+
when 'pause', 'reply', 'setname'
|
176
|
+
@node.call_all(command, &block).first
|
177
|
+
else assign_node(command).call(command, &block)
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
def send_cluster_command(command, &block)
|
182
|
+
subcommand = command[1].to_s.downcase
|
183
|
+
case subcommand
|
184
|
+
when 'addslots', 'delslots', 'failover', 'forget', 'meet', 'replicate',
|
185
|
+
'reset', 'set-config-epoch', 'setslot'
|
186
|
+
raise OrchestrationCommandNotSupported, 'cluster', subcommand
|
187
|
+
when 'saveconfig' then @node.call_all(command, &block).first
|
188
|
+
else assign_node(command).call(command, &block)
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
def send_script_command(command, &block)
|
193
|
+
case command[1].to_s.downcase
|
194
|
+
when 'debug', 'kill'
|
195
|
+
@node.call_all(command, &block).first
|
196
|
+
when 'flush', 'load'
|
197
|
+
@node.call_master(command, &block).first
|
198
|
+
else assign_node(command).call(command, &block)
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
def send_pubsub_command(command, &block)
|
203
|
+
case command[1].to_s.downcase
|
204
|
+
when 'channels' then @node.call_all(command, &block).flatten.uniq.sort
|
205
|
+
when 'numsub'
|
206
|
+
@node.call_all(command, &block).reject(&:empty?).map { |e| Hash[*e] }
|
207
|
+
.reduce({}) { |a, e| a.merge(e) { |_, v1, v2| v1 + v2 } }
|
208
|
+
when 'numpat' then @node.call_all(command, &block).reduce(:+)
|
209
|
+
else assign_node(command).call(command, &block)
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
# @see https://redis.io/topics/cluster-spec#redirection-and-resharding
|
214
|
+
# Redirection and resharding
|
215
|
+
def try_send(node, method_name, *args, retry_count: 3, &block)
|
216
|
+
node.public_send(method_name, *args, &block)
|
217
|
+
rescue CommandError => err
|
218
|
+
if err.message.start_with?('MOVED')
|
219
|
+
assign_redirection_node(err.message).public_send(method_name, *args, &block)
|
220
|
+
elsif err.message.start_with?('ASK')
|
221
|
+
raise if retry_count <= 0
|
222
|
+
node = assign_asking_node(err.message)
|
223
|
+
node.call(%i[asking])
|
224
|
+
retry_count -= 1
|
225
|
+
retry
|
226
|
+
else
|
227
|
+
raise
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
def assign_redirection_node(err_msg)
|
232
|
+
_, slot, node_key = err_msg.split(' ')
|
233
|
+
slot = slot.to_i
|
234
|
+
@slot.put(slot, node_key)
|
235
|
+
find_node(node_key)
|
236
|
+
end
|
237
|
+
|
238
|
+
def assign_asking_node(err_msg)
|
239
|
+
_, _, node_key = err_msg.split(' ')
|
240
|
+
find_node(node_key)
|
241
|
+
end
|
242
|
+
|
243
|
+
def assign_node(command)
|
244
|
+
node_key = find_node_key(command)
|
245
|
+
find_node(node_key)
|
246
|
+
end
|
247
|
+
|
248
|
+
def find_node_key(command)
|
249
|
+
key = @command.extract_first_key(command)
|
250
|
+
return if key.empty?
|
251
|
+
|
252
|
+
slot = KeySlotConverter.convert(key)
|
253
|
+
return unless @slot.exists?(slot)
|
254
|
+
|
255
|
+
if @command.should_send_to_master?(command)
|
256
|
+
@slot.find_node_key_of_master(slot)
|
257
|
+
else
|
258
|
+
@slot.find_node_key_of_slave(slot)
|
259
|
+
end
|
260
|
+
end
|
261
|
+
|
262
|
+
def find_node(node_key)
|
263
|
+
return @node.sample if node_key.nil?
|
264
|
+
@node.find_by(node_key)
|
265
|
+
rescue Node::ReloadNeeded
|
266
|
+
update_cluster_info!(node_key)
|
267
|
+
@node.find_by(node_key)
|
268
|
+
end
|
269
|
+
|
270
|
+
def update_cluster_info!(node_key = nil)
|
271
|
+
unless node_key.nil?
|
272
|
+
host, port = NodeKey.split(node_key)
|
273
|
+
@option.add_node(host, port)
|
274
|
+
end
|
275
|
+
|
276
|
+
@node.map(&:disconnect)
|
277
|
+
@node, @slot = fetch_cluster_info!(@option)
|
278
|
+
end
|
279
|
+
|
280
|
+
def extract_keys_in_pipeline(pipeline)
|
281
|
+
node_keys = pipeline.commands.map { |cmd| find_node_key(cmd) }.compact.uniq
|
282
|
+
command_keys = pipeline.commands.map { |cmd| @command.extract_first_key(cmd) }.reject(&:empty?)
|
283
|
+
[node_keys, command_keys]
|
284
|
+
end
|
285
|
+
end
|
286
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../errors'
|
4
|
+
|
5
|
+
class Redis
|
6
|
+
class Cluster
|
7
|
+
# Keep details about Redis commands for Redis Cluster Client.
|
8
|
+
# @see https://redis.io/commands/command
|
9
|
+
class Command
|
10
|
+
def initialize(details)
|
11
|
+
@details = pick_details(details)
|
12
|
+
end
|
13
|
+
|
14
|
+
def extract_first_key(command)
|
15
|
+
i = determine_first_key_position(command)
|
16
|
+
return '' if i == 0
|
17
|
+
|
18
|
+
key = command[i].to_s
|
19
|
+
hash_tag = extract_hash_tag(key)
|
20
|
+
hash_tag.empty? ? key : hash_tag
|
21
|
+
end
|
22
|
+
|
23
|
+
def should_send_to_master?(command)
|
24
|
+
dig_details(command, :write)
|
25
|
+
end
|
26
|
+
|
27
|
+
def should_send_to_slave?(command)
|
28
|
+
dig_details(command, :readonly)
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def pick_details(details)
|
34
|
+
details.map do |command, detail|
|
35
|
+
[command, {
|
36
|
+
first_key_position: detail[:first],
|
37
|
+
write: detail[:flags].include?('write'),
|
38
|
+
readonly: detail[:flags].include?('readonly')
|
39
|
+
}]
|
40
|
+
end.to_h
|
41
|
+
end
|
42
|
+
|
43
|
+
def dig_details(command, key)
|
44
|
+
name = command.first.to_s
|
45
|
+
return unless @details.key?(name)
|
46
|
+
|
47
|
+
@details.fetch(name).fetch(key)
|
48
|
+
end
|
49
|
+
|
50
|
+
def determine_first_key_position(command)
|
51
|
+
case command.first.to_s.downcase
|
52
|
+
when 'eval', 'evalsha', 'migrate', 'zinterstore', 'zunionstore' then 3
|
53
|
+
when 'object' then 2
|
54
|
+
when 'memory'
|
55
|
+
command[1].to_s.casecmp('usage').zero? ? 2 : 0
|
56
|
+
when 'scan', 'sscan', 'hscan', 'zscan'
|
57
|
+
determine_optional_key_position(command, 'match')
|
58
|
+
when 'xread', 'xreadgroup'
|
59
|
+
determine_optional_key_position(command, 'streams')
|
60
|
+
else
|
61
|
+
dig_details(command, :first_key_position).to_i
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def determine_optional_key_position(command, option_name)
|
66
|
+
idx = command.map(&:to_s).map(&:downcase).index(option_name)
|
67
|
+
idx.nil? ? 0 : idx + 1
|
68
|
+
end
|
69
|
+
|
70
|
+
# @see https://redis.io/topics/cluster-spec#keys-hash-tags Keys hash tags
|
71
|
+
def extract_hash_tag(key)
|
72
|
+
s = key.index('{')
|
73
|
+
e = key.index('}', s.to_i + 1)
|
74
|
+
|
75
|
+
return '' if s.nil? || e.nil?
|
76
|
+
|
77
|
+
key[s + 1..e - 1]
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|