Skip to content

Commit d304c99

Browse files
committed
Add the :nearest mode, which selects the closest node by ping, regardless of role
1 parent 72898ce commit d304c99

File tree

2 files changed

+93
-25
lines changed

2 files changed

+93
-25
lines changed

lib/redis/client.rb

Lines changed: 44 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -502,7 +502,8 @@ def check(client)
502502

503503
class Sentinel < Connector
504504
EXPECTED_ROLES = {
505-
"nearest_slave" => "slave"
505+
"nearest_slave" => "slave",
506+
"nearest" => "any"
506507
}
507508

508509
def initialize(options)
@@ -520,14 +521,15 @@ def check(client)
520521
# Check the instance is really of the role we are looking for.
521522
# We can't assume the command is supported since it was introduced
522523
# recently and this client should work with old stuff.
524+
expected_role = EXPECTED_ROLES.fetch(@role, @role)
523525
begin
524526
role = client.call([:role])[0]
525527
rescue Redis::CommandError
526528
# Assume the test is passed if we can't get a reply from ROLE...
527-
role = EXPECTED_ROLES.fetch(@role, @role)
529+
role = expected_role
528530
end
529531

530-
if role != EXPECTED_ROLES.fetch(@role, @role)
532+
if role != expected_role && "any" != expected_role
531533
client.disconnect
532534
raise ConnectionError, "Instance role mismatch. Expected #{EXPECTED_ROLES.fetch(@role, @role)}, got #{role}."
533535
end
@@ -539,6 +541,8 @@ def resolve
539541
resolve_master
540542
when "slave"
541543
resolve_slave
544+
when "nearest"
545+
resolve_nearest
542546
when "nearest_slave"
543547
resolve_nearest_slave
544548
else
@@ -591,30 +595,49 @@ def resolve_slave
591595
end
592596
end
593597

598+
def resolve_nearest
599+
resolve_nearest_for [:master, :slaves]
600+
end
601+
594602
def resolve_nearest_slave
603+
resolve_nearest_for [:slaves]
604+
end
605+
606+
def resolve_nearest_for(types)
595607
sentinel_detect do |client|
596-
if reply = client.call(["sentinel", "slaves", @master])
597-
ok_slaves = reply.map {|r| Hash[*r] }.select {|r| r["master-link-status"] == "ok" }
598-
599-
ok_slaves.each do |slave|
600-
client = Client.new @options.merge(
601-
:host => slave["ip"],
602-
:port => slave["port"],
603-
:reconnect_attempts => 0
604-
)
605-
begin
606-
client.call [:ping]
607-
start = Time.now
608-
client.call [:ping]
609-
slave["response_time"] = (Time.now - start).to_f
610-
ensure
611-
client.disconnect
608+
ok_nodes = []
609+
types.each do |type|
610+
if reply = client.call(["sentinel", type, @master])
611+
reply = [reply] if type == :master
612+
ok_nodes += reply.map {|r| Hash[*r] }.select do |r|
613+
case type
614+
when :master
615+
r["role-reported"] == "master"
616+
when :slaves
617+
r["master-link-status"] == "ok" && !r.fetch("flags", "").match(/s_down|disconnected/)
618+
end
612619
end
613620
end
621+
end
614622

615-
slave = ok_slaves.sort_by {|slave| slave["response_time"] }.first
616-
{:host => slave.fetch("ip"), :port => slave.fetch("port")} if slave
623+
ok_nodes.each do |node|
624+
client = Client.new @options.merge(
625+
:host => node["ip"],
626+
:port => node["port"],
627+
:reconnect_attempts => 0
628+
)
629+
begin
630+
client.call [:ping]
631+
start = Time.now
632+
client.call [:ping]
633+
node["response_time"] = (Time.now - start).to_f
634+
ensure
635+
client.disconnect
636+
end
617637
end
638+
639+
node = ok_nodes.sort_by {|node| node["response_time"] }.first
640+
{:host => node.fetch("ip"), :port => node.fetch("port")} if node
618641
end
619642
end
620643

