Skip to content

Commit

Permalink
Add the :nearest mode, which selects the closest node by ping, regard…
Browse files Browse the repository at this point in the history
…less of role
  • Loading branch information
cheald committed Aug 17, 2020
1 parent 3893355 commit 0e52d43
Show file tree
Hide file tree
Showing 2 changed files with 53 additions and 29 deletions.
65 changes: 44 additions & 21 deletions lib/redis/client.rb
Expand Up @@ -530,7 +530,8 @@ def check(client); end

class Sentinel < Connector
EXPECTED_ROLES = {
"nearest_slave" => "slave"
"nearest_slave" => "slave",
"nearest" => "any"
}

def initialize(options)
Expand All @@ -547,14 +548,15 @@ def check(client)
# Check the instance is really of the role we are looking for.
# We can't assume the command is supported since it was introduced
# recently and this client should work with old stuff.
expected_role = EXPECTED_ROLES.fetch(@role, @role)
begin
role = client.call([:role])[0]
rescue Redis::CommandError
# Assume the test is passed if we can't get a reply from ROLE...
role = EXPECTED_ROLES.fetch(@role, @role)
role = expected_role
end

if role != EXPECTED_ROLES.fetch(@role, @role)
if role != expected_role && "any" != expected_role
client.disconnect
raise ConnectionError, "Instance role mismatch. Expected #{EXPECTED_ROLES.fetch(@role, @role)}, got #{role}."
end
Expand All @@ -566,6 +568,8 @@ def resolve
resolve_master
when "slave"
resolve_slave
when "nearest"
resolve_nearest
when "nearest_slave"
resolve_nearest_slave
else
Expand Down Expand Up @@ -629,30 +633,49 @@ def resolve_slave
end
end

def resolve_nearest
resolve_nearest_for [:master, :slaves]
end

def resolve_nearest_slave
resolve_nearest_for [:slaves]
end

def resolve_nearest_for(types)
sentinel_detect do |client|
if reply = client.call(["sentinel", "slaves", @master])
ok_slaves = reply.map {|r| Hash[*r] }.select {|r| r["master-link-status"] == "ok" }

ok_slaves.each do |slave|
client = Client.new @options.merge(
:host => slave["ip"],
:port => slave["port"],
:reconnect_attempts => 0
)
begin
client.call [:ping]
start = Time.now
client.call [:ping]
slave["response_time"] = (Time.now - start).to_f
ensure
client.disconnect
ok_nodes = []
types.each do |type|
if reply = client.call(["sentinel", type, @master])
reply = [reply] if type == :master
ok_nodes += reply.map {|r| Hash[*r] }.select do |r|
case type
when :master
r["role-reported"] == "master"
when :slaves
r["master-link-status"] == "ok" && !r.fetch("flags", "").match(/s_down|disconnected/)
end
end
end
end

slave = ok_slaves.sort_by {|slave| slave["response_time"] }.first
{:host => slave.fetch("ip"), :port => slave.fetch("port")} if slave
ok_nodes.each do |node|
client = Client.new @options.merge(
:host => node["ip"],
:port => node["port"],
:reconnect_attempts => 0
)
begin
client.call [:ping]
start = Time.now
client.call [:ping]
node["response_time"] = (Time.now - start).to_f
ensure
client.disconnect
end
end

node = ok_nodes.sort_by {|node| node["response_time"] }.first
{:host => node.fetch("ip"), :port => node.fetch("port")} if node
end
end

Expand Down
17 changes: 9 additions & 8 deletions test/sentinel_test.rb
Expand Up @@ -378,13 +378,13 @@ def test_sentinel_with_string_option_keys
assert_equal [%w[get-master-addr-by-name master1]], commands
end

def test_sentinel_nearest_slave
def test_sentinel_nearest
sentinels = [{:host => "127.0.0.1", :port => 26381}]

master = { :role => lambda { ["master"] } }
s1 = { :role => lambda { ["slave"] }, :slave_id => lambda { ["1"] }, :ping => lambda { ["OK"] } }
s2 = { :role => lambda { ["slave"] }, :slave_id => lambda { ["2"] }, :ping => lambda { sleep 0.1; ["OK"] } }
s3 = { :role => lambda { ["slave"] }, :slave_id => lambda { ["3"] }, :ping => lambda { sleep 0.2; ["OK"] } }
master = { :role => lambda { ["master"] }, :node_id => lambda { ["master"] }, :ping => lambda { ["OK"] } }
s1 = { :role => lambda { ["slave"] }, :node_id => lambda { ["1"] }, :ping => lambda { sleep 0.1; ["OK"] } }
s2 = { :role => lambda { ["slave"] }, :node_id => lambda { ["2"] }, :ping => lambda { sleep 0.2; ["OK"] } }
s3 = { :role => lambda { ["slave"] }, :node_id => lambda { ["3"] }, :ping => lambda { sleep 0.3; ["OK"] } }

5.times do
RedisMock.start(master) do |master_port|
Expand All @@ -396,6 +396,8 @@ def test_sentinel_nearest_slave
{
:sentinel => lambda do |command, *args|
case command
when "master"
%W[role-reported master ip 127.0.0.1 port #{master_port}]
when "slaves"
[
%W[master-link-status down ip 127.0.0.1 port #{s1_port}],
Expand All @@ -411,14 +413,13 @@ def test_sentinel_nearest_slave

RedisMock.start(sentinel.call(master_port)) do |sen_port|
sentinels[0][:port] = sen_port
redis = Redis.new(:url => "redis://master1", :sentinels => sentinels, :role => :nearest_slave)
assert_equal redis.slave_id, ["2"]
redis = Redis.new(:url => "redis://master1", :sentinels => sentinels, :role => :nearest)
assert_equal ["master"], redis.node_id
end
end
end
end
end
end

end
end

0 comments on commit 0e52d43

Please sign in to comment.