redis-sentinel 1.1.2 → 1.1.3
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/README.md +25 -4
- data/example/test_wait_for_failover.rb +19 -0
- data/lib/redis-sentinel/client.rb +40 -7
- data/lib/redis-sentinel/version.rb +1 -1
- data/spec/redis-sentinel/client_spec.rb +92 -8
- metadata +3 -2
data/README.md
CHANGED
@@ -27,6 +27,15 @@ Specify the sentinel servers and master name
|
|
27
27
|
|
28
28
|
Redis.new(master_name: "master1", sentinels: [{host: "localhost", port: 26379}, {host: "localhost", port: 26380}])
|
29
29
|
|
30
|
+
There are two additional options:
|
31
|
+
|
32
|
+
1. `:failover_reconnect_timeout` (seconds) will block for that long when
|
33
|
+
redis is unreachable to give failover enough time to take place. Does
|
34
|
+
not wait if not given, or time given is 0.
|
35
|
+
|
36
|
+
2. `:failover_reconnect_wait` (seconds) how long to sleep after each
|
37
|
+
failed reconnect during a failover event. Defaults to 0.1s.
|
38
|
+
|
30
39
|
## Example
|
31
40
|
|
32
41
|
start redis master server, listen on port 16379
|
@@ -56,10 +65,22 @@ $ bundle exec ruby example/test.rb
|
|
56
65
|
|
57
66
|
You will see output "bar" every second. Let's try the failover process.
|
58
67
|
|
59
|
-
1.
|
60
|
-
2.
|
61
|
-
3.
|
62
|
-
|
68
|
+
1. Stop redis master server.
|
69
|
+
2. You will see error message output.
|
70
|
+
3. Redis sentinel promote redis slave server to master. During this time
|
71
|
+
you will see errors instead of "bar" while the failover is happening.
|
72
|
+
4. Then you will see correct "bar" output every second again.
|
73
|
+
|
74
|
+
## Example of Failover Timeout
|
75
|
+
Run the same example code above but run:
|
76
|
+
|
77
|
+
```
|
78
|
+
$ bundle exec ruby example/test_wait_for_failover.rb
|
79
|
+
```
|
80
|
+
|
81
|
+
You will see the stream of "bar" will stop while failover is taking
|
82
|
+
place and will resume once it has completed, provided that failover
|
83
|
+
takes less than 30 seconds.
|
63
84
|
|
64
85
|
## Contributing
|
65
86
|
|
@@ -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
|
@@ -1,10 +1,16 @@
|
|
1
1
|
require "redis"
|
2
2
|
|
3
3
|
class Redis::Client
|
4
|
+
DEFAULT_FAILOVER_RECONNECT_WAIT_SECONDS = 0.1
|
5
|
+
|
4
6
|
class_eval do
|
5
7
|
def initialize_with_sentinel(options={})
|
6
|
-
@master_name = options
|
7
|
-
@sentinels = options
|
8
|
+
@master_name = fetch_option(options, :master_name)
|
9
|
+
@sentinels = fetch_option(options, :sentinels)
|
10
|
+
@failover_reconnect_timeout = fetch_option(options, :failover_reconnect_timeout)
|
11
|
+
@failover_reconnect_wait = fetch_option(options, :failover_reconnect_wait) ||
|
12
|
+
DEFAULT_FAILOVER_RECONNECT_WAIT_SECONDS
|
13
|
+
|
8
14
|
initialize_without_sentinel(options)
|
9
15
|
end
|
10
16
|
|
@@ -12,8 +18,14 @@ class Redis::Client
|
|
12
18
|
alias initialize initialize_with_sentinel
|
13
19
|
|
14
20
|
def connect_with_sentinel
|
15
|
-
|
16
|
-
|
21
|
+
if sentinel?
|
22
|
+
auto_retry_with_timeout do
|
23
|
+
discover_master
|
24
|
+
connect_without_sentinel
|
25
|
+
end
|
26
|
+
else
|
27
|
+
connect_without_sentinel
|
28
|
+
end
|
17
29
|
end
|
18
30
|
|
19
31
|
alias connect_without_sentinel connect
|
@@ -23,6 +35,17 @@ class Redis::Client
|
|
23
35
|
@master_name && @sentinels
|
24
36
|
end
|
25
37
|
|
38
|
+
def auto_retry_with_timeout(&block)
|
39
|
+
deadline = @failover_reconnect_timeout.to_i + Time.now.to_f
|
40
|
+
begin
|
41
|
+
block.call
|
42
|
+
rescue Redis::CannotConnectError
|
43
|
+
raise if Time.now.to_f > deadline
|
44
|
+
sleep @failover_reconnect_wait
|
45
|
+
retry
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
26
49
|
def try_next_sentinel
|
27
50
|
@sentinels << @sentinels.shift
|
28
51
|
if @logger && @logger.debug?
|
@@ -32,10 +55,8 @@ class Redis::Client
|
|
32
55
|
end
|
33
56
|
|
34
57
|
def discover_master
|
35
|
-
masters = []
|
36
|
-
|
37
58
|
while true
|
38
|
-
sentinel =
|
59
|
+
sentinel = redis_sentinels[@sentinels[0]]
|
39
60
|
|
40
61
|
begin
|
41
62
|
host, port = sentinel.sentinel("get-master-addr-by-name", @master_name)
|
@@ -50,5 +71,17 @@ class Redis::Client
|
|
50
71
|
end
|
51
72
|
end
|
52
73
|
end
|
74
|
+
|
75
|
+
private
|
76
|
+
|
77
|
+
def fetch_option(options, key)
|
78
|
+
options.delete(key) || options.delete(key.to_s)
|
79
|
+
end
|
80
|
+
|
81
|
+
def redis_sentinels
|
82
|
+
@redis_sentinels ||= Hash.new do |hash, config|
|
83
|
+
hash[config] = Redis.new(config)
|
84
|
+
end
|
85
|
+
end
|
53
86
|
end
|
54
87
|
end
|
@@ -1,6 +1,14 @@
|
|
1
1
|
require "spec_helper"
|
2
2
|
|
3
3
|
describe Redis::Client do
|
4
|
+
let(:redis) { mock("Redis", :sentinel => ["remote.server", 8888])}
|
5
|
+
|
6
|
+
subject { Redis::Client.new(:master_name => "master",
|
7
|
+
:sentinels => [{:host => "localhost", :port => 26379},
|
8
|
+
{:host => "localhost", :port => 26380}]) }
|
9
|
+
|
10
|
+
before { Redis.stub(:new).and_return(redis) }
|
11
|
+
|
4
12
|
context "#sentinel?" do
|
5
13
|
it "should be true if passing sentiels and master_name options" do
|
6
14
|
expect(Redis::Client.new(:master_name => "master", :sentinels => [{:host => "localhost", :port => 26379}, {:host => "localhost", :port => 26380}])).to be_sentinel
|
@@ -20,21 +28,97 @@ describe Redis::Client do
|
|
20
28
|
end
|
21
29
|
|
22
30
|
context "#try_next_sentinel" do
|
23
|
-
let(:client) { Redis::Client.new(:master_name => "master", :sentinels => [{:host => "localhost", :port => 26379}, {:host => "localhost", :port => 26380}]) }
|
24
|
-
|
25
31
|
it "should return next sentinel server" do
|
26
|
-
expect(
|
32
|
+
expect(subject.try_next_sentinel).to eq({:host => "localhost", :port => 26380})
|
27
33
|
end
|
28
34
|
end
|
29
35
|
|
30
36
|
context "#discover_master" do
|
31
|
-
|
32
|
-
|
37
|
+
it "gets the current master" do
|
38
|
+
redis.should_receive(:sentinel).
|
39
|
+
with("get-master-addr-by-name", "master")
|
40
|
+
subject.discover_master
|
41
|
+
end
|
33
42
|
|
34
43
|
it "should update options" do
|
35
|
-
|
36
|
-
expect(
|
37
|
-
expect(
|
44
|
+
subject.discover_master
|
45
|
+
expect(subject.host).to eq "remote.server"
|
46
|
+
expect(subject.port).to eq 8888
|
47
|
+
end
|
48
|
+
|
49
|
+
describe "memoizing sentinel connections" do
|
50
|
+
it "does not reconnect to the sentinels" do
|
51
|
+
Redis.should_receive(:new).once
|
52
|
+
|
53
|
+
subject.discover_master
|
54
|
+
subject.discover_master
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
context "#auto_retry_with_timeout" do
|
60
|
+
context "no failover reconnect timeout set" do
|
61
|
+
subject { Redis::Client.new }
|
62
|
+
|
63
|
+
it "does not sleep" do
|
64
|
+
subject.should_not_receive(:sleep)
|
65
|
+
expect do
|
66
|
+
subject.auto_retry_with_timeout { raise Redis::CannotConnectError }
|
67
|
+
end.to raise_error(Redis::CannotConnectError)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
context "the failover reconnect timeout is set" do
|
72
|
+
subject { Redis::Client.new(:failover_reconnect_timeout => 3) }
|
73
|
+
|
74
|
+
before(:each) do
|
75
|
+
subject.stub(:sleep)
|
76
|
+
end
|
77
|
+
|
78
|
+
it "only raises after the failover_reconnect_timeout" do
|
79
|
+
called_counter = 0
|
80
|
+
Time.stub(:now).and_return(100, 101, 102, 103, 104, 105)
|
81
|
+
|
82
|
+
begin
|
83
|
+
subject.auto_retry_with_timeout do
|
84
|
+
called_counter += 1
|
85
|
+
raise Redis::CannotConnectError
|
86
|
+
end
|
87
|
+
rescue Redis::CannotConnectError
|
88
|
+
end
|
89
|
+
|
90
|
+
called_counter.should == 4
|
91
|
+
end
|
92
|
+
|
93
|
+
it "sleeps the default wait time" do
|
94
|
+
Time.stub(:now).and_return(100, 101, 105)
|
95
|
+
subject.should_receive(:sleep).with(0.1)
|
96
|
+
begin
|
97
|
+
subject.auto_retry_with_timeout { raise Redis::CannotConnectError }
|
98
|
+
rescue Redis::CannotConnectError
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
it "does not catch other errors" do
|
103
|
+
subject.should_not_receive(:sleep)
|
104
|
+
expect do
|
105
|
+
subject.auto_retry_with_timeout { raise Redis::ConnectionError }
|
106
|
+
end.to raise_error(Redis::ConnectionError)
|
107
|
+
end
|
108
|
+
|
109
|
+
context "configured wait time" do
|
110
|
+
subject { Redis::Client.new(:failover_reconnect_timeout => 3,
|
111
|
+
:failover_reconnect_wait => 0.01) }
|
112
|
+
|
113
|
+
it "uses the configured wait time" do
|
114
|
+
Time.stub(:now).and_return(100, 101, 105)
|
115
|
+
subject.should_receive(:sleep).with(0.01)
|
116
|
+
begin
|
117
|
+
subject.auto_retry_with_timeout { raise Redis::CannotConnectError }
|
118
|
+
rescue Redis::CannotConnectError
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
38
122
|
end
|
39
123
|
end
|
40
124
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: redis-sentinel
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.1.
|
4
|
+
version: 1.1.3
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2013-01-
|
12
|
+
date: 2013-01-30 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: redis
|
@@ -61,6 +61,7 @@ files:
|
|
61
61
|
- example/redis-sentinel2.conf
|
62
62
|
- example/redis-slave.conf
|
63
63
|
- example/test.rb
|
64
|
+
- example/test_wait_for_failover.rb
|
64
65
|
- lib/redis-sentinel.rb
|
65
66
|
- lib/redis-sentinel/client.rb
|
66
67
|
- lib/redis-sentinel/version.rb
|