Skip to content

Commit fd69a00

Browse files
committed
Refactor Sentinel support to make Redis::Client less aware of Sentinel.
1 parent e07eefc commit fd69a00

File tree

2 files changed

+100
-87
lines changed

2 files changed

+100
-87
lines changed

examples/consistency.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -107,8 +107,8 @@ def test
107107
end
108108
end
109109

110-
Sentinels = [{:host => "127.0.0.1", :port => 26380},
111-
{:host => "127.0.0.1", :port => 26381}]
112-
r = Redis.new(:url => "sentinel://mymaster", :sentinels => Sentinels, :role => :master)
110+
Sentinels = [{:host => "127.0.0.1", :port => 26379},
111+
{:host => "127.0.0.1", :port => 26380}]
112+
r = Redis.new(:url => "redis://master1", :sentinels => Sentinels, :role => :master)
113113
tester = ConsistencyTester.new(r)
114114
tester.test

lib/redis/client.rb

Lines changed: 97 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,12 @@ def initialize(options = {})
7575
@logger = @options[:logger]
7676
@connection = nil
7777
@command_map = {}
78+
79+
if options.include?(:sentinels)
80+
@connector = Connector::Sentinel.new(@options)
81+
else
82+
@connector = Connector.new(@options)
83+
end
7884
end
7985

8086
def connect
@@ -85,25 +91,7 @@ def connect
8591
establish_connection
8692
call [:auth, password] if password
8793
call [:select, db] if db != 0
88-
if @options[:sentinels]
89-
# Check the instance is really of the role we are looking for.
90-
# We can't assume the command is supported since it was
91-
# introduced recently and this client should work with old stuff.
92-
role = nil
93-
begin
94-
role = call [:role]
95-
rescue
96-
# Assume the test is passed if we can't get a reply from ROLE...
97-
role = [@options[:role].to_s]
98-
end
99-
100-
# Raise an error on role mismatch. TODO: we could do better, wait
101-
# some time and connect again...
102-
if role[0] != @options[:role].to_s
103-
disconnect
104-
raise ConnectionError, "Instance role mismatch, try again."
105-
end
106-
end
94+
@connector.check(self)
10795
end
10896

10997
self
@@ -319,60 +307,8 @@ def logging(commands)
319307
end
320308
end
321309

322-
def set_addr_via_sentinel
323-
responder = nil # The Sentinel that was able to reply
324-
325-
# Try one Sentinel after the other, using the list provided
326-
# by the user.
327-
@options[:sentinels].each{|sentinel|
328-
begin
329-
if !sentinel[:link]
330-
sentinel[:link] = Redis.new(:host => sentinel[:host],
331-
:port => sentinel[:port],
332-
:timeout => 0.300)
333-
end
334-
if @options[:role] == :master
335-
reply = sentinel[:link].client.call(["sentinel","get-master-addr-by-name",@options[:mastername]])
336-
next if !reply
337-
# Got it, set :host and :port
338-
@options[:host] = reply[0]
339-
@options[:port] = reply[1]
340-
responder = sentinel
341-
break
342-
elsif @options[:role] == :slave
343-
reply = sentinel[:link].client.call(["sentinel","slaves",@options[:mastername]])
344-
slaves = []
345-
reply.each{|slave|
346-
slaves << Hash[*slave]
347-
}
348-
random_slave = slaves[rand(slaves.length)]
349-
@options[:host] = random_slave['ip']
350-
@options[:port] = random_slave['port']
351-
responder = sentinel
352-
break
353-
else
354-
raise ArgumentError, "Unknown instance role #{@options[:role]}"
355-
end
356-
rescue
357-
next; # Try the next one on error
358-
end
359-
}
360-
361-
if responder
362-
# If we were able to obtain the address, make sure to put the Sentinel
363-
# that was able to reply as the first in the list.
364-
@options[:sentinels].delete(responder)
365-
@options[:sentinels].unshift(responder)
366-
else
367-
raise ConnectionError, "Unable to fetch #{@options[:role]} via Sentinel."
368-
end
369-
end
370-
371310
def establish_connection
372-
if @options[:sentinels]
373-
set_addr_via_sentinel
374-
end
375-
@connection = @options[:driver].connect(@options.dup)
311+
@connection = @options[:driver].connect(@connector.resolve.dup)
376312
rescue TimeoutError
377313
raise CannotConnectError, "Timed out connecting to Redis on #{location}"
378314
rescue Errno::ECONNREFUSED
@@ -444,12 +380,7 @@ def _parse_options(options)
444380
defaults[:port] = uri.port if uri.port
445381
defaults[:password] = CGI.unescape(uri.password) if uri.password
446382
defaults[:db] = uri.path[1..-1].to_i if uri.path
447-
elsif uri.scheme == "sentinel"
448-
defaults[:scheme] = uri.scheme
449-
defaults[:mastername] = uri.host
450383
defaults[:role] = :master
451-
defaults[:password] = CGI.unescape(uri.password) if uri.password
452-
defaults[:db] = uri.path[1..-1].to_i if uri.path
453384
else
454385
raise ArgumentError, "invalid uri scheme '#{uri.scheme}'"
455386
end
@@ -465,13 +396,6 @@ def _parse_options(options)
465396
options[:scheme] = "unix"
466397
options.delete(:host)
467398
options.delete(:port)
468-
elsif options[:mastername]
469-
# Sentinel
470-
options.delete(:host)
471-
options.delete(:port)
472-
if options[:sentinels].nil?
473-
raise ArgumentError, "list of Sentinels required"
474-
end
475399
else
476400
# TCP socket
477401
options[:host] = options[:host].to_s
@@ -519,5 +443,94 @@ def _parse_driver(driver)
519443

