redis-sentinel 1.1.2 → 1.1.3

Sign up to get free protection for your applications and to get access to all the features.
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. stop redis master server
60
- 2. you will see error message output
61
- 3. redis sentinel promote redis slave server to master
62
- 4. then you will see correct "bar" output every second again
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.delete(:master_name) || options.delete("master_name")
7
- @sentinels = options.delete(:sentinels) || options.delete("sentinels")
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
- discover_master if sentinel?
16
- connect_without_sentinel
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 = Redis.new(@sentinels[0])
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,5 +1,5 @@
1
1
  class Redis
2
2
  module Sentinel
3
- VERSION = "1.1.2"
3
+ VERSION = "1.1.3"
4
4
  end
5
5
  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(client.try_next_sentinel).to eq({:host => "localhost", :port => 26380})
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
- let(:client) { Redis::Client.new(:master_name => "master", :sentinels => [{:host => "localhost", :port => 26379}, {:host => "localhost", :port => 26380}]) }
32
- before { Redis.any_instance.should_receive(:sentinel).with("get-master-addr-by-name", "master").and_return(["remote.server", 8888]) }
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
- client.discover_master
36
- expect(client.host).to eq "remote.server"
37
- expect(client.port).to eq 8888
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.2
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-20 00:00:00.000000000 Z
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