redis 3.3.5 → 4.5.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 +5 -5
- data/CHANGELOG.md +161 -2
- data/README.md +144 -79
- data/lib/redis/client.rb +166 -90
- data/lib/redis/cluster/command.rb +81 -0
- data/lib/redis/cluster/command_loader.rb +33 -0
- data/lib/redis/cluster/key_slot_converter.rb +72 -0
- data/lib/redis/cluster/node.rb +108 -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 +93 -0
- data/lib/redis/cluster/slot.rb +86 -0
- data/lib/redis/cluster/slot_loader.rb +49 -0
- data/lib/redis/cluster.rb +291 -0
- data/lib/redis/connection/command_helper.rb +7 -10
- data/lib/redis/connection/hiredis.rb +6 -5
- data/lib/redis/connection/registry.rb +2 -1
- data/lib/redis/connection/ruby.rb +128 -129
- data/lib/redis/connection/synchrony.rb +21 -8
- data/lib/redis/connection.rb +4 -2
- data/lib/redis/distributed.rb +194 -72
- data/lib/redis/errors.rb +48 -0
- data/lib/redis/hash_ring.rb +30 -73
- data/lib/redis/pipeline.rb +55 -15
- data/lib/redis/subscribe.rb +11 -12
- data/lib/redis/version.rb +3 -1
- data/lib/redis.rb +1451 -403
- metadata +49 -202
- data/.gitignore +0 -16
- data/.travis/Gemfile +0 -11
- data/.travis.yml +0 -89
- 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/sentinel.conf +0 -9
- data/examples/sentinel/start +0 -49
- data/examples/sentinel.rb +0 -41
- 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
@@ -0,0 +1,108 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../errors'
|
4
|
+
|
5
|
+
class Redis
|
6
|
+
class Cluster
|
7
|
+
# Keep client list of node for Redis Cluster Client
|
8
|
+
class Node
|
9
|
+
include Enumerable
|
10
|
+
|
11
|
+
ReloadNeeded = Class.new(StandardError)
|
12
|
+
|
13
|
+
ROLE_SLAVE = 'slave'
|
14
|
+
|
15
|
+
def initialize(options, node_flags = {}, with_replica = false)
|
16
|
+
@with_replica = with_replica
|
17
|
+
@node_flags = node_flags
|
18
|
+
@clients = build_clients(options)
|
19
|
+
end
|
20
|
+
|
21
|
+
def each(&block)
|
22
|
+
@clients.values.each(&block)
|
23
|
+
end
|
24
|
+
|
25
|
+
def sample
|
26
|
+
@clients.values.sample
|
27
|
+
end
|
28
|
+
|
29
|
+
def find_by(node_key)
|
30
|
+
@clients.fetch(node_key)
|
31
|
+
rescue KeyError
|
32
|
+
raise ReloadNeeded
|
33
|
+
end
|
34
|
+
|
35
|
+
def call_all(command, &block)
|
36
|
+
try_map { |_, client| client.call(command, &block) }.values
|
37
|
+
end
|
38
|
+
|
39
|
+
def call_master(command, &block)
|
40
|
+
try_map do |node_key, client|
|
41
|
+
next if slave?(node_key)
|
42
|
+
|
43
|
+
client.call(command, &block)
|
44
|
+
end.values
|
45
|
+
end
|
46
|
+
|
47
|
+
def call_slave(command, &block)
|
48
|
+
return call_master(command, &block) if replica_disabled?
|
49
|
+
|
50
|
+
try_map do |node_key, client|
|
51
|
+
next if master?(node_key)
|
52
|
+
|
53
|
+
client.call(command, &block)
|
54
|
+
end.values
|
55
|
+
end
|
56
|
+
|
57
|
+
def process_all(commands, &block)
|
58
|
+
try_map { |_, client| client.process(commands, &block) }.values
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
def replica_disabled?
|
64
|
+
!@with_replica
|
65
|
+
end
|
66
|
+
|
67
|
+
def master?(node_key)
|
68
|
+
!slave?(node_key)
|
69
|
+
end
|
70
|
+
|
71
|
+
def slave?(node_key)
|
72
|
+
@node_flags[node_key] == ROLE_SLAVE
|
73
|
+
end
|
74
|
+
|
75
|
+
def build_clients(options)
|
76
|
+
clients = options.map do |node_key, option|
|
77
|
+
next if replica_disabled? && slave?(node_key)
|
78
|
+
|
79
|
+
option = option.merge(readonly: true) if slave?(node_key)
|
80
|
+
|
81
|
+
client = Client.new(option)
|
82
|
+
[node_key, client]
|
83
|
+
end
|
84
|
+
|
85
|
+
clients.compact.to_h
|
86
|
+
end
|
87
|
+
|
88
|
+
def try_map
|
89
|
+
errors = {}
|
90
|
+
results = {}
|
91
|
+
|
92
|
+
@clients.each do |node_key, client|
|
93
|
+
begin
|
94
|
+
reply = yield(node_key, client)
|
95
|
+
results[node_key] = reply unless reply.nil?
|
96
|
+
rescue CommandError => err
|
97
|
+
errors[node_key] = err
|
98
|
+
next
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
return results if errors.empty?
|
103
|
+
|
104
|
+
raise CommandErrorCollection, errors
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Redis
|
4
|
+
class Cluster
|
5
|
+
# Node key's format is `<ip>:<port>`.
|
6
|
+
# It is different from node id.
|
7
|
+
# Node id is internal identifying code in Redis Cluster.
|
8
|
+
module NodeKey
|
9
|
+
DELIMITER = ':'
|
10
|
+
|
11
|
+
module_function
|
12
|
+
|
13
|
+
def optionize(node_key)
|
14
|
+
host, port = split(node_key)
|
15
|
+
{ host: host, port: port }
|
16
|
+
end
|
17
|
+
|
18
|
+
def split(node_key)
|
19
|
+
node_key.split(DELIMITER)
|
20
|
+
end
|
21
|
+
|
22
|
+
def build_from_uri(uri)
|
23
|
+
"#{uri.host}#{DELIMITER}#{uri.port}"
|
24
|
+
end
|
25
|
+
|
26
|
+
def build_from_host_port(host, port)
|
27
|
+
"#{host}#{DELIMITER}#{port}"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../errors'
|
4
|
+
|
5
|
+
class Redis
|
6
|
+
class Cluster
|
7
|
+
# Load and hashify node info for Redis Cluster Client
|
8
|
+
module NodeLoader
|
9
|
+
module_function
|
10
|
+
|
11
|
+
def load_flags(nodes)
|
12
|
+
info = {}
|
13
|
+
|
14
|
+
nodes.each do |node|
|
15
|
+
info = fetch_node_info(node)
|
16
|
+
info.empty? ? next : break
|
17
|
+
end
|
18
|
+
|
19
|
+
return info unless info.empty?
|
20
|
+
|
21
|
+
raise CannotConnectError, 'Redis client could not connect to any cluster nodes'
|
22
|
+
end
|
23
|
+
|
24
|
+
def fetch_node_info(node)
|
25
|
+
node.call(%i[cluster nodes])
|
26
|
+
.split("\n")
|
27
|
+
.map { |str| str.split(' ') }
|
28
|
+
.map { |arr| [arr[1].split('@').first, (arr[2].split(',') & %w[master slave]).first] }
|
29
|
+
.to_h
|
30
|
+
rescue CannotConnectError, ConnectionError, CommandError
|
31
|
+
{} # can retry on another node
|
32
|
+
end
|
33
|
+
|
34
|
+
private_class_method :fetch_node_info
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../errors'
|
4
|
+
require_relative 'node_key'
|
5
|
+
require 'uri'
|
6
|
+
|
7
|
+
class Redis
|
8
|
+
class Cluster
|
9
|
+
# Keep options for Redis Cluster Client
|
10
|
+
class Option
|
11
|
+
DEFAULT_SCHEME = 'redis'
|
12
|
+
SECURE_SCHEME = 'rediss'
|
13
|
+
VALID_SCHEMES = [DEFAULT_SCHEME, SECURE_SCHEME].freeze
|
14
|
+
|
15
|
+
def initialize(options)
|
16
|
+
options = options.dup
|
17
|
+
node_addrs = options.delete(:cluster)
|
18
|
+
@node_opts = build_node_options(node_addrs)
|
19
|
+
@replica = options.delete(:replica) == true
|
20
|
+
add_common_node_option_if_needed(options, @node_opts, :scheme)
|
21
|
+
add_common_node_option_if_needed(options, @node_opts, :username)
|
22
|
+
add_common_node_option_if_needed(options, @node_opts, :password)
|
23
|
+
@options = options
|
24
|
+
end
|
25
|
+
|
26
|
+
def per_node_key
|
27
|
+
@node_opts.map { |opt| [NodeKey.build_from_host_port(opt[:host], opt[:port]), @options.merge(opt)] }
|
28
|
+
.to_h
|
29
|
+
end
|
30
|
+
|
31
|
+
def use_replica?
|
32
|
+
@replica
|
33
|
+
end
|
34
|
+
|
35
|
+
def update_node(addrs)
|
36
|
+
@node_opts = build_node_options(addrs)
|
37
|
+
end
|
38
|
+
|
39
|
+
def add_node(host, port)
|
40
|
+
@node_opts << { host: host, port: port }
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
def build_node_options(addrs)
|
46
|
+
raise InvalidClientOptionError, 'Redis option of `cluster` must be an Array' unless addrs.is_a?(Array)
|
47
|
+
|
48
|
+
addrs.map { |addr| parse_node_addr(addr) }
|
49
|
+
end
|
50
|
+
|
51
|
+
def parse_node_addr(addr)
|
52
|
+
case addr
|
53
|
+
when String
|
54
|
+
parse_node_url(addr)
|
55
|
+
when Hash
|
56
|
+
parse_node_option(addr)
|
57
|
+
else
|
58
|
+
raise InvalidClientOptionError, 'Redis option of `cluster` must includes String or Hash'
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def parse_node_url(addr)
|
63
|
+
uri = URI(addr)
|
64
|
+
raise InvalidClientOptionError, "Invalid uri scheme #{addr}" unless VALID_SCHEMES.include?(uri.scheme)
|
65
|
+
|
66
|
+
db = uri.path.split('/')[1]&.to_i
|
67
|
+
|
68
|
+
{ scheme: uri.scheme, username: uri.user, password: uri.password, host: uri.host, port: uri.port, db: db }
|
69
|
+
.reject { |_, v| v.nil? || v == '' }
|
70
|
+
rescue URI::InvalidURIError => err
|
71
|
+
raise InvalidClientOptionError, err.message
|
72
|
+
end
|
73
|
+
|
74
|
+
def parse_node_option(addr)
|
75
|
+
addr = addr.map { |k, v| [k.to_sym, v] }.to_h
|
76
|
+
if addr.values_at(:host, :port).any?(&:nil?)
|
77
|
+
raise InvalidClientOptionError, 'Redis option of `cluster` must includes `:host` and `:port` keys'
|
78
|
+
end
|
79
|
+
|
80
|
+
addr
|
81
|
+
end
|
82
|
+
|
83
|
+
# Redis cluster node returns only host and port information.
|
84
|
+
# So we should complement additional information such as:
|
85
|
+
# scheme, username, password and so on.
|
86
|
+
def add_common_node_option_if_needed(options, node_opts, key)
|
87
|
+
return options if options[key].nil? && node_opts.first[key].nil?
|
88
|
+
|
89
|
+
options[key] ||= node_opts.first[key]
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Redis
|
4
|
+
class Cluster
|
5
|
+
# Keep slot and node key map for Redis Cluster Client
|
6
|
+
class Slot
|
7
|
+
ROLE_SLAVE = 'slave'
|
8
|
+
|
9
|
+
def initialize(available_slots, node_flags = {}, with_replica = false)
|
10
|
+
@with_replica = with_replica
|
11
|
+
@node_flags = node_flags
|
12
|
+
@map = build_slot_node_key_map(available_slots)
|
13
|
+
end
|
14
|
+
|
15
|
+
def exists?(slot)
|
16
|
+
@map.key?(slot)
|
17
|
+
end
|
18
|
+
|
19
|
+
def find_node_key_of_master(slot)
|
20
|
+
return nil unless exists?(slot)
|
21
|
+
|
22
|
+
@map[slot][:master]
|
23
|
+
end
|
24
|
+
|
25
|
+
def find_node_key_of_slave(slot)
|
26
|
+
return nil unless exists?(slot)
|
27
|
+
return find_node_key_of_master(slot) if replica_disabled?
|
28
|
+
|
29
|
+
@map[slot][:slaves].sample
|
30
|
+
end
|
31
|
+
|
32
|
+
def put(slot, node_key)
|
33
|
+
# Since we're sharing a hash for build_slot_node_key_map, duplicate it
|
34
|
+
# if it already exists instead of preserving as-is.
|
35
|
+
@map[slot] = @map[slot] ? @map[slot].dup : { master: nil, slaves: [] }
|
36
|
+
|
37
|
+
if master?(node_key)
|
38
|
+
@map[slot][:master] = node_key
|
39
|
+
elsif !@map[slot][:slaves].include?(node_key)
|
40
|
+
@map[slot][:slaves] << node_key
|
41
|
+
end
|
42
|
+
|
43
|
+
nil
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
def replica_disabled?
|
49
|
+
!@with_replica
|
50
|
+
end
|
51
|
+
|
52
|
+
def master?(node_key)
|
53
|
+
!slave?(node_key)
|
54
|
+
end
|
55
|
+
|
56
|
+
def slave?(node_key)
|
57
|
+
@node_flags[node_key] == ROLE_SLAVE
|
58
|
+
end
|
59
|
+
|
60
|
+
# available_slots is mapping of node_key to list of slot ranges
|
61
|
+
def build_slot_node_key_map(available_slots)
|
62
|
+
by_ranges = {}
|
63
|
+
available_slots.each do |node_key, slots_arr|
|
64
|
+
by_ranges[slots_arr] ||= { master: nil, slaves: [] }
|
65
|
+
|
66
|
+
if master?(node_key)
|
67
|
+
by_ranges[slots_arr][:master] = node_key
|
68
|
+
elsif !by_ranges[slots_arr][:slaves].include?(node_key)
|
69
|
+
by_ranges[slots_arr][:slaves] << node_key
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
by_slot = {}
|
74
|
+
by_ranges.each do |slots_arr, nodes|
|
75
|
+
slots_arr.each do |slots|
|
76
|
+
slots.each do |slot|
|
77
|
+
by_slot[slot] = nodes
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
by_slot
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../errors'
|
4
|
+
require_relative 'node_key'
|
5
|
+
|
6
|
+
class Redis
|
7
|
+
class Cluster
|
8
|
+
# Load and hashify slot info for Redis Cluster Client
|
9
|
+
module SlotLoader
|
10
|
+
module_function
|
11
|
+
|
12
|
+
def load(nodes)
|
13
|
+
info = {}
|
14
|
+
|
15
|
+
nodes.each do |node|
|
16
|
+
info = fetch_slot_info(node)
|
17
|
+
info.empty? ? next : break
|
18
|
+
end
|
19
|
+
|
20
|
+
return info unless info.empty?
|
21
|
+
|
22
|
+
raise CannotConnectError, 'Redis client could not connect to any cluster nodes'
|
23
|
+
end
|
24
|
+
|
25
|
+
def fetch_slot_info(node)
|
26
|
+
hash_with_default_arr = Hash.new { |h, k| h[k] = [] }
|
27
|
+
node.call(%i[cluster slots])
|
28
|
+
.flat_map { |arr| parse_slot_info(arr, default_ip: node.host) }
|
29
|
+
.each_with_object(hash_with_default_arr) { |arr, h| h[arr[0]] << arr[1] }
|
30
|
+
rescue CannotConnectError, ConnectionError, CommandError
|
31
|
+
{} # can retry on another node
|
32
|
+
end
|
33
|
+
|
34
|
+
def parse_slot_info(arr, default_ip:)
|
35
|
+
first_slot, last_slot = arr[0..1]
|
36
|
+
slot_range = (first_slot..last_slot).freeze
|
37
|
+
arr[2..-1].map { |addr| [stringify_node_key(addr, default_ip), slot_range] }
|
38
|
+
end
|
39
|
+
|
40
|
+
def stringify_node_key(arr, default_ip)
|
41
|
+
ip, port = arr
|
42
|
+
ip = default_ip if ip.empty? # When cluster is down
|
43
|
+
NodeKey.build_from_host_port(ip, port)
|
44
|
+
end
|
45
|
+
|
46
|
+
private_class_method :fetch_slot_info, :parse_slot_info, :stringify_node_key
|
47
|
+
end
|
48
|
+
end
|
49
|
+
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 = pipeline.commands.map { |cmd| find_node_key(cmd, primary_only: true) }.compact.uniq
|
82
|
+
if node_keys.size > 1
|
83
|
+
raise(CrossSlotPipeliningError,
|
84
|
+
pipeline.commands.map { |cmd| @command.extract_first_key(cmd) }.reject(&:empty?).uniq)
|
85
|
+
end
|
86
|
+
|
87
|
+
try_send(find_node(node_keys.first), :call_pipeline, pipeline)
|
88
|
+
end
|
89
|
+
|
90
|
+
def call_with_timeout(command, timeout, &block)
|
91
|
+
node = assign_node(command)
|
92
|
+
try_send(node, :call_with_timeout, command, timeout, &block)
|
93
|
+
end
|
94
|
+
|
95
|
+
def call_without_timeout(command, &block)
|
96
|
+
call_with_timeout(command, 0, &block)
|
97
|
+
end
|
98
|
+
|
99
|
+
def process(commands, &block)
|
100
|
+
if commands.size == 1 &&
|
101
|
+
%w[unsubscribe punsubscribe].include?(commands.first.first.to_s.downcase) &&
|
102
|
+
commands.first.size == 1
|
103
|
+
|
104
|
+
# Node is indeterminate. We do just a best-effort try here.
|
105
|
+
@node.process_all(commands, &block)
|
106
|
+
else
|
107
|
+
node = assign_node(commands.first)
|
108
|
+
try_send(node, :process, commands, &block)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
private
|
113
|
+
|
114
|
+
def fetch_cluster_info!(option)
|
115
|
+
node = Node.new(option.per_node_key)
|
116
|
+
available_slots = SlotLoader.load(node)
|
117
|
+
node_flags = NodeLoader.load_flags(node)
|
118
|
+
option.update_node(available_slots.keys.map { |k| NodeKey.optionize(k) })
|
119
|
+
[Node.new(option.per_node_key, node_flags, option.use_replica?),
|
120
|
+
Slot.new(available_slots, node_flags, option.use_replica?)]
|
121
|
+
ensure
|
122
|
+
node&.each(&:disconnect)
|
123
|
+
end
|
124
|
+
|
125
|
+
def fetch_command_details(nodes)
|
126
|
+
details = CommandLoader.load(nodes)
|
127
|
+
Command.new(details)
|
128
|
+
end
|
129
|
+
|
130
|
+
def send_command(command, &block)
|
131
|
+
cmd = command.first.to_s.downcase
|
132
|
+
case cmd
|
133
|
+
when 'acl', 'auth', 'bgrewriteaof', 'bgsave', 'quit', 'save'
|
134
|
+
@node.call_all(command, &block).first
|
135
|
+
when 'flushall', 'flushdb'
|
136
|
+
@node.call_master(command, &block).first
|
137
|
+
when 'wait' then @node.call_master(command, &block).reduce(:+)
|
138
|
+
when 'keys' then @node.call_slave(command, &block).flatten.sort
|
139
|
+
when 'dbsize' then @node.call_slave(command, &block).reduce(:+)
|
140
|
+
when 'lastsave' then @node.call_all(command, &block).sort
|
141
|
+
when 'role' then @node.call_all(command, &block)
|
142
|
+
when 'config' then send_config_command(command, &block)
|
143
|
+
when 'client' then send_client_command(command, &block)
|
144
|
+
when 'cluster' then send_cluster_command(command, &block)
|
145
|
+
when 'readonly', 'readwrite', 'shutdown'
|
146
|
+
raise OrchestrationCommandNotSupported, cmd
|
147
|
+
when 'memory' then send_memory_command(command, &block)
|
148
|
+
when 'script' then send_script_command(command, &block)
|
149
|
+
when 'pubsub' then send_pubsub_command(command, &block)
|
150
|
+
when 'discard', 'exec', 'multi', 'unwatch'
|
151
|
+
raise AmbiguousNodeError, cmd
|
152
|
+
else
|
153
|
+
node = assign_node(command)
|
154
|
+
try_send(node, :call, command, &block)
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
def send_config_command(command, &block)
|
159
|
+
case command[1].to_s.downcase
|
160
|
+
when 'resetstat', 'rewrite', 'set'
|
161
|
+
@node.call_all(command, &block).first
|
162
|
+
else assign_node(command).call(command, &block)
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
def send_memory_command(command, &block)
|
167
|
+
case command[1].to_s.downcase
|
168
|
+
when 'stats' then @node.call_all(command, &block)
|
169
|
+
when 'purge' then @node.call_all(command, &block).first
|
170
|
+
else assign_node(command).call(command, &block)
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
def send_client_command(command, &block)
|
175
|
+
case command[1].to_s.downcase
|
176
|
+
when 'list' then @node.call_all(command, &block).flatten
|
177
|
+
when 'pause', 'reply', 'setname'
|
178
|
+
@node.call_all(command, &block).first
|
179
|
+
else assign_node(command).call(command, &block)
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
def send_cluster_command(command, &block)
|
184
|
+
subcommand = command[1].to_s.downcase
|
185
|
+
case subcommand
|
186
|
+
when 'addslots', 'delslots', 'failover', 'forget', 'meet', 'replicate',
|
187
|
+
'reset', 'set-config-epoch', 'setslot'
|
188
|
+
raise OrchestrationCommandNotSupported, 'cluster', subcommand
|
189
|
+
when 'saveconfig' then @node.call_all(command, &block).first
|
190
|
+
else assign_node(command).call(command, &block)
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
def send_script_command(command, &block)
|
195
|
+
case command[1].to_s.downcase
|
196
|
+
when 'debug', 'kill'
|
197
|
+
@node.call_all(command, &block).first
|
198
|
+
when 'flush', 'load'
|
199
|
+
@node.call_master(command, &block).first
|
200
|
+
else assign_node(command).call(command, &block)
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
def send_pubsub_command(command, &block)
|
205
|
+
case command[1].to_s.downcase
|
206
|
+
when 'channels' then @node.call_all(command, &block).flatten.uniq.sort
|
207
|
+
when 'numsub'
|
208
|
+
@node.call_all(command, &block).reject(&:empty?).map { |e| Hash[*e] }
|
209
|
+
.reduce({}) { |a, e| a.merge(e) { |_, v1, v2| v1 + v2 } }
|
210
|
+
when 'numpat' then @node.call_all(command, &block).reduce(:+)
|
211
|
+
else assign_node(command).call(command, &block)
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
# @see https://redis.io/topics/cluster-spec#redirection-and-resharding
|
216
|
+
# Redirection and resharding
|
217
|
+
def try_send(node, method_name, *args, retry_count: 3, &block)
|
218
|
+
node.public_send(method_name, *args, &block)
|
219
|
+
rescue CommandError => err
|
220
|
+
if err.message.start_with?('MOVED')
|
221
|
+
raise if retry_count <= 0
|
222
|
+
|
223
|
+
node = assign_redirection_node(err.message)
|
224
|
+
retry_count -= 1
|
225
|
+
retry
|
226
|
+
elsif err.message.start_with?('ASK')
|
227
|
+
raise if retry_count <= 0
|
228
|
+
|
229
|
+
node = assign_asking_node(err.message)
|
230
|
+
node.call(%i[asking])
|
231
|
+
retry_count -= 1
|
232
|
+
retry
|
233
|
+
else
|
234
|
+
raise
|
235
|
+
end
|
236
|
+
rescue CannotConnectError
|
237
|
+
update_cluster_info!
|
238
|
+
raise
|
239
|
+
end
|
240
|
+
|
241
|
+
def assign_redirection_node(err_msg)
|
242
|
+
_, slot, node_key = err_msg.split(' ')
|
243
|
+
slot = slot.to_i
|
244
|
+
@slot.put(slot, node_key)
|
245
|
+
find_node(node_key)
|
246
|
+
end
|
247
|
+
|
248
|
+
def assign_asking_node(err_msg)
|
249
|
+
_, _, node_key = err_msg.split(' ')
|
250
|
+
find_node(node_key)
|
251
|
+
end
|
252
|
+
|
253
|
+
def assign_node(command)
|
254
|
+
node_key = find_node_key(command)
|
255
|
+
find_node(node_key)
|
256
|
+
end
|
257
|
+
|
258
|
+
def find_node_key(command, primary_only: false)
|
259
|
+
key = @command.extract_first_key(command)
|
260
|
+
return if key.empty?
|
261
|
+
|
262
|
+
slot = KeySlotConverter.convert(key)
|
263
|
+
return unless @slot.exists?(slot)
|
264
|
+
|
265
|
+
if @command.should_send_to_master?(command) || primary_only
|
266
|
+
@slot.find_node_key_of_master(slot)
|
267
|
+
else
|
268
|
+
@slot.find_node_key_of_slave(slot)
|
269
|
+
end
|
270
|
+
end
|
271
|
+
|
272
|
+
def find_node(node_key)
|
273
|
+
return @node.sample if node_key.nil?
|
274
|
+
|
275
|
+
@node.find_by(node_key)
|
276
|
+
rescue Node::ReloadNeeded
|
277
|
+
update_cluster_info!(node_key)
|
278
|
+
@node.find_by(node_key)
|
279
|
+
end
|
280
|
+
|
281
|
+
def update_cluster_info!(node_key = nil)
|
282
|
+
unless node_key.nil?
|
283
|
+
host, port = NodeKey.split(node_key)
|
284
|
+
@option.add_node(host, port)
|
285
|
+
end
|
286
|
+
|
287
|
+
@node.map(&:disconnect)
|
288
|
+
@node, @slot = fetch_cluster_info!(@option)
|
289
|
+
end
|
290
|
+
end
|
291
|
+
end
|