beetle 0.1 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. data/README.rdoc +18 -8
  2. data/beetle.gemspec +37 -121
  3. data/bin/beetle +9 -0
  4. data/examples/README.rdoc +0 -2
  5. data/examples/rpc.rb +3 -2
  6. data/ext/mkrf_conf.rb +19 -0
  7. data/lib/beetle/base.rb +1 -8
  8. data/lib/beetle/client.rb +16 -14
  9. data/lib/beetle/commands/configuration_client.rb +73 -0
  10. data/lib/beetle/commands/configuration_server.rb +85 -0
  11. data/lib/beetle/commands.rb +30 -0
  12. data/lib/beetle/configuration.rb +70 -7
  13. data/lib/beetle/deduplication_store.rb +50 -38
  14. data/lib/beetle/handler.rb +2 -5
  15. data/lib/beetle/logging.rb +7 -0
  16. data/lib/beetle/message.rb +11 -13
  17. data/lib/beetle/publisher.rb +2 -2
  18. data/lib/beetle/r_c.rb +2 -1
  19. data/lib/beetle/redis_configuration_client.rb +136 -0
  20. data/lib/beetle/redis_configuration_server.rb +301 -0
  21. data/lib/beetle/redis_ext.rb +79 -0
  22. data/lib/beetle/redis_master_file.rb +35 -0
  23. data/lib/beetle/redis_server_info.rb +65 -0
  24. data/lib/beetle/subscriber.rb +4 -1
  25. data/lib/beetle.rb +2 -2
  26. data/test/beetle/configuration_test.rb +14 -2
  27. data/test/beetle/deduplication_store_test.rb +61 -43
  28. data/test/beetle/message_test.rb +28 -4
  29. data/test/beetle/redis_configuration_client_test.rb +97 -0
  30. data/test/beetle/redis_configuration_server_test.rb +278 -0
  31. data/test/beetle/redis_ext_test.rb +71 -0
  32. data/test/beetle/redis_master_file_test.rb +39 -0
  33. data/test/test_helper.rb +13 -1
  34. metadata +59 -50
  35. data/.gitignore +0 -5
  36. data/MIT-LICENSE +0 -20
  37. data/Rakefile +0 -114
  38. data/TODO +0 -7
  39. data/doc/redundant_queues.graffle +0 -7744
  40. data/etc/redis-master.conf +0 -189
  41. data/etc/redis-slave.conf +0 -189
  42. data/examples/redis_failover.rb +0 -65
  43. data/script/start_rabbit +0 -29
  44. data/snafu.rb +0 -55
  45. data/test/beetle/bla.rb +0 -0
  46. data/test/beetle.yml +0 -81
  47. data/tmp/master/.gitignore +0 -2
  48. data/tmp/slave/.gitignore +0 -3
