redis_failover 0.5.2 → 0.5.3

Sign up to get free protection for your applications and to get access to all the features.
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 zookeeper host:port pairs (required)
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 zookeeper host:port pairs (required)
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
@@ -20,8 +20,8 @@ module RedisFailover
20
20
  end
21
21
  end
22
22
 
23
- opts.on('-z', '--zkservers zookeeper servers',
24
- 'Comma-separated zookeeper host:port pairs (required)') do |servers|
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 zookeeper host:port pairs (required)
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
- @zkclient.stat(@znode, :watch => true)
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.message}")
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.message}")
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.message}")
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 zookeeper node #{@znode}")
192
+ logger.info("Deleted ZooKeeper node #{@znode}")
192
193
  rescue ZK::Exceptions::NoNode => ex
193
- logger.info("Tried to delete missing znode: #{ex.message}")
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 zookeeper node #{@znode}")
199
+ logger.info("Created ZooKeeper node #{@znode}")
199
200
  rescue ZK::Exceptions::NodeExists
200
201
  # best effort
201
202
  end
@@ -3,7 +3,7 @@ module RedisFailover
3
3
  class NodeWatcher
4
4
  include Util
5
5
 
6
- WATCHER_SLEEP_TIME = 3
6
+ WATCHER_SLEEP_TIME = 2
7
7
 
8
8
  def initialize(manager, node, max_failures)
9
9
  @manager = manager
@@ -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
 
@@ -1,3 +1,3 @@
1
1
  module RedisFailover
2
- VERSION = "0.5.2"
2
+ VERSION = "0.5.3"
3
3
  end
@@ -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 ZookeeperExceptions::ZookeeperException::SessionExpired
60
- logger.info("Zookeeper client session expired, rebuilding client.")
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 zookeeper servers #{@servers}")
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; end
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
- client.current_master.should_receive(:del)
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.should_receive(:get)
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.2
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-17 00:00:00.000000000 Z
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: &70127944589860 !ruby/object:Gem::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: *70127944589860
24
+ version_requirements: *70306532639260
25
25
  - !ruby/object:Gem::Dependency
26
26
  name: redis-namespace
27
- requirement: &70127944589440 !ruby/object:Gem::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: *70127944589440
35
+ version_requirements: *70306532638840
36
36
  - !ruby/object:Gem::Dependency
37
37
  name: multi_json
38
- requirement: &70127944589000 !ruby/object:Gem::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: *70127944589000
46
+ version_requirements: *70306532638420
47
47
  - !ruby/object:Gem::Dependency
48
48
  name: zk
49
- requirement: &70127944588580 !ruby/object:Gem::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: *70127944588580
57
+ version_requirements: *70306532638000
58
58
  - !ruby/object:Gem::Dependency
59
59
  name: rake
60
- requirement: &70127944588160 !ruby/object:Gem::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: *70127944588160
68
+ version_requirements: *70306532637580
69
69
  - !ruby/object:Gem::Dependency
70
70
  name: rspec
71
- requirement: &70127944587740 !ruby/object:Gem::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: *70127944587740
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: -3828155279649118741
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: -3828155279649118741
141
+ hash: -4476900958412029501
142
142
  requirements: []
143
143
  rubyforge_project:
144
144
  rubygems_version: 1.8.16