Skip to content

Commit 3893355

Browse files
committed
Add the :nearest_slave role for Sentinel mode
This will cause the client to measure roundtrip latency to each slave and select the slave with the lowest latency. The intent for this is to enable sentinel-managed clusters of servers for which eventually-consistent reads are acceptable, but to maintain minimum latencies between any individual client-slave pair. The case I did this for is is shared web application caching across multiple datacenters, where you would not want Redis to connect to a slave in another datacenter, but you would want all datacenters to share a cache. Remove trailing comma from client creation options; should fix 1.8 builds If we can't get the role, use a translated role Ensure that ping test clients are always disconnected after use. Don't assume that a good slave was found.
1 parent 9fd381b commit 3893355

File tree

2 files changed

+81
-3
lines changed

2 files changed

+81
-3
lines changed

lib/redis/client.rb

100644100755
Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -529,6 +529,10 @@ def resolve
529529
def check(client); end
530530

531531
class Sentinel < Connector
532+
EXPECTED_ROLES = {
533+
"nearest_slave" => "slave"
534+
}
535+
532536
def initialize(options)
533537
super(options)
534538

@@ -547,12 +551,12 @@ def check(client)
547551
role = client.call([:role])[0]
548552
rescue Redis::CommandError
549553
# Assume the test is passed if we can't get a reply from ROLE...
550-
role = @role
554+
role = EXPECTED_ROLES.fetch(@role, @role)
551555
end
552556

553-
if role != @role
557+
if role != EXPECTED_ROLES.fetch(@role, @role)
554558
client.disconnect
555-
raise ConnectionError, "Instance role mismatch. Expected #{@role}, got #{role}."
559+
raise ConnectionError, "Instance role mismatch. Expected #{EXPECTED_ROLES.fetch(@role, @role)}, got #{role}."
556560
end
557561
end
558562

@@ -562,6 +566,8 @@ def resolve
562566
resolve_master
563567
when "slave"
564568
resolve_slave
569+
when "nearest_slave"
570+
resolve_nearest_slave
565571
else
566572
raise ArgumentError, "Unknown instance role #{@role}"
567573
end
@@ -622,6 +628,34 @@ def resolve_slave
622628
end
623629
end
624630
end
631+
632+
def resolve_nearest_slave
633+
sentinel_detect do |client|
634+
if reply = client.call(["sentinel", "slaves", @master])
635+
ok_slaves = reply.map {|r| Hash[*r] }.select {|r| r["master-link-status"] == "ok" }
636+
637+
ok_slaves.each do |slave|
638+
client = Client.new @options.merge(
639+
:host => slave["ip"],
640+
:port => slave["port"],
641+
:reconnect_attempts => 0
642+
)
643+
begin
644+
client.call [:ping]
645+
start = Time.now
646+
client.call [:ping]
647+
slave["response_time"] = (Time.now - start).to_f
648+
ensure
649+
client.disconnect
650+
end
651+
end
652+
653+
slave = ok_slaves.sort_by {|slave| slave["response_time"] }.first
654+
{:host => slave.fetch("ip"), :port => slave.fetch("port")} if slave
655+
end
656+
end
657+
end
658+
625659
end
626660
end
627661
end

test/sentinel_test.rb

100644100755
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -377,4 +377,48 @@ def test_sentinel_with_string_option_keys
377377

378378
assert_equal [%w[get-master-addr-by-name master1]], commands
379379
end
380+
381+
def test_sentinel_nearest_slave
382+
sentinels = [{:host => "127.0.0.1", :port => 26381}]
383+
384+
master = { :role => lambda { ["master"] } }
385+
s1 = { :role => lambda { ["slave"] }, :slave_id => lambda { ["1"] }, :ping => lambda { ["OK"] } }
386+
s2 = { :role => lambda { ["slave"] }, :slave_id => lambda { ["2"] }, :ping => lambda { sleep 0.1; ["OK"] } }
387+
s3 = { :role => lambda { ["slave"] }, :slave_id => lambda { ["3"] }, :ping => lambda { sleep 0.2; ["OK"] } }
388+
389+
5.times do
390+
RedisMock.start(master) do |master_port|
391+
RedisMock.start(s1) do |s1_port|
392+
RedisMock.start(s2) do |s2_port|
393+
RedisMock.start(s3) do |s3_port|
394+
395+
sentinel = lambda do |port|
396+
{
397+
:sentinel => lambda do |command, *args|
398+
case command
399+
when "slaves"
400+
[
401+
%W[master-link-status down ip 127.0.0.1 port #{s1_port}],
402+
%W[master-link-status ok ip 127.0.0.1 port #{s2_port}],
403+
%W[master-link-status ok ip 127.0.0.1 port #{s3_port}]
404+
].shuffle
405+
else
406+
["127.0.0.1", port.to_s]
407+
end
408+
end
409+
}
410+
end
411+
412+
RedisMock.start(sentinel.call(master_port)) do |sen_port|
413+
sentinels[0][:port] = sen_port
414+
redis = Redis.new(:url => "redis://master1", :sentinels => sentinels, :role => :nearest_slave)
415+
assert_equal redis.slave_id, ["2"]
416+
end
417+
end
418+
end
419+
end
420+
end
421+
end
422+
423+
end
380424
end

0 commit comments

Comments
 (0)