@@ -0,0 +1,301 @@
1
+ module Beetle
2
+ # A RedisConfigurationServer is the supervisor part of beetle's
3
+ # redis failover solution
4
+ #
5
+ # An single instance of RedisConfigurationServer works as a supervisor for
6
+ # several RedisConfigurationClient instances. It is responsible for watching
7
+ # the redis master and electing and publishing a new master in case of failure.
8
+ #
9
+ # It will make sure that all configured RedisConfigurationClient instances
10
+ # do not use the old master anymore before making a switch, to prevent
11
+ # inconsistent data.
12
+ #
13
+ # Usually started via <tt>beetle configuration_server</tt> command.
14
+ class RedisConfigurationServer
15
+ include Logging
16
+ include RedisMasterFile
17
+
18
+ # the current redis master
19
+ attr_reader :current_master
20
+
21
+ # the current token used to detect correct message order
22
+ attr_reader :current_token
23
+
24
+ def initialize #:nodoc:
25
+ @client_ids = Set.new(config.redis_configuration_client_ids.split(","))
26
+ @current_token = (Time.now.to_f * 1000).to_i
27
+ @client_pong_ids_received = Set.new
28
+ @client_invalidated_ids_received = Set.new
29
+ MessageDispatcher.configuration_server = self
30
+ end
31
+
32
+ # Redis system status information (an instance of class RedisServerInfo)
33
+ def redis
34
+ @redis ||= RedisServerInfo.new(config, :timeout => 3)
35
+ end
36
+
37
+ # Beetle::Client instance for communication with the RedisConfigurationServer
38
+ def beetle
39
+ @beetle ||= build_beetle
40
+ end
41
+
42
+ def config #:nodoc:
43
+ beetle.config
44
+ end
45
+
46
+ # start watching redis
47
+ def start
48
+ verify_redis_master_file_string
49
+ check_redis_configuration
50
+ redis.refresh
51
+ determine_initial_master
52
+ log_start
53
+ beetle.listen do
54
+ master_watcher.watch
55
+ end
56
+ end
57
+
58
+ # test if redis is currently being watched
59
+ def paused?
60
+ master_watcher.paused?
61
+ end
62
+
63
+ # called by the message dispatcher when a "pong" message from a RedisConfigurationClient is received
64
+ def pong(payload)
65
+ id = payload["id"]
66
+ token = payload["token"]
67
+ logger.info "Received pong message from id '#{id}' with token '#{token}'"
68
+ return unless redeem_token(token)
69
+ @client_pong_ids_received << id
70
+ if all_client_pong_ids_received?
71
+ logger.debug "All client pong messages received"
72
+ @available_timer.cancel if @available_timer
73
+ invalidate_current_master
74
+ end
75
+ end
76
+
77
+ # called by the message dispatcher when a "client_invalidated" message from a RedisConfigurationClient is received
78
+ def client_invalidated(payload)
79
+ id = payload["id"]
80
+ token = payload["token"]
81
+ logger.info "Received client_invalidated message from id '#{id}' with token '#{token}'"
82
+ return unless redeem_token(token)
83
+ @client_invalidated_ids_received << id
84
+ if all_client_invalidated_ids_received?
85
+ logger.debug "All client invalidated messages received"
86
+ @invalidate_timer.cancel if @invalidate_timer
87
+ switch_master
88
+ end
89
+ end
90
+
91
+ # called from RedisWatcher when watched redis becomes unavailable
92
+ def master_unavailable!
93
+ msg = "Redis master '#{current_master.server}' not available"
94
+ master_watcher.pause
95
+ logger.warn(msg)
96
+ beetle.publish(:system_notification, {"message" => msg}.to_json)
97
+
98
+ if @client_ids.empty?
99
+ switch_master
100
+ else
101
+ start_invalidation
102
+ end
103
+ end
104
+
105
+ # called from RedisWatcher when watched redis is available
106
+ def master_available!
107
+ publish_master(current_master)
108
+ configure_slaves(current_master)
109
+ end
110
+
111
+ # check whether the current master is still online and still a master
112
+ def master_available?
113
+ redis.masters.include?(current_master)
114
+ end
115
+
116
+ private
117
+
118
+ def check_redis_configuration
119
+ raise ConfigurationError.new("Redis failover needs two or more redis servers") if redis.instances.size < 2
120
+ end
121
+
122
+ def log_start
123
+ logger.info "RedisConfigurationServer starting"
124
+ logger.info "AMQP servers : #{config.servers}"
125
+ logger.info "Client ids : #{config.redis_configuration_client_ids}"
126
+ logger.info "Redis servers : #{config.redis_servers}"
127
+ logger.info "Redis master : #{current_master.server}"
128
+ end
129
+
130
+ def build_beetle
131
+ system = Beetle.config.system_name
132
+ Beetle::Client.new.configure :exchange => system, :auto_delete => true do |config|
133
+ config.message :client_invalidated
134
+ config.queue :client_invalidated, :amqp_name => "#{system}_client_invalidated"
135
+ config.message :pong
136
+ config.queue :pong, :amqp_name => "#{system}_pong"
137
+ config.message :ping
138
+ config.message :invalidate
139
+ config.message :reconfigure
140
+ config.message :system_notification
141
+
142
+ config.handler [:pong, :client_invalidated], MessageDispatcher
143
+ end
144
+ end
145
+
146
+ def master_watcher
147
+ @master_watcher ||= RedisWatcher.new(self)
148
+ end
149
+
150
+ def determine_initial_master
151
+ if master_file_exists? && @current_master = redis_master_from_master_file
152
+ if redis.slaves.include?(current_master)
153
+ master_unavailable!
154
+ elsif redis.unknowns.include?(current_master)
155
+ master_unavailable!
156
+ end
157
+ else
158
+ write_redis_master_file(current_master.server) if @current_master = redis.auto_detect_master
159
+ end
160
+ current_master or raise NoRedisMaster.new("failed to determine initial redis master")
161
+ end
162
+
163
+ def determine_new_master
164
+ redis.unknowns.include?(current_master) ? redis.slaves_of(current_master).first : current_master
165
+ end
166
+
167
+ def redeem_token(token)
168
+ valid_token = token == @current_token
169
+ logger.info "Ignored message (token was '#{token.inspect}', but expected '#{@current_token.inspect}')" unless valid_token
170
+ valid_token
171
+ end
172
+
173
+ def start_invalidation
174
+ @client_pong_ids_received.clear
175
+ @client_invalidated_ids_received.clear
176
+ check_all_clients_available
177
+ end
178
+
179
+ def check_all_clients_available
180
+ generate_new_token
181
+ beetle.publish(:ping, payload_with_current_token)
182
+ @available_timer = EM::Timer.new(config.redis_configuration_client_timeout) { cancel_invalidation }
183
+ end
184
+
185
+ def invalidate_current_master
186
+ generate_new_token
187
+ beetle.publish(:invalidate, payload_with_current_token)
188
+ @invalidate_timer = EM::Timer.new(config.redis_configuration_client_timeout) { cancel_invalidation }
189
+ end
190
+
191
+ def cancel_invalidation
192
+ logger.warn "Redis master invalidation cancelled: 'pong' received from '#{@client_pong_ids_received.to_a.join(',')}', 'client_invalidated' received from '#{@client_invalidated_ids_received.to_a.join(',')}'"
193
+ generate_new_token
194
+ master_watcher.continue
195
+ end
196
+
197
+ def generate_new_token
198
+ @current_token += 1
199
+ end
200
+
201
+ def payload_with_current_token(message = {})
202
+ message["token"] = @current_token
203
+ message.to_json
204
+ end
205
+
206
+ def all_client_pong_ids_received?
207
+ @client_ids == @client_pong_ids_received
208
+ end
209
+
210
+ def all_client_invalidated_ids_received?
211
+ @client_ids == @client_invalidated_ids_received
212
+ end
213
+
214
+ def switch_master
215
+ if new_master = determine_new_master
216
+ msg = "Setting redis master to '#{new_master.server}' (was '#{current_master.server}')"
217
+ logger.warn(msg)
218
+ beetle.publish(:system_notification, {"message" => msg}.to_json)
219
+
220
+ new_master.master!
221
+ @current_master = new_master
222
+ else
223
+ msg = "Redis master could not be switched, no slave available to become new master, promoting old master"
224
+ logger.error(msg)
225
+ beetle.publish(:system_notification, {"message" => msg}.to_json)
226
+ end
227
+
228
+ publish_master(current_master)
229
+ master_watcher.continue
230
+ end
231
+
232
+ def publish_master(master)
233
+ logger.info "Publishing reconfigure message with server '#{master.server}'"
234
+ beetle.publish(:reconfigure, payload_with_current_token({"server" => master.server}))
235
+ end
236
+
237
+ def configure_slaves(master)
238
+ (redis.masters-[master]).each do |r|
239
+ logger.info "Reconfiguring '#{r.server}' as a slave of '#{master.server}'"
240
+ r.slave_of!(master.host, master.port)
241
+ end
242
+ end
243
+
244
+ # Periodically checks a redis server for availability
245
+ class RedisWatcher #:nodoc:
246
+ include Logging
247
+
248
+ def initialize(configuration_server)
249
+ @configuration_server = configuration_server
250
+ @retries = 0
251
+ @paused = true
252
+ @master_retry_interval = configuration_server.config.redis_configuration_master_retry_interval
253
+ @master_retries = configuration_server.config.redis_configuration_master_retries
254
+ end
255
+
256
+ def pause
257
+ logger.info "Pause checking availability of redis servers"
258
+ @watch_timer.cancel if @watch_timer
259
+ @watch_timer = nil
260
+ @paused = true
261
+ end
262
+
263
+ def watch
264
+ @watch_timer ||=
265
+ begin
266
+ logger.info "Start watching redis servers every #{@master_retry_interval} seconds"
267
+ EventMachine::add_periodic_timer(@master_retry_interval) { check_availability }
268
+ end
269
+ @paused = false
270
+ end
271
+ alias continue watch
272
+
273
+ def paused?
274
+ @paused
275
+ end
276
+
277
+ private
278
+
279
+ def check_availability
280
+ @configuration_server.redis.refresh
281
+ if @configuration_server.master_available?
282
+ @configuration_server.master_available!
283
+ else
284
+ logger.warn "Redis master not available! (Retries left: #{@master_retries - (@retries + 1)})"
285
+ if (@retries+=1) >= @master_retries
286
+ @retries = 0
287
+ @configuration_server.master_unavailable!
288
+ end
289
+ end
290
+ end
291
+ end
292
+
293
+ # Dispatches messages from the queue to methods in RedisConfigurationServer
294
+ class MessageDispatcher < Beetle::Handler #:nodoc:
295
+ cattr_accessor :configuration_server
296
+ def process
297
+ @@configuration_server.__send__(message.header.routing_key, ActiveSupport::JSON.decode(message.data))
298
+ end
299
+ end
300
+ end
301
+ end
@@ -0,0 +1,79 @@
1
+ # Redis convenience and compatibility layer
2
+ class Redis #:nodoc:
3
+ def self.from_server_string(server_string, options = {})
4
+ host, port = server_string.split(':')
5
+ options = {:host => host, :port => port}.update(options)
6
+ new(options)
7
+ end
8
+
9
+ # Redis 2 removed some useful methods. add them back.
10
+ def host; @client.host; end
11
+ def port; @client.port; end
12
+ def server; "#{host}:#{port}"; end
13
+
14
+ def master!
15
+ slaveof("no", "one")
16
+ end
17
+
18
+ def slave_of!(host, port)
19
+ slaveof(host, port)
20
+ end
21
+
22
+ # Redis 2 tries to establish a connection on inspect. this is evil!
23
+ def inspect
24
+ super
25
+ end
26
+
27
+ def info_with_rescue
28
+ info
29
+ rescue Exception
30
+ {}
31
+ end
32
+
33
+ def available?
34
+ info_with_rescue != {}
35
+ end
36
+
37
+ def role
38
+ info_with_rescue["role"] || "unknown"
39
+ end
40
+
41
+ def master?
42
+ role == "master"
43
+ end
44
+
45
+ def slave?
46
+ role == "slave"
47
+ end
48
+
49
+ def slave_of?(host, port)
50
+ info = info_with_rescue
51
+ info["role"] == "slave" && info["master_host"] == host && info["master_port"] == port.to_s
52
+ end
53
+ end
54
+
55
+ class Redis::Client #:nodoc:
56
+ protected
57
+ def connect_to(host, port)
58
+ if @timeout != 0 and Redis::Timer
59
+ begin
60
+ Redis::Timer.timeout(@timeout){ @sock = TCPSocket.new(host, port) }
61
+ rescue Timeout::Error
62
+ @sock = nil
63
+ raise Timeout::Error, "Timeout connecting to the server"
64
+ end
65
+ else
66
+ @sock = TCPSocket.new(host, port)
67
+ end
68
+
69
+ @sock.setsockopt Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1
70
+
71
+ # If the timeout is set we set the low level socket options in order
72
+ # to make sure a blocking read will return after the specified number
73
+ # of seconds. This hack is from memcached ruby client.
74
+ self.timeout = @timeout
75
+
76
+ rescue Errno::ECONNREFUSED
77
+ raise Errno::ECONNREFUSED, "Unable to connect to Redis on #{host}:#{port}"
78
+ end
79
+ end
@@ -0,0 +1,35 @@
1
+ module Beetle
2
+ module RedisMasterFile #:nodoc:
3
+ private
4
+ def master_file_exists?
5
+ File.exist?(master_file)
6
+ end
7
+
8
+ def redis_master_from_master_file
9
+ redis.find(read_redis_master_file)
10
+ end
11
+
12
+ def clear_redis_master_file
13
+ write_redis_master_file("")
14
+ end
15
+
16
+ def read_redis_master_file
17
+ File.read(master_file).chomp
18
+ end
19
+
20
+ def write_redis_master_file(redis_server_string)
21
+ logger.warn "Writing '#{redis_server_string}' to redis master file '#{master_file}'"
22
+ File.open(master_file, "w"){|f| f.puts redis_server_string }
23
+ end
24
+
25
+ def master_file
26
+ config.redis_server
27
+ end
28
+
29
+ def verify_redis_master_file_string
30
+ if master_file =~ /^[0-9a-z.]+:[0-9]+$/
31
+ raise ConfigurationError.new("To use the redis failover, redis_server config option must point to a file")
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,65 @@
1
+ module Beetle
2
+ # class used by the RedisConfigurationServer to hold information about the current state of the configured redis servers
3
+ class RedisServerInfo
4
+ include Logging
5
+
6
+ def initialize(config, options) #:nodoc:
7
+ @config = config
8
+ @options = options
9
+ reset
10
+ end
11
+
12
+ # all configured redis servers
13
+ def instances
14
+ @instances ||= @config.redis_servers.split(/ *, */).map{|s| Redis.from_server_string(s, @options)}
15
+ end
16
+
17
+ # fetches the server from the insatnces whith the given <tt>server</tt> string
18
+ def find(server)
19
+ instances.find{|r| r.server == server}
20
+ end
21
+
22
+ # refresh connectivity/role information
23
+ def refresh
24
+ logger.debug "Updating redis server info"
25
+ reset
26
+ instances.each {|r| @server_info[r.role] << r}
27
+ end
28
+
29
+ # subset of instances which are masters
30
+ def masters
31
+ @server_info["master"]
32
+ end
33
+
34
+ # subset of instances which are slaves
35
+ def slaves
36
+ @server_info["slave"]
37
+ end
38
+
39
+ # subset of instances which are not reachable
40
+ def unknowns
41
+ @server_info["unknown"]
42
+ end
43
+
44
+ # subset of instances which are set up as slaves of the given <tt>master</tt>
45
+ def slaves_of(master)
46
+ slaves.select{|r| r.slave_of?(master.host, master.port)}
47
+ end
48
+
49
+ # determine a master if we have one master and all other servers are slaves
50
+ def auto_detect_master
51
+ return nil unless master_and_slaves_reachable?
52
+ masters.first
53
+ end
54
+
55
+ private
56
+
57
+ def master_and_slaves_reachable?
58
+ masters.size == 1 && slaves.size == instances.size - 1
59
+ end
60
+
61
+ def reset
62
+ @server_info = Hash.new {|h,k| h[k]= []}
63
+ end
64
+ end
65
+ end
@@ -10,7 +10,7 @@ module Beetle
10
10
  @mqs = {}
