redis_failover 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,4 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.9.2
4
+ - 1.9.3
data/Changes.md CHANGED
@@ -1,3 +1,9 @@
1
+ 0.3.0
2
+ -----------
3
+ - Integrated travis-ci
4
+ - Added background monitor to client for proactively detecting
5
+ changes to current set of redis nodes
6
+
1
7
  0.2.0
2
8
  -----------
3
9
  - Added retry support for contacting failover server from client
data/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # Automatic Redis Failover Client/Server
2
2
 
3
+ [![Build Status](https://secure.travis-ci.org/ryanlecompte/redis_failover.png?branch=master)](http://travis-ci.org/ryanlecompte/redis_failover)
4
+
3
5
  Redis Failover attempts to provides a full automatic master/slave failover solution for Ruby. Redis does not provide
4
6
  an automatic failover capability when configured for master/slave replication. When the master node dies,
5
7
  a new master must be manually brought online and assigned as the slave's new master. This manual
@@ -86,11 +86,11 @@ module RedisFailover
86
86
  @retry = options[:retry_failure] || true
87
87
  @max_retries = @retry ? options.fetch(:max_retries, 3) : 0
88
88
  @server_url = "http://#{options[:host]}:#{options[:port]}/redis_servers"
89
- @redis_servers = nil
90
89
  @master = nil
91
90
  @slaves = []
92
91
  @lock = Mutex.new
93
92
  build_clients
93
+ start_background_monitor
94
94
  end
95
95
 
96
96
  def method_missing(method, *args, &block)
@@ -106,7 +106,7 @@ module RedisFailover
106
106
  end
107
107
 
108
108
  def inspect
109
- "#<RedisFailover::Client - master: #{master_info}, slaves: #{slaves_info})>"
109
+ "#<RedisFailover::Client - master: #{master_name}, slaves: #{slave_names})>"
110
110
  end
111
111
  alias_method :to_s, :inspect
112
112
 
@@ -141,9 +141,10 @@ module RedisFailover
141
141
  end
142
142
 
143
143
  def master
144
- if @master
145
- verify_role!(@master, :master)
146
- return @master
144
+ master = @master
145
+ if master
146
+ verify_role!(master, :master)
147
+ return master
147
148
  end
148
149
  raise NoMasterError
149
150
  end
@@ -162,16 +163,19 @@ module RedisFailover
162
163
  tries = 0
163
164
 
164
165
  begin
165
- logger.info('Attempting to fetch nodes and build redis clients.')
166
- servers = fetch_redis_servers
167
- master = new_clients_for(servers[:master]).first if servers[:master]
168
- slaves = new_clients_for(*servers[:slaves])
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])
169
173
 
170
174
  # once clients are successfully created, swap the references
171
175
  @master = master
172
176
  @slaves = slaves
173
177
  rescue => ex
174
- logger.error("Failed to fetch servers from #{@server_url} - #{ex.message}")
178
+ logger.error("Failed to fetch nodes from #{@server_url} - #{ex.message}")
175
179
  logger.error(ex.backtrace.join("\n"))
176
180
 
177
181
  if tries < @max_retries
@@ -184,11 +188,11 @@ module RedisFailover
184
188
  end
185
189
  end
186
190
 
187
- def fetch_redis_servers
191
+ def fetch_nodes
188
192
  open(@server_url) do |io|
189
- servers = symbolize_keys(MultiJson.decode(io))
190
- logger.info("Fetched servers: #{servers}")
191
- servers
193
+ nodes = symbolize_keys(MultiJson.decode(io))
194
+ logger.info("Fetched nodes: #{nodes}")
195
+ nodes
192
196
  end
193
197
  end
194
198
 
@@ -203,26 +207,52 @@ module RedisFailover
203
207
  end
204
208
  end
205
209
 
206
- def master_info
207
- return "none" unless @master
208
- name_for(@master)
210
+ def master_name
211
+ address_for(@master) || 'none'
209
212
  end
210
213
 
211
- def slaves_info
212
- return "none" if @slaves.empty?
213
- @slaves.map { |slave| name_for(slave) }.join(', ')
214
+ def slave_names
215
+ return 'none' if @slaves.empty?
216
+ addresses_for(@slaves).join(', ')
214
217
  end
215
218
 
216
219
  def verify_role!(node, role)
217
220
  current_role = node.info['role']
218
221
  if current_role.to_sym != role
219
- raise InvalidNodeRoleError.new(name_for(node), role, current_role)
222
+ raise InvalidNodeRoleError.new(address_for(node), role, current_role)
220
223
  end
221
224
  role
222
225
  end
223
226
 
224
- def name_for(node)
227
+ def addresses_for(nodes)
228
+ nodes.map { |node| address_for(node) }
229
+ end
230
+
231
+ def address_for(node)
232
+ return unless node
225
233
  "#{node.client.host}:#{node.client.port}"
226
234
  end
235
+
236
+ def nodes_changed?(new_nodes)
237
+ return true if address_for(@master) != new_nodes[:master]
238
+ return true if different?(addresses_for(@slaves), new_nodes[:slaves])
239
+ false
240
+ end
241
+
242
+ # Spawns a background thread to periodically fetch the latest
243
+ # set of nodes from the redis failover server.
244
+ def start_background_monitor
245
+ Thread.new do
246
+ loop do
247
+ sleep(10)
248
+ begin
249
+ build_clients
250
+ rescue => ex
251
+ logger.error("Failed to poll for new nodes from #{@server_url} - #{ex.message}")
252
+ logger.error(ex.backtrace.join("\n"))
253
+ end
254
+ end
255
+ end
256
+ end
227
257
  end
228
258
  end
@@ -7,6 +7,10 @@ module RedisFailover
7
7
  Hash[hash.map { |k, v| [k.to_sym, v] }]
8
8
  end
9
9
 
10
+ def different?(ary_a, ary_b)
11
+ ((ary_a | ary_b) - (ary_a & ary_b)).size > 0
12
+ end
13
+
10
14
  def self.logger
11
15
  @logger ||= begin
12
16
  logger = Logger.new(STDOUT)
@@ -1,3 +1,3 @@
1
1
  module RedisFailover
2
- VERSION = "0.2.0"
2
+ VERSION = "0.3.0"
3
3
  end
@@ -11,7 +11,7 @@ module RedisFailover
11
11
  @slaves
12
12
  end
13
13
 
14
- def fetch_redis_servers
14
+ def fetch_nodes
15
15
  {
16
16
  :master => 'localhost:6379',
17
17
  :slaves => ['localhost:1111'],
@@ -31,6 +31,18 @@ module RedisFailover
31
31
  it 'properly parses slaves' do
32
32
  client.current_slaves.first.to_s.should == 'localhost:1111'
33
33
  end
34
+
35
+ it 'does not rebuild clients if hosts have not changed' do
36
+ class << client
37
+ attr_reader :built_new_client
38
+ def new_clients_for(*)
39
+ @built_new_client = true
40
+ end
41
+ end
42
+
43
+ 5.times { client.send(:build_clients) }
44
+ client.built_new_client.should_not be_true
45
+ end
34
46
  end
35
47
 
36
48
  describe '#dispatch' do
@@ -52,6 +64,15 @@ module RedisFailover
52
64
  @reconnected = true
53
65
  super
54
66
  end
67
+
68
+ def fetch_nodes
69
+ @calls ||= 0
70
+ {
71
+ :master => "localhost:222#{@calls += 1}",
72
+ :slaves => ['localhost:1111'],
73
+ :unreachable => []
74
+ }
75
+ end
55
76
  end
56
77
 
57
78
  client.current_master.make_unreachable!
@@ -7,5 +7,15 @@ module RedisFailover
7
7
  Util.symbolize_keys('a' => 1, 'b' => 2).should == {:a => 1, :b => 2}
8
8
  end
9
9
  end
10
+
11
+ describe '.different?' do
12
+ it 'handles different arrays' do
13
+ Util.different?([1,2,3], [1,5,3]).should be_true
14
+ end
15
+
16
+ it 'handles non-different arrays' do
17
+ Util.different?([1,2,3], [3,2,1]).should be_false
18
+ end
19
+ end
10
20
  end
11
21
  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.2.0
4
+ version: 0.3.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-13 00:00:00.000000000 Z
12
+ date: 2012-04-14 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: redis
16
- requirement: &70198326575220 !ruby/object:Gem::Requirement
16
+ requirement: &70202974273640 !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: *70198326575220
24
+ version_requirements: *70202974273640
25
25
  - !ruby/object:Gem::Dependency
26
26
  name: redis-namespace
27
- requirement: &70198326574780 !ruby/object:Gem::Requirement
27
+ requirement: &70202974273200 !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: *70198326574780
35
+ version_requirements: *70202974273200
36
36
  - !ruby/object:Gem::Dependency
37
37
  name: multi_json
38
- requirement: &70198326574360 !ruby/object:Gem::Requirement
38
+ requirement: &70202974272780 !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: *70198326574360
46
+ version_requirements: *70202974272780
47
47
  - !ruby/object:Gem::Dependency
48
48
  name: sinatra
49
- requirement: &70198326573940 !ruby/object:Gem::Requirement
49
+ requirement: &70202974272360 !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: *70198326573940
57
+ version_requirements: *70202974272360
58
58
  - !ruby/object:Gem::Dependency
59
59
  name: rake
60
- requirement: &70198326573520 !ruby/object:Gem::Requirement
60
+ requirement: &70202974271940 !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: *70198326573520
68
+ version_requirements: *70202974271940
69
69
  - !ruby/object:Gem::Dependency
70
70
  name: rspec
71
- requirement: &70198326573100 !ruby/object:Gem::Requirement
71
+ requirement: &70202974271520 !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: *70198326573100
79
+ version_requirements: *70202974271520
80
80
  description: Redis Failover provides a full automatic master/slave failover solution
81
81
  for Ruby
82
82
  email:
@@ -87,6 +87,7 @@ extensions: []
87
87
  extra_rdoc_files: []
88
88
  files:
89
89
  - .gitignore
90
+ - .travis.yml
90
91
  - Changes.md
91
92
  - Gemfile
92
93
  - LICENSE
@@ -128,7 +129,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
128
129
  version: '0'
129
130
  segments:
130
131
  - 0
131
- hash: -200241163739368802
132
+ hash: -1781006209108187327
132
133
  required_rubygems_version: !ruby/object:Gem::Requirement
133
134
  none: false
134
135
  requirements:
@@ -137,7 +138,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
137
138
  version: '0'
138
139
  segments:
139
140
  - 0
140
- hash: -200241163739368802
141
+ hash: -1781006209108187327
141
142
  requirements: []
142
143
  rubyforge_project:
143
144
  rubygems_version: 1.8.16