From d304c994701c57305bc4469c2212e712810e3b69 Mon Sep 17 00:00:00 2001 From: Chris Heald Date: Thu, 21 Apr 2016 00:53:37 -0700 Subject: [PATCH] Add the :nearest mode, which selects the closest node by ping, regardless of role --- lib/redis/client.rb | 65 +++++++++++++++++++++++++++++-------------- test/sentinel_test.rb | 53 ++++++++++++++++++++++++++++++++--- 2 files changed, 93 insertions(+), 25 deletions(-) diff --git a/lib/redis/client.rb b/lib/redis/client.rb index 73b75bafd..2e4946ec3 100755 --- a/lib/redis/client.rb +++ b/lib/redis/client.rb @@ -502,7 +502,8 @@ def check(client) class Sentinel < Connector EXPECTED_ROLES = { - "nearest_slave" => "slave" + "nearest_slave" => "slave", + "nearest" => "any" } def initialize(options) @@ -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 @@ -539,6 +541,8 @@ def resolve resolve_master when "slave" resolve_slave + when "nearest" + resolve_nearest when "nearest_slave" resolve_nearest_slave else @@ -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 diff --git a/test/sentinel_test.rb b/test/sentinel_test.rb index c3a71069e..2f9bc5282 100755 --- a/test/sentinel_test.rb +++ b/test/sentinel_test.rb @@ -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| @@ -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