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

Add the :nearest_slave role for Sentinel mode #588

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
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
63 changes: 60 additions & 3 deletions lib/redis/client.rb 100644 → 100755
Expand Up @@ -529,6 +529,11 @@ def resolve
def check(client); end

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

def initialize(options)
super(options)

Expand All @@ -543,16 +548,17 @@ 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 = @role
role = expected_role
end

if role != @role
if role != expected_role && expected_role != "any"
client.disconnect
raise ConnectionError, "Instance role mismatch. Expected #{@role}, got #{role}."
raise ConnectionError, "Instance role mismatch. Expected #{expected_role}, got #{role}."
end
end

Expand All @@ -562,6 +568,10 @@ def resolve
resolve_master
when "slave"
resolve_slave
when "nearest"
resolve_nearest
when "nearest_slave"
resolve_nearest_slave
else
raise ArgumentError, "Unknown instance role #{@role}"
end
Expand Down Expand Up @@ -622,6 +632,53 @@ def resolve_slave
end
end
end

def resolve_nearest
resolve_nearest_for %I(master slaves)
end

def resolve_nearest_slave
resolve_nearest_for %I(slaves)
end

def resolve_nearest_for(types)
sentinel_detect do |client|
ok_nodes = []
types.each do |type|
reply = client.call(["sentinel", type, @master])
next unless reply

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

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.min_by { |n| n["response_time"] }
{ host: node.fetch("ip"), port: node.fetch("port") } if node
end
end
end
end
end
Expand Down
44 changes: 44 additions & 0 deletions test/sentinel_test.rb 100644 → 100755
Expand Up @@ -377,4 +377,48 @@ def test_sentinel_with_string_option_keys

assert_equal [%w[get-master-addr-by-name master1]], commands
end

def test_sentinel_nearest
sentinels = [{ host: "127.0.0.1", port: 26_381 }]

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