11
11
  end
12
12
 
13
- # the client calls this method to subcsribe to all queues on all servers which have
13
+ # the client calls this method to subscribe to all queues on all servers which have
14
14
  # handlers registered for the given list of messages. this method does the following
15
15
  # things:
16
16
  #
@@ -105,6 +105,9 @@ module Beetle
105
105
  sleep 1
106
106
  mq(server).recover
107
107
  elsif reply_to = header.properties[:reply_to]
108
+ # require 'ruby-debug'
109
+ # Debugger.start
110
+ # debugger
108
111
  status = result == Beetle::RC::OK ? "OK" : "FAILED"
109
112
  exchange = MQ::Exchange.new(mq(server), :direct, "", :key => reply_to)
110
113
  exchange.publish(m.handler_result.to_s, :headers => {:status => status})
data/lib/beetle.rb CHANGED
@@ -17,8 +17,6 @@ module Beetle
17
17
  class UnknownQueue < Error; end
18
18
  # raised when no redis master server can be found
19
19
  class NoRedisMaster < Error; end
20
- # raised when two redis master servers are found
21
- class TwoRedisMasters < Error; end
22
20
 
23
21
  # AMQP options for exchange creation
24
22
  EXCHANGE_CREATION_KEYS = [:auto_delete, :durable, :internal, :nowait, :passive]
