beetle 0.1 → 0.2.1

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.rb +2 -2
  8. data/lib/beetle/base.rb +1 -8
  9. data/lib/beetle/client.rb +16 -14
  10. data/lib/beetle/commands.rb +30 -0
  11. data/lib/beetle/commands/configuration_client.rb +73 -0
  12. data/lib/beetle/commands/configuration_server.rb +85 -0
  13. data/lib/beetle/configuration.rb +70 -7
  14. data/lib/beetle/deduplication_store.rb +50 -38
  15. data/lib/beetle/handler.rb +2 -5
  16. data/lib/beetle/logging.rb +7 -0
  17. data/lib/beetle/message.rb +11 -13
  18. data/lib/beetle/publisher.rb +12 -4
  19. data/lib/beetle/r_c.rb +2 -1
  20. data/lib/beetle/redis_configuration_client.rb +136 -0
  21. data/lib/beetle/redis_configuration_server.rb +301 -0
  22. data/lib/beetle/redis_ext.rb +79 -0
  23. data/lib/beetle/redis_master_file.rb +35 -0
  24. data/lib/beetle/redis_server_info.rb +65 -0
  25. data/lib/beetle/subscriber.rb +4 -1
  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/publisher_test.rb +17 -3
  30. data/test/beetle/redis_configuration_client_test.rb +97 -0
  31. data/test/beetle/redis_configuration_server_test.rb +278 -0
  32. data/test/beetle/redis_ext_test.rb +71 -0
  33. data/test/beetle/redis_master_file_test.rb +39 -0
  34. data/test/test_helper.rb +13 -1
  35. metadata +162 -69
  36. data/.gitignore +0 -5
  37. data/MIT-LICENSE +0 -20
  38. data/Rakefile +0 -114
  39. data/TODO +0 -7
  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.yml +0 -81
  46. data/test/beetle/bla.rb +0 -0
  47. data/tmp/master/.gitignore +0 -2
  48. data/tmp/slave/.gitignore +0 -3
@@ -0,0 +1,97 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../test_helper')
2
+
3
+ module Beetle
4
+ class RedisConfigurationClientTest < Test::Unit::TestCase
5
+ def setup
6
+ Beetle.config.redis_servers = "redis:0,redis:1"
7
+ @client = RedisConfigurationClient.new
8
+ Client.any_instance.stubs(:listen)
9
+ @client.stubs(:touch_master_file)
10
+ @client.stubs(:verify_redis_master_file_string)
11
+ end
12
+
13
+ test "config should return the beetle config" do
14
+ assert_equal Beetle.config, @client.config
15
+ end
16
+
17
+ test "ping message should answer with pong" do
18
+ @client.expects(:pong!)
19
+ @client.ping("token" => 1)
20
+ end
21
+
22
+ test "pong should publish a pong message" do
23
+ @client.beetle.expects(:publish)
24
+ @client.send(:pong!)
25
+ end
26
+
27
+ test "invalidation should send an invalidation message and clear the redis master file" do
28
+ @client.expects(:clear_redis_master_file)
29
+ @client.beetle.expects(:publish).with(:client_invalidated, anything)
30
+ @client.send(:invalidate!)
31
+ end
32
+
33
+ test "should ignore outdated invalidate messages" do
34
+ new_payload = {"token" => 2}
35
+ old_payload = {"token" => 1}
36
+
37
+ @client.expects(:invalidate!).once
38
+
39
+ @client.invalidate(new_payload)
40
+ @client.invalidate(old_payload)
41
+ end
42
+
43
+ test "should ignore invalidate messages when current master is still a master" do
44
+ @client.instance_variable_set :@current_master, stub(:master? => true)
45
+ @client.expects(:invalidate!).never
46
+ @client.invalidate("token" => 1)
47
+ end
48
+
49
+ test "should ignore outdated reconfigure messages" do
50
+ new_payload = {"token" => 2, "server" => "master:2"}
51
+ old_payload = {"token" => 1, "server" => "master:1"}
52
+ @client.stubs(:read_redis_master_file).returns("")
53
+
54
+ @client.expects(:write_redis_master_file).once
55
+
56
+ @client.reconfigure(new_payload)
57
+ @client.reconfigure(old_payload)
58
+ end
59
+
60
+ test "should clear redis master file if redis from master file is slave" do
61
+ @client.stubs(:redis_master_from_master_file).returns(stub(:master? => false))
62
+ @client.expects(:clear_redis_master_file)
63
+ @client.start
64
+ end
65
+
66
+ test "should clear redis master file if redis from master file is not available" do
67
+ @client.stubs(:redis_master_from_master_file).returns(nil)
68
+ @client.expects(:clear_redis_master_file)
69
+ @client.start
70
+ end
71
+
72
+ test "the dispatcher should just forward messages to the client" do
73
+ dispatcher_class = RedisConfigurationClient.class_eval "MessageDispatcher"
74
+ dispatcher_class.configuration_client = @client
75
+ dispatcher = dispatcher_class.new
76
+ payload = {"token" => 1}
77
+ dispatcher.stubs(:message).returns(stub(:data => payload.to_json, :header => stub(:routing_key=> "ping")))
78
+ @client.expects(:ping).with(payload)
79
+ dispatcher.send(:process)
80
+ end
81
+
82
+ test "determine_initial_master should return nil if there is no file" do
83
+ @client.expects(:master_file_exists?).returns(false)
84
+ assert_nil @client.send(:determine_initial_master)
85
+ assert_nil @client.current_master
86
+ end
87
+
88
+ test "determine_initial_master should instantiate a new redis if there is a file with content" do
89
+ @client.expects(:master_file_exists?).returns(true)
90
+ @client.expects(:read_redis_master_file).returns("localhost:6379")
91
+ master = @client.send(:determine_initial_master)
92
+ assert_equal "master", master.role
93
+ assert_equal master, @client.current_master
94
+ end
95
+
96
+ end
97
+ end
@@ -0,0 +1,278 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../test_helper')
2
+
3
+ module Beetle
4
+ class RedisConfigurationServerTest < Test::Unit::TestCase
5
+ def setup
6
+ Beetle.config.redis_configuration_client_ids = "rc-client-1,rc-client-2"
7
+ @server = RedisConfigurationServer.new
8
+ EventMachine.stubs(:add_timer).yields
9
+ end
10
+
11
+ test "should exit when started with less than two redis configured" do
12
+ Beetle.config.redis_servers = ""
13
+ assert_raise Beetle::ConfigurationError do
14
+ @server.start
15
+ end
16
+ end
17
+
18
+ test "should initialize the current token for messages to not reuse old tokens" do
19
+ sleep 0.1
20
+ later_server = RedisConfigurationServer.new
21
+ assert later_server.current_token > @server.current_token
22
+ end
23
+
24
+ test "should ignore outdated client_invalidated messages" do
25
+ @server.instance_variable_set(:@current_token, 2)
26
+ @server.client_invalidated("id" => "rc-client-1", "token" => 2)
27
+ old_token = 1.minute.ago.to_f
28
+ @server.client_invalidated("id" => "rc-client-2", "token" => 1)
29
+
30
+ assert_equal(["rc-client-1"].to_set, @server.instance_variable_get(:@client_invalidated_ids_received))
31
+ end
32
+
33
+ test "should ignore outdated pong messages" do
34
+ @server.instance_variable_set(:@current_token, 2)
35
+ @server.pong("id" => "rc-client-1", "token" => 2)
36
+ old_token = 1.minute.ago.to_f
37
+ @server.pong("id" => "rc-client-2", "token" => 1)
38
+
39
+ assert_equal(["rc-client-1"].to_set, @server.instance_variable_get(:@client_pong_ids_received))
40
+ end
41
+
42
+ test "the dispatcher should just forward messages to the server" do
43
+ dispatcher_class = RedisConfigurationServer.class_eval "MessageDispatcher"
44
+ dispatcher_class.configuration_server = @server
45
+ dispatcher = dispatcher_class.new
46
+ payload = {"token" => 1}
47
+ dispatcher.stubs(:message).returns(stub(:data => payload.to_json, :header => stub(:routing_key=> "pong")))
48
+ @server.expects(:pong).with(payload)
49
+ dispatcher.send(:process)
50
+ end
51
+
52
+ test "if a new master is available, it should be published and the available slaves should be configured" do
53
+ redis = Redis.new
54
+ other_master = Redis.new(:port => 6380)
55
+ other_master.expects(:slave_of!).with(redis.host, redis.port)
56
+ @server.stubs(:current_master).returns(redis)
57
+ @server.redis.instance_variable_set(:@server_info, {"master" => [redis, other_master], "slave" => [], "unknown" => []})
58
+ payload = @server.send(:payload_with_current_token, {"server" => redis.server})
59
+ @server.beetle.expects(:publish).with(:reconfigure, payload)
60
+ @server.master_available!
61
+ end
62
+ end
63
+
64
+ class RedisConfigurationServerInvalidationTest < Test::Unit::TestCase
65
+ def setup
66
+ Beetle.config.redis_configuration_client_ids = "rc-client-1,rc-client-2"
67
+ Beetle.config.redis_servers = "redis:0,redis:1"
68
+ @server = RedisConfigurationServer.new
69
+ @server.instance_variable_set(:@current_master, stub('redis stub', :server => 'stubbed_server', :available? => false))
70
+ @server.stubs(:verify_redis_master_file_string)
71
+ @server.beetle.stubs(:listen).yields
72
+ @server.beetle.stubs(:publish)
73
+ EM::Timer.stubs(:new).returns(true)
74
+ EventMachine.stubs(:add_periodic_timer).yields
75
+ end
76
+
77
+ test "should pause watching of the redis master when it becomes unavailable" do
78
+ @server.expects(:determine_initial_master)
79
+ EM.stubs(:add_periodic_timer).returns(stub("timer", :cancel => true))
80
+ @server.start
81
+ assert !@server.paused?
82
+ @server.master_unavailable!
83
+ assert @server.paused?
84
+ end
85
+
86
+ test "should setup an invalidation timeout" do
87
+ EM::Timer.expects(:new).yields
88
+ @server.expects(:cancel_invalidation)
89
+ @server.master_unavailable!
90
+ end
91
+
92
+ test "should continue watching after the invalidation timeout has expired" do
93
+ EM::Timer.expects(:new).yields
94
+ @server.master_unavailable!
95
+ assert !@server.paused?
96
+ end
97
+
98
+ test "should invalidate the current master after receiving all pong messages" do
99
+ EM::Timer.expects(:new).yields.returns(:timer)
100
+ @server.beetle.expects(:publish).with(:invalidate, anything)
101
+ @server.expects(:cancel_invalidation)
102
+ @server.expects(:redeem_token).with(1).twice.returns(true)
103
+ @server.pong("token" => 1, "id" => "rc-client-1")
104
+ @server.pong("token" => 1, "id" => "rc-client-2")
105
+ end
106
+
107
+ test "should switch the current master after receiving all client_invalidated messages" do
108
+ @server.expects(:redeem_token).with(1).twice.returns(true)
109
+ @server.expects(:switch_master)
110
+ @server.client_invalidated("token" => 1, "id" => "rc-client-1")
111
+ @server.client_invalidated("token" => 1, "id" => "rc-client-2")
112
+ end
113
+
114
+ test "should switch the current master immediately if there are no clients" do
115
+ @server.instance_variable_set :@client_ids, Set.new
116
+ @server.expects(:switch_master)
117
+ @server.master_unavailable!
118
+ end
119
+
120
+ test "switching the master should turn the new master candidate into a master" do
121
+ new_master = stub(:master! => nil, :server => "jo:6379")
122
+ @server.beetle.expects(:publish).with(:system_notification, anything)
123
+ @server.expects(:determine_new_master).returns(new_master)
124
+ @server.send :switch_master
125
+ assert_equal new_master, @server.current_master
126
+ end
127
+
128
+ test "switching the master should resort to the old master if no candidate can be found" do
129
+ old_master = @server.current_master
130
+ @server.beetle.expects(:publish).with(:system_notification, anything)
131
+ @server.expects(:determine_new_master).returns(nil)
132
+ @server.send :switch_master
133
+ assert_equal old_master, @server.current_master
134
+ end
135
+
136
+ test "checking the availability of redis servers should publish the available servers as long as the master is available" do
137
+ @server.expects(:master_available?).returns(true)
138
+ @server.expects(:master_available!)
139
+ @server.send(:master_watcher).send(:check_availability)
140
+ end
141
+
142
+ test "checking the availability of redis servers should call master_unavailable after trying the specified number of times" do
143
+ @server.stubs(:master_available?).returns(false)
144
+ @server.expects(:master_unavailable!)
145
+ watcher = @server.send(:master_watcher)
146
+ watcher.instance_variable_set :@master_retries, 0
147
+ watcher.send(:check_availability)
148
+ end
149
+ end
150
+
151
+ class RedisConfigurationServerInitialRedisMasterDeterminationTest < Test::Unit::TestCase
152
+ def setup
153
+ EM::Timer.stubs(:new).returns(true)
154
+ EventMachine.stubs(:add_periodic_timer).yields
155
+ @client = Client.new(Configuration.new)
156
+ @client.stubs(:listen).yields
157
+ @client.stubs(:publish)
158
+ @client.config.redis_configuration_client_ids = "rc-client-1,rc-client-2"
159
+ @server = RedisConfigurationServer.new
160
+ @server.stubs(:beetle).returns(@client)
161
+ @server.stubs(:write_redis_master_file)
162
+ @redis_master = build_master_redis_stub
163
+ @redis_slave = build_slave_redis_stub
164
+ @server.instance_variable_set(:@redis, build_redis_server_info(@redis_master, @redis_slave))
165
+ end
166
+
167
+ test "should not try to auto-detect if the master file contains a server string" do
168
+ @server.expects(:master_file_exists?).returns(true)
169
+ @server.stubs(:read_redis_master_file).returns("foobar:0000")
170
+
171
+ @server.redis.expects(:auto_detect_master).never
172
+ @server.expects(:redis_master_from_master_file).returns(@redis_master)
173
+ @server.send(:determine_initial_master)
174
+ end
175
+
176
+ test "should try to auto-detect if the master file is empty" do
177
+ @server.expects(:master_file_exists?).returns(true)
178
+ @server.stubs(:read_redis_master_file).returns("")
179
+
180
+ @server.redis.expects(:auto_detect_master).returns(@redis_master)
181
+ @server.send(:determine_initial_master)
182
+ end
183
+
184
+ test "should try to auto-detect if the master file is not present" do
185
+ @server.expects(:master_file_exists?).returns(false)
186
+
187
+ @server.redis.expects(:auto_detect_master).returns(@redis_master)
188
+ @server.send(:determine_initial_master)
189
+ end
190
+
191
+ test "should use redis master from successful auto-detection" do
192
+ @server.expects(:master_file_exists?).returns(false)
193
+
194
+ @server.expects(:write_redis_master_file).with(@redis_master.server)
195
+ @server.send(:determine_initial_master)
196
+ assert_equal @redis_master, @server.current_master
197
+ end
198
+
199
+ test "should use redis master if master in file is the only master" do
200
+ @server.expects(:master_file_exists?).returns(true)
201
+ @server.stubs(:redis_master_from_master_file).returns(@redis_master)
202
+
203
+ @server.send(:determine_initial_master)
204
+ assert_equal @redis_master, @server.current_master
205
+ end
206
+
207
+ test "should start master switch if master in file is slave" do
208
+ @server.instance_variable_set(:@redis, build_redis_server_info(@redis_slave))
209
+ @server.expects(:master_file_exists?).returns(true)
210
+ @server.stubs(:redis_master_from_master_file).returns(@redis_slave)
211
+
212
+ @server.expects(:master_unavailable!)
213
+ @server.send(:determine_initial_master)
214
+ end
215
+
216
+ test "should use master from master file if multiple masters are available" do
217
+ redis_master2 = build_master_redis_stub
218
+ @server.instance_variable_set(:@redis, build_redis_server_info(@redis_master, redis_master2))
219
+ @server.expects(:master_file_exists?).returns(true)
220
+ @server.stubs(:redis_master_from_master_file).returns(@redis_master)
221
+
222
+ @server.send(:determine_initial_master)
223
+ assert_equal @redis_master, @server.current_master
224
+ end
225
+
226
+ test "should start master switch if master in file is not available" do
227
+ not_available_redis_master = build_unknown_redis_stub
228
+ @server.instance_variable_set(:@redis, build_redis_server_info(not_available_redis_master, @redis_slave))
229
+ @server.expects(:master_file_exists?).returns(true)
230
+ @server.stubs(:redis_master_from_master_file).returns(not_available_redis_master)
231
+
232
+ @server.expects(:master_unavailable!)
233
+ @server.send(:determine_initial_master)
234
+ end
235
+
236
+ test "should raise an exception if both master file and auto-detection fails" do
237
+ not_available_redis_master = build_unknown_redis_stub
238
+ not_available_redis_slave = build_unknown_redis_stub
239
+ @server.instance_variable_set(:@redis, build_redis_server_info(not_available_redis_master, not_available_redis_slave))
240
+ @server.expects(:master_file_exists?).returns(true)
241
+ @server.expects(:read_redis_master_file).returns("")
242
+ @server.redis.expects(:auto_detect_master).returns(nil)
243
+
244
+ assert_raises Beetle::NoRedisMaster do
245
+ @server.send(:determine_initial_master)
246
+ end
247
+ end
248
+
249
+ test "should detect a new redis_master" do
250
+ not_available_redis_master = build_unknown_redis_stub
251
+ @redis_slave.expects(:slave_of?).returns(true)
252
+ @server.instance_variable_set(:@current_master, not_available_redis_master)
253
+ @server.instance_variable_set(:@redis, build_redis_server_info(@redis_slave, not_available_redis_master))
254
+ assert_equal @redis_slave, @server.send(:determine_new_master)
255
+ end
256
+
257
+ private
258
+
259
+ def build_master_redis_stub
260
+ stub("redis master", :host => "stubbed_master", :port => 0, :server => "stubbed_master:0", :available? => true, :master? => true, :slave? => false, :role => "master")
261
+ end
262
+
263
+ def build_slave_redis_stub
264
+ stub("redis slave", :host => "stubbed_slave", :port => 0, :server => "stubbed_slave:0", :available? => true, :master? => false, :slave? => true, :role => "slave")
265
+ end
266
+
267
+ def build_unknown_redis_stub
268
+ stub("redis unknown", :host => "stubbed_unknown", :port => 0, :server => "stubbed_unknown:0", :available? => false, :master? => false, :slave? => false, :role => "unknown")
269
+ end
270
+
271
+ def build_redis_server_info(*redis_instances)
272
+ info = RedisServerInfo.new(Beetle.config, :timeout => 1)
273
+ info.instance_variable_set :@instances, redis_instances
274
+ redis_instances.each{|redis| info.send("#{redis.role}s") << redis }
275
+ info
276
+ end
277
+ end
278
+ end
@@ -0,0 +1,71 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../test_helper')
2
+
3
+ module Beetle
4
+
5
+ class NonExistentRedisTest < Test::Unit::TestCase
6
+ def setup
7
+ @r = Redis.new(:host => "localhost", :port => 6390)
8
+ end
9
+
10
+ test "should return an empty hash for the info_with_rescue call" do
11
+ assert_equal({}, @r.info_with_rescue)
12
+ end
13
+
14
+ test "should have a role of unknown" do
15
+ assert_equal "unknown", @r.role
16
+ end
17
+
18
+ test "should not be available" do
19
+ assert !@r.available?
20
+ end
21
+
22
+ test "should not be a master" do
23
+ assert !@r.master?
24
+ end
25
+
26
+ test "should not be a slave" do
27
+ assert !@r.slave?
28
+ assert !@r.slave_of?("localhost", 6379)
29
+ end
30
+
31
+ test "should not try toconnect to the redis server on inspect" do
32
+ assert_nothing_raised { @r.inspect }
33
+ end
34
+ end
35
+
36
+ class AddedRedisMethodsTest < Test::Unit::TestCase
37
+ def setup
38
+ @r = Redis.new(:host => "localhost", :port => 6390)
39
+ end
40
+
41
+ test "should return the host, port and server string" do
42
+ assert_equal "localhost", @r.host
43
+ assert_equal 6390, @r.port
44
+ assert_equal "localhost:6390", @r.server
45
+ end
46
+
47
+ test "should stop slavery" do
48
+ @r.expects(:slaveof).with("no", "one")
49
+ @r.master!
50
+ end
51
+
52
+ test "should support slavery" do
53
+ @r.expects(:slaveof).with("localhost", 6379)
54
+ @r.slave_of!("localhost", 6379)
55
+ end
56
+ end
57
+
58
+ class RedisTimeoutTest < Test::Unit::TestCase
59
+ test "should use Redis::Timer if timeout is greater 0" do
60
+ r = Redis.new(:host => "localhost", :port => 6390, :timeout => 1)
61
+ Redis::Timer.expects(:timeout).with(1).raises(Timeout::Error)
62
+ assert_equal({}, r.info_with_rescue)
63
+ end
64
+
65
+ test "should not use Redis::Timer if timeout 0" do
66
+ r = Redis.new(:host => "localhost", :port => 6390, :timeout => 0)
67
+ Redis::Timer.expects(:timeout).never
68
+ assert_equal({}, r.info_with_rescue)
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,39 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../test_helper')
2
+
3
+ module Beetle
4
+
5
+ class RedisMasterFileTest < Test::Unit::TestCase
6
+ include Logging
7
+ include RedisMasterFile
8
+
9
+ def setup
10
+ File.open(master_file, "w"){|f| f.puts "localhost:6379"}
11
+ end
12
+
13
+ def teardown
14
+ File.unlink(master_file) if File.exist?(master_file)
15
+ end
16
+
17
+ test "should be able to check existence" do
18
+ assert master_file_exists?
19
+ File.unlink(master_file)
20
+ assert !master_file_exists?
21
+ end
22
+
23
+ test "should be able to read and write the master file"do
24
+ write_redis_master_file("localhost:6380")
25
+ assert_equal "localhost:6380", read_redis_master_file
26
+ end
27
+
28
+ test "should be able to clear the master file" do
29
+ logger.expects(:warn)
30
+ clear_redis_master_file
31
+ assert_equal "", read_redis_master_file
32
+ end
33
+
34
+ private
35
+ def master_file
36
+ "/tmp/mumu.txt"
37
+ end
38
+ end
39
+ end