520444
driver
521445
end
446+
447+
class Connector
448+
def initialize(options)
449+
@options = options
450+
end
451+
452+
def resolve
453+
@options
454+
end
455+
456+
def check(client)
457+
end
458+
459+
class Sentinel < Connector
460+
def initialize(options)
461+
super(options)
462+
463+
@sentinels = options.fetch(:sentinels).dup
464+
@role = options[:role].to_s
465+
@master = options[:host]
466+
end
467+
468+
def check(client)
469+
# Check the instance is really of the role we are looking for.
470+
# We can't assume the command is supported since it was introduced
471+
# recently and this client should work with old stuff.
472+
begin
473+
role = client.call([:role])[0]
474+
rescue
475+
# Assume the test is passed if we can't get a reply from ROLE...
476+
role = @role
477+
end
478+
479+
if role != @role
480+
disconnect
481+
raise ConnectionError, "Instance role mismatch. Expected #{@role}, got #{role}."
482+
end
483+
end
484+
485+
def resolve
486+
result = case @role
487+
when "master"
488+
resolve_master
489+
when "slave"
490+
resolve_slave
491+
else
492+
raise ArgumentError, "Unknown instance role #{@role}"
493+
end
494+
495+
result || (raise ConnectionError, "Unable to fetch #{@role} via Sentinel.")
496+
end
497+
498+
def sentinel_detect
499+
@sentinels.each do |sentinel|
500+
client = Client.new(:host => sentinel[:host], :port => sentinel[:port], :timeout => 0.3)
501+
502+
begin
503+
if result = yield(client)
504+
# This sentinel responded. Make sure we ask it first next time.
505+
@sentinels.delete(sentinel)
506+
@sentinels.unshift(sentinel)
507+
508+
return result
509+
end
510+
ensure
511+
client.disconnect
512+
end
513+
end
514+
end
515+
516+
def resolve_master
517+
sentinel_detect do |client|
518+
if reply = client.call(["sentinel", "get-master-addr-by-name", @master])
519+
{host: reply[0], port: reply[1]}
520+
end
521+
end
522+
end
523+
524+
def resolve_slave
525+
sentinel_detect do |client|
526+
if reply = client.call(["sentinel", "slaves", @master])
527+
slave = Hash[*reply.sample]
528+
529+
{host: slave.fetch("ip"), port: slave.fetch("port")}
530+
end
531+
end
532+
end
533+
end
534+
end
522535
end
523536
end

0 commit comments

Comments
 (0)