redis-sentinel2 1.3.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +17 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +41 -0
- data/CONTRIBUTING.md +12 -0
- data/Gemfile +6 -0
- data/MIT-LICENSE +22 -0
- data/README.md +107 -0
- data/Rakefile +19 -0
- data/example/redis-master.conf +540 -0
- data/example/redis-sentinel1.conf +6 -0
- data/example/redis-sentinel2.conf +6 -0
- data/example/redis-slave.conf +541 -0
- data/example/test.rb +18 -0
- data/example/test_wait_for_failover.rb +19 -0
- data/example/test_wait_for_failover_write.rb +21 -0
- data/lib/em-synchrony/redis-sentinel.rb +13 -0
- data/lib/redis-sentinel.rb +2 -0
- data/lib/redis-sentinel/client.rb +121 -0
- data/lib/redis-sentinel/version.rb +5 -0
- data/redis-sentinel.gemspec +26 -0
- data/spec/redis-sentinel/client_spec.rb +187 -0
- data/spec/redis-sentinel/em_client_spec.rb +45 -0
- data/spec/spec_helper.rb +13 -0
- metadata +176 -0
data/example/test.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'redis'
|
2
|
+
require 'redis-sentinel'
|
3
|
+
|
4
|
+
redis = Redis.new(:master_name => "example-test",
|
5
|
+
:sentinels => [
|
6
|
+
{:host => "localhost", :port => 26379},
|
7
|
+
{:host => "localhost", :port => 26380}
|
8
|
+
])
|
9
|
+
redis.set "foo", "bar"
|
10
|
+
|
11
|
+
while true
|
12
|
+
begin
|
13
|
+
puts redis.get "foo"
|
14
|
+
rescue => e
|
15
|
+
puts "failed?", e
|
16
|
+
end
|
17
|
+
sleep 1
|
18
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'redis'
|
2
|
+
require 'redis-sentinel'
|
3
|
+
|
4
|
+
redis = Redis.new(:master_name => "example-test",
|
5
|
+
:sentinels => [
|
6
|
+
{:host => "localhost", :port => 26379},
|
7
|
+
{:host => "localhost", :port => 26380}
|
8
|
+
],
|
9
|
+
:failover_reconnect_timeout => 30)
|
10
|
+
redis.set "foo", "bar"
|
11
|
+
|
12
|
+
while true
|
13
|
+
begin
|
14
|
+
puts redis.get "foo"
|
15
|
+
rescue => e
|
16
|
+
puts "failover took too long to recover", e
|
17
|
+
end
|
18
|
+
sleep 1
|
19
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'redis'
|
2
|
+
require 'redis-sentinel'
|
3
|
+
|
4
|
+
redis = Redis.new(:master_name => "example-test",
|
5
|
+
:sentinels => [
|
6
|
+
{:host => "localhost", :port => 26379},
|
7
|
+
{:host => "localhost", :port => 26380}
|
8
|
+
],
|
9
|
+
:failover_reconnect_timeout => 30,
|
10
|
+
:failover_reconnect_wait => 0.0001)
|
11
|
+
|
12
|
+
redis.set "foo", 1
|
13
|
+
|
14
|
+
while true
|
15
|
+
begin
|
16
|
+
puts redis.incr "foo"
|
17
|
+
rescue Redis::CannotConnectError => e
|
18
|
+
puts "failover took too long to recover", e
|
19
|
+
end
|
20
|
+
sleep 1
|
21
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
require 'redis/connection/synchrony' unless defined? Redis::Connection::Synchrony
|
2
|
+
require 'redis-sentinel'
|
3
|
+
|
4
|
+
class Redis::Client
|
5
|
+
class_eval do
|
6
|
+
private
|
7
|
+
def sleep(seconds)
|
8
|
+
f = Fiber.current
|
9
|
+
EM::Timer.new(seconds) { f.resume }
|
10
|
+
Fiber.yield
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,121 @@
|
|
1
|
+
require "redis"
|
2
|
+
|
3
|
+
class Redis::Client
|
4
|
+
DEFAULT_FAILOVER_RECONNECT_WAIT_SECONDS = 0.1
|
5
|
+
|
6
|
+
class_eval do
|
7
|
+
def initialize_with_sentinel(options={})
|
8
|
+
options = options.dup # Don't touch my options
|
9
|
+
@master_name = fetch_option(options, :master_name)
|
10
|
+
@master_password = fetch_option(options, :master_password)
|
11
|
+
@sentinels = fetch_option(options, :sentinels)
|
12
|
+
@failover_reconnect_timeout = fetch_option(options, :failover_reconnect_timeout)
|
13
|
+
@failover_reconnect_wait = fetch_option(options, :failover_reconnect_wait) ||
|
14
|
+
DEFAULT_FAILOVER_RECONNECT_WAIT_SECONDS
|
15
|
+
|
16
|
+
initialize_without_sentinel(options)
|
17
|
+
end
|
18
|
+
|
19
|
+
alias initialize_without_sentinel initialize
|
20
|
+
alias initialize initialize_with_sentinel
|
21
|
+
|
22
|
+
def connect_with_sentinel
|
23
|
+
if sentinel?
|
24
|
+
auto_retry_with_timeout do
|
25
|
+
discover_master
|
26
|
+
connect_without_sentinel
|
27
|
+
end
|
28
|
+
else
|
29
|
+
connect_without_sentinel
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
alias connect_without_sentinel connect
|
34
|
+
alias connect connect_with_sentinel
|
35
|
+
|
36
|
+
def sentinel?
|
37
|
+
@master_name && @sentinels
|
38
|
+
end
|
39
|
+
|
40
|
+
def auto_retry_with_timeout(&block)
|
41
|
+
deadline = @failover_reconnect_timeout.to_i + Time.now.to_f
|
42
|
+
begin
|
43
|
+
block.call
|
44
|
+
rescue Redis::CannotConnectError
|
45
|
+
raise if Time.now.to_f > deadline
|
46
|
+
sleep @failover_reconnect_wait
|
47
|
+
retry
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def try_next_sentinel
|
52
|
+
@sentinels << @sentinels.shift
|
53
|
+
if @logger && @logger.debug?
|
54
|
+
@logger.debug "Trying next sentinel: #{@sentinels[0][:host]}:#{@sentinels[0][:port]}"
|
55
|
+
end
|
56
|
+
return @sentinels[0]
|
57
|
+
end
|
58
|
+
|
59
|
+
def discover_master
|
60
|
+
while true
|
61
|
+
sentinel = redis_sentinels[@sentinels[0]]
|
62
|
+
|
63
|
+
begin
|
64
|
+
host, port = sentinel.sentinel("get-master-addr-by-name", @master_name)
|
65
|
+
if !host && !port
|
66
|
+
raise Redis::ConnectionError.new("No master named: #{@master_name}")
|
67
|
+
end
|
68
|
+
is_down, runid = sentinel.sentinel("is-master-down-by-addr", host, port)
|
69
|
+
break
|
70
|
+
rescue Redis::CannotConnectError
|
71
|
+
try_next_sentinel
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
if is_down.to_s == "1" || runid == '?'
|
76
|
+
raise Redis::CannotConnectError.new("The master: #{@master_name} is currently not available.")
|
77
|
+
else
|
78
|
+
self.host = host
|
79
|
+
self.port = port
|
80
|
+
self.password = @master_password
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def reconnect_with_sentinels
|
85
|
+
redis_sentinels.each do |config, sentinel|
|
86
|
+
sentinel.client.reconnect
|
87
|
+
end
|
88
|
+
reconnect_without_sentinels
|
89
|
+
end
|
90
|
+
|
91
|
+
alias reconnect_without_sentinels reconnect
|
92
|
+
alias reconnect reconnect_with_sentinels
|
93
|
+
|
94
|
+
def call_with_readonly_protection(*args, &block)
|
95
|
+
tries = 0
|
96
|
+
call_without_readonly_protection(*args, &block)
|
97
|
+
rescue Redis::CommandError => e
|
98
|
+
if e.message == "READONLY You can't write against a read only slave."
|
99
|
+
reconnect
|
100
|
+
retry if (tries += 1) < 4
|
101
|
+
else
|
102
|
+
raise
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
alias call_without_readonly_protection call
|
107
|
+
alias call call_with_readonly_protection
|
108
|
+
|
109
|
+
private
|
110
|
+
|
111
|
+
def fetch_option(options, key)
|
112
|
+
options.delete(key) || options.delete(key.to_s)
|
113
|
+
end
|
114
|
+
|
115
|
+
def redis_sentinels
|
116
|
+
@redis_sentinels ||= Hash.new do |hash, config|
|
117
|
+
hash[config] = Redis.new(config)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'redis-sentinel/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |gem|
|
7
|
+
gem.name = "redis-sentinel2"
|
8
|
+
gem.version = Redis::Sentinel::VERSION
|
9
|
+
gem.authors = ["Richard Huang"]
|
10
|
+
gem.email = ["flyerhzm@gmail.com"]
|
11
|
+
gem.description = %q{another redis automatic master/slave failover solution for ruby by using built-in redis sentinel. This version works with redis 2.2.2}
|
12
|
+
gem.summary = %q{another redis automatic master/slave failover solution for ruby by using built-in redis sentinel. This version works with redis 2.2.2}
|
13
|
+
gem.homepage = "https://github.com/flyerhzm/redis-sentinel"
|
14
|
+
|
15
|
+
gem.files = `git ls-files`.split($/)
|
16
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
17
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
18
|
+
gem.require_paths = ["lib"]
|
19
|
+
|
20
|
+
gem.add_dependency "redis", '2.2.2'
|
21
|
+
gem.add_development_dependency "rake"
|
22
|
+
gem.add_development_dependency "rspec"
|
23
|
+
gem.add_development_dependency "eventmachine"
|
24
|
+
gem.add_development_dependency "em-synchrony"
|
25
|
+
gem.add_development_dependency "hiredis"
|
26
|
+
end
|
@@ -0,0 +1,187 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Redis::Client do
|
4
|
+
let(:client) { double("Client", :reconnect => true) }
|
5
|
+
let(:redis) { double("Redis", :sentinel => ["remote.server", 8888], :client => client) }
|
6
|
+
|
7
|
+
subject { Redis::Client.new(:master_name => "master", :master_password => "foobar",
|
8
|
+
:sentinels => [{:host => "localhost", :port => 26379},
|
9
|
+
{:host => "localhost", :port => 26380}]) }
|
10
|
+
|
11
|
+
before { Redis.stub(:new).and_return(redis) }
|
12
|
+
|
13
|
+
context "#sentinel?" do
|
14
|
+
it "should be true if passing sentiels and master_name options" do
|
15
|
+
expect(Redis::Client.new(:master_name => "master", :sentinels => [{:host => "localhost", :port => 26379}, {:host => "localhost", :port => 26380}])).to be_sentinel
|
16
|
+
end
|
17
|
+
|
18
|
+
it "should not be true if not passing sentinels and master_name options" do
|
19
|
+
expect(Redis::Client.new).not_to be_sentinel
|
20
|
+
end
|
21
|
+
|
22
|
+
it "should not be true if passing sentinels option but not master_name option" do
|
23
|
+
expect(Redis::Client.new(:sentinels => [{:host => "localhost", :port => 26379}, {:host => "localhost", :port => 26380}])).not_to be_sentinel
|
24
|
+
end
|
25
|
+
|
26
|
+
it "should not be true if passing master_name option but not sentinels option" do
|
27
|
+
expect(Redis::Client.new(:master_name => "master")).not_to be_sentinel
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
context "#try_next_sentinel" do
|
32
|
+
it "should return next sentinel server" do
|
33
|
+
expect(subject.try_next_sentinel).to eq({:host => "localhost", :port => 26380})
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
context "#discover_master" do
|
38
|
+
it "gets the current master" do
|
39
|
+
redis.should_receive(:sentinel).
|
40
|
+
with("get-master-addr-by-name", "master")
|
41
|
+
redis.should_receive(:sentinel).
|
42
|
+
with("is-master-down-by-addr", "remote.server", 8888)
|
43
|
+
subject.discover_master
|
44
|
+
end
|
45
|
+
|
46
|
+
it "should update options" do
|
47
|
+
redis.should_receive(:sentinel).
|
48
|
+
with("is-master-down-by-addr", "remote.server", 8888).once.
|
49
|
+
and_return([0, "abc"])
|
50
|
+
subject.discover_master
|
51
|
+
expect(subject.host).to eq "remote.server"
|
52
|
+
expect(subject.port).to eq 8888
|
53
|
+
expect(subject.password).to eq "foobar"
|
54
|
+
end
|
55
|
+
|
56
|
+
it "should not update options before newly promoted master is ready" do
|
57
|
+
redis.should_receive(:sentinel).
|
58
|
+
with("is-master-down-by-addr", "remote.server", 8888).twice.
|
59
|
+
and_return([1, "abc"], [0, "?"])
|
60
|
+
2.times do
|
61
|
+
expect do
|
62
|
+
subject.discover_master
|
63
|
+
end.to raise_error(Redis::CannotConnectError, /currently not available/)
|
64
|
+
expect(subject.host).not_to eq "remote.server"
|
65
|
+
expect(subject.port).not_to eq 8888
|
66
|
+
expect(subject.password).not_to eq "foobar"
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
it "should not use a password" do
|
71
|
+
Redis.should_receive(:new).with({:host => "localhost", :port => 26379})
|
72
|
+
redis.should_receive(:sentinel).with("get-master-addr-by-name", "master")
|
73
|
+
redis.should_receive(:sentinel).with("is-master-down-by-addr", "remote.server", 8888)
|
74
|
+
|
75
|
+
redis = Redis::Client.new(:master_name => "master", :sentinels => [{:host => "localhost", :port => 26379}])
|
76
|
+
redis.discover_master
|
77
|
+
|
78
|
+
expect(redis.host).to eq "remote.server"
|
79
|
+
expect(redis.port).to eq 8888
|
80
|
+
expect(redis.password).to eq nil
|
81
|
+
end
|
82
|
+
|
83
|
+
it "should select next sentinel" do
|
84
|
+
Redis.should_receive(:new).with({:host => "localhost", :port => 26379})
|
85
|
+
redis.should_receive(:sentinel).
|
86
|
+
with("get-master-addr-by-name", "master").
|
87
|
+
and_raise(Redis::CannotConnectError)
|
88
|
+
Redis.should_receive(:new).with({:host => "localhost", :port => 26380})
|
89
|
+
redis.should_receive(:sentinel).
|
90
|
+
with("get-master-addr-by-name", "master")
|
91
|
+
redis.should_receive(:sentinel).
|
92
|
+
with("is-master-down-by-addr", "remote.server", 8888)
|
93
|
+
subject.discover_master
|
94
|
+
expect(subject.host).to eq "remote.server"
|
95
|
+
expect(subject.port).to eq 8888
|
96
|
+
expect(subject.password).to eq "foobar"
|
97
|
+
end
|
98
|
+
|
99
|
+
describe "memoizing sentinel connections" do
|
100
|
+
it "does not reconnect to the sentinels" do
|
101
|
+
Redis.should_receive(:new).once
|
102
|
+
|
103
|
+
subject.discover_master
|
104
|
+
subject.discover_master
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
context "#auto_retry_with_timeout" do
|
110
|
+
context "no failover reconnect timeout set" do
|
111
|
+
subject { Redis::Client.new }
|
112
|
+
|
113
|
+
it "does not sleep" do
|
114
|
+
subject.should_not_receive(:sleep)
|
115
|
+
expect do
|
116
|
+
subject.auto_retry_with_timeout { raise Redis::CannotConnectError }
|
117
|
+
end.to raise_error(Redis::CannotConnectError)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
context "the failover reconnect timeout is set" do
|
122
|
+
subject { Redis::Client.new(:failover_reconnect_timeout => 3) }
|
123
|
+
|
124
|
+
before(:each) do
|
125
|
+
subject.stub(:sleep)
|
126
|
+
end
|
127
|
+
|
128
|
+
it "only raises after the failover_reconnect_timeout" do
|
129
|
+
called_counter = 0
|
130
|
+
Time.stub(:now).and_return(100, 101, 102, 103, 104, 105)
|
131
|
+
|
132
|
+
begin
|
133
|
+
subject.auto_retry_with_timeout do
|
134
|
+
called_counter += 1
|
135
|
+
raise Redis::CannotConnectError
|
136
|
+
end
|
137
|
+
rescue Redis::CannotConnectError
|
138
|
+
end
|
139
|
+
|
140
|
+
called_counter.should == 4
|
141
|
+
end
|
142
|
+
|
143
|
+
it "sleeps the default wait time" do
|
144
|
+
Time.stub(:now).and_return(100, 101, 105)
|
145
|
+
subject.should_receive(:sleep).with(0.1)
|
146
|
+
begin
|
147
|
+
subject.auto_retry_with_timeout { raise Redis::CannotConnectError }
|
148
|
+
rescue Redis::CannotConnectError
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
it "does not catch other errors" do
|
153
|
+
subject.should_not_receive(:sleep)
|
154
|
+
expect do
|
155
|
+
subject.auto_retry_with_timeout { raise Redis::ConnectionError }
|
156
|
+
end.to raise_error(Redis::ConnectionError)
|
157
|
+
end
|
158
|
+
|
159
|
+
context "configured wait time" do
|
160
|
+
subject { Redis::Client.new(:failover_reconnect_timeout => 3,
|
161
|
+
:failover_reconnect_wait => 0.01) }
|
162
|
+
|
163
|
+
it "uses the configured wait time" do
|
164
|
+
Time.stub(:now).and_return(100, 101, 105)
|
165
|
+
subject.should_receive(:sleep).with(0.01)
|
166
|
+
begin
|
167
|
+
subject.auto_retry_with_timeout { raise Redis::CannotConnectError }
|
168
|
+
rescue Redis::CannotConnectError
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
context "#reconnect" do
|
176
|
+
it "calls reconnect on each sentinel client" do
|
177
|
+
subject.stub(:connect)
|
178
|
+
subject.discover_master
|
179
|
+
subject.send(:redis_sentinels).each do |config, sentinel|
|
180
|
+
sentinel.client.should_receive(:reconnect)
|
181
|
+
end
|
182
|
+
|
183
|
+
subject.reconnect
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
require "em-synchrony/redis-sentinel"
|
3
|
+
require "eventmachine"
|
4
|
+
|
5
|
+
describe Redis::Client do
|
6
|
+
context "#auto_retry_with_timeout" do
|
7
|
+
subject { described_class.new(:failover_reconnect_timeout => 3,
|
8
|
+
:failover_reconnect_wait => 0.1) }
|
9
|
+
context "configured wait time" do
|
10
|
+
|
11
|
+
it "uses the wait time and blocks em" do
|
12
|
+
Time.stub(:now).and_return(100, 101, 105)
|
13
|
+
flag = false; EM.next_tick { flag = true }
|
14
|
+
subject.should_receive(:sleep).with(0.1).and_return(0.1)
|
15
|
+
begin
|
16
|
+
subject.auto_retry_with_timeout { raise Redis::CannotConnectError }
|
17
|
+
rescue Redis::CannotConnectError
|
18
|
+
end
|
19
|
+
flag.should be_false
|
20
|
+
end
|
21
|
+
|
22
|
+
it "uses the wait time and doesn't block em" do
|
23
|
+
Time.stub(:now).and_return(100, 101, 105)
|
24
|
+
flag = false; EM.next_tick { flag = true }
|
25
|
+
begin
|
26
|
+
subject.auto_retry_with_timeout { raise Redis::CannotConnectError }
|
27
|
+
rescue Redis::CannotConnectError
|
28
|
+
end
|
29
|
+
flag.should be_true
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
around(:each) do |testcase|
|
35
|
+
EM.run do
|
36
|
+
Fiber.new do
|
37
|
+
begin
|
38
|
+
testcase.call
|
39
|
+
ensure
|
40
|
+
EM.stop
|
41
|
+
end
|
42
|
+
end.resume
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|