Skip to content

Commit d1ee530

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 00c0f50 commit d1ee530

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
@@ -483,6 +483,10 @@ def check(client)
483483
end
484484

485485
class Sentinel < Connector
486+
EXPECTED_ROLES = {
487+
"nearest_slave" => "slave"
488+
}
489+
486490
def initialize(options)
487491
super(options)
488492

@@ -502,12 +506,12 @@ def check(client)
502506
role = client.call([:role])[0]
503507
rescue Redis::CommandError
504508
# Assume the test is passed if we can't get a reply from ROLE...
505-
role = @role
509+
role = EXPECTED_ROLES.fetch(@role, @role)
506510
end
507511

508-
if role != @role
512+
if role != EXPECTED_ROLES.fetch(@role, @role)
509513
client.disconnect
510-
raise ConnectionError, "Instance role mismatch. Expected #{@role}, got #{role}."
514+
raise ConnectionError, "Instance role mismatch. Expected #{EXPECTED_ROLES.fetch(@role, @role)}, got #{role}."
511515
end
512516
end
513517

@@ -517,6 +521,8 @@ def resolve
517521
resolve_master
518522
when "slave"
519523
resolve_slave
524+
when "nearest_slave"
525+
resolve_nearest_slave
520526
else
521527
raise ArgumentError, "Unknown instance role #{@role}"
522528
end
@@ -566,6 +572,34 @@ def resolve_slave
566572
end
567573
end
568574
end
575+
576+
def resolve_nearest_slave
577+
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
594+
end
595+
end
596+
597+
slave = ok_slaves.sort_by {|slave| slave["response_time"] }.first
598+
{:host => slave.fetch("ip"), :port => slave.fetch("port")} if slave
599+
end
600+
end
601+
end
602+
569603
end
570604
end
571605
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)