test/sentinel_test.rb

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -253,13 +253,58 @@ def test_sentinel_retries
253253
assert_match(/No sentinels available/, ex.message)
254254
end
255255

256+
def test_sentinel_nearest
257+
sentinels = [{:host => "127.0.0.1", :port => 26381}]
258+
259+
master = { :role => lambda { ["master"] }, :node_id => lambda { ["master"] }, :ping => lambda { ["OK"] } }
260+
s1 = { :role => lambda { ["slave"] }, :node_id => lambda { ["1"] }, :ping => lambda { sleep 0.1; ["OK"] } }
261+
s2 = { :role => lambda { ["slave"] }, :node_id => lambda { ["2"] }, :ping => lambda { sleep 0.2; ["OK"] } }
262+
s3 = { :role => lambda { ["slave"] }, :node_id => lambda { ["3"] }, :ping => lambda { sleep 0.3; ["OK"] } }
263+
264+
5.times do
265+
RedisMock.start(master) do |master_port|
266+
RedisMock.start(s1) do |s1_port|
267+
RedisMock.start(s2) do |s2_port|
268+
RedisMock.start(s3) do |s3_port|
269+
270+
sentinel = lambda do |port|
271+
{
272+
:sentinel => lambda do |command, *args|
273+
case command
274+
when "master"
275+
%W[role-reported master ip 127.0.0.1 port #{master_port}]
276+
when "slaves"
277+
[
278+
%W[master-link-status down ip 127.0.0.1 port #{s1_port}],
279+
%W[master-link-status ok ip 127.0.0.1 port #{s2_port}],
280+
%W[master-link-status ok ip 127.0.0.1 port #{s3_port}]
281+
].shuffle
282+
else
283+
["127.0.0.1", port.to_s]
284+
end
285+
end
286+
}
287+
end
288+
289+
RedisMock.start(sentinel.call(master_port)) do |sen_port|
290+
sentinels[0][:port] = sen_port
291+
redis = Redis.new(:url => "redis://master1", :sentinels => sentinels, :role => :nearest)
292+
assert_equal ["master"], redis.node_id
293+
end
294+
end
295+
end
296+
end
297+
end
298+
end
299+
end
300+
256301
def test_sentinel_nearest_slave
257302
sentinels = [{:host => "127.0.0.1", :port => 26381}]
258303

259304
master = { :role => lambda { ["master"] } }
260-
s1 = { :role => lambda { ["slave"] }, :slave_id => lambda { ["1"] }, :ping => lambda { ["OK"] } }
261-
s2 = { :role => lambda { ["slave"] }, :slave_id => lambda { ["2"] }, :ping => lambda { sleep 0.1; ["OK"] } }
262-
s3 = { :role => lambda { ["slave"] }, :slave_id => lambda { ["3"] }, :ping => lambda { sleep 0.2; ["OK"] } }
305+
s1 = { :role => lambda { ["slave"] }, :node_id => lambda { ["1"] }, :ping => lambda { ["OK"] } }
306+
s2 = { :role => lambda { ["slave"] }, :node_id => lambda { ["2"] }, :ping => lambda { sleep 0.1; ["OK"] } }
307+
s3 = { :role => lambda { ["slave"] }, :node_id => lambda { ["3"] }, :ping => lambda { sleep 0.2; ["OK"] } }
263308

264309
5.times do
265310
RedisMock.start(master) do |master_port|
@@ -287,7 +332,7 @@ def test_sentinel_nearest_slave
287332
RedisMock.start(sentinel.call(master_port)) do |sen_port|
288333
sentinels[0][:port] = sen_port
289334
redis = Redis.new(:url => "redis://master1", :sentinels => sentinels, :role => :nearest_slave)
290-
assert_equal redis.slave_id, ["2"]
335+
assert_equal redis.node_id, ["2"]
291336
end
292337
end
293338
end

0 commit comments

Comments
 (0)