redis_failover 0.2.0 → 0.3.0

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