beetle 0.1 → 0.2.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.
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