Skip to content

Commit 72898ce

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 ea9f1d2 commit 72898ce

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
@@ -501,6 +501,10 @@ def check(client)
501501
end
502502

503503
class Sentinel < Connector
504+
EXPECTED_ROLES = {
505+
"nearest_slave" => "slave"
506+
}
507+
504508
def initialize(options)
505509
super(options)
506510

@@ -520,12 +524,12 @@ def check(client)
520524
role = client.call([:role])[0]
521525
rescue Redis::CommandError
522526
# Assume the test is passed if we can't get a reply from ROLE...
523-
role = @role
527+
role = EXPECTED_ROLES.fetch(@role, @role)
524528
end
525529

526-
if role != @role
530+
if role != EXPECTED_ROLES.fetch(@role, @role)
527531
client.disconnect
528-
raise ConnectionError, "Instance role mismatch. Expected #{@role}, got #{role}."
532+
raise ConnectionError, "Instance role mismatch. Expected #{EXPECTED_ROLES.fetch(@role, @role)}, got #{role}."
529533
end
530534
end
531535

@@ -535,6 +539,8 @@ def resolve
535539
resolve_master
536540
when "slave"
537541
resolve_slave
542+
when "nearest_slave"
543+
resolve_nearest_slave
538544
else
539545
raise ArgumentError, "Unknown instance role #{@role}"
540546
end
@@ -584,6 +590,34 @@ def resolve_slave
584590
end
585591
end
586592
end
593+
594+
def resolve_nearest_slave
595+
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
612+
end
613+
end
614+
615+
slave = ok_slaves.sort_by {|slave| slave["response_time"] }.first
616+
{:host => slave.fetch("ip"), :port => slave.fetch("port")} if slave
617+
end
618+
end
619+
end
620+
587621
end
588622
end
589623
end

test/sentinel_test.rb

100644100755
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,4 +252,48 @@ def test_sentinel_retries
252252

253253
assert_match(/No sentinels available/, ex.message)
254254
end
255+
256+
def test_sentinel_nearest_slave
257+
sentinels = [{:host => "127.0.0.1", :port => 26381}]
258+
259+
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"] } }
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 "slaves"
275+
[
276+
%W[master-link-status down ip 127.0.0.1 port #{s1_port}],
277+
%W[master-link-status ok ip 127.0.0.1 port #{s2_port}],
278+
%W[master-link-status ok ip 127.0.0.1 port #{s3_port}]
279+
].shuffle
280+
else
281+
["127.0.0.1", port.to_s]
282+
end
283+
end
284+
}
285+
end
286+
287+
RedisMock.start(sentinel.call(master_port)) do |sen_port|
288+
sentinels[0][:port] = sen_port
289+
redis = Redis.new(:url => "redis://master1", :sentinels => sentinels, :role => :nearest_slave)
290+
assert_equal redis.slave_id, ["2"]
291+
end
292+
end
293+
end
294+
end
295+
end
296+
end
297+
298+
end
255299
end

0 commit comments

Comments
 (0)