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