redis_failover 0.3.0 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- data/Changes.md +11 -2
- data/README.md +24 -5
- data/lib/redis_failover.rb +1 -0
- data/lib/redis_failover/cli.rb +1 -1
- data/lib/redis_failover/client.rb +35 -31
- data/lib/redis_failover/errors.rb +14 -3
- data/lib/redis_failover/node.rb +65 -27
- data/lib/redis_failover/node_manager.rb +53 -17
- data/lib/redis_failover/node_watcher.rb +28 -13
- data/lib/redis_failover/version.rb +1 -1
- data/spec/client_spec.rb +10 -6
- data/spec/node_manager_spec.rb +49 -16
- data/spec/node_spec.rb +18 -10
- data/spec/node_watcher_spec.rb +30 -15
- data/spec/support/node_manager_stub.rb +15 -10
- data/spec/support/redis_stub.rb +26 -11
- metadata +16 -16
data/Changes.md
CHANGED
@@ -1,8 +1,17 @@
|
|
1
|
+
0.4.0
|
2
|
+
-----------
|
3
|
+
- No longer force newly available slaves to master if already slaves of that master
|
4
|
+
- Honor a node's slave-serve-stale-data configuration option; do not mark a sync-with-master-in-progress slave as available if its slave-serve-stale-data is disabled
|
5
|
+
- Change reachable/unreachable wording to available/unavailable
|
6
|
+
- Added node reconciliation, i.e. if a node comes back up, make sure that the node manager and the node agree on current role
|
7
|
+
- More efficient use of redis client connections
|
8
|
+
- Raise proper error for unsupported operations (i.e., those that don't make sense for a failover client)
|
9
|
+
- Properly handle any hanging node operations in the failover server
|
10
|
+
|
1
11
|
0.3.0
|
2
12
|
-----------
|
3
13
|
- Integrated travis-ci
|
4
|
-
- Added background monitor to client for proactively detecting
|
5
|
-
changes to current set of redis nodes
|
14
|
+
- Added background monitor to client for proactively detecting changes to current set of redis nodes
|
6
15
|
|
7
16
|
0.2.0
|
8
17
|
-----------
|
data/README.md
CHANGED
@@ -13,15 +13,15 @@ master, and all reads go to one of the N configured slaves.
|
|
13
13
|
This gem attempts to address both the server and client problems. A redis failover server runs as a background
|
14
14
|
daemon and monitors all of your configured master/slave nodes. When the server starts up, it
|
15
15
|
automatically discovers who is the master and who are the slaves. Watchers are setup for each of
|
16
|
-
the redis nodes. As soon as a node is detected as being offline, it will be moved to an "
|
16
|
+
the redis nodes. As soon as a node is detected as being offline, it will be moved to an "unavailable" state.
|
17
17
|
If the node that went offline was the master, then one of the slaves will be promoted as the new master.
|
18
18
|
All existing slaves will be automatically reconfigured to point to the new master for replication.
|
19
|
-
All nodes marked as
|
20
|
-
If so, the newly
|
19
|
+
All nodes marked as unavailable will be periodically checked to see if they have been brought back online.
|
20
|
+
If so, the newly available nodes will be configured as slaves and brought back into the list of live
|
21
21
|
servers. Note that detection of a node going down should be nearly instantaneous, since the mechanism
|
22
22
|
used to keep tabs on a node is via a blocking Redis BLPOP call (no polling). This call fails nearly
|
23
23
|
immediately when the node actually goes offline. To avoid false positives (i.e., intermittent flaky
|
24
|
-
network interruption), the server will only mark a node as
|
24
|
+
network interruption), the server will only mark a node as unavailable if it fails to communicate with
|
25
25
|
it 3 times (this is configurable via --max-failures, see configuration options below).
|
26
26
|
|
27
27
|
This gem provides a RedisFailover::Client wrapper that is master/slave aware. The client is configured
|
@@ -55,7 +55,7 @@ following options:
|
|
55
55
|
-P, --port port Server port
|
56
56
|
-p, --password password Redis password
|
57
57
|
-n, --nodes nodes Comma-separated redis host:port pairs
|
58
|
-
--max-failures count Max failures before server marks node
|
58
|
+
--max-failures count Max failures before server marks node unavailable (default 3)
|
59
59
|
-h, --help Display all options
|
60
60
|
|
61
61
|
To start the server for a simple master/slave configuration, use the following:
|
@@ -87,6 +87,25 @@ The full set of options that can be passed to RedisFailover::Client are:
|
|
87
87
|
:retry_failure - indicate if failures should be retried (default true)
|
88
88
|
:max_retries - max retries for a failure (default 3)
|
89
89
|
|
90
|
+
## Requirements
|
91
|
+
|
92
|
+
redis_failover is actively tested against MRI 1.9.2/1.9.3. Other rubies may work, although I don't actively test against them. 1.8 is not supported.
|
93
|
+
|
94
|
+
## Considerations
|
95
|
+
|
96
|
+
Note that by default the failover server will mark slaves that are currently syncing with their master as "available" based on the configuration value set for "slave-serve-stale-data" in redis.conf. By default this value is set to "yes" in the configuration, which means that slaves still syncing with their master will be available for servicing read requests. If you don't want this behavior, just set "slave-serve-stale-data" to "no" in your redis.conf file.
|
97
|
+
|
98
|
+
## Limitations
|
99
|
+
|
100
|
+
The redis_failover gem currently has limitations. It currently does not gracefully handle network partitions. In cases where
|
101
|
+
the network splits, it is possible that more than one master could exist until the failover server sees all of the nodes again.
|
102
|
+
If the failover client gets split from the failover server, it's also possible that it could be talking to a stale master. This would get corrected once the client could successfully reach the failover server again to fetch the latest set of master/slave nodes. This is a limitation that I hope to address in a future release. The gem can not guarantee data consistencies until this is addressed.
|
103
|
+
|
104
|
+
## TODO
|
105
|
+
|
106
|
+
- Integrate with ZooKeeper for full reliability / data consistency.
|
107
|
+
- Rework specs to work against a set of real Redis nodes as opposed to stubs.
|
108
|
+
|
90
109
|
## Resources
|
91
110
|
|
92
111
|
To learn more about Redis master/slave replication, see the [Redis documentation](http://redis.io/topics/replication).
|
data/lib/redis_failover.rb
CHANGED
data/lib/redis_failover/cli.rb
CHANGED
@@ -24,7 +24,7 @@ module RedisFailover
|
|
24
24
|
end
|
25
25
|
|
26
26
|
opts.on('--max-failures count',
|
27
|
-
'Max failures before server marks node
|
27
|
+
'Max failures before server marks node unavailable (default 3)') do |max|
|
28
28
|
options[:max_failures] = Integer(max)
|
29
29
|
end
|
30
30
|
|
@@ -9,7 +9,6 @@ module RedisFailover
|
|
9
9
|
RETRY_WAIT_TIME = 3
|
10
10
|
REDIS_ERRORS = Errno.constants.map { |c| Errno.const_get(c) }.freeze
|
11
11
|
REDIS_READ_OPS = Set[
|
12
|
-
:dbsize,
|
13
12
|
:echo,
|
14
13
|
:exists,
|
15
14
|
:get,
|
@@ -22,26 +21,21 @@ module RedisFailover
|
|
22
21
|
:hlen,
|
23
22
|
:hmget,
|
24
23
|
:hvals,
|
25
|
-
:info,
|
26
24
|
:keys,
|
27
|
-
:lastsave,
|
28
25
|
:lindex,
|
29
26
|
:llen,
|
30
27
|
:lrange,
|
31
28
|
:mapped_hmget,
|
32
29
|
:mapped_mget,
|
33
30
|
:mget,
|
34
|
-
:ping,
|
35
31
|
:scard,
|
36
32
|
:sdiff,
|
37
|
-
:select,
|
38
33
|
:sinter,
|
39
34
|
:sismember,
|
40
35
|
:smembers,
|
41
36
|
:srandmember,
|
42
37
|
:strlen,
|
43
38
|
:sunion,
|
44
|
-
:ttl,
|
45
39
|
:type,
|
46
40
|
:zcard,
|
47
41
|
:zcount,
|
@@ -54,6 +48,12 @@ module RedisFailover
|
|
54
48
|
:zscore
|
55
49
|
].freeze
|
56
50
|
|
51
|
+
UNSUPPORTED_OPS = Set[
|
52
|
+
:select,
|
53
|
+
:ttl,
|
54
|
+
:dbsize,
|
55
|
+
].freeze
|
56
|
+
|
57
57
|
# Performance optimization: to avoid unnecessary method_missing calls,
|
58
58
|
# we proactively define methods that dispatch to the underlying redis
|
59
59
|
# calls.
|
@@ -88,7 +88,6 @@ module RedisFailover
|
|
88
88
|
@server_url = "http://#{options[:host]}:#{options[:port]}/redis_servers"
|
89
89
|
@master = nil
|
90
90
|
@slaves = []
|
91
|
-
@lock = Mutex.new
|
92
91
|
build_clients
|
93
92
|
start_background_monitor
|
94
93
|
end
|
@@ -117,6 +116,7 @@ module RedisFailover
|
|
117
116
|
end
|
118
117
|
|
119
118
|
def dispatch(method, *args, &block)
|
119
|
+
verify_supported!(method)
|
120
120
|
tries = 0
|
121
121
|
|
122
122
|
begin
|
@@ -159,32 +159,30 @@ module RedisFailover
|
|
159
159
|
end
|
160
160
|
|
161
161
|
def build_clients
|
162
|
-
|
163
|
-
tries = 0
|
164
|
-
|
165
|
-
begin
|
166
|
-
logger.info('Checking for new redis nodes.')
|
167
|
-
nodes = fetch_nodes
|
168
|
-
return unless nodes_changed?(nodes)
|
169
|
-
|
170
|
-
logger.info('Node change detected, rebuilding clients.')
|
171
|
-
master = new_clients_for(nodes[:master]).first if nodes[:master]
|
172
|
-
slaves = new_clients_for(*nodes[:slaves])
|
173
|
-
|
174
|
-
# once clients are successfully created, swap the references
|
175
|
-
@master = master
|
176
|
-
@slaves = slaves
|
177
|
-
rescue => ex
|
178
|
-
logger.error("Failed to fetch nodes from #{@server_url} - #{ex.message}")
|
179
|
-
logger.error(ex.backtrace.join("\n"))
|
180
|
-
|
181
|
-
if tries < @max_retries
|
182
|
-
tries += 1
|
183
|
-
sleep(RETRY_WAIT_TIME) && retry
|
184
|
-
end
|
162
|
+
tries = 0
|
185
163
|
|
186
|
-
|
164
|
+
begin
|
165
|
+
logger.info('Checking for new redis nodes.')
|
166
|
+
nodes = fetch_nodes
|
167
|
+
return unless nodes_changed?(nodes)
|
168
|
+
|
169
|
+
logger.info('Node change detected, rebuilding clients.')
|
170
|
+
master = new_clients_for(nodes[:master]).first if nodes[:master]
|
171
|
+
slaves = new_clients_for(*nodes[:slaves])
|
172
|
+
|
173
|
+
# once clients are successfully created, swap the references
|
174
|
+
@master = master
|
175
|
+
@slaves = slaves
|
176
|
+
rescue => ex
|
177
|
+
logger.error("Failed to fetch nodes from #{@server_url} - #{ex.message}")
|
178
|
+
logger.error(ex.backtrace.join("\n"))
|
179
|
+
|
180
|
+
if tries < @max_retries
|
181
|
+
tries += 1
|
182
|
+
sleep(RETRY_WAIT_TIME) && retry
|
187
183
|
end
|
184
|
+
|
185
|
+
raise FailoverServerUnavailableError.new(@server_url)
|
188
186
|
end
|
189
187
|
end
|
190
188
|
|
@@ -224,6 +222,12 @@ module RedisFailover
|
|
224
222
|
role
|
225
223
|
end
|
226
224
|
|
225
|
+
def verify_supported!(method)
|
226
|
+
if UNSUPPORTED_OPS.include?(method)
|
227
|
+
raise UnsupportedOperationError.new(method)
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
227
231
|
def addresses_for(nodes)
|
228
232
|
nodes.map { |node| address_for(node) }
|
229
233
|
end
|
@@ -1,5 +1,10 @@
|
|
1
1
|
module RedisFailover
|
2
2
|
class Error < StandardError
|
3
|
+
attr_reader :original
|
4
|
+
def initialize(msg = nil, original = $!)
|
5
|
+
super(msg)
|
6
|
+
@original = original
|
7
|
+
end
|
3
8
|
end
|
4
9
|
|
5
10
|
class InvalidNodeError < Error
|
@@ -11,7 +16,7 @@ module RedisFailover
|
|
11
16
|
end
|
12
17
|
end
|
13
18
|
|
14
|
-
class
|
19
|
+
class NodeUnavailableError < Error
|
15
20
|
def initialize(node)
|
16
21
|
super("Node: #{node}")
|
17
22
|
end
|
@@ -23,9 +28,9 @@ module RedisFailover
|
|
23
28
|
class NoSlaveError < Error
|
24
29
|
end
|
25
30
|
|
26
|
-
class
|
31
|
+
class FailoverServerUnavailableError < Error
|
27
32
|
def initialize(failover_server_url)
|
28
|
-
super("Unable to
|
33
|
+
super("Unable to access #{failover_server_url}")
|
29
34
|
end
|
30
35
|
end
|
31
36
|
|
@@ -35,4 +40,10 @@ module RedisFailover
|
|
35
40
|
"it was a #{assumed}, but it's now a #{actual}")
|
36
41
|
end
|
37
42
|
end
|
43
|
+
|
44
|
+
class UnsupportedOperationError < Error
|
45
|
+
def initialize(operation)
|
46
|
+
super("Operation `#{operation}` is currently unsupported")
|
47
|
+
end
|
48
|
+
end
|
38
49
|
end
|
data/lib/redis_failover/node.rb
CHANGED
@@ -3,6 +3,8 @@ module RedisFailover
|
|
3
3
|
class Node
|
4
4
|
include Util
|
5
5
|
|
6
|
+
MAX_OP_WAIT_TIME = 5
|
7
|
+
|
6
8
|
attr_reader :host, :port
|
7
9
|
|
8
10
|
def initialize(options = {})
|
@@ -11,12 +13,6 @@ module RedisFailover
|
|
11
13
|
@password = options[:password]
|
12
14
|
end
|
13
15
|
|
14
|
-
def ping
|
15
|
-
perform_operation do
|
16
|
-
redis.ping
|
17
|
-
end
|
18
|
-
end
|
19
|
-
|
20
16
|
def master?
|
21
17
|
role == 'master'
|
22
18
|
end
|
@@ -25,29 +21,48 @@ module RedisFailover
|
|
25
21
|
!master?
|
26
22
|
end
|
27
23
|
|
28
|
-
def
|
29
|
-
|
30
|
-
|
24
|
+
def slave_of?(master)
|
25
|
+
current_master == master
|
26
|
+
end
|
27
|
+
|
28
|
+
def current_master
|
29
|
+
info = fetch_info
|
30
|
+
return unless info[:role] == 'slave'
|
31
|
+
Node.new(:host => info[:master_host], :port => info[:master_port].to_i)
|
32
|
+
end
|
33
|
+
|
34
|
+
# Waits until something interesting happens. If the connection
|
35
|
+
# with this node dies, the blpop call will raise an error. If
|
36
|
+
# the blpop call returns without error, then this will be due to
|
37
|
+
# a graceful shutdown signaled by #wakeup or a timeout.
|
38
|
+
def wait
|
39
|
+
perform_operation do |redis|
|
40
|
+
redis.blpop(wait_key, MAX_OP_WAIT_TIME - 3)
|
31
41
|
redis.del(wait_key)
|
32
42
|
end
|
33
43
|
end
|
34
44
|
|
35
|
-
def
|
36
|
-
perform_operation do
|
45
|
+
def wakeup
|
46
|
+
perform_operation do |redis|
|
37
47
|
redis.lpush(wait_key, '1')
|
38
48
|
end
|
39
49
|
end
|
40
50
|
|
41
51
|
def make_slave!(master)
|
42
|
-
perform_operation do
|
43
|
-
|
52
|
+
perform_operation do |redis|
|
53
|
+
unless slave_of?(master)
|
54
|
+
redis.slaveof(master.host, master.port)
|
55
|
+
wakeup
|
56
|
+
end
|
44
57
|
end
|
45
58
|
end
|
46
59
|
|
47
60
|
def make_master!
|
48
|
-
perform_operation do
|
49
|
-
|
50
|
-
|
61
|
+
perform_operation do |redis|
|
62
|
+
unless master?
|
63
|
+
redis.slaveof('no', 'one')
|
64
|
+
wakeup
|
65
|
+
end
|
51
66
|
end
|
52
67
|
end
|
53
68
|
|
@@ -70,32 +85,55 @@ module RedisFailover
|
|
70
85
|
to_s.hash
|
71
86
|
end
|
72
87
|
|
73
|
-
|
88
|
+
def fetch_info
|
89
|
+
perform_operation do |redis|
|
90
|
+
symbolize_keys(redis.info)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
alias_method :ping, :fetch_info
|
74
94
|
|
75
|
-
def
|
76
|
-
|
95
|
+
def prohibits_stale_reads?
|
96
|
+
perform_operation do |redis|
|
97
|
+
redis.config('get', 'slave-serve-stale-data').last == 'no'
|
98
|
+
end
|
77
99
|
end
|
78
100
|
|
79
|
-
def
|
80
|
-
perform_operation do
|
81
|
-
|
101
|
+
def syncing_with_master?
|
102
|
+
perform_operation do |redis|
|
103
|
+
fetch_info[:master_sync_in_progress] == '1'
|
82
104
|
end
|
83
105
|
end
|
84
106
|
|
107
|
+
private
|
108
|
+
|
109
|
+
def role
|
110
|
+
fetch_info[:role]
|
111
|
+
end
|
112
|
+
|
85
113
|
def wait_key
|
86
114
|
@wait_key ||= "_redis_failover_#{SecureRandom.hex(32)}"
|
87
115
|
end
|
88
116
|
|
89
|
-
def
|
117
|
+
def new_client
|
90
118
|
Redis.new(:host => @host, :password => @password, :port => @port)
|
91
|
-
rescue
|
92
|
-
raise NodeUnreachableError.new(self)
|
93
119
|
end
|
94
120
|
|
95
121
|
def perform_operation
|
96
|
-
|
122
|
+
redis = nil
|
123
|
+
Timeout.timeout(MAX_OP_WAIT_TIME) do
|
124
|
+
redis = new_client
|
125
|
+
yield redis
|
126
|
+
end
|
97
127
|
rescue
|
98
|
-
raise
|
128
|
+
raise NodeUnavailableError.new(self)
|
129
|
+
ensure
|
130
|
+
if redis
|
131
|
+
begin
|
132
|
+
redis.client.disconnect
|
133
|
+
rescue
|
134
|
+
raise NodeUnavailableError.new(self)
|
135
|
+
end
|
136
|
+
end
|
99
137
|
end
|
100
138
|
end
|
101
139
|
end
|
@@ -6,7 +6,7 @@ module RedisFailover
|
|
6
6
|
def initialize(options)
|
7
7
|
@options = options
|
8
8
|
@master, @slaves = parse_nodes
|
9
|
-
@
|
9
|
+
@unavailable = []
|
10
10
|
@queue = Queue.new
|
11
11
|
@lock = Mutex.new
|
12
12
|
end
|
@@ -25,7 +25,7 @@ module RedisFailover
|
|
25
25
|
{
|
26
26
|
:master => @master ? @master.to_s : nil,
|
27
27
|
:slaves => @slaves.map(&:to_s),
|
28
|
-
:
|
28
|
+
:unavailable => @unavailable.map(&:to_s)
|
29
29
|
}
|
30
30
|
end
|
31
31
|
end
|
@@ -42,12 +42,13 @@ module RedisFailover
|
|
42
42
|
node, state = state_change
|
43
43
|
begin
|
44
44
|
case state
|
45
|
-
when :
|
46
|
-
when :
|
45
|
+
when :unavailable then handle_unavailable(node)
|
46
|
+
when :available then handle_available(node)
|
47
|
+
when :syncing then handle_syncing(node)
|
47
48
|
else raise InvalidNodeStateError.new(node, state)
|
48
49
|
end
|
49
|
-
rescue
|
50
|
-
# node suddenly became
|
50
|
+
rescue NodeUnavailableError
|
51
|
+
# node suddenly became unavailable, silently
|
51
52
|
# handle since the watcher will take care of
|
52
53
|
# keeping track of the node
|
53
54
|
end
|
@@ -55,25 +56,27 @@ module RedisFailover
|
|
55
56
|
end
|
56
57
|
end
|
57
58
|
|
58
|
-
def
|
59
|
+
def handle_unavailable(node)
|
59
60
|
# no-op if we already know about this node
|
60
|
-
return if @
|
61
|
-
logger.info("Handling
|
61
|
+
return if @unavailable.include?(node)
|
62
|
+
logger.info("Handling unavailable node: #{node}")
|
62
63
|
|
63
|
-
@
|
64
|
+
@unavailable << node
|
64
65
|
# find a new master if this node was a master
|
65
66
|
if node == @master
|
66
|
-
logger.info("Demoting currently
|
67
|
+
logger.info("Demoting currently unavailable master #{node}.")
|
67
68
|
promote_new_master
|
68
69
|
else
|
69
70
|
@slaves.delete(node)
|
70
71
|
end
|
71
72
|
end
|
72
73
|
|
73
|
-
def
|
74
|
+
def handle_available(node)
|
75
|
+
reconcile(node)
|
76
|
+
|
74
77
|
# no-op if we already know about this node
|
75
78
|
return if @master == node || @slaves.include?(node)
|
76
|
-
logger.info("Handling
|
79
|
+
logger.info("Handling available node: #{node}")
|
77
80
|
|
78
81
|
if @master
|
79
82
|
# master already exists, make a slave
|
@@ -84,7 +87,20 @@ module RedisFailover
|
|
84
87
|
promote_new_master(node)
|
85
88
|
end
|
86
89
|
|
87
|
-
@
|
90
|
+
@unavailable.delete(node)
|
91
|
+
end
|
92
|
+
|
93
|
+
def handle_syncing(node)
|
94
|
+
reconcile(node)
|
95
|
+
|
96
|
+
if node.prohibits_stale_reads?
|
97
|
+
logger.info("Node #{node} not ready yet, still syncing with master.")
|
98
|
+
@unavailable << node
|
99
|
+
return
|
100
|
+
end
|
101
|
+
|
102
|
+
# otherwise, we can use this node
|
103
|
+
handle_available(node)
|
88
104
|
end
|
89
105
|
|
90
106
|
def promote_new_master(node = nil)
|
@@ -104,7 +120,7 @@ module RedisFailover
|
|
104
120
|
end
|
105
121
|
|
106
122
|
def parse_nodes
|
107
|
-
nodes = @options[:nodes].map { |opts| Node.new(opts) }
|
123
|
+
nodes = @options[:nodes].map { |opts| Node.new(opts) }.uniq
|
108
124
|
raise NoMasterError unless master = find_master(nodes)
|
109
125
|
slaves = nodes - [master]
|
110
126
|
|
@@ -127,7 +143,7 @@ module RedisFailover
|
|
127
143
|
nodes.find do |node|
|
128
144
|
begin
|
129
145
|
node.master?
|
130
|
-
rescue
|
146
|
+
rescue NodeUnavailableError
|
131
147
|
# will eventually be handled by watcher
|
132
148
|
false
|
133
149
|
end
|
@@ -139,10 +155,30 @@ module RedisFailover
|
|
139
155
|
@slaves.each do |slave|
|
140
156
|
begin
|
141
157
|
slave.make_slave!(@master)
|
142
|
-
rescue
|
158
|
+
rescue NodeUnavailableError
|
143
159
|
# will also be detected by watcher
|
144
160
|
end
|
145
161
|
end
|
146
162
|
end
|
163
|
+
|
164
|
+
# It's possible that a newly available node may have been restarted
|
165
|
+
# and completely lost its dynamically set run-time role by the node
|
166
|
+
# manager. This method ensures that the node resumes its role as
|
167
|
+
# determined by the manager.
|
168
|
+
def reconcile(node)
|
169
|
+
return if @master == node && node.master?
|
170
|
+
return if @master && node.slave_of?(@master)
|
171
|
+
|
172
|
+
if @master == node && !node.master?
|
173
|
+
# we think the node is a master, but the node doesn't
|
174
|
+
node.make_master!
|
175
|
+
return
|
176
|
+
end
|
177
|
+
|
178
|
+
# verify that node is a slave for the current master
|
179
|
+
if @master && !node.slave_of?(@master)
|
180
|
+
node.make_slave!(@master)
|
181
|
+
end
|
182
|
+
end
|
147
183
|
end
|
148
184
|
end
|
@@ -1,6 +1,10 @@
|
|
1
1
|
module RedisFailover
|
2
|
-
# Watches a specific redis node for its
|
2
|
+
# Watches a specific redis node for its availability.
|
3
3
|
class NodeWatcher
|
4
|
+
include Util
|
5
|
+
|
6
|
+
WATCHER_SLEEP_TIME = 3
|
7
|
+
|
4
8
|
def initialize(manager, node, max_failures)
|
5
9
|
@manager = manager
|
6
10
|
@node = node
|
@@ -16,7 +20,7 @@ module RedisFailover
|
|
16
20
|
|
17
21
|
def shutdown
|
18
22
|
@done = true
|
19
|
-
@node.
|
23
|
+
@node.wakeup
|
20
24
|
@monitor_thread.join if @monitor_thread
|
21
25
|
rescue
|
22
26
|
# best effort
|
@@ -27,20 +31,31 @@ module RedisFailover
|
|
27
31
|
def monitor_node
|
28
32
|
failures = 0
|
29
33
|
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
@node.wait_until_unreachable
|
36
|
-
rescue NodeUnreachableError
|
37
|
-
failures += 1
|
38
|
-
if failures >= @max_failures
|
39
|
-
@manager.notify_state_change(@node, :unreachable)
|
34
|
+
loop do
|
35
|
+
begin
|
36
|
+
return if @done
|
37
|
+
sleep(WATCHER_SLEEP_TIME)
|
38
|
+
@node.ping
|
40
39
|
failures = 0
|
40
|
+
|
41
|
+
if @node.syncing_with_master?
|
42
|
+
notify(:syncing)
|
43
|
+
else
|
44
|
+
notify(:available)
|
45
|
+
@node.wait
|
46
|
+
end
|
47
|
+
rescue NodeUnavailableError
|
48
|
+
failures += 1
|
49
|
+
if failures >= @max_failures
|
50
|
+
notify(:unavailable)
|
51
|
+
failures = 0
|
52
|
+
end
|
41
53
|
end
|
42
|
-
sleep(3) && retry
|
43
54
|
end
|
44
55
|
end
|
56
|
+
|
57
|
+
def notify(state)
|
58
|
+
@manager.notify_state_change(@node, state)
|
59
|
+
end
|
45
60
|
end
|
46
61
|
end
|
data/spec/client_spec.rb
CHANGED
@@ -15,7 +15,7 @@ module RedisFailover
|
|
15
15
|
{
|
16
16
|
:master => 'localhost:6379',
|
17
17
|
:slaves => ['localhost:1111'],
|
18
|
-
:
|
18
|
+
:unavailable => []
|
19
19
|
}
|
20
20
|
end
|
21
21
|
end
|
@@ -57,7 +57,7 @@ module RedisFailover
|
|
57
57
|
client.get('foo')
|
58
58
|
end
|
59
59
|
|
60
|
-
it 'reconnects with redis failover server when node is
|
60
|
+
it 'reconnects with redis failover server when node is unavailable' do
|
61
61
|
class << client
|
62
62
|
attr_reader :reconnected
|
63
63
|
def build_clients
|
@@ -70,26 +70,30 @@ module RedisFailover
|
|
70
70
|
{
|
71
71
|
:master => "localhost:222#{@calls += 1}",
|
72
72
|
:slaves => ['localhost:1111'],
|
73
|
-
:
|
73
|
+
:unavailable => []
|
74
74
|
}
|
75
75
|
end
|
76
76
|
end
|
77
77
|
|
78
|
-
client.current_master.
|
78
|
+
client.current_master.make_unavailable!
|
79
79
|
client.del('foo')
|
80
80
|
client.reconnected.should be_true
|
81
81
|
end
|
82
82
|
|
83
|
-
it 'fails hard when the failover server is
|
83
|
+
it 'fails hard when the failover server is unavailable' do
|
84
84
|
expect do
|
85
85
|
Client.new(:host => 'foo', :port => 123445)
|
86
|
-
end.to raise_error(
|
86
|
+
end.to raise_error(FailoverServerUnavailableError)
|
87
87
|
end
|
88
88
|
|
89
89
|
it 'properly detects when a node has changed roles' do
|
90
90
|
client.current_master.change_role_to('slave')
|
91
91
|
expect { client.send(:master) }.to raise_error(InvalidNodeRoleError)
|
92
92
|
end
|
93
|
+
|
94
|
+
it 'raises error for unsupported operations' do
|
95
|
+
expect { client.select }.to raise_error(UnsupportedOperationError)
|
96
|
+
end
|
93
97
|
end
|
94
98
|
end
|
95
99
|
end
|
data/spec/node_manager_spec.rb
CHANGED
@@ -9,17 +9,17 @@ module RedisFailover
|
|
9
9
|
manager.nodes.should == {
|
10
10
|
:master => 'master:6379',
|
11
11
|
:slaves => ['slave:6379'],
|
12
|
-
:
|
12
|
+
:unavailable => []
|
13
13
|
}
|
14
14
|
end
|
15
15
|
end
|
16
16
|
|
17
|
-
describe '#
|
17
|
+
describe '#handle_unavailable' do
|
18
18
|
context 'slave dies' do
|
19
|
-
it 'moves slave to
|
19
|
+
it 'moves slave to unavailable list' do
|
20
20
|
slave = manager.slaves.first
|
21
|
-
manager.
|
22
|
-
manager.nodes[:
|
21
|
+
manager.force_unavailable(slave)
|
22
|
+
manager.nodes[:unavailable].should include(slave.to_s)
|
23
23
|
end
|
24
24
|
end
|
25
25
|
|
@@ -27,43 +27,57 @@ module RedisFailover
|
|
27
27
|
before(:each) do
|
28
28
|
@slave = manager.slaves.first
|
29
29
|
@master = manager.master
|
30
|
-
manager.
|
30
|
+
manager.force_unavailable(@master)
|
31
31
|
end
|
32
32
|
|
33
33
|
it 'promotes slave to master' do
|
34
34
|
manager.master.should == @slave
|
35
35
|
end
|
36
36
|
|
37
|
-
it 'moves master to
|
38
|
-
manager.nodes[:
|
37
|
+
it 'moves master to unavailable list' do
|
38
|
+
manager.nodes[:unavailable].should include(@master.to_s)
|
39
39
|
end
|
40
40
|
end
|
41
41
|
end
|
42
42
|
|
43
|
-
describe '#
|
43
|
+
describe '#handle_available' do
|
44
44
|
before(:each) do
|
45
|
-
# force to be
|
45
|
+
# force to be unavailable first
|
46
46
|
@slave = manager.slaves.first
|
47
|
-
manager.
|
47
|
+
manager.force_unavailable(@slave)
|
48
48
|
end
|
49
49
|
|
50
50
|
context 'slave node with a master present' do
|
51
|
-
it 'removes slave from
|
52
|
-
manager.
|
53
|
-
manager.nodes[:
|
51
|
+
it 'removes slave from unavailable list' do
|
52
|
+
manager.force_available(@slave)
|
53
|
+
manager.nodes[:unavailable].should be_empty
|
54
54
|
manager.nodes[:slaves].should include(@slave.to_s)
|
55
55
|
end
|
56
|
+
|
57
|
+
it 'makes node a slave of new master' do
|
58
|
+
manager.master = Node.new(:host => 'foo', :port => '7892')
|
59
|
+
manager.force_available(@slave)
|
60
|
+
@slave.fetch_info.should == {
|
61
|
+
:role => 'slave',
|
62
|
+
:master_host => 'foo',
|
63
|
+
:master_port => '7892'}
|
64
|
+
end
|
65
|
+
|
66
|
+
it 'does not invoke slaveof operation if master has not changed' do
|
67
|
+
@slave.redis.should_not_receive(:slaveof)
|
68
|
+
manager.force_available(@slave)
|
69
|
+
end
|
56
70
|
end
|
57
71
|
|
58
72
|
context 'slave node with no master present' do
|
59
73
|
before(:each) do
|
60
74
|
@master = manager.master
|
61
|
-
manager.
|
75
|
+
manager.force_unavailable(@master)
|
62
76
|
end
|
63
77
|
|
64
78
|
it 'promotes slave to master' do
|
65
79
|
manager.master.should be_nil
|
66
|
-
manager.
|
80
|
+
manager.force_available(@slave)
|
67
81
|
manager.master.should == @slave
|
68
82
|
end
|
69
83
|
|
@@ -72,5 +86,24 @@ module RedisFailover
|
|
72
86
|
end
|
73
87
|
end
|
74
88
|
end
|
89
|
+
|
90
|
+
describe '#handle_syncing' do
|
91
|
+
context 'prohibits stale reads' do
|
92
|
+
it 'adds node to unavailable list' do
|
93
|
+
slave = manager.slaves.first
|
94
|
+
manager.force_syncing(slave, false)
|
95
|
+
manager.nodes[:unavailable].should include(slave.to_s)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
context 'allows stale reads' do
|
100
|
+
it 'makes node available' do
|
101
|
+
slave = manager.slaves.first
|
102
|
+
manager.force_syncing(slave, true)
|
103
|
+
manager.nodes[:unavailable].should_not include(slave.to_s)
|
104
|
+
manager.nodes[:slaves].should include(slave.to_s)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
75
108
|
end
|
76
109
|
end
|
data/spec/node_spec.rb
CHANGED
@@ -20,13 +20,13 @@ module RedisFailover
|
|
20
20
|
end
|
21
21
|
|
22
22
|
describe '#ping' do
|
23
|
-
it 'responds properly if node is
|
23
|
+
it 'responds properly if node is available' do
|
24
24
|
expect { node.ping }.to_not raise_error
|
25
25
|
end
|
26
26
|
|
27
|
-
it 'responds properly if node is
|
28
|
-
node.redis.
|
29
|
-
expect { node.ping }.to raise_error(
|
27
|
+
it 'responds properly if node is unavailable' do
|
28
|
+
node.redis.make_unavailable!
|
29
|
+
expect { node.ping }.to raise_error(NodeUnavailableError)
|
30
30
|
end
|
31
31
|
end
|
32
32
|
|
@@ -53,24 +53,32 @@ module RedisFailover
|
|
53
53
|
end
|
54
54
|
end
|
55
55
|
|
56
|
-
describe '#
|
56
|
+
describe '#wait' do
|
57
57
|
it 'should wait until node dies' do
|
58
|
-
thread = Thread.new { node.
|
58
|
+
thread = Thread.new { node.wait }
|
59
59
|
thread.should be_alive
|
60
|
-
node.redis.
|
60
|
+
node.redis.make_unavailable!
|
61
61
|
expect { thread.value }.to raise_error
|
62
62
|
end
|
63
63
|
end
|
64
64
|
|
65
|
-
describe '#
|
65
|
+
describe '#wakeup' do
|
66
66
|
it 'should gracefully stop waiting' do
|
67
|
-
thread = Thread.new { node.
|
67
|
+
thread = Thread.new { node.wait }
|
68
68
|
thread.should be_alive
|
69
|
-
node.
|
69
|
+
node.wakeup
|
70
70
|
sleep 0.2
|
71
71
|
thread.should_not be_alive
|
72
72
|
thread.value.should be_nil
|
73
73
|
end
|
74
74
|
end
|
75
|
+
|
76
|
+
describe '#perform_operation' do
|
77
|
+
it 'raises error for any operation that hangs for too long' do
|
78
|
+
expect do
|
79
|
+
node.send(:perform_operation) { 1_000_000.times { sleep 0.1 } }
|
80
|
+
end.to raise_error(NodeUnavailableError)
|
81
|
+
end
|
82
|
+
end
|
75
83
|
end
|
76
84
|
end
|
data/spec/node_watcher_spec.rb
CHANGED
@@ -20,23 +20,38 @@ module RedisFailover
|
|
20
20
|
let(:node) { Node.new(:host => 'host', :port => 123).extend(RedisStubSupport) }
|
21
21
|
|
22
22
|
describe '#watch' do
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
23
|
+
context 'node is not syncing with master' do
|
24
|
+
it 'properly informs manager of unavailable node' do
|
25
|
+
watcher = NodeWatcher.new(node_manager, node, 1)
|
26
|
+
watcher.watch
|
27
|
+
sleep(3)
|
28
|
+
node.redis.make_unavailable!
|
29
|
+
sleep(3)
|
30
|
+
watcher.shutdown
|
31
|
+
node_manager.state_for(node).should == :unavailable
|
32
|
+
end
|
33
|
+
|
34
|
+
it 'properly informs manager of available node' do
|
35
|
+
node_manager.notify_state_change(node, :unavailable)
|
36
|
+
watcher = NodeWatcher.new(node_manager, node, 1)
|
37
|
+
watcher.watch
|
38
|
+
sleep(3)
|
39
|
+
watcher.shutdown
|
40
|
+
node_manager.state_for(node).should == :available
|
41
|
+
end
|
31
42
|
end
|
32
43
|
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
44
|
+
context 'node is syncing with master' do
|
45
|
+
it 'properly informs manager of syncing node' do
|
46
|
+
node_manager.notify_state_change(node, :unavailable)
|
47
|
+
node.redis.slaveof('masterhost', 9876)
|
48
|
+
node.redis.force_sync_with_master(true)
|
49
|
+
watcher = NodeWatcher.new(node_manager, node, 1)
|
50
|
+
watcher.watch
|
51
|
+
sleep(3)
|
52
|
+
watcher.shutdown
|
53
|
+
node_manager.state_for(node).should == :syncing
|
54
|
+
end
|
40
55
|
end
|
41
56
|
end
|
42
57
|
end
|
@@ -1,5 +1,7 @@
|
|
1
1
|
module RedisFailover
|
2
2
|
class NodeManagerStub < NodeManager
|
3
|
+
attr_accessor :master
|
4
|
+
|
3
5
|
def parse_nodes
|
4
6
|
master = Node.new(:host => 'master')
|
5
7
|
slave = Node.new(:host => 'slave')
|
@@ -9,10 +11,6 @@ module RedisFailover
|
|
9
11
|
[master, [slave]]
|
10
12
|
end
|
11
13
|
|
12
|
-
def master
|
13
|
-
@master
|
14
|
-
end
|
15
|
-
|
16
14
|
def slaves
|
17
15
|
@slaves
|
18
16
|
end
|
@@ -26,17 +24,24 @@ module RedisFailover
|
|
26
24
|
@thread.value
|
27
25
|
end
|
28
26
|
|
29
|
-
def
|
27
|
+
def force_unavailable(node)
|
28
|
+
start_processing
|
29
|
+
node.redis.make_unavailable!
|
30
|
+
notify_state_change(node, :unavailable)
|
31
|
+
stop_processing
|
32
|
+
end
|
33
|
+
|
34
|
+
def force_available(node)
|
30
35
|
start_processing
|
31
|
-
node.redis.
|
32
|
-
notify_state_change(node, :
|
36
|
+
node.redis.make_available!
|
37
|
+
notify_state_change(node, :available)
|
33
38
|
stop_processing
|
34
39
|
end
|
35
40
|
|
36
|
-
def
|
41
|
+
def force_syncing(node, serve_stale_reads)
|
37
42
|
start_processing
|
38
|
-
node.redis.
|
39
|
-
notify_state_change(node, :
|
43
|
+
node.redis.force_sync_with_master(serve_stale_reads)
|
44
|
+
notify_state_change(node, :syncing)
|
40
45
|
stop_processing
|
41
46
|
end
|
42
47
|
end
|
data/spec/support/redis_stub.rb
CHANGED
@@ -6,6 +6,7 @@ module RedisFailover
|
|
6
6
|
class Proxy
|
7
7
|
def initialize(queue, opts = {})
|
8
8
|
@info = {'role' => 'master'}
|
9
|
+
@config = {'slave-serve-stale-data' => 'yes'}
|
9
10
|
@queue = queue
|
10
11
|
end
|
11
12
|
|
@@ -25,8 +26,12 @@ module RedisFailover
|
|
25
26
|
def slaveof(host, port)
|
26
27
|
if host == 'no' && port == 'one'
|
27
28
|
@info['role'] = 'master'
|
29
|
+
@info.delete('master_host')
|
30
|
+
@info.delete('master_port')
|
28
31
|
else
|
29
32
|
@info['role'] = 'slave'
|
33
|
+
@info['master_host'] = host
|
34
|
+
@info['master_port'] = port.to_s
|
30
35
|
end
|
31
36
|
end
|
32
37
|
|
@@ -34,26 +39,35 @@ module RedisFailover
|
|
34
39
|
@info.dup
|
35
40
|
end
|
36
41
|
|
37
|
-
def ping
|
38
|
-
'pong'
|
39
|
-
end
|
40
|
-
|
41
42
|
def change_role_to(role)
|
42
43
|
@info['role'] = role
|
43
44
|
end
|
45
|
+
|
46
|
+
def config(action, attribute)
|
47
|
+
[action, @config[attribute]]
|
48
|
+
end
|
49
|
+
|
50
|
+
def force_sync_with_master(serve_stale_reads)
|
51
|
+
@config['slave-serve-stale-data'] = serve_stale_reads ? 'yes' : 'no'
|
52
|
+
@info['master_sync_in_progress'] = '1'
|
53
|
+
end
|
54
|
+
|
55
|
+
def force_sync_done
|
56
|
+
@info['master_sync_in_progress'] = '0'
|
57
|
+
end
|
44
58
|
end
|
45
59
|
|
46
|
-
attr_reader :host, :port, :
|
60
|
+
attr_reader :host, :port, :available
|
47
61
|
def initialize(opts = {})
|
48
62
|
@host = opts[:host]
|
49
63
|
@port = Integer(opts[:port])
|
50
64
|
@queue = Queue.new
|
51
65
|
@proxy = Proxy.new(@queue, opts)
|
52
|
-
@
|
66
|
+
@available = true
|
53
67
|
end
|
54
68
|
|
55
69
|
def method_missing(method, *args, &block)
|
56
|
-
if @
|
70
|
+
if @available
|
57
71
|
@proxy.send(method, *args, &block)
|
58
72
|
else
|
59
73
|
raise Errno::ECONNREFUSED
|
@@ -64,13 +78,13 @@ module RedisFailover
|
|
64
78
|
@proxy.change_role_to(role)
|
65
79
|
end
|
66
80
|
|
67
|
-
def
|
68
|
-
@
|
81
|
+
def make_available!
|
82
|
+
@available = true
|
69
83
|
end
|
70
84
|
|
71
|
-
def
|
85
|
+
def make_unavailable!
|
72
86
|
@queue << Errno::ECONNREFUSED
|
73
|
-
@
|
87
|
+
@available = false
|
74
88
|
end
|
75
89
|
|
76
90
|
def to_s
|
@@ -86,5 +100,6 @@ module RedisFailover
|
|
86
100
|
def redis
|
87
101
|
@redis ||= RedisStub.new(:host => @host, :port => @port)
|
88
102
|
end
|
103
|
+
alias_method :new_client, :redis
|
89
104
|
end
|
90
105
|
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.4.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-15 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: redis
|
16
|
-
requirement: &
|
16
|
+
requirement: &70210559246440 !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: *70210559246440
|
25
25
|
- !ruby/object:Gem::Dependency
|
26
26
|
name: redis-namespace
|
27
|
-
requirement: &
|
27
|
+
requirement: &70210559246000 !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: *70210559246000
|
36
36
|
- !ruby/object:Gem::Dependency
|
37
37
|
name: multi_json
|
38
|
-
requirement: &
|
38
|
+
requirement: &70210559245580 !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: *70210559245580
|
47
47
|
- !ruby/object:Gem::Dependency
|
48
48
|
name: sinatra
|
49
|
-
requirement: &
|
49
|
+
requirement: &70210559245160 !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: *70210559245160
|
58
58
|
- !ruby/object:Gem::Dependency
|
59
59
|
name: rake
|
60
|
-
requirement: &
|
60
|
+
requirement: &70210559244740 !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: *70210559244740
|
69
69
|
- !ruby/object:Gem::Dependency
|
70
70
|
name: rspec
|
71
|
-
requirement: &
|
71
|
+
requirement: &70210559244320 !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: *70210559244320
|
80
80
|
description: Redis Failover provides a full automatic master/slave failover solution
|
81
81
|
for Ruby
|
82
82
|
email:
|
@@ -129,7 +129,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
129
129
|
version: '0'
|
130
130
|
segments:
|
131
131
|
- 0
|
132
|
-
hash: -
|
132
|
+
hash: -4081812195470581448
|
133
133
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
134
134
|
none: false
|
135
135
|
requirements:
|
@@ -138,7 +138,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
138
138
|
version: '0'
|
139
139
|
segments:
|
140
140
|
- 0
|
141
|
-
hash: -
|
141
|
+
hash: -4081812195470581448
|
142
142
|
requirements: []
|
143
143
|
rubyforge_project:
|
144
144
|
rubygems_version: 1.8.16
|