redis_failover 0.1.1 → 0.2.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/Changes.md +10 -4
- data/README.md +4 -2
- data/lib/redis_failover/client.rb +42 -12
- data/lib/redis_failover/errors.rb +13 -0
- data/lib/redis_failover/node_manager.rb +18 -21
- data/lib/redis_failover/version.rb +1 -1
- data/spec/client_spec.rb +12 -0
- data/spec/support/redis_stub.rb +11 -3
- metadata +16 -16
data/Changes.md
CHANGED
@@ -1,9 +1,15 @@
|
|
1
|
-
0.
|
1
|
+
0.2.0
|
2
2
|
-----------
|
3
|
-
|
4
|
-
-
|
3
|
+
- Added retry support for contacting failover server from client
|
4
|
+
- Client now verifies proper master/slave role before attempting operation
|
5
|
+
- General edge case cleanup for NodeManager
|
5
6
|
|
6
7
|
0.1.1
|
7
8
|
-----------
|
8
9
|
|
9
|
-
- Fix option parser require
|
10
|
+
- Fix option parser require
|
11
|
+
|
12
|
+
0.1.0
|
13
|
+
-----------
|
14
|
+
|
15
|
+
- First release
|
data/README.md
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
# Redis Failover Client/Server
|
1
|
+
# Automatic Redis Failover Client/Server
|
2
2
|
|
3
3
|
Redis Failover attempts to provides a full automatic master/slave failover solution for Ruby. Redis does not provide
|
4
4
|
an automatic failover capability when configured for master/slave replication. When the master node dies,
|
@@ -18,7 +18,9 @@ All nodes marked as unreachable will be periodically checked to see if they have
|
|
18
18
|
If so, the newly reachable nodes will be configured as slaves and brought back into the list of live
|
19
19
|
servers. Note that detection of a node going down should be nearly instantaneous, since the mechanism
|
20
20
|
used to keep tabs on a node is via a blocking Redis BLPOP call (no polling). This call fails nearly
|
21
|
-
immediately when the node actually goes offline.
|
21
|
+
immediately when the node actually goes offline. To avoid false positives (i.e., intermittent flaky
|
22
|
+
network interruption), the server will only mark a node as unreachable if it fails to communicate with
|
23
|
+
it 3 times (this is configurable via --max-failures, see configuration options below).
|
22
24
|
|
23
25
|
This gem provides a RedisFailover::Client wrapper that is master/slave aware. The client is configured
|
24
26
|
with a single host/port pair that points to redis failover server. The client will automatically
|
@@ -6,6 +6,7 @@ module RedisFailover
|
|
6
6
|
class Client
|
7
7
|
include Util
|
8
8
|
|
9
|
+
RETRY_WAIT_TIME = 3
|
9
10
|
REDIS_ERRORS = Errno.constants.map { |c| Errno.const_get(c) }.freeze
|
10
11
|
REDIS_READ_OPS = Set[
|
11
12
|
:dbsize,
|
@@ -83,8 +84,8 @@ module RedisFailover
|
|
83
84
|
@namespace = options[:namespace]
|
84
85
|
@password = options[:password]
|
85
86
|
@retry = options[:retry_failure] || true
|
86
|
-
@max_retries = options
|
87
|
-
@
|
87
|
+
@max_retries = @retry ? options.fetch(:max_retries, 3) : 0
|
88
|
+
@server_url = "http://#{options[:host]}:#{options[:port]}/redis_servers"
|
88
89
|
@redis_servers = nil
|
89
90
|
@master = nil
|
90
91
|
@slaves = []
|
@@ -117,6 +118,7 @@ module RedisFailover
|
|
117
118
|
|
118
119
|
def dispatch(method, *args, &block)
|
119
120
|
tries = 0
|
121
|
+
|
120
122
|
begin
|
121
123
|
if REDIS_READ_OPS.include?(method)
|
122
124
|
# send read operations to a slave
|
@@ -125,14 +127,13 @@ module RedisFailover
|
|
125
127
|
# direct everything else to master
|
126
128
|
master.send(method, *args, &block)
|
127
129
|
end
|
128
|
-
rescue
|
130
|
+
rescue Error, *REDIS_ERRORS
|
129
131
|
logger.error("No suitable node available for operation `#{method}.`")
|
130
|
-
sleep(3)
|
131
132
|
build_clients
|
132
133
|
|
133
|
-
if
|
134
|
+
if tries < @max_retries
|
134
135
|
tries += 1
|
135
|
-
retry
|
136
|
+
sleep(RETRY_WAIT_TIME) && retry
|
136
137
|
end
|
137
138
|
|
138
139
|
raise
|
@@ -140,16 +141,26 @@ module RedisFailover
|
|
140
141
|
end
|
141
142
|
|
142
143
|
def master
|
143
|
-
@master
|
144
|
+
if @master
|
145
|
+
verify_role!(@master, :master)
|
146
|
+
return @master
|
147
|
+
end
|
148
|
+
raise NoMasterError
|
144
149
|
end
|
145
150
|
|
146
151
|
def slave
|
147
152
|
# pick a slave, if none available fallback to master
|
148
|
-
@slaves.sample
|
153
|
+
if slave = @slaves.sample
|
154
|
+
verify_role!(slave, :slave)
|
155
|
+
return slave
|
156
|
+
end
|
157
|
+
master
|
149
158
|
end
|
150
159
|
|
151
160
|
def build_clients
|
152
161
|
@lock.synchronize do
|
162
|
+
tries = 0
|
163
|
+
|
153
164
|
begin
|
154
165
|
logger.info('Attempting to fetch nodes and build redis clients.')
|
155
166
|
servers = fetch_redis_servers
|
@@ -160,14 +171,21 @@ module RedisFailover
|
|
160
171
|
@master = master
|
161
172
|
@slaves = slaves
|
162
173
|
rescue => ex
|
163
|
-
logger.error("Failed to fetch servers from #{@
|
174
|
+
logger.error("Failed to fetch servers from #{@server_url} - #{ex.message}")
|
164
175
|
logger.error(ex.backtrace.join("\n"))
|
176
|
+
|
177
|
+
if tries < @max_retries
|
178
|
+
tries += 1
|
179
|
+
sleep(RETRY_WAIT_TIME) && retry
|
180
|
+
end
|
181
|
+
|
182
|
+
raise FailoverServerUnreachableError.new(@server_url)
|
165
183
|
end
|
166
184
|
end
|
167
185
|
end
|
168
186
|
|
169
187
|
def fetch_redis_servers
|
170
|
-
open(@
|
188
|
+
open(@server_url) do |io|
|
171
189
|
servers = symbolize_keys(MultiJson.decode(io))
|
172
190
|
logger.info("Fetched servers: #{servers}")
|
173
191
|
servers
|
@@ -187,12 +205,24 @@ module RedisFailover
|
|
187
205
|
|
188
206
|
def master_info
|
189
207
|
return "none" unless @master
|
190
|
-
|
208
|
+
name_for(@master)
|
191
209
|
end
|
192
210
|
|
193
211
|
def slaves_info
|
194
212
|
return "none" if @slaves.empty?
|
195
|
-
@slaves.map { |
|
213
|
+
@slaves.map { |slave| name_for(slave) }.join(', ')
|
214
|
+
end
|
215
|
+
|
216
|
+
def verify_role!(node, role)
|
217
|
+
current_role = node.info['role']
|
218
|
+
if current_role.to_sym != role
|
219
|
+
raise InvalidNodeRoleError.new(name_for(node), role, current_role)
|
220
|
+
end
|
221
|
+
role
|
222
|
+
end
|
223
|
+
|
224
|
+
def name_for(node)
|
225
|
+
"#{node.client.host}:#{node.client.port}"
|
196
226
|
end
|
197
227
|
end
|
198
228
|
end
|
@@ -22,4 +22,17 @@ module RedisFailover
|
|
22
22
|
|
23
23
|
class NoSlaveError < Error
|
24
24
|
end
|
25
|
+
|
26
|
+
class FailoverServerUnreachableError < Error
|
27
|
+
def initialize(failover_server_url)
|
28
|
+
super("Unable to reach #{failover_server_url}")
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
class InvalidNodeRoleError < Error
|
33
|
+
def initialize(node, assumed, actual)
|
34
|
+
super("Invalid role detected for node #{node}, client thought " +
|
35
|
+
"it was a #{assumed}, but it's now a #{actual}")
|
36
|
+
end
|
37
|
+
end
|
25
38
|
end
|
@@ -60,6 +60,7 @@ module RedisFailover
|
|
60
60
|
return if @unreachable.include?(node)
|
61
61
|
logger.info("Handling unreachable node: #{node}")
|
62
62
|
|
63
|
+
@unreachable << node
|
63
64
|
# find a new master if this node was a master
|
64
65
|
if node == @master
|
65
66
|
logger.info("Demoting currently unreachable master #{node}.")
|
@@ -67,8 +68,6 @@ module RedisFailover
|
|
67
68
|
else
|
68
69
|
@slaves.delete(node)
|
69
70
|
end
|
70
|
-
|
71
|
-
@unreachable << node
|
72
71
|
end
|
73
72
|
|
74
73
|
def handle_reachable(node)
|
@@ -76,33 +75,32 @@ module RedisFailover
|
|
76
75
|
return if @master == node || @slaves.include?(node)
|
77
76
|
logger.info("Handling reachable node: #{node}")
|
78
77
|
|
79
|
-
@unreachable.delete(node)
|
80
|
-
@slaves << node
|
81
78
|
if @master
|
82
79
|
# master already exists, make a slave
|
83
80
|
node.make_slave!(@master)
|
81
|
+
@slaves << node
|
84
82
|
else
|
85
83
|
# no master exists, make this the new master
|
86
|
-
promote_new_master
|
84
|
+
promote_new_master(node)
|
87
85
|
end
|
86
|
+
|
87
|
+
@unreachable.delete(node)
|
88
88
|
end
|
89
89
|
|
90
|
-
def promote_new_master
|
90
|
+
def promote_new_master(node = nil)
|
91
91
|
@master = nil
|
92
92
|
|
93
|
-
|
94
|
-
|
93
|
+
# make a specific node or slave the new master
|
94
|
+
candidate = node || @slaves.pop
|
95
|
+
unless candidate
|
96
|
+
logger.error('Failed to promote a new master since no candidate available.')
|
95
97
|
return
|
96
98
|
end
|
97
99
|
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
# switch existing slaves to point to new master
|
104
|
-
redirect_slaves
|
105
|
-
logger.info("Successfully promoted #{node} to master.")
|
100
|
+
candidate.make_master!
|
101
|
+
@master = candidate
|
102
|
+
redirect_slaves_to_master
|
103
|
+
logger.info("Successfully promoted #{candidate} to master.")
|
106
104
|
end
|
107
105
|
|
108
106
|
def parse_nodes
|
@@ -118,7 +116,7 @@ module RedisFailover
|
|
118
116
|
|
119
117
|
def spawn_watchers
|
120
118
|
@watchers = [@master, *@slaves].map do |node|
|
121
|
-
|
119
|
+
NodeWatcher.new(self, node, @options[:max_failures] || 3)
|
122
120
|
end
|
123
121
|
@watchers.each(&:watch)
|
124
122
|
end
|
@@ -136,14 +134,13 @@ module RedisFailover
|
|
136
134
|
end
|
137
135
|
end
|
138
136
|
|
139
|
-
def
|
140
|
-
# redirect each slave to
|
141
|
-
# currently down, it will be handled by its watcher
|
137
|
+
def redirect_slaves_to_master
|
138
|
+
# redirect each slave to the current master
|
142
139
|
@slaves.each do |slave|
|
143
140
|
begin
|
144
141
|
slave.make_slave!(@master)
|
145
142
|
rescue NodeUnreachableError
|
146
|
-
# will
|
143
|
+
# will also be detected by watcher
|
147
144
|
end
|
148
145
|
end
|
149
146
|
end
|
data/spec/client_spec.rb
CHANGED
@@ -40,6 +40,7 @@ module RedisFailover
|
|
40
40
|
end
|
41
41
|
|
42
42
|
it 'routes read operations to a slave' do
|
43
|
+
client.current_slaves.first.change_role_to('slave')
|
43
44
|
client.current_slaves.first.should_receive(:get)
|
44
45
|
client.get('foo')
|
45
46
|
end
|
@@ -57,6 +58,17 @@ module RedisFailover
|
|
57
58
|
client.del('foo')
|
58
59
|
client.reconnected.should be_true
|
59
60
|
end
|
61
|
+
|
62
|
+
it 'fails hard when the failover server is unreachable' do
|
63
|
+
expect do
|
64
|
+
Client.new(:host => 'foo', :port => 123445)
|
65
|
+
end.to raise_error(FailoverServerUnreachableError)
|
66
|
+
end
|
67
|
+
|
68
|
+
it 'properly detects when a node has changed roles' do
|
69
|
+
client.current_master.change_role_to('slave')
|
70
|
+
expect { client.send(:master) }.to raise_error(InvalidNodeRoleError)
|
71
|
+
end
|
60
72
|
end
|
61
73
|
end
|
62
74
|
end
|
data/spec/support/redis_stub.rb
CHANGED
@@ -5,7 +5,7 @@ module RedisFailover
|
|
5
5
|
class RedisStub
|
6
6
|
class Proxy
|
7
7
|
def initialize(queue, opts = {})
|
8
|
-
@info = {
|
8
|
+
@info = {'role' => 'master'}
|
9
9
|
@queue = queue
|
10
10
|
end
|
11
11
|
|
@@ -24,9 +24,9 @@ module RedisFailover
|
|
24
24
|
|
25
25
|
def slaveof(host, port)
|
26
26
|
if host == 'no' && port == 'one'
|
27
|
-
@info[
|
27
|
+
@info['role'] = 'master'
|
28
28
|
else
|
29
|
-
@info[
|
29
|
+
@info['role'] = 'slave'
|
30
30
|
end
|
31
31
|
end
|
32
32
|
|
@@ -37,6 +37,10 @@ module RedisFailover
|
|
37
37
|
def ping
|
38
38
|
'pong'
|
39
39
|
end
|
40
|
+
|
41
|
+
def change_role_to(role)
|
42
|
+
@info['role'] = role
|
43
|
+
end
|
40
44
|
end
|
41
45
|
|
42
46
|
attr_reader :host, :port, :reachable
|
@@ -56,6 +60,10 @@ module RedisFailover
|
|
56
60
|
end
|
57
61
|
end
|
58
62
|
|
63
|
+
def change_role_to(role)
|
64
|
+
@proxy.change_role_to(role)
|
65
|
+
end
|
66
|
+
|
59
67
|
def make_reachable!
|
60
68
|
@reachable = true
|
61
69
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: redis_failover
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,11 +9,11 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2012-04-
|
12
|
+
date: 2012-04-13 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: redis
|
16
|
-
requirement: &
|
16
|
+
requirement: &70198326575220 !ruby/object:Gem::Requirement
|
17
17
|
none: false
|
18
18
|
requirements:
|
19
19
|
- - ! '>='
|
@@ -21,10 +21,10 @@ dependencies:
|
|
21
21
|
version: '0'
|
22
22
|
type: :runtime
|
23
23
|
prerelease: false
|
24
|
-
version_requirements: *
|
24
|
+
version_requirements: *70198326575220
|
25
25
|
- !ruby/object:Gem::Dependency
|
26
26
|
name: redis-namespace
|
27
|
-
requirement: &
|
27
|
+
requirement: &70198326574780 !ruby/object:Gem::Requirement
|
28
28
|
none: false
|
29
29
|
requirements:
|
30
30
|
- - ! '>='
|
@@ -32,10 +32,10 @@ dependencies:
|
|
32
32
|
version: '0'
|
33
33
|
type: :runtime
|
34
34
|
prerelease: false
|
35
|
-
version_requirements: *
|
35
|
+
version_requirements: *70198326574780
|
36
36
|
- !ruby/object:Gem::Dependency
|
37
37
|
name: multi_json
|
38
|
-
requirement: &
|
38
|
+
requirement: &70198326574360 !ruby/object:Gem::Requirement
|
39
39
|
none: false
|
40
40
|
requirements:
|
41
41
|
- - ! '>='
|
@@ -43,10 +43,10 @@ dependencies:
|
|
43
43
|
version: '0'
|
44
44
|
type: :runtime
|
45
45
|
prerelease: false
|
46
|
-
version_requirements: *
|
46
|
+
version_requirements: *70198326574360
|
47
47
|
- !ruby/object:Gem::Dependency
|
48
48
|
name: sinatra
|
49
|
-
requirement: &
|
49
|
+
requirement: &70198326573940 !ruby/object:Gem::Requirement
|
50
50
|
none: false
|
51
51
|
requirements:
|
52
52
|
- - ! '>='
|
@@ -54,10 +54,10 @@ dependencies:
|
|
54
54
|
version: '0'
|
55
55
|
type: :runtime
|
56
56
|
prerelease: false
|
57
|
-
version_requirements: *
|
57
|
+
version_requirements: *70198326573940
|
58
58
|
- !ruby/object:Gem::Dependency
|
59
59
|
name: rake
|
60
|
-
requirement: &
|
60
|
+
requirement: &70198326573520 !ruby/object:Gem::Requirement
|
61
61
|
none: false
|
62
62
|
requirements:
|
63
63
|
- - ! '>='
|
@@ -65,10 +65,10 @@ dependencies:
|
|
65
65
|
version: '0'
|
66
66
|
type: :development
|
67
67
|
prerelease: false
|
68
|
-
version_requirements: *
|
68
|
+
version_requirements: *70198326573520
|
69
69
|
- !ruby/object:Gem::Dependency
|
70
70
|
name: rspec
|
71
|
-
requirement: &
|
71
|
+
requirement: &70198326573100 !ruby/object:Gem::Requirement
|
72
72
|
none: false
|
73
73
|
requirements:
|
74
74
|
- - ! '>='
|
@@ -76,7 +76,7 @@ dependencies:
|
|
76
76
|
version: '0'
|
77
77
|
type: :development
|
78
78
|
prerelease: false
|
79
|
-
version_requirements: *
|
79
|
+
version_requirements: *70198326573100
|
80
80
|
description: Redis Failover provides a full automatic master/slave failover solution
|
81
81
|
for Ruby
|
82
82
|
email:
|
@@ -128,7 +128,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
128
128
|
version: '0'
|
129
129
|
segments:
|
130
130
|
- 0
|
131
|
-
hash:
|
131
|
+
hash: -200241163739368802
|
132
132
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
133
133
|
none: false
|
134
134
|
requirements:
|
@@ -137,7 +137,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
137
137
|
version: '0'
|
138
138
|
segments:
|
139
139
|
- 0
|
140
|
-
hash:
|
140
|
+
hash: -200241163739368802
|
141
141
|
requirements: []
|
142
142
|
rubyforge_project:
|
143
143
|
rubygems_version: 1.8.16
|