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 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