Skip to content

Commit 14b86ef

Browse files
committed
Add the :nearest mode, which selects the closest node by ping, regardless of role
1 parent d1ee530 commit 14b86ef

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
@@ -484,7 +484,8 @@ def check(client)
484484

485485
class Sentinel < Connector
486486
EXPECTED_ROLES = {
487-
"nearest_slave" => "slave"
487+
"nearest_slave" => "slave",
488+
"nearest" => "any"
488489
}
489490

490491
def initialize(options)
@@ -502,14 +503,15 @@ def check(client)
502503
# Check the instance is really of the role we are looking for.
503504
# We can't assume the command is supported since it was introduced
504505
# recently and this client should work with old stuff.
506+
expected_role = EXPECTED_ROLES.fetch(@role, @role)
505507
begin
506508
role = client.call([:role])[0]
507509
rescue Redis::CommandError
508510
# Assume the test is passed if we can't get a reply from ROLE...
509-
role = EXPECTED_ROLES.fetch(@role, @role)
511+
role = expected_role
510512
end
511513

512-
if role != EXPECTED_ROLES.fetch(@role, @role)
514+
if role != expected_role && "any" != expected_role
513515
client.disconnect
514516
raise ConnectionError, "Instance role mismatch. Expected #{EXPECTED_ROLES.fetch(@role, @role)}, got #{role}."
515517
end
@@ -521,6 +523,8 @@ def resolve
521523
resolve_master
522524
when "slave"
523525
resolve_slave
526+
when "nearest"
527+
resolve_nearest
524528
when "nearest_slave"
525529
resolve_nearest_slave
526530
else
@@ -573,30 +577,49 @@ def resolve_slave
573577
end
574578
end
575579

580+
def resolve_nearest
581+
resolve_nearest_for [:master, :slaves]
582+
end
583+
576584
def resolve_nearest_slave
585+
resolve_nearest_for [:slaves]
586+
end
587+
588+
def resolve_nearest_for(types)
577589
sentinel_detect do |client|
578-
if reply = client.call(["sentinel", "slaves", @master])
579-
ok_slaves = reply.map {|r| Hash[*r] }.select {|r| r["master-link-status"] == "ok" }
580-
581-
ok_slaves.each do |slave|
582-
client = Client.new @options.merge(
583-
:host => slave["ip"],
584-
:port => slave["port"],
585-
:reconnect_attempts => 0
586-
)
587-
begin
588-
client.call [:ping]
589-
start = Time.now
590-
client.call [:ping]
591-
slave["response_time"] = (Time.now - start).to_f
592-
ensure
593-
client.disconnect
590+
ok_nodes = []
591+
types.each do |type|
592+
if reply = client.call(["sentinel", type, @master])
593+
reply = [reply] if type == :master
594+
ok_nodes += reply.map {|r| Hash[*r] }.select do |r|
595+
case type
596+
when :master
597+
r["role-reported"] == "master"
598+
when :slaves
599+
r["master-link-status"] == "ok" && !r.fetch("flags", "").match(/s_down|disconnected/)
600+
end
594601
end
595602
end
603+
end
596604

597-
slave = ok_slaves.sort_by {|slave| slave["response_time"] }.first
598-
{:host => slave.fetch("ip"), :port => slave.fetch("port")} if slave
605+
ok_nodes.each do |node|
606+
client = Client.new @options.merge(
607+
:host => node["ip"],
608+
:port => node["port"],
609+
:reconnect_attempts => 0
610+
)
611+
begin
612+
client.call [:ping]
613+
start = Time.now
614+
client.call [:ping]
615+
node["response_time"] = (Time.now - start).to_f
616+
ensure
617+
client.disconnect
618+
end
599619
end
620+
621+
node = ok_nodes.sort_by {|node| node["response_time"] }.first
622+
{:host => node.fetch("ip"), :port => node.fetch("port")} if node
600623
end
601624
end
602625

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)