redis-sentinel 1.3.0 → 1.4.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|