redis-sentinel2 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|