redis_failover 0.5.2 → 0.5.3
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 +5 -0
- data/README.md +7 -2
- data/lib/redis_failover/cli.rb +2 -2
- data/lib/redis_failover/client.rb +22 -5
- data/lib/redis_failover/errors.rb +6 -0
- data/lib/redis_failover/node_manager.rb +5 -4
- data/lib/redis_failover/node_watcher.rb +1 -1
- data/lib/redis_failover/runner.rb +1 -1
- data/lib/redis_failover/version.rb +1 -1
- data/lib/redis_failover/zk_client.rb +14 -3
- data/spec/client_spec.rb +18 -3
- metadata +16 -16
data/Changes.md
CHANGED
@@ -1,3 +1,8 @@
|
|
1
|
+
0.5.3
|
2
|
+
-----------
|
3
|
+
- Handle more ZK exceptions as candidates for reconnecting the client on error.
|
4
|
+
- Add safety check to actively purge redis clients if a RedisFailover::Client hasn't heard from the Node Manager in a certain time window.
|
5
|
+
|
1
6
|
0.5.2
|
2
7
|
-----------
|
3
8
|
- Always try to create path before setting current state in Node Manager.
|
data/README.md
CHANGED
@@ -58,7 +58,7 @@ following options:
|
|
58
58
|
Usage: redis_node_manager [OPTIONS]
|
59
59
|
-p, --password password Redis password (optional)
|
60
60
|
-n, --nodes redis nodes Comma-separated redis host:port pairs (required)
|
61
|
-
-z zookeeper servers, Comma-separated
|
61
|
+
-z zookeeper servers, Comma-separated ZooKeeper host:port pairs (required)
|
62
62
|
--zkservers
|
63
63
|
--znode-path path Znode path override for storing redis server list (optional)
|
64
64
|
--max-failures count Max failures before manager marks node unavailable (default 3)
|
@@ -85,7 +85,7 @@ a drop-in replacement for your existing pure redis client usage.
|
|
85
85
|
|
86
86
|
The full set of options that can be passed to RedisFailover::Client are:
|
87
87
|
|
88
|
-
:zkservers - comma-separated
|
88
|
+
:zkservers - comma-separated ZooKeeper host:port pairs (required)
|
89
89
|
:znode_path - the Znode path override for redis server list (optional)
|
90
90
|
:password - password for redis nodes (optional)
|
91
91
|
:namespace - namespace for redis nodes (optional)
|
@@ -114,6 +114,7 @@ The full set of options that can be passed to RedisFailover::Client are:
|
|
114
114
|
- To learn more about Redis master/slave replication, see the [Redis documentation](http://redis.io/topics/replication).
|
115
115
|
- To learn more about ZooKeeper, see the official [ZooKeeper](http://zookeeper.apache.org/) site.
|
116
116
|
- See the [Quick ZooKeeper Guide](https://github.com/ryanlecompte/redis_failover/wiki/Quick-ZooKeeper-Guide) for a quick guide to getting ZooKeeper up and running with redis_failover.
|
117
|
+
- To learn more about how ZooKeeper handles network partitions, see [ZooKeeper Failure Scenarios](http://wiki.apache.org/hadoop/ZooKeeper/FailureScenarios)
|
117
118
|
|
118
119
|
## License
|
119
120
|
|
@@ -125,6 +126,10 @@ Ryan LeCompte
|
|
125
126
|
|
126
127
|
[@ryanlecompte](https://twitter.com/ryanlecompte)
|
127
128
|
|
129
|
+
## Acknowledgements
|
130
|
+
|
131
|
+
Special thanks to [Eric Lindvall](https://github.com/eric) and [Jonathan Simms](https://github.com/slyphon) for their invaluable ZooKeeper advice and guidance!
|
132
|
+
|
128
133
|
## Contributing
|
129
134
|
|
130
135
|
1. Fork it
|
data/lib/redis_failover/cli.rb
CHANGED
@@ -20,8 +20,8 @@ module RedisFailover
|
|
20
20
|
end
|
21
21
|
end
|
22
22
|
|
23
|
-
opts.on('-z', '--zkservers
|
24
|
-
'Comma-separated
|
23
|
+
opts.on('-z', '--zkservers ZooKeeper servers',
|
24
|
+
'Comma-separated ZooKeeper host:port pairs (required)') do |servers|
|
25
25
|
options[:zkservers] = servers
|
26
26
|
end
|
27
27
|
|
@@ -5,6 +5,7 @@ module RedisFailover
|
|
5
5
|
class Client
|
6
6
|
include Util
|
7
7
|
|
8
|
+
ZNODE_UPDATE_TIMEOUT = 9
|
8
9
|
RETRY_WAIT_TIME = 3
|
9
10
|
REDIS_READ_OPS = Set[
|
10
11
|
:echo,
|
@@ -65,7 +66,7 @@ module RedisFailover
|
|
65
66
|
#
|
66
67
|
# Options:
|
67
68
|
#
|
68
|
-
# :zkservers - comma-separated
|
69
|
+
# :zkservers - comma-separated ZooKeeper host:port pairs (required)
|
69
70
|
# :znode_path - the Znode path override for redis server list (optional)
|
70
71
|
# :password - password for redis nodes (optional)
|
71
72
|
# :namespace - namespace for redis nodes (optional)
|
@@ -109,6 +110,7 @@ module RedisFailover
|
|
109
110
|
|
110
111
|
def setup_zookeeper_client
|
111
112
|
@zkclient = ZkClient.new(@zkservers)
|
113
|
+
update_znode_timestamp
|
112
114
|
|
113
115
|
# when session expires, purge client list
|
114
116
|
@zkclient.on_session_expiration do
|
@@ -128,10 +130,12 @@ module RedisFailover
|
|
128
130
|
# register a watcher for future changes
|
129
131
|
@zkclient.watcher.register(@znode) do |event|
|
130
132
|
if event.node_created? || event.node_changed?
|
133
|
+
update_znode_timestamp
|
131
134
|
build_clients
|
132
135
|
elsif event.node_deleted?
|
133
|
-
|
136
|
+
update_znode_timestamp
|
134
137
|
purge_clients
|
138
|
+
@zkclient.stat(@znode, :watch => true)
|
135
139
|
else
|
136
140
|
logger.error("Unknown ZK node event: #{event.inspect}")
|
137
141
|
end
|
@@ -143,9 +147,13 @@ module RedisFailover
|
|
143
147
|
end
|
144
148
|
|
145
149
|
def dispatch(method, *args, &block)
|
150
|
+
unless recently_heard_from_node_manager?
|
151
|
+
purge_clients
|
152
|
+
raise MissingNodeManagerError.new(ZNODE_UPDATE_TIMEOUT)
|
153
|
+
end
|
154
|
+
|
146
155
|
verify_supported!(method)
|
147
156
|
tries = 0
|
148
|
-
|
149
157
|
begin
|
150
158
|
if REDIS_READ_OPS.include?(method)
|
151
159
|
# send read operations to a slave
|
@@ -155,7 +163,7 @@ module RedisFailover
|
|
155
163
|
master.send(method, *args, &block)
|
156
164
|
end
|
157
165
|
rescue *CONNECTIVITY_ERRORS => ex
|
158
|
-
logger.error("Error while handling operation `#{method}` - #{ex.
|
166
|
+
logger.error("Error while handling operation `#{method}` - #{ex.inspect}")
|
159
167
|
logger.error(ex.backtrace.join("\n"))
|
160
168
|
|
161
169
|
if tries < @max_retries
|
@@ -202,7 +210,7 @@ module RedisFailover
|
|
202
210
|
@slaves = new_slaves
|
203
211
|
rescue StandardError, *CONNECTIVITY_ERRORS => ex
|
204
212
|
purge_clients
|
205
|
-
logger.error("Failed to fetch nodes from #{@zkservers} - #{ex.
|
213
|
+
logger.error("Failed to fetch nodes from #{@zkservers} - #{ex.inspect}")
|
206
214
|
logger.error(ex.backtrace.join("\n"))
|
207
215
|
|
208
216
|
if tries < @max_retries
|
@@ -292,5 +300,14 @@ module RedisFailover
|
|
292
300
|
@slaves = []
|
293
301
|
end
|
294
302
|
end
|
303
|
+
|
304
|
+
def update_znode_timestamp
|
305
|
+
@last_znode_timestamp = Time.now
|
306
|
+
end
|
307
|
+
|
308
|
+
def recently_heard_from_node_manager?
|
309
|
+
return false unless @last_znode_timestamp
|
310
|
+
Time.now - @last_znode_timestamp <= ZNODE_UPDATE_TIMEOUT
|
311
|
+
end
|
295
312
|
end
|
296
313
|
end
|
@@ -40,4 +40,10 @@ module RedisFailover
|
|
40
40
|
super("Operation `#{operation}` is currently unsupported")
|
41
41
|
end
|
42
42
|
end
|
43
|
+
|
44
|
+
class MissingNodeManagerError < Error
|
45
|
+
def initialize(timeout)
|
46
|
+
super("Failed to hear from Node Manager within #{timeout} seconds")
|
47
|
+
end
|
48
|
+
end
|
43
49
|
end
|
@@ -23,6 +23,7 @@ module RedisFailover
|
|
23
23
|
end
|
24
24
|
|
25
25
|
def shutdown
|
26
|
+
@queue << nil
|
26
27
|
@watchers.each(&:shutdown)
|
27
28
|
end
|
28
29
|
|
@@ -42,7 +43,7 @@ module RedisFailover
|
|
42
43
|
# flush current state
|
43
44
|
write_state
|
44
45
|
rescue StandardError, *CONNECTIVITY_ERRORS => ex
|
45
|
-
logger.error("Error while handling #{state_change.inspect}: #{ex.
|
46
|
+
logger.error("Error while handling #{state_change.inspect}: #{ex.inspect}")
|
46
47
|
logger.error(ex.backtrace.join("\n"))
|
47
48
|
end
|
48
49
|
end
|
@@ -188,14 +189,14 @@ module RedisFailover
|
|
188
189
|
|
189
190
|
def delete_path
|
190
191
|
@zkclient.delete(@znode)
|
191
|
-
logger.info("Deleted
|
192
|
+
logger.info("Deleted ZooKeeper node #{@znode}")
|
192
193
|
rescue ZK::Exceptions::NoNode => ex
|
193
|
-
logger.info("Tried to delete missing znode: #{ex.
|
194
|
+
logger.info("Tried to delete missing znode: #{ex.inspect}")
|
194
195
|
end
|
195
196
|
|
196
197
|
def create_path
|
197
198
|
@zkclient.create(@znode, encode(current_nodes), :ephemeral => true)
|
198
|
-
logger.info("Created
|
199
|
+
logger.info("Created ZooKeeper node #{@znode}")
|
199
200
|
rescue ZK::Exceptions::NodeExists
|
200
201
|
# best effort
|
201
202
|
end
|
@@ -6,7 +6,7 @@ module RedisFailover
|
|
6
6
|
@node_manager = NodeManager.new(options)
|
7
7
|
trap_signals
|
8
8
|
node_manager_thread = Thread.new { @node_manager.start }
|
9
|
-
Util.logger.info("Redis Node Manager successfully started
|
9
|
+
Util.logger.info("Redis Node Manager v#{VERSION} successfully started (#{RUBY_DESCRIPTION})")
|
10
10
|
node_manager_thread.join
|
11
11
|
end
|
12
12
|
|
@@ -5,6 +5,16 @@ module RedisFailover
|
|
5
5
|
include Util
|
6
6
|
|
7
7
|
MAX_RECONNECTS = 3
|
8
|
+
RECONNECTABLE_ERRORS = [
|
9
|
+
ZookeeperExceptions::ZookeeperException::SessionExpired,
|
10
|
+
ZookeeperExceptions::ZookeeperException::SystemError,
|
11
|
+
ZookeeperExceptions::ZookeeperException::ConnectionLoss,
|
12
|
+
ZookeeperExceptions::ZookeeperException::OperationTimeOut,
|
13
|
+
ZookeeperExceptions::ZookeeperException::AuthFailed,
|
14
|
+
ZookeeperExceptions::ZookeeperException::SessionMoved,
|
15
|
+
ZookeeperExceptions::ZookeeperException::ConnectionClosed,
|
16
|
+
ZookeeperExceptions::ZookeeperException::NotConnected
|
17
|
+
].freeze
|
8
18
|
|
9
19
|
def initialize(servers)
|
10
20
|
@servers = servers
|
@@ -56,8 +66,9 @@ module RedisFailover
|
|
56
66
|
tries = 0
|
57
67
|
begin
|
58
68
|
yield
|
59
|
-
rescue
|
60
|
-
logger.
|
69
|
+
rescue *RECONNECTABLE_ERRORS => ex
|
70
|
+
logger.error("ZooKeeper client connection error - rebuilding client: #{ex.inspect}")
|
71
|
+
logger.error(ex.backtrace.join("\n"))
|
61
72
|
if tries < MAX_RECONNECTS
|
62
73
|
tries += 1
|
63
74
|
@on_session_expiration.call if @on_session_expiration
|
@@ -77,7 +88,7 @@ module RedisFailover
|
|
77
88
|
else
|
78
89
|
@client = ZK.new(@servers)
|
79
90
|
end
|
80
|
-
logger.info("Communicating with
|
91
|
+
logger.info("Communicating with ZooKeeper servers #{@servers}")
|
81
92
|
end
|
82
93
|
end
|
83
94
|
end
|
data/spec/client_spec.rb
CHANGED
@@ -19,7 +19,9 @@ module RedisFailover
|
|
19
19
|
}
|
20
20
|
end
|
21
21
|
|
22
|
-
def setup_zookeeper_client
|
22
|
+
def setup_zookeeper_client
|
23
|
+
update_znode_timestamp
|
24
|
+
end
|
23
25
|
end
|
24
26
|
|
25
27
|
describe Client do
|
@@ -37,14 +39,22 @@ module RedisFailover
|
|
37
39
|
|
38
40
|
describe '#dispatch' do
|
39
41
|
it 'routes write operations to master' do
|
40
|
-
|
42
|
+
called = false
|
43
|
+
client.current_master.define_singleton_method(:del) do |*args|
|
44
|
+
called = true
|
45
|
+
end
|
41
46
|
client.del('foo')
|
47
|
+
called.should be_true
|
42
48
|
end
|
43
49
|
|
44
50
|
it 'routes read operations to a slave' do
|
51
|
+
called = false
|
45
52
|
client.current_slaves.first.change_role_to('slave')
|
46
|
-
client.current_slaves.first.
|
53
|
+
client.current_slaves.first.define_singleton_method(:get) do |*args|
|
54
|
+
called = true
|
55
|
+
end
|
47
56
|
client.get('foo')
|
57
|
+
called.should be_true
|
48
58
|
end
|
49
59
|
|
50
60
|
it 'reconnects when node is unavailable' do
|
@@ -78,6 +88,11 @@ module RedisFailover
|
|
78
88
|
it 'raises error for unsupported operations' do
|
79
89
|
expect { client.select }.to raise_error(UnsupportedOperationError)
|
80
90
|
end
|
91
|
+
|
92
|
+
it 'raises error when no communication from Node Manager within certain time window' do
|
93
|
+
client.instance_variable_set(:@last_znode_timestamp, Time.at(0))
|
94
|
+
expect { client.del('foo') }.to raise_error(MissingNodeManagerError)
|
95
|
+
end
|
81
96
|
end
|
82
97
|
end
|
83
98
|
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.5.
|
4
|
+
version: 0.5.3
|
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-18 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: redis
|
16
|
-
requirement: &
|
16
|
+
requirement: &70306532639260 !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: *70306532639260
|
25
25
|
- !ruby/object:Gem::Dependency
|
26
26
|
name: redis-namespace
|
27
|
-
requirement: &
|
27
|
+
requirement: &70306532638840 !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: *70306532638840
|
36
36
|
- !ruby/object:Gem::Dependency
|
37
37
|
name: multi_json
|
38
|
-
requirement: &
|
38
|
+
requirement: &70306532638420 !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: *70306532638420
|
47
47
|
- !ruby/object:Gem::Dependency
|
48
48
|
name: zk
|
49
|
-
requirement: &
|
49
|
+
requirement: &70306532638000 !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: *70306532638000
|
58
58
|
- !ruby/object:Gem::Dependency
|
59
59
|
name: rake
|
60
|
-
requirement: &
|
60
|
+
requirement: &70306532637580 !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: *70306532637580
|
69
69
|
- !ruby/object:Gem::Dependency
|
70
70
|
name: rspec
|
71
|
-
requirement: &
|
71
|
+
requirement: &70306532637160 !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: *70306532637160
|
80
80
|
description: Redis Failover is a ZooKeeper-based 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: -4476900958412029501
|
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: -4476900958412029501
|
142
142
|
requirements: []
|
143
143
|
rubyforge_project:
|
144
144
|
rubygems_version: 1.8.16
|