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 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