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 Jun 23, 2017
1 parent 72898ce commit d304c99
Show file tree
Hide file tree
Showing 2 changed files with 93 additions and 25 deletions.
65 changes: 44 additions & 21 deletions lib/redis/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -502,7 +502,8 @@ def check(client)

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

def initialize(options)
Expand All @@ -520,14 +521,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 @@ -539,6 +541,8 @@ def resolve
resolve_master
when "slave"
resolve_slave
when "nearest"
resolve_nearest
when "nearest_slave"
resolve_nearest_slave
else
Expand Down Expand Up @@ -591,30 +595,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
53 changes: 49 additions & 4 deletions test/sentinel_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -253,13 +253,58 @@ def test_sentinel_retries
assert_match(/No sentinels available/, ex.message)
end

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

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|
RedisMock.start(s1) do |s1_port|
RedisMock.start(s2) do |s2_port|
RedisMock.start(s3) do |s3_port|

sentinel = lambda do |port|
{
: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}],
%W[master-link-status ok ip 127.0.0.1 port #{s2_port}],
%W[master-link-status ok ip 127.0.0.1 port #{s3_port}]
].shuffle
else
["127.0.0.1", port.to_s]
end
end
}
end

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

def test_sentinel_nearest_slave
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"] } }
s1 = { :role => lambda { ["slave"] }, :node_id => lambda { ["1"] }, :ping => lambda { ["OK"] } }
s2 = { :role => lambda { ["slave"] }, :node_id => lambda { ["2"] }, :ping => lambda { sleep 0.1; ["OK"] } }
s3 = { :role => lambda { ["slave"] }, :node_id => lambda { ["3"] }, :ping => lambda { sleep 0.2; ["OK"] } }

5.times do
RedisMock.start(master) do |master_port|
Expand Down Expand Up @@ -287,7 +332,7 @@ 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"]
assert_equal redis.node_id, ["2"]
end
end
end
Expand Down

0 comments on commit d304c99

Please sign in to comment.