Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix cluster connecting option bug #889

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 2 additions & 3 deletions lib/redis/cluster.rb
Expand Up @@ -112,12 +112,11 @@ def fetch_cluster_info!(option)
node = Node.new(option.per_node_key)
available_slots = SlotLoader.load(node)
node_flags = NodeLoader.load_flags(node)
available_node_urls = NodeKey.to_node_urls(available_slots.keys, secure: option.secure?)
option.update_node(available_node_urls)
option.update_node(available_slots.keys.map { |k| NodeKey.optionize(k) })
[Node.new(option.per_node_key, node_flags, option.use_replica?),
Slot.new(available_slots, node_flags, option.use_replica?)]
ensure
node.map(&:disconnect)
node&.each(&:disconnect)
end

def fetch_command_details(nodes)
Expand Down
10 changes: 3 additions & 7 deletions lib/redis/cluster/node_key.rb
Expand Up @@ -6,17 +6,13 @@ class Cluster
# It is different from node id.
# Node id is internal identifying code in Redis Cluster.
module NodeKey
DEFAULT_SCHEME = 'redis'
SECURE_SCHEME = 'rediss'
DELIMITER = ':'

module_function

def to_node_urls(node_keys, secure:)
scheme = secure ? SECURE_SCHEME : DEFAULT_SCHEME
node_keys
.map { |k| k.split(DELIMITER) }
.map { |k| URI::Generic.build(scheme: scheme, host: k[0], port: k[1].to_i).to_s }
def optionize(node_key)
host, port = split(node_key)
{ host: host, port: port }
end

def split(node_key)
Expand Down
36 changes: 23 additions & 13 deletions lib/redis/cluster/option.rb
Expand Up @@ -15,35 +15,33 @@ class Option
def initialize(options)
options = options.dup
node_addrs = options.delete(:cluster)
@node_uris = build_node_uris(node_addrs)
@node_opts = build_node_options(node_addrs)
@replica = options.delete(:replica) == true
add_common_node_option_if_needed(options, @node_opts, :scheme)
add_common_node_option_if_needed(options, @node_opts, :password)
@options = options
end

def per_node_key
@node_uris.map { |uri| [NodeKey.build_from_uri(uri), @options.merge(url: uri.to_s)] }
@node_opts.map { |opt| [NodeKey.build_from_host_port(opt[:host], opt[:port]), @options.merge(opt)] }
.to_h
end

def secure?
@node_uris.any? { |uri| uri.scheme == SECURE_SCHEME } || @options[:ssl_params] || false
end

def use_replica?
@replica
end

def update_node(addrs)
@node_uris = build_node_uris(addrs)
@node_opts = build_node_options(addrs)
end

def add_node(host, port)
@node_uris << parse_node_hash(host: host, port: port)
@node_opts << { host: host, port: port }
end

private

def build_node_uris(addrs)
def build_node_options(addrs)
raise InvalidClientOptionError, 'Redis option of `cluster` must be an Array' unless addrs.is_a?(Array)
addrs.map { |addr| parse_node_addr(addr) }
end
Expand All @@ -53,7 +51,7 @@ def parse_node_addr(addr)
when String
parse_node_url(addr)
when Hash
parse_node_hash(addr)
parse_node_option(addr)
else
raise InvalidClientOptionError, 'Redis option of `cluster` must includes String or Hash'
end
Expand All @@ -62,15 +60,27 @@ def parse_node_addr(addr)
def parse_node_url(addr)
uri = URI(addr)
raise InvalidClientOptionError, "Invalid uri scheme #{addr}" unless VALID_SCHEMES.include?(uri.scheme)
uri

db = uri.path.split('/')[1]&.to_i
{ scheme: uri.scheme, password: uri.password, host: uri.host, port: uri.port, db: db }.reject { |_, v| v.nil? }
rescue URI::InvalidURIError => err
raise InvalidClientOptionError, err.message
end

def parse_node_hash(addr)
def parse_node_option(addr)
addr = addr.map { |k, v| [k.to_sym, v] }.to_h
raise InvalidClientOptionError, 'Redis option of `cluster` must includes `:host` and `:port` keys' if addr.values_at(:host, :port).any?(&:nil?)
URI::Generic.build(scheme: DEFAULT_SCHEME, host: addr[:host], port: addr[:port].to_i)

addr
end

# Redis cluster node returns only host and port information.
# So we should complement additional information such as:
# scheme, password and so on.
def add_common_node_option_if_needed(options, node_opts, key)
return options if options[key].nil? && node_opts.first[key].nil?

options[key] ||= node_opts.first[key]
end
end
end
Expand Down
22 changes: 5 additions & 17 deletions makefile
Expand Up @@ -22,23 +22,11 @@ define kill-redis
(ls $1 > /dev/null 2>&1 && kill $$(cat $1) && rm -f $1) || true
endef

all:
@make --no-print-directory start_all
@make --no-print-directory test
@make --no-print-directory stop_all

start_all:
@make --no-print-directory start
@make --no-print-directory start_slave
@make --no-print-directory start_sentinel
@make --no-print-directory start_cluster
@make --no-print-directory create_cluster

stop_all:
@make --no-print-directory stop_sentinel
@make --no-print-directory stop_slave
@make --no-print-directory stop
@make --no-print-directory stop_cluster
all: start_all test stop_all

start_all: start start_slave start_sentinel start_cluster create_cluster

stop_all: stop_sentinel stop_slave stop stop_cluster

${TMP}:
@mkdir -p $@
Expand Down
42 changes: 34 additions & 8 deletions test/cluster_client_options_test.rb
Expand Up @@ -7,20 +7,44 @@ class TestClusterClientOptions < Minitest::Test
include Helper::Cluster

def test_option_class
option = Redis::Cluster::Option.new(cluster: %w[rediss://127.0.0.1:7000], replica: true)
assert_equal({ '127.0.0.1:7000' => { url: 'rediss://127.0.0.1:7000' } }, option.per_node_key)
assert_equal true, option.secure?
option = Redis::Cluster::Option.new(cluster: %w[redis://127.0.0.1:7000], replica: true)
assert_equal({ '127.0.0.1:7000' => { scheme: 'redis', host: '127.0.0.1', port: 7000 } }, option.per_node_key)
assert_equal true, option.use_replica?

option = Redis::Cluster::Option.new(cluster: %w[redis://127.0.0.1:7000], replica: false)
assert_equal({ '127.0.0.1:7000' => { url: 'redis://127.0.0.1:7000' } }, option.per_node_key)
assert_equal false, option.secure?
assert_equal({ '127.0.0.1:7000' => { scheme: 'redis', host: '127.0.0.1', port: 7000 } }, option.per_node_key)
assert_equal false, option.use_replica?

option = Redis::Cluster::Option.new(cluster: %w[redis://127.0.0.1:7000])
assert_equal({ '127.0.0.1:7000' => { url: 'redis://127.0.0.1:7000' } }, option.per_node_key)
assert_equal false, option.secure?
assert_equal({ '127.0.0.1:7000' => { scheme: 'redis', host: '127.0.0.1', port: 7000 } }, option.per_node_key)
assert_equal false, option.use_replica?

option = Redis::Cluster::Option.new(cluster: %w[rediss://johndoe:foobar@127.0.0.1:7000/1/namespace])
assert_equal({ '127.0.0.1:7000' => { scheme: 'rediss', password: 'foobar', host: '127.0.0.1', port: 7000, db: 1 } }, option.per_node_key)

option = Redis::Cluster::Option.new(cluster: %w[rediss://127.0.0.1:7000], scheme: 'redis')
assert_equal({ '127.0.0.1:7000' => { scheme: 'rediss', host: '127.0.0.1', port: 7000 } }, option.per_node_key)

option = Redis::Cluster::Option.new(cluster: %w[redis://:bazzap@127.0.0.1:7000], password: 'foobar')
assert_equal({ '127.0.0.1:7000' => { scheme: 'redis', password: 'bazzap', host: '127.0.0.1', port: 7000 } }, option.per_node_key)

option = Redis::Cluster::Option.new(cluster: %w[redis://127.0.0.1:7000/0], db: 1)
assert_equal({ '127.0.0.1:7000' => { scheme: 'redis', host: '127.0.0.1', port: 7000, db: 0 } }, option.per_node_key)

option = Redis::Cluster::Option.new(cluster: [{ host: '127.0.0.1', port: 7000 }])
assert_equal({ '127.0.0.1:7000' => { host: '127.0.0.1', port: 7000 } }, option.per_node_key)

assert_raises(Redis::InvalidClientOptionError) do
Redis::Cluster::Option.new(cluster: nil)
end

assert_raises(Redis::InvalidClientOptionError) do
Redis::Cluster::Option.new(cluster: %w[invalid_uri])
end

assert_raises(Redis::InvalidClientOptionError) do
Redis::Cluster::Option.new(cluster: [{ host: '127.0.0.1' }])
end
end

def test_client_accepts_valid_node_configs
Expand All @@ -43,7 +67,9 @@ def test_client_ignores_invalid_options
end

def test_client_works_even_if_so_many_unavailable_nodes_specified
nodes = (6001..7005).map { |port| "redis://127.0.0.1:#{port}" }
min = 7000
max = min + Process.getrlimit(Process::RLIMIT_NOFILE).first / 3 * 2
nodes = (min..max).map { |port| "redis://127.0.0.1:#{port}" }
redis = build_another_client(cluster: nodes)

assert_equal 'PONG', redis.ping
Expand Down