redis-sentinel2 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
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,2 @@
1
+ require "redis-sentinel/version"
2
+ require "redis-sentinel/client"
@@ -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,5 @@
1
+ class Redis
2
+ module Sentinel
3
+ VERSION = "1.3.0"
4
+ end
5
+ 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