@@ -37,6 +35,8 @@ module Beetle
37
35
  autoload File.basename(libfile)[/^(.*)\.rb$/, 1].classify, libfile
38
36
  end
39
37
 
38
+ require "#{lib_dir}/redis_ext"
39
+
40
40
  # returns the default configuration object and yields it if a block is given
41
41
  def self.config
42
42
  #:yields: config
@@ -1,5 +1,17 @@
1
1
  require File.expand_path(File.dirname(__FILE__) + '/../test_helper')
2
-
2
+ require 'tempfile'
3
3
 
4
4
  module Beetle
5
- end
5
+ class ConfigurationTest < Test::Unit::TestCase
6
+ test "should load it's settings from a config file if that file exists" do
7
+ config = Configuration.new
8
+ old_value = config.gc_threshold
9
+ new_value = old_value + 1
10
+ config_file_content = "gc_threshold: #{new_value}\n"
11
+ IO.expects(:read).returns(config_file_content)
12
+
13
+ config.config_file = "some/path/to/a/file"
14
+ assert_equal new_value, config.gc_threshold
15
+ end
16
+ end
17
+ end
@@ -14,75 +14,93 @@ module Beetle
14
14
  end
15
15
 
16
16
  test "msetnx returns 0 or 1" do
17
- assert_equal 1, @r.msetnx("a" => 1, "b" => 2)
17
+ assert_equal 1, @r.msetnx("a", 1, "b", 2)
18
18
  assert_equal "1", @r.get("a")
19
19
  assert_equal "2", @r.get("b")
20
- assert_equal 0, @r.msetnx("a" => 3, "b" => 4)
20
+ assert_equal 0, @r.msetnx("a", 3, "b", 4)
21
21
  assert_equal "1", @r.get("a")
22
22
  assert_equal "2", @r.get("b")
23
23
  end
24
24
  end
25
25
 
26
- class RedisFailoverTest < Test::Unit::TestCase
26
+ class RedisServerStringTest < Test::Unit::TestCase
27
+ def setup
28
+ @original_redis_server = Beetle.config.redis_server
29
+ @store = DeduplicationStore.new
30
+ @server_string = "my_test_host_from_file:9999"
31
+ Beetle.config.redis_server = @server_string
32
+ end
33
+
34
+ def teardown
35
+ Beetle.config.redis_server = @original_redis_server
36
+ end
37
+
38
+ test "redis should match the redis server string" do
39
+ assert_equal @server_string, @store.redis.server
40
+ end
41
+ end
42
+
43
+ class RedisServerFileTest < Test::Unit::TestCase
27
44
  def setup
28
- @store = DeduplicationStore.new("localhost:1, localhost:2")
45
+ @original_redis_server = Beetle.config.redis_server
46
+ @store = DeduplicationStore.new
47
+ @server_string = "my_test_host_from_file:6379"
48
+ Beetle.config.redis_server = redis_test_master_file(@server_string)
49
+ end
50
+
51
+ def teardown
52
+ Beetle.config.redis_server = @original_redis_server
53
+ end
54
+
55
+ test "redis should match the redis master file" do
56
+ assert_equal @server_string, @store.redis.server
29
57
  end
30
58
 
31
- test "redis instances should be created for all servers" do
32
- instances = @store.redis_instances
33
- assert_equal ["localhost:1", "localhost:2" ], instances.map(&:server)
59
+ test "redis should be nil if the redis master file is blank" do
60
+ redis_test_master_file("")
61
+ assert_nil @store.redis
34
62
  end
35
63
 
36
- test "searching a redis master should find one if there is one" do
37
- instances = @store.redis_instances
38
- instances.first.expects(:info).returns(:role => "slave")
39
- instances.second.expects(:info).returns(:role => "master")
40
- assert_equal instances.second, @store.redis
64
+ test "should keep using the current redis if the redis master file hasn't changed since the last request" do
65
+ @store.expects(:read_master_file).once.returns("localhost:1")
66
+ 2.times { @store.redis }
41
67
  end
42
68
 
43
- test "searching a redis master should find one even if one cannot be accessed" do
44
- instances = @store.redis_instances
45
- instances.first.expects(:info).raises("murks")
46
- instances.second.expects(:info).returns(:role => "master")
47
- assert_equal instances.second, @store.redis
69
+ test "should return nil if the master file doesn't exist" do
70
+ Beetle.config.redis_server = "/tmp/__i_don_not_exist__.txt"
71
+ assert_equal nil, @store.redis_master_from_master_file
48
72
  end
49
73
 
50
- test "searching a redis master should raise an exception if there is none" do
51
- instances = @store.redis_instances
52
- instances.first.expects(:info).returns(:role => "slave")
53
- instances.second.expects(:info).returns(:role => "slave")
54
- assert_raises(NoRedisMaster) { @store.find_redis_master }
74
+ private
75
+ def redis_test_master_file(server_string)
76
+ path = File.expand_path("../../../tmp/redis-master-for-unit-tests", __FILE__)
77
+ File.open(path, "w"){|f| f.puts server_string}
78
+ path
55
79
  end
80
+ end
56
81
 
57
- test "searching a redis master should raise an exception if there is more than one" do
58
- instances = @store.redis_instances
59
- instances.first.expects(:info).returns(:role => "master")
60
- instances.second.expects(:info).returns(:role => "master")
61
- assert_raises(TwoRedisMasters) { @store.find_redis_master }
82
+ class RedisFailoverTest < Test::Unit::TestCase
83
+ def setup
84
+ @store = DeduplicationStore.new
85
+ Beetle.config.expects(:redis_failover_timeout).returns(1)
62
86
  end
63
87
 
64
88
  test "a redis operation protected with a redis failover block should succeed if it can find a new master" do
65
- instances = @store.redis_instances
89
+ redis1 = stub("redis 1")
90
+ redis2 = stub("redis 2")
66
91
  s = sequence("redis accesses")
67
- instances.first.expects(:info).returns(:role => "master").in_sequence(s)
68
- instances.second.expects(:info).returns(:role => "slave").in_sequence(s)
69
- assert_equal instances.first, @store.redis
70
- instances.first.expects(:get).with("foo:x").raises("disconnected").in_sequence(s)
71
- instances.first.expects(:info).raises("disconnected").in_sequence(s)
72
- instances.second.expects(:info).returns(:role => "master").in_sequence(s)
73
- instances.second.expects(:get).with("foo:x").returns("42").in_sequence(s)
92
+ @store.expects(:redis).returns(redis1).in_sequence(s)
93
+ redis1.expects(:get).with("foo:x").raises("disconnected").in_sequence(s)
94
+ @store.expects(:redis).returns(redis2).in_sequence(s)
95
+ redis2.expects(:get).with("foo:x").returns("42").in_sequence(s)
74
96
  assert_equal("42", @store.get("foo", "x"))
75
97
  end
76
98
 
77
99
  test "a redis operation protected with a redis failover block should fail if it cannot find a new master" do
78
- instances = @store.redis_instances
79
- instances.first.expects(:info).returns(:role => "master")
80
- instances.second.expects(:info).returns(:role => "slave")
81
- assert_equal instances.first, @store.redis
82
- instances.first.stubs(:get).with("foo:x").raises("disconnected")
83
- instances.first.stubs(:info).raises("disconnected")
84
- instances.second.stubs(:info).returns(:role => "slave")
85
- @store.expects(:sleep).times(119)
100
+ redis1 = stub()
101
+ @store.stubs(:redis).returns(redis1)
102
+ redis1.stubs(:get).with("foo:x").raises("disconnected")
103
+ @store.stubs(:sleep)
86
104
  assert_raises(NoRedisMaster) { @store.get("foo", "x") }
87
105
  end
88
106
  end