redis-sentinel 1.3.0 → 1.4.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +5 -0
- data/README.md +12 -13
- data/example/redis-sentinel1.conf +0 -1
- data/example/redis-sentinel2.conf +0 -1
- data/example/redis-sentinel3.conf +5 -0
- data/lib/redis-sentinel/client.rb +51 -29
- data/lib/redis-sentinel/version.rb +1 -1
- data/spec/redis-sentinel/client_spec.rb +87 -86
- data/spec/redis-sentinel/em_client_spec.rb +5 -5
- metadata +4 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0abad2c1cbcc6e342c7f362b9d6c6ad6ebca6a81
|
4
|
+
data.tar.gz: e300689d63cec3fec3f9d2ace82d61d828baf4b3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5472ab6c41d452eb5550ed58698871956a57deb51d354c95e154d768b4390871f3f302170ac03987029ad02a5973a471864e4034d2e0195385b9f4f6839d667f
|
7
|
+
data.tar.gz: 4c781a86527902cfc4a9fee13b7b0139b39245eadb894f98647e2f3e0a158c5d5128d2218577830b1b1d0d22df963126a7c1f376522952eb1c1501e0f960eb59
|
data/CHANGELOG.md
CHANGED
data/README.md
CHANGED
@@ -13,6 +13,11 @@ Add this line to your application's Gemfile:
|
|
13
13
|
|
14
14
|
gem 'redis-sentinel'
|
15
15
|
|
16
|
+
If you are using redis-server less than 2.6.10, please use
|
17
|
+
redis-sentinel 1.3.0
|
18
|
+
|
19
|
+
gem 'redis-sentinel', '~> 1.3.0'
|
20
|
+
|
16
21
|
And then execute:
|
17
22
|
|
18
23
|
$ bundle
|
@@ -27,6 +32,11 @@ Specify the sentinel servers and master name
|
|
27
32
|
|
28
33
|
Redis.new(master_name: "master1", sentinels: [{host: "localhost", port: 26379}, {host: "localhost", port: 26380}])
|
29
34
|
|
35
|
+
Sentinels can also be specified using a URI. This URI syntax is required when using Rails.config.cache_store:
|
36
|
+
|
37
|
+
config.cache_store = :redis_store, { master_name: "master1",
|
38
|
+
sentinels: ['sentinel://localhost:26379', 'sentinel://localhost:26380'] }
|
39
|
+
|
30
40
|
There are two additional options:
|
31
41
|
|
32
42
|
1. `:failover_reconnect_timeout` (seconds) will block for that long when
|
@@ -55,6 +65,7 @@ Start 2 sentinel servers
|
|
55
65
|
```
|
56
66
|
$ redis-server example/redis-sentinel1.conf --sentinel
|
57
67
|
$ redis-server example/redis-sentinel2.conf --sentinel
|
68
|
+
$ redis-server example/redis-sentinel3.conf --sentinel
|
58
69
|
```
|
59
70
|
|
60
71
|
Run example/test.rb, which will query value of key "foo" every second.
|
@@ -84,19 +95,7 @@ takes less than 30 seconds.
|
|
84
95
|
|
85
96
|
## Authors and Contributors
|
86
97
|
|
87
|
-
|
88
|
-
* [Donald Plummer](https://github.com/dplummer) - Add wait / timeout for
|
89
|
-
redis connection
|
90
|
-
* [Rafał Michalski](https://github.com/royaltm) - Ensure promoted slave
|
91
|
-
become master / Add redis synchrony support
|
92
|
-
* [Zachary Anker](https://github.com/zanker) - Add redis authentication
|
93
|
-
support
|
94
|
-
* [Nick Deteffen](https://github.com/nick-desteffen) - Add ability to
|
95
|
-
reconnect all redis sentinel clients
|
96
|
-
* [Carlos Paramio](https://github.com/carlosparamio) - Avoid the config
|
97
|
-
gets modified
|
98
|
-
* [Michael Gee](https://github.com/mikegee) - Reconnect if redis suddenly
|
99
|
-
becomes read-only.
|
98
|
+
[https://github.com/flyerhzm/redis-sentinel/graphs/contributors](https://github.com/flyerhzm/redis-sentinel/graphs/contributors)
|
100
99
|
|
101
100
|
Please fork and contribute, any help in making this project better is appreciated!
|
102
101
|
|
@@ -4,11 +4,14 @@ class Redis::Client
|
|
4
4
|
DEFAULT_FAILOVER_RECONNECT_WAIT_SECONDS = 0.1
|
5
5
|
|
6
6
|
class_eval do
|
7
|
+
attr_reader :current_sentinel
|
8
|
+
attr_reader :current_sentinel_options
|
9
|
+
|
7
10
|
def initialize_with_sentinel(options={})
|
8
11
|
options = options.dup # Don't touch my options
|
9
12
|
@master_name = fetch_option(options, :master_name)
|
10
13
|
@master_password = fetch_option(options, :master_password)
|
11
|
-
@
|
14
|
+
@sentinels_options = _parse_sentinel_options(fetch_option(options, :sentinels))
|
12
15
|
@failover_reconnect_timeout = fetch_option(options, :failover_reconnect_timeout)
|
13
16
|
@failover_reconnect_wait = fetch_option(options, :failover_reconnect_wait) ||
|
14
17
|
DEFAULT_FAILOVER_RECONNECT_WAIT_SECONDS
|
@@ -34,7 +37,7 @@ class Redis::Client
|
|
34
37
|
alias connect connect_with_sentinel
|
35
38
|
|
36
39
|
def sentinel?
|
37
|
-
@master_name && @
|
40
|
+
@master_name && @sentinels_options
|
38
41
|
end
|
39
42
|
|
40
43
|
def auto_retry_with_timeout(&block)
|
@@ -49,45 +52,52 @@ class Redis::Client
|
|
49
52
|
end
|
50
53
|
|
51
54
|
def try_next_sentinel
|
52
|
-
|
53
|
-
if
|
54
|
-
@logger.debug "Trying next sentinel: #{
|
55
|
+
sentinel_options = @sentinels_options.shift
|
56
|
+
if sentinel_options
|
57
|
+
@logger.debug "Trying next sentinel: #{sentinel_options[:host]}:#{sentinel_options[:port]}" if @logger && @logger.debug?
|
58
|
+
@current_sentinel_options = sentinel_options
|
59
|
+
@current_sentinel = Redis.new sentinel_options
|
60
|
+
else
|
61
|
+
raise Redis::CannotConnectError
|
55
62
|
end
|
56
|
-
|
63
|
+
end
|
64
|
+
|
65
|
+
def refresh_sentinels_list
|
66
|
+
responses = current_sentinel.sentinel("sentinels", @master_name)
|
67
|
+
@sentinels_options = responses.map do |response|
|
68
|
+
{:host => response[3], :port => response[5]}
|
69
|
+
end.unshift(:host => current_sentinel_options[:host], :port => current_sentinel_options[:port])
|
57
70
|
end
|
58
71
|
|
59
72
|
def discover_master
|
60
73
|
while true
|
61
|
-
|
74
|
+
try_next_sentinel
|
62
75
|
|
63
76
|
begin
|
64
|
-
|
65
|
-
if
|
66
|
-
|
77
|
+
master_host, master_port = current_sentinel.sentinel("get-master-addr-by-name", @master_name)
|
78
|
+
if master_host && master_port
|
79
|
+
# An ip:port pair
|
80
|
+
@options.merge!(:host => master_host, :port => master_port.to_i, :password => @master_password)
|
81
|
+
refresh_sentinels_list
|
82
|
+
break
|
83
|
+
else
|
84
|
+
# A null reply
|
67
85
|
end
|
68
|
-
|
69
|
-
|
86
|
+
rescue Redis::CommandError
|
87
|
+
# An -IDONTKNOWN reply
|
70
88
|
rescue Redis::CannotConnectError
|
71
|
-
|
89
|
+
# faile to connect to current sentinel server
|
72
90
|
end
|
73
91
|
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
|
-
@options.merge!(:host => host, :port => port.to_i, :password => @master_password)
|
79
|
-
end
|
80
92
|
end
|
81
93
|
|
82
|
-
def
|
83
|
-
|
84
|
-
|
85
|
-
end
|
86
|
-
reconnect_without_sentinels
|
94
|
+
def disconnect_with_sentinels
|
95
|
+
current_sentinel.client.disconnect if current_sentinel
|
96
|
+
disconnect_without_sentinels
|
87
97
|
end
|
88
98
|
|
89
|
-
alias
|
90
|
-
alias
|
99
|
+
alias disconnect_without_sentinels disconnect
|
100
|
+
alias disconnect disconnect_with_sentinels
|
91
101
|
|
92
102
|
def call_with_readonly_protection(*args, &block)
|
93
103
|
tries = 0
|
@@ -110,10 +120,22 @@ class Redis::Client
|
|
110
120
|
options.delete(key) || options.delete(key.to_s)
|
111
121
|
end
|
112
122
|
|
113
|
-
def
|
114
|
-
|
115
|
-
|
123
|
+
def _parse_sentinel_options(options)
|
124
|
+
return if options.nil?
|
125
|
+
|
126
|
+
sentinel_options = []
|
127
|
+
options.each do |sentinel_option|
|
128
|
+
if sentinel_option.is_a?(Hash)
|
129
|
+
sentinel_options << sentinel_option
|
130
|
+
else
|
131
|
+
uri = URI.parse(sentinel_option)
|
132
|
+
sentinel_options << {
|
133
|
+
host: uri.host,
|
134
|
+
port: uri.port
|
135
|
+
}
|
136
|
+
end
|
116
137
|
end
|
138
|
+
sentinel_options
|
117
139
|
end
|
118
140
|
end
|
119
141
|
end
|
@@ -4,105 +4,107 @@ describe Redis::Client do
|
|
4
4
|
let(:client) { double("Client", :reconnect => true) }
|
5
5
|
let(:redis) { double("Redis", :sentinel => ["remote.server", 8888], :client => client) }
|
6
6
|
|
7
|
+
let(:sentinels) do
|
8
|
+
[
|
9
|
+
{ :host => "localhost", :port => 26379 },
|
10
|
+
'sentinel://localhost:26380'
|
11
|
+
]
|
12
|
+
end
|
13
|
+
|
7
14
|
subject { Redis::Client.new(:master_name => "master", :master_password => "foobar",
|
8
|
-
:sentinels =>
|
9
|
-
{:host => "localhost", :port => 26380}]) }
|
15
|
+
:sentinels => sentinels) }
|
10
16
|
|
11
|
-
before
|
17
|
+
before do
|
18
|
+
allow(Redis).to receive(:new).and_return(redis)
|
19
|
+
end
|
12
20
|
|
13
21
|
context "#sentinel?" do
|
14
22
|
it "should be true if passing sentiels and master_name options" do
|
15
|
-
expect(
|
23
|
+
expect(subject).to be_sentinel
|
16
24
|
end
|
17
25
|
|
18
|
-
it "should not be true if not passing sentinels and
|
26
|
+
it "should not be true if not passing sentinels and master_name options" do
|
19
27
|
expect(Redis::Client.new).not_to be_sentinel
|
20
28
|
end
|
21
29
|
|
22
30
|
it "should not be true if passing sentinels option but not master_name option" do
|
23
|
-
|
31
|
+
client = Redis::Client.new(
|
32
|
+
:sentinels => [
|
33
|
+
{:host => "localhost", :port => 26379},
|
34
|
+
{:host => "localhost", :port => 26380}
|
35
|
+
])
|
36
|
+
expect(client).not_to be_sentinel
|
24
37
|
end
|
25
38
|
|
26
39
|
it "should not be true if passing master_name option but not sentinels option" do
|
27
|
-
|
40
|
+
client = Redis::Client.new(:master_name => "master")
|
41
|
+
expect(client).not_to be_sentinel
|
28
42
|
end
|
29
|
-
end
|
30
43
|
|
31
|
-
|
32
|
-
|
33
|
-
|
44
|
+
it "should be true if passing master_name, and sentinels as uri" do
|
45
|
+
client = Redis::Client.new(:master_name => "master",
|
46
|
+
:sentinels => %w(sentinel://localhost:26379 sentinel://localhost:26380))
|
47
|
+
expect(client).to be_sentinel
|
34
48
|
end
|
35
49
|
end
|
36
50
|
|
37
|
-
context "#
|
38
|
-
it "
|
39
|
-
|
40
|
-
|
41
|
-
redis.should_receive(:sentinel).
|
42
|
-
with("is-master-down-by-addr", "remote.server", 8888)
|
43
|
-
subject.discover_master
|
51
|
+
context "#try_next_sentinel" do
|
52
|
+
it "returns next sentinel server" do
|
53
|
+
expect(Redis).to receive(:new).with(:host => "localhost", :port => 26379).and_return(redis)
|
54
|
+
subject.try_next_sentinel
|
44
55
|
end
|
45
56
|
|
46
|
-
it "
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
expect
|
52
|
-
expect(subject.port).to eq 8888
|
53
|
-
expect(subject.password).to eq "foobar"
|
57
|
+
it "raises an error if no available sentinel server" do
|
58
|
+
client = Redis::Client.new(
|
59
|
+
:master_name => "master",
|
60
|
+
:sentinels => []
|
61
|
+
)
|
62
|
+
expect { client.try_next_sentinel }.to raise_error(Redis::CannotConnectError)
|
54
63
|
end
|
64
|
+
end
|
55
65
|
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
66
|
+
context "#refresh_sentinels_list" do
|
67
|
+
it "gets all sentinels list" do
|
68
|
+
sentinel = double('sentinel')
|
69
|
+
allow(subject).to receive(:current_sentinel_options).and_return(:host => "localhost", :port => 26379)
|
70
|
+
expect(subject).to receive(:current_sentinel).and_return(sentinel)
|
71
|
+
expect(sentinel).to receive(:sentinel).with("sentinels", "master").and_return([
|
72
|
+
["name", "localhost:26381", "ip", "localhost", "port", 26380],
|
73
|
+
["name", "localhost:26381", "ip", "localhost", "port", 26381]
|
74
|
+
])
|
75
|
+
subject.refresh_sentinels_list
|
76
|
+
expect(subject.instance_variable_get(:@sentinels_options)).to eq [
|
77
|
+
{:host => "localhost", :port => 26379},
|
78
|
+
{:host => "localhost", :port => 26380},
|
79
|
+
{:host => "localhost", :port => 26381}
|
80
|
+
]
|
68
81
|
end
|
82
|
+
end
|
69
83
|
|
70
|
-
|
71
|
-
|
72
|
-
redis.
|
73
|
-
redis.
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
expect(redis.host).to eq "remote.server"
|
79
|
-
expect(redis.port).to eq 8888
|
80
|
-
expect(redis.password).to eq nil
|
84
|
+
context "#discover_master" do
|
85
|
+
it "updates master config options" do
|
86
|
+
expect(redis).to receive(:sentinel).with("get-master-addr-by-name", "master").and_return(["master", 8888])
|
87
|
+
expect(redis).to receive(:sentinel).with("sentinels", "master").and_return([{:host => "sentinel", :port => 8888}])
|
88
|
+
subject.discover_master
|
89
|
+
expect(subject.host).to eq "master"
|
90
|
+
expect(subject.port).to eq 8888
|
81
91
|
end
|
82
92
|
|
83
|
-
it "
|
84
|
-
|
85
|
-
redis.
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
with("get-master-addr-by-name", "master")
|
91
|
-
redis.should_receive(:sentinel).
|
92
|
-
with("is-master-down-by-addr", "remote.server", 8888)
|
93
|
+
it "selects next sentinel if failed to connect to current_sentinel" do
|
94
|
+
expect(subject).to receive(:current_sentinel).and_return(redis)
|
95
|
+
expect(redis).to receive(:sentinel).with("get-master-addr-by-name", "master").and_raise(Redis::CannotConnectError)
|
96
|
+
sentinel = double('sentinel')
|
97
|
+
expect(subject).to receive(:current_sentinel).and_return(sentinel)
|
98
|
+
expect(sentinel).to receive(:sentinel).with("get-master-addr-by-name", "master").and_return(["master", 8888])
|
99
|
+
allow(subject).to receive(:refresh_sentinels_list)
|
93
100
|
subject.discover_master
|
94
|
-
expect(subject.host).to eq "
|
101
|
+
expect(subject.host).to eq "master"
|
95
102
|
expect(subject.port).to eq 8888
|
96
|
-
expect(subject.password).to eq "foobar"
|
97
103
|
end
|
98
104
|
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
subject.discover_master
|
104
|
-
subject.discover_master
|
105
|
-
end
|
105
|
+
it "raises error if try_next_sentinel raises error" do
|
106
|
+
expect(subject).to receive(:try_next_sentinel).and_raise(Redis::CannotConnectError)
|
107
|
+
expect { subject.discover_master }.to raise_error(Redis::CannotConnectError)
|
106
108
|
end
|
107
109
|
end
|
108
110
|
|
@@ -111,7 +113,7 @@ describe Redis::Client do
|
|
111
113
|
subject { Redis::Client.new }
|
112
114
|
|
113
115
|
it "does not sleep" do
|
114
|
-
subject.
|
116
|
+
expect(subject).not_to receive(:sleep)
|
115
117
|
expect do
|
116
118
|
subject.auto_retry_with_timeout { raise Redis::CannotConnectError }
|
117
119
|
end.to raise_error(Redis::CannotConnectError)
|
@@ -122,12 +124,12 @@ describe Redis::Client do
|
|
122
124
|
subject { Redis::Client.new(:failover_reconnect_timeout => 3) }
|
123
125
|
|
124
126
|
before(:each) do
|
125
|
-
subject.
|
127
|
+
allow(subject).to receive(:sleep)
|
126
128
|
end
|
127
129
|
|
128
130
|
it "only raises after the failover_reconnect_timeout" do
|
129
131
|
called_counter = 0
|
130
|
-
Time.
|
132
|
+
allow(Time).to receive(:now).and_return(100, 101, 102, 103, 104, 105)
|
131
133
|
|
132
134
|
begin
|
133
135
|
subject.auto_retry_with_timeout do
|
@@ -137,12 +139,12 @@ describe Redis::Client do
|
|
137
139
|
rescue Redis::CannotConnectError
|
138
140
|
end
|
139
141
|
|
140
|
-
called_counter.
|
142
|
+
expect(called_counter).to eq(4)
|
141
143
|
end
|
142
144
|
|
143
145
|
it "sleeps the default wait time" do
|
144
|
-
Time.
|
145
|
-
subject.
|
146
|
+
allow(Time).to receive(:now).and_return(100, 101, 105)
|
147
|
+
expect(subject).to receive(:sleep).with(0.1)
|
146
148
|
begin
|
147
149
|
subject.auto_retry_with_timeout { raise Redis::CannotConnectError }
|
148
150
|
rescue Redis::CannotConnectError
|
@@ -150,7 +152,7 @@ describe Redis::Client do
|
|
150
152
|
end
|
151
153
|
|
152
154
|
it "does not catch other errors" do
|
153
|
-
subject.
|
155
|
+
expect(subject).not_to receive(:sleep)
|
154
156
|
expect do
|
155
157
|
subject.auto_retry_with_timeout { raise Redis::ConnectionError }
|
156
158
|
end.to raise_error(Redis::ConnectionError)
|
@@ -161,8 +163,8 @@ describe Redis::Client do
|
|
161
163
|
:failover_reconnect_wait => 0.01) }
|
162
164
|
|
163
165
|
it "uses the configured wait time" do
|
164
|
-
Time.
|
165
|
-
subject.
|
166
|
+
allow(Time).to receive(:now).and_return(100, 101, 105)
|
167
|
+
expect(subject).to receive(:sleep).with(0.01)
|
166
168
|
begin
|
167
169
|
subject.auto_retry_with_timeout { raise Redis::CannotConnectError }
|
168
170
|
rescue Redis::CannotConnectError
|
@@ -172,15 +174,14 @@ describe Redis::Client do
|
|
172
174
|
end
|
173
175
|
end
|
174
176
|
|
175
|
-
context "#
|
176
|
-
it "calls
|
177
|
-
|
178
|
-
|
179
|
-
subject.
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
subject.reconnect
|
177
|
+
context "#disconnect" do
|
178
|
+
it "calls disconnect on each sentinel client" do
|
179
|
+
sentinel = double('sentinel')
|
180
|
+
client = double('client')
|
181
|
+
allow(subject).to receive(:current_sentinel).and_return(sentinel)
|
182
|
+
expect(sentinel).to receive(:client).and_return(client)
|
183
|
+
expect(client).to receive(:disconnect)
|
184
|
+
subject.disconnect
|
184
185
|
end
|
185
186
|
end
|
186
187
|
|
@@ -9,24 +9,24 @@ describe Redis::Client do
|
|
9
9
|
context "configured wait time" do
|
10
10
|
|
11
11
|
it "uses the wait time and blocks em" do
|
12
|
-
Time.
|
12
|
+
allow(Time).to receive(:now).and_return(100, 101, 105)
|
13
13
|
flag = false; EM.next_tick { flag = true }
|
14
|
-
subject.
|
14
|
+
expect(subject).to receive(:sleep).with(0.1).and_return(0.1)
|
15
15
|
begin
|
16
16
|
subject.auto_retry_with_timeout { raise Redis::CannotConnectError }
|
17
17
|
rescue Redis::CannotConnectError
|
18
18
|
end
|
19
|
-
flag.
|
19
|
+
expect(flag).to be_false
|
20
20
|
end
|
21
21
|
|
22
22
|
it "uses the wait time and doesn't block em" do
|
23
|
-
Time.
|
23
|
+
allow(Time).to receive(:now).and_return(100, 101, 105)
|
24
24
|
flag = false; EM.next_tick { flag = true }
|
25
25
|
begin
|
26
26
|
subject.auto_retry_with_timeout { raise Redis::CannotConnectError }
|
27
27
|
rescue Redis::CannotConnectError
|
28
28
|
end
|
29
|
-
flag.
|
29
|
+
expect(flag).to be_true
|
30
30
|
end
|
31
31
|
end
|
32
32
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: redis-sentinel
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.4.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Richard Huang
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2014-01-08 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: redis
|
@@ -114,6 +114,7 @@ files:
|
|
114
114
|
- example/redis-master.conf
|
115
115
|
- example/redis-sentinel1.conf
|
116
116
|
- example/redis-sentinel2.conf
|
117
|
+
- example/redis-sentinel3.conf
|
117
118
|
- example/redis-slave.conf
|
118
119
|
- example/test.rb
|
119
120
|
- example/test_wait_for_failover.rb
|
@@ -145,7 +146,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
145
146
|
version: '0'
|
146
147
|
requirements: []
|
147
148
|
rubyforge_project:
|
148
|
-
rubygems_version: 2.0.
|
149
|
+
rubygems_version: 2.0.14
|
149
150
|
signing_key:
|
150
151
|
specification_version: 4
|
151
152
|
summary: another redis automatic master/slave failover solution for ruby by using
|