beetle 0.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.
- data/.gitignore +5 -0
- data/MIT-LICENSE +20 -0
- data/README.rdoc +82 -0
- data/Rakefile +114 -0
- data/TODO +7 -0
- data/beetle.gemspec +127 -0
- data/etc/redis-master.conf +189 -0
- data/etc/redis-slave.conf +189 -0
- data/examples/README.rdoc +14 -0
- data/examples/attempts.rb +66 -0
- data/examples/handler_class.rb +64 -0
- data/examples/handling_exceptions.rb +73 -0
- data/examples/multiple_exchanges.rb +48 -0
- data/examples/multiple_queues.rb +43 -0
- data/examples/redis_failover.rb +65 -0
- data/examples/redundant.rb +65 -0
- data/examples/rpc.rb +45 -0
- data/examples/simple.rb +39 -0
- data/lib/beetle.rb +57 -0
- data/lib/beetle/base.rb +78 -0
- data/lib/beetle/client.rb +252 -0
- data/lib/beetle/configuration.rb +31 -0
- data/lib/beetle/deduplication_store.rb +152 -0
- data/lib/beetle/handler.rb +95 -0
- data/lib/beetle/message.rb +336 -0
- data/lib/beetle/publisher.rb +187 -0
- data/lib/beetle/r_c.rb +40 -0
- data/lib/beetle/subscriber.rb +144 -0
- data/script/start_rabbit +29 -0
- data/snafu.rb +55 -0
- data/test/beetle.yml +81 -0
- data/test/beetle/base_test.rb +52 -0
- data/test/beetle/bla.rb +0 -0
- data/test/beetle/client_test.rb +305 -0
- data/test/beetle/configuration_test.rb +5 -0
- data/test/beetle/deduplication_store_test.rb +90 -0
- data/test/beetle/handler_test.rb +105 -0
- data/test/beetle/message_test.rb +744 -0
- data/test/beetle/publisher_test.rb +407 -0
- data/test/beetle/r_c_test.rb +9 -0
- data/test/beetle/subscriber_test.rb +263 -0
- data/test/beetle_test.rb +5 -0
- data/test/test_helper.rb +20 -0
- data/tmp/master/.gitignore +2 -0
- data/tmp/slave/.gitignore +3 -0
- metadata +192 -0
@@ -0,0 +1,407 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../test_helper')
|
2
|
+
|
3
|
+
|
4
|
+
module Beetle
|
5
|
+
class PublisherTest < Test::Unit::TestCase
|
6
|
+
def setup
|
7
|
+
client = Client.new
|
8
|
+
@pub = Publisher.new(client)
|
9
|
+
end
|
10
|
+
|
11
|
+
test "acccessing a bunny for a server which doesn't have one should create it and associate it with the server" do
|
12
|
+
@pub.expects(:new_bunny).returns(42)
|
13
|
+
assert_equal 42, @pub.send(:bunny)
|
14
|
+
bunnies = @pub.instance_variable_get("@bunnies")
|
15
|
+
assert_equal(42, bunnies[@pub.server])
|
16
|
+
end
|
17
|
+
|
18
|
+
test "new bunnies should be created using current host and port and they should be started" do
|
19
|
+
m = mock("dummy")
|
20
|
+
expected_bunny_options = {
|
21
|
+
:host => @pub.send(:current_host), :port => @pub.send(:current_port),
|
22
|
+
:logging => false, :user => "guest", :pass => "guest", :vhost => "/"
|
23
|
+
}
|
24
|
+
Bunny.expects(:new).with(expected_bunny_options).returns(m)
|
25
|
+
m.expects(:start)
|
26
|
+
assert_equal m, @pub.send(:new_bunny)
|
27
|
+
end
|
28
|
+
|
29
|
+
test "initially there should be no bunnies" do
|
30
|
+
assert_equal({}, @pub.instance_variable_get("@bunnies"))
|
31
|
+
end
|
32
|
+
|
33
|
+
test "initially there should be no dead servers" do
|
34
|
+
assert_equal({}, @pub.instance_variable_get("@dead_servers"))
|
35
|
+
end
|
36
|
+
|
37
|
+
test "stop! should shut down bunny and clean internal data structures" do
|
38
|
+
b = mock("bunny")
|
39
|
+
b.expects(:stop).raises(Exception.new)
|
40
|
+
@pub.expects(:bunny).returns(b)
|
41
|
+
@pub.send(:stop!)
|
42
|
+
assert_equal({}, @pub.send(:exchanges))
|
43
|
+
assert_equal({}, @pub.send(:queues))
|
44
|
+
assert_nil @pub.instance_variable_get(:@bunnies)[@pub.server]
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
|
49
|
+
class PublisherPublishingTest < Test::Unit::TestCase
|
50
|
+
def setup
|
51
|
+
@client = Client.new
|
52
|
+
@pub = Publisher.new(@client)
|
53
|
+
@pub.stubs(:bind_queues_for_exchange)
|
54
|
+
@client.register_queue("mama", :exchange => "mama-exchange")
|
55
|
+
@client.register_message("mama", :ttl => 1.hour, :exchange => "mama-exchange")
|
56
|
+
@opts = { :ttl => 1.hour , :key => "mama", :persistent => true}
|
57
|
+
@data = 'XXX'
|
58
|
+
end
|
59
|
+
|
60
|
+
test "failover publishing should try to recycle dead servers before trying to publish the message" do
|
61
|
+
@pub.servers << "localhost:3333"
|
62
|
+
@pub.send(:mark_server_dead)
|
63
|
+
publishing = sequence('publishing')
|
64
|
+
@pub.expects(:recycle_dead_servers).in_sequence(publishing)
|
65
|
+
@pub.expects(:publish_with_failover).with("mama-exchange", "mama", @data, @opts).in_sequence(publishing)
|
66
|
+
@pub.publish("mama", @data)
|
67
|
+
end
|
68
|
+
|
69
|
+
test "redundant publishing should try to recycle dead servers before trying to publish the message" do
|
70
|
+
@pub.servers << "localhost:3333"
|
71
|
+
@pub.send(:mark_server_dead)
|
72
|
+
publishing = sequence('publishing')
|
73
|
+
@pub.expects(:recycle_dead_servers).in_sequence(publishing)
|
74
|
+
@pub.expects(:publish_with_redundancy).with("mama-exchange", "mama", @data, @opts.merge(:redundant => true)).in_sequence(publishing)
|
75
|
+
@pub.publish("mama", @data, :redundant => true)
|
76
|
+
end
|
77
|
+
|
78
|
+
test "publishing should fail over to the next server" do
|
79
|
+
failover = sequence('failover')
|
80
|
+
@pub.expects(:select_next_server).in_sequence(failover)
|
81
|
+
e = mock("exchange")
|
82
|
+
@pub.expects(:exchange).with("mama-exchange").returns(e).in_sequence(failover)
|
83
|
+
e.expects(:publish).raises(Bunny::ConnectionError).in_sequence(failover)
|
84
|
+
@pub.expects(:stop!).in_sequence(failover)
|
85
|
+
@pub.expects(:mark_server_dead).in_sequence(failover)
|
86
|
+
@pub.publish_with_failover("mama-exchange", "mama", @data, @opts)
|
87
|
+
end
|
88
|
+
|
89
|
+
test "redundant publishing should send the message to two servers" do
|
90
|
+
redundant = sequence("redundant")
|
91
|
+
@pub.servers = ["someserver", "someotherserver"]
|
92
|
+
@pub.server = "someserver"
|
93
|
+
|
94
|
+
e = mock("exchange")
|
95
|
+
@pub.expects(:exchange).with("mama-exchange").returns(e).in_sequence(redundant)
|
96
|
+
e.expects(:publish).in_sequence(redundant)
|
97
|
+
@pub.expects(:exchange).with("mama-exchange").returns(e).in_sequence(redundant)
|
98
|
+
e.expects(:publish).in_sequence(redundant)
|
99
|
+
|
100
|
+
assert_equal 2, @pub.publish_with_redundancy("mama-exchange", "mama", @data, @opts)
|
101
|
+
end
|
102
|
+
|
103
|
+
test "redundant publishing should return 1 if the message was published to one server only" do
|
104
|
+
redundant = sequence("redundant")
|
105
|
+
@pub.servers = ["someserver", "someotherserver"]
|
106
|
+
@pub.server = "someserver"
|
107
|
+
|
108
|
+
e = mock("exchange")
|
109
|
+
@pub.expects(:exchange).with("mama-exchange").returns(e).in_sequence(redundant)
|
110
|
+
e.expects(:publish).raises(Bunny::ConnectionError).in_sequence(redundant)
|
111
|
+
@pub.expects(:exchange).with("mama-exchange").returns(e).in_sequence(redundant)
|
112
|
+
e.expects(:publish).in_sequence(redundant)
|
113
|
+
|
114
|
+
assert_equal 1, @pub.publish_with_redundancy("mama-exchange", "mama", @data, @opts)
|
115
|
+
end
|
116
|
+
|
117
|
+
test "redundant publishing should return 0 if the message was published to no server" do
|
118
|
+
redundant = sequence("redundant")
|
119
|
+
@pub.servers = ["someserver", "someotherserver"]
|
120
|
+
@pub.server = "someserver"
|
121
|
+
|
122
|
+
e = mock("exchange")
|
123
|
+
@pub.expects(:exchange).with("mama-exchange").returns(e).in_sequence(redundant)
|
124
|
+
e.expects(:publish).raises(Bunny::ConnectionError).in_sequence(redundant)
|
125
|
+
@pub.expects(:exchange).with("mama-exchange").returns(e).in_sequence(redundant)
|
126
|
+
e.expects(:publish).raises(Bunny::ConnectionError).in_sequence(redundant)
|
127
|
+
|
128
|
+
assert_equal 0, @pub.publish_with_redundancy("mama-exchange", "mama", @data, @opts)
|
129
|
+
end
|
130
|
+
|
131
|
+
test "redundant publishing should fallback to failover publishing if less than one server is available" do
|
132
|
+
@pub.server = ["a server"]
|
133
|
+
@pub.expects(:publish_with_failover).with("mama-exchange", "mama", @data, @opts).returns(1)
|
134
|
+
assert_equal 1, @pub.publish_with_redundancy("mama-exchange", "mama", @data, @opts)
|
135
|
+
end
|
136
|
+
|
137
|
+
test "redundant publishing should publish to two of three servers if one server is dead" do
|
138
|
+
@pub.servers = %w(server1 server2 server3)
|
139
|
+
@pub.server = "server1"
|
140
|
+
redundant = sequence("redundant")
|
141
|
+
|
142
|
+
e = mock("exchange")
|
143
|
+
|
144
|
+
@pub.expects(:exchange).returns(e).in_sequence(redundant)
|
145
|
+
e.expects(:publish).in_sequence(redundant)
|
146
|
+
|
147
|
+
@pub.expects(:exchange).returns(e).in_sequence(redundant)
|
148
|
+
e.expects(:publish).raises(Bunny::ConnectionError).in_sequence(redundant)
|
149
|
+
@pub.expects(:stop!).in_sequence(redundant)
|
150
|
+
|
151
|
+
@pub.expects(:exchange).returns(e).in_sequence(redundant)
|
152
|
+
e.expects(:publish).in_sequence(redundant)
|
153
|
+
|
154
|
+
assert_equal 2, @pub.publish_with_redundancy("mama-exchange", "mama", @data, @opts)
|
155
|
+
end
|
156
|
+
|
157
|
+
test "publishing should use the message ttl passed in the options hash to encode the message body" do
|
158
|
+
opts = {:ttl => 1.day}
|
159
|
+
Message.expects(:publishing_options).with(:ttl => 1.day).returns({})
|
160
|
+
@pub.expects(:select_next_server)
|
161
|
+
e = mock("exchange")
|
162
|
+
@pub.expects(:exchange).returns(e)
|
163
|
+
e.expects(:publish)
|
164
|
+
assert_equal 1, @pub.publish_with_failover("mama-exchange", "mama", @data, opts)
|
165
|
+
end
|
166
|
+
|
167
|
+
test "publishing with redundancy should use the message ttl passed in the options hash to encode the message body" do
|
168
|
+
opts = {:ttl => 1.day}
|
169
|
+
Message.expects(:publishing_options).with(:ttl => 1.day).returns({})
|
170
|
+
@pub.expects(:select_next_server)
|
171
|
+
e = mock("exchange")
|
172
|
+
@pub.expects(:exchange).returns(e)
|
173
|
+
e.expects(:publish)
|
174
|
+
assert_equal 1, @pub.publish_with_redundancy("mama-exchange", "mama", @data, opts)
|
175
|
+
end
|
176
|
+
|
177
|
+
test "publishing should use the message ttl from the message configuration if no ttl is passed in via the options hash" do
|
178
|
+
@pub.expects(:publish_with_failover).with("mama-exchange", "mama", @data, @opts).returns(1)
|
179
|
+
assert_equal 1, @pub.publish("mama", @data)
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
class PublisherQueueManagementTest < Test::Unit::TestCase
|
184
|
+
def setup
|
185
|
+
@client = Client.new
|
186
|
+
@pub = Publisher.new(@client)
|
187
|
+
end
|
188
|
+
|
189
|
+
test "initially there should be no queues for the current server" do
|
190
|
+
assert_equal({}, @pub.send(:queues))
|
191
|
+
assert !@pub.send(:queues)["some_queue"]
|
192
|
+
end
|
193
|
+
|
194
|
+
test "binding a queue should create it using the config and bind it to the exchange with the name specified" do
|
195
|
+
@client.register_queue("some_queue", :exchange => "some_exchange", :key => "haha.#")
|
196
|
+
@pub.expects(:exchange).with("some_exchange").returns(:the_exchange)
|
197
|
+
q = mock("queue")
|
198
|
+
q.expects(:bind).with(:the_exchange, {:key => "haha.#"})
|
199
|
+
m = mock("Bunny")
|
200
|
+
m.expects(:queue).with("some_queue", :durable => true, :passive => false, :auto_delete => false, :exclusive => false).returns(q)
|
201
|
+
@pub.expects(:bunny).returns(m)
|
202
|
+
|
203
|
+
@pub.send(:queue, "some_queue")
|
204
|
+
assert_equal q, @pub.send(:queues)["some_queue"]
|
205
|
+
end
|
206
|
+
|
207
|
+
test "should bind the defined queues for the used exchanges when publishing" do
|
208
|
+
@client.register_queue('test_queue_1', :exchange => 'test_exchange')
|
209
|
+
@client.register_queue('test_queue_2', :exchange => 'test_exchange')
|
210
|
+
@client.register_queue('test_queue_3', :exchange => 'test_exchange_2')
|
211
|
+
@pub.expects(:bind_queue!).returns(1).times(3)
|
212
|
+
@pub.send(:bind_queues_for_exchange, 'test_exchange')
|
213
|
+
@pub.send(:bind_queues_for_exchange, 'test_exchange_2')
|
214
|
+
end
|
215
|
+
|
216
|
+
test "should not rebind the defined queues for the used exchanges if they already have been bound" do
|
217
|
+
@client.register_queue('test_queue_1', :exchange => 'test_exchange')
|
218
|
+
@client.register_queue('test_queue_2', :exchange => 'test_exchange')
|
219
|
+
@pub.expects(:bind_queue!).twice
|
220
|
+
@pub.send(:bind_queues_for_exchange, 'test_exchange')
|
221
|
+
@pub.send(:bind_queues_for_exchange, 'test_exchange')
|
222
|
+
end
|
223
|
+
|
224
|
+
test "call the queue binding method when publishing" do
|
225
|
+
data = "XXX"
|
226
|
+
opts = {}
|
227
|
+
@client.register_queue("mama", :exchange => "mama-exchange")
|
228
|
+
@client.register_message("mama", :ttl => 1.hour, :exchange => "mama-exchange")
|
229
|
+
e = stub('exchange', 'publish')
|
230
|
+
@pub.expects(:exchange).with('mama-exchange').returns(e)
|
231
|
+
@pub.expects(:bind_queues_for_exchange).with('mama-exchange').returns(true)
|
232
|
+
@pub.publish('mama', data)
|
233
|
+
end
|
234
|
+
|
235
|
+
test "purging a queue should purge the queues on all servers" do
|
236
|
+
@pub.servers = %w(a b)
|
237
|
+
queue = mock("queue")
|
238
|
+
s = sequence("purging")
|
239
|
+
@pub.expects(:set_current_server).with("a").in_sequence(s)
|
240
|
+
@pub.expects(:queue).with("queue").returns(queue).in_sequence(s)
|
241
|
+
queue.expects(:purge).in_sequence(s)
|
242
|
+
@pub.expects(:set_current_server).with("b").in_sequence(s)
|
243
|
+
@pub.expects(:queue).with("queue").returns(queue).in_sequence(s)
|
244
|
+
queue.expects(:purge).in_sequence(s)
|
245
|
+
@pub.purge("queue")
|
246
|
+
end
|
247
|
+
end
|
248
|
+
|
249
|
+
class PublisherExchangeManagementTest < Test::Unit::TestCase
|
250
|
+
def setup
|
251
|
+
@client = Client.new
|
252
|
+
@pub = Publisher.new(@client)
|
253
|
+
end
|
254
|
+
|
255
|
+
test "initially there should be no exchanges for the current server" do
|
256
|
+
assert_equal({}, @pub.send(:exchanges))
|
257
|
+
end
|
258
|
+
|
259
|
+
test "accessing a given exchange should create it using the config. further access should return the created exchange" do
|
260
|
+
m = mock("Bunny")
|
261
|
+
m.expects(:exchange).with("some_exchange", :type => :topic, :durable => true).returns(42)
|
262
|
+
@client.register_exchange("some_exchange", :type => :topic, :durable => true)
|
263
|
+
@pub.expects(:bunny).returns(m)
|
264
|
+
ex = @pub.send(:exchange, "some_exchange")
|
265
|
+
assert @pub.send(:exchanges).include?("some_exchange")
|
266
|
+
ex2 = @pub.send(:exchange, "some_exchange")
|
267
|
+
assert_equal ex2, ex
|
268
|
+
end
|
269
|
+
end
|
270
|
+
|
271
|
+
class PublisherServerManagementTest < Test::Unit::TestCase
|
272
|
+
def setup
|
273
|
+
@client = Client.new
|
274
|
+
@pub = Publisher.new(@client)
|
275
|
+
end
|
276
|
+
|
277
|
+
test "marking the current server as dead should add it to the dead servers hash and remove it from the active servers list" do
|
278
|
+
@pub.servers = ["localhost:1111", "localhost:2222"]
|
279
|
+
@pub.send(:set_current_server, "localhost:2222")
|
280
|
+
@pub.send(:mark_server_dead)
|
281
|
+
assert_equal ["localhost:1111"], @pub.servers
|
282
|
+
dead = @pub.instance_variable_get "@dead_servers"
|
283
|
+
assert_equal ["localhost:2222"], dead.keys
|
284
|
+
assert_kind_of Time, dead["localhost:2222"]
|
285
|
+
end
|
286
|
+
|
287
|
+
test "recycle_dead_servers should move servers from the dead server hash to the servers list only if the have been markd dead for longer than 10 seconds" do
|
288
|
+
@pub.servers = ["a:1", "b:2"]
|
289
|
+
@pub.send(:set_current_server, "a:1")
|
290
|
+
@pub.send(:mark_server_dead)
|
291
|
+
assert_equal ["b:2"], @pub.servers
|
292
|
+
dead = @pub.instance_variable_get("@dead_servers")
|
293
|
+
dead["a:1"] = 9.seconds.ago
|
294
|
+
@pub.send(:recycle_dead_servers)
|
295
|
+
assert_equal ["a:1"], dead.keys
|
296
|
+
dead["a:1"] = 11.seconds.ago
|
297
|
+
@pub.send(:recycle_dead_servers)
|
298
|
+
assert_equal ["b:2", "a:1"], @pub.servers
|
299
|
+
assert_equal({}, dead)
|
300
|
+
end
|
301
|
+
|
302
|
+
test "select_next_server should cycle through the list of all servers" do
|
303
|
+
@pub.servers = ["a:1", "b:2"]
|
304
|
+
@pub.send(:set_current_server, "a:1")
|
305
|
+
@pub.send(:select_next_server)
|
306
|
+
assert_equal "b:2", @pub.server
|
307
|
+
@pub.send(:select_next_server)
|
308
|
+
assert_equal "a:1", @pub.server
|
309
|
+
end
|
310
|
+
|
311
|
+
test "select_next_server should return 0 if there are no servers to publish to" do
|
312
|
+
@pub.servers = []
|
313
|
+
logger = mock('logger')
|
314
|
+
logger.expects(:error).returns(true)
|
315
|
+
@pub.expects(:logger).returns(logger)
|
316
|
+
assert_equal 0, @pub.send(:select_next_server)
|
317
|
+
end
|
318
|
+
|
319
|
+
test "stop should shut down all bunnies" do
|
320
|
+
@pub.servers = ["localhost:1111", "localhost:2222"]
|
321
|
+
s = sequence("shutdown")
|
322
|
+
bunny = mock("bunny")
|
323
|
+
@pub.expects(:set_current_server).with("localhost:1111").in_sequence(s)
|
324
|
+
@pub.expects(:bunny).returns(bunny).in_sequence(s)
|
325
|
+
bunny.expects(:stop).in_sequence(s)
|
326
|
+
@pub.expects(:set_current_server).with("localhost:2222").in_sequence(s)
|
327
|
+
@pub.expects(:bunny).returns(bunny).in_sequence(s)
|
328
|
+
bunny.expects(:stop).in_sequence(s)
|
329
|
+
@pub.stop
|
330
|
+
end
|
331
|
+
end
|
332
|
+
|
333
|
+
|
334
|
+
class RPCTest < Test::Unit::TestCase
|
335
|
+
def setup
|
336
|
+
@client = Client.new
|
337
|
+
@pub = Publisher.new(@client)
|
338
|
+
@client.register_message(:test, :exchange => :some_exchange)
|
339
|
+
end
|
340
|
+
|
341
|
+
test "rpc should return a timeout status if bunny throws an exception" do
|
342
|
+
bunny = mock("bunny")
|
343
|
+
@pub.expects(:bunny).returns(bunny)
|
344
|
+
bunny.expects(:queue).raises(Bunny::ConnectionError.new)
|
345
|
+
s = sequence("rpc")
|
346
|
+
@pub.expects(:select_next_server).in_sequence(s)
|
347
|
+
@pub.expects(:bind_queues_for_exchange).with("some_exchange").in_sequence(s)
|
348
|
+
@pub.expects(:stop!)
|
349
|
+
assert_equal "TIMEOUT", @pub.rpc("test", "hello").first
|
350
|
+
end
|
351
|
+
|
352
|
+
test "rpc should return a timeout status if the answer doesn't arrive in time" do
|
353
|
+
bunny = mock("bunny")
|
354
|
+
reply_queue = mock("reply_queue")
|
355
|
+
exchange = mock("exchange")
|
356
|
+
@pub.expects(:bunny).returns(bunny)
|
357
|
+
bunny.expects(:queue).returns(reply_queue)
|
358
|
+
reply_queue.stubs(:name).returns("reply_queue")
|
359
|
+
s = sequence("rpc")
|
360
|
+
@pub.expects(:select_next_server).in_sequence(s)
|
361
|
+
@pub.expects(:bind_queues_for_exchange).with("some_exchange").in_sequence(s)
|
362
|
+
@pub.expects(:exchange).with("some_exchange").returns(exchange).in_sequence(s)
|
363
|
+
exchange.expects(:publish).in_sequence(s)
|
364
|
+
reply_queue.expects(:subscribe).with(:message_max => 1, :timeout => 10).in_sequence(s)
|
365
|
+
assert_equal "TIMEOUT", @pub.rpc("test", "hello").first
|
366
|
+
end
|
367
|
+
|
368
|
+
test "rpc should recover dead servers before selecting the next server" do
|
369
|
+
@pub.servers << "localhost:3333"
|
370
|
+
@pub.send(:mark_server_dead)
|
371
|
+
bunny = mock("bunny")
|
372
|
+
reply_queue = mock("reply_queue")
|
373
|
+
exchange = mock("exchange")
|
374
|
+
@pub.expects(:bunny).returns(bunny)
|
375
|
+
bunny.expects(:queue).returns(reply_queue)
|
376
|
+
reply_queue.stubs(:name).returns("reply_queue")
|
377
|
+
s = sequence("rpc")
|
378
|
+
@pub.expects(:recycle_dead_servers).in_sequence(s)
|
379
|
+
@pub.expects(:select_next_server).in_sequence(s)
|
380
|
+
@pub.expects(:bind_queues_for_exchange).with("some_exchange").in_sequence(s)
|
381
|
+
@pub.expects(:exchange).with("some_exchange").returns(exchange).in_sequence(s)
|
382
|
+
exchange.expects(:publish).in_sequence(s)
|
383
|
+
reply_queue.expects(:subscribe).with(:message_max => 1, :timeout => 10).in_sequence(s)
|
384
|
+
assert_equal "TIMEOUT", @pub.rpc("test", "hello").first
|
385
|
+
end
|
386
|
+
|
387
|
+
test "rpc should fetch the result and the status code from the reply message" do
|
388
|
+
bunny = mock("bunny")
|
389
|
+
reply_queue = mock("reply_queue")
|
390
|
+
exchange = mock("exchange")
|
391
|
+
@pub.expects(:bunny).returns(bunny)
|
392
|
+
bunny.expects(:queue).returns(reply_queue)
|
393
|
+
reply_queue.stubs(:name).returns("reply_queue")
|
394
|
+
s = sequence("rpc")
|
395
|
+
@pub.expects(:select_next_server).in_sequence(s)
|
396
|
+
@pub.expects(:bind_queues_for_exchange).with("some_exchange").in_sequence(s)
|
397
|
+
@pub.expects(:exchange).with("some_exchange").returns(exchange).in_sequence(s)
|
398
|
+
exchange.expects(:publish).in_sequence(s)
|
399
|
+
header = mock("header")
|
400
|
+
header.expects(:properties).returns({:headers => {:status => "OK"}})
|
401
|
+
msg = {:payload => 1, :header => header}
|
402
|
+
reply_queue.expects(:subscribe).with(:message_max => 1, :timeout => 10).in_sequence(s).yields(msg)
|
403
|
+
assert_equal ["OK",1], @pub.rpc("test", "hello")
|
404
|
+
end
|
405
|
+
|
406
|
+
end
|
407
|
+
end
|
@@ -0,0 +1,9 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../test_helper')
|
2
|
+
|
3
|
+
module Beetle
|
4
|
+
class ReturnCodesTest < Test::Unit::TestCase
|
5
|
+
test "inspecting a return code should display the name of the returncode" do
|
6
|
+
assert_equal "Beetle::RC::OK", Beetle::RC::OK.inspect
|
7
|
+
end
|
8
|
+
end
|
9
|
+
end
|
@@ -0,0 +1,263 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../test_helper')
|
2
|
+
|
3
|
+
module Beetle
|
4
|
+
class SubscriberTest < Test::Unit::TestCase
|
5
|
+
def setup
|
6
|
+
client = Client.new
|
7
|
+
@sub = client.send(:subscriber)
|
8
|
+
end
|
9
|
+
|
10
|
+
test "initially there should be no amqp connections" do
|
11
|
+
assert_equal({}, @sub.instance_variable_get("@amqp_connections"))
|
12
|
+
end
|
13
|
+
|
14
|
+
test "initially there should be no instances of MQ" do
|
15
|
+
assert_equal({}, @sub.instance_variable_get("@mqs"))
|
16
|
+
end
|
17
|
+
|
18
|
+
test "acccessing an amq_connection for a server which doesn't have one should create it and associate it with the server" do
|
19
|
+
@sub.expects(:new_amqp_connection).returns(42)
|
20
|
+
# TODO: smarter way to test? what triggers the amqp_connection private method call?
|
21
|
+
assert_equal 42, @sub.send(:amqp_connection)
|
22
|
+
connections = @sub.instance_variable_get("@amqp_connections")
|
23
|
+
assert_equal 42, connections[@sub.server]
|
24
|
+
end
|
25
|
+
|
26
|
+
test "new amqp connections should be created using current host and port" do
|
27
|
+
m = mock("dummy")
|
28
|
+
expected_amqp_options = {
|
29
|
+
:host => @sub.send(:current_host), :port => @sub.send(:current_port),
|
30
|
+
:user => "guest", :pass => "guest", :vhost => "/"
|
31
|
+
}
|
32
|
+
AMQP.expects(:connect).with(expected_amqp_options).returns(m)
|
33
|
+
# TODO: smarter way to test? what triggers the amqp_connection private method call?
|
34
|
+
assert_equal m, @sub.send(:new_amqp_connection)
|
35
|
+
end
|
36
|
+
|
37
|
+
test "mq instances should be created for the current server if accessed" do
|
38
|
+
@sub.expects(:amqp_connection).returns(11)
|
39
|
+
mq_mock = mock('mq')
|
40
|
+
mq_mock.expects(:prefetch).with(1).returns(42)
|
41
|
+
MQ.expects(:new).with(11).returns(mq_mock)
|
42
|
+
assert_equal 42, @sub.send(:mq)
|
43
|
+
mqs = @sub.instance_variable_get("@mqs")
|
44
|
+
assert_equal 42, mqs[@sub.server]
|
45
|
+
end
|
46
|
+
|
47
|
+
test "stop! should stop the event loop" do
|
48
|
+
EM.expects(:stop_event_loop)
|
49
|
+
@sub.send(:stop!)
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
|
54
|
+
class SubscriberQueueManagementTest < Test::Unit::TestCase
|
55
|
+
def setup
|
56
|
+
@client = Client.new
|
57
|
+
@sub = @client.send(:subscriber)
|
58
|
+
end
|
59
|
+
|
60
|
+
test "initially there should be no queues for the current server" do
|
61
|
+
assert_equal({}, @sub.send(:queues))
|
62
|
+
assert !@sub.send(:queues)["some_queue"]
|
63
|
+
end
|
64
|
+
|
65
|
+
test "binding a queue should create it using the config and bind it to the exchange with the name specified" do
|
66
|
+
@client.register_queue("some_queue", "durable" => true, "exchange" => "some_exchange", "key" => "haha.#")
|
67
|
+
@sub.expects(:exchange).with("some_exchange").returns(:the_exchange)
|
68
|
+
q = mock("queue")
|
69
|
+
q.expects(:bind).with(:the_exchange, {:key => "haha.#"})
|
70
|
+
m = mock("MQ")
|
71
|
+
m.expects(:queue).with("some_queue", :durable => true, :passive => false, :auto_delete => false, :exclusive => false).returns(q)
|
72
|
+
@sub.expects(:mq).returns(m)
|
73
|
+
|
74
|
+
@sub.send(:queue, "some_queue")
|
75
|
+
assert_equal q, @sub.send(:queues)["some_queue"]
|
76
|
+
end
|
77
|
+
|
78
|
+
test "binding queues should iterate over all servers" do
|
79
|
+
s = sequence("binding")
|
80
|
+
@client.register_queue(:x)
|
81
|
+
@client.register_queue(:y)
|
82
|
+
@client.register_handler(%w(x y)){}
|
83
|
+
@sub.servers = %w(a b)
|
84
|
+
@sub.expects(:set_current_server).with("a").in_sequence(s)
|
85
|
+
@sub.expects(:queue).with("x").in_sequence(s)
|
86
|
+
@sub.expects(:queue).with("y").in_sequence(s)
|
87
|
+
@sub.expects(:set_current_server).with("b").in_sequence(s)
|
88
|
+
@sub.expects(:queue).with("x").in_sequence(s)
|
89
|
+
@sub.expects(:queue).with("y").in_sequence(s)
|
90
|
+
@sub.send(:bind_queues, %W(x y))
|
91
|
+
end
|
92
|
+
|
93
|
+
end
|
94
|
+
|
95
|
+
class SubscriberExchangeManagementTest < Test::Unit::TestCase
|
96
|
+
def setup
|
97
|
+
@client = Client.new
|
98
|
+
@sub = @client.send(:subscriber)
|
99
|
+
end
|
100
|
+
|
101
|
+
test "initially there should be no exchanges for the current server" do
|
102
|
+
assert_equal({}, @sub.send(:exchanges))
|
103
|
+
end
|
104
|
+
|
105
|
+
test "accessing a given exchange should create it using the config. further access should return the created exchange" do
|
106
|
+
@client.register_exchange("some_exchange", "type" => "topic", "durable" => true)
|
107
|
+
m = mock("AMQP")
|
108
|
+
m.expects(:topic).with("some_exchange", :durable => true).returns(42)
|
109
|
+
@sub.expects(:mq).returns(m)
|
110
|
+
ex = @sub.send(:exchange, "some_exchange")
|
111
|
+
assert @sub.send(:exchanges).include?("some_exchange")
|
112
|
+
ex2 = @sub.send(:exchange, "some_exchange")
|
113
|
+
assert_equal ex2, ex
|
114
|
+
end
|
115
|
+
|
116
|
+
test "should create exchanges for all exchanges passed to create_exchanges, for all servers" do
|
117
|
+
@sub.servers = %w(x y)
|
118
|
+
@client.register_queue(:donald, :exchange => 'duck')
|
119
|
+
@client.register_queue(:mickey)
|
120
|
+
@client.register_queue(:mouse, :exchange => 'mickey')
|
121
|
+
|
122
|
+
exchange_creation = sequence("exchange creation")
|
123
|
+
@sub.expects(:set_current_server).with('x').in_sequence(exchange_creation)
|
124
|
+
@sub.expects(:create_exchange!).with("duck", anything).in_sequence(exchange_creation)
|
125
|
+
@sub.expects(:create_exchange!).with("mickey", anything).in_sequence(exchange_creation)
|
126
|
+
@sub.expects(:set_current_server).with('y', anything).in_sequence(exchange_creation)
|
127
|
+
@sub.expects(:create_exchange!).with("duck", anything).in_sequence(exchange_creation)
|
128
|
+
@sub.expects(:create_exchange!).with("mickey", anything).in_sequence(exchange_creation)
|
129
|
+
@sub.send(:create_exchanges, %w(duck mickey))
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
class CallBackExecutionTest < Test::Unit::TestCase
|
134
|
+
def setup
|
135
|
+
client = Client.new
|
136
|
+
@queue = "somequeue"
|
137
|
+
client.register_queue(@queue)
|
138
|
+
@sub = client.send(:subscriber)
|
139
|
+
@exception = Exception.new "murks"
|
140
|
+
@handler = Handler.create(lambda{|*args| raise @exception})
|
141
|
+
@callback = @sub.send(:create_subscription_callback, "my myessage", @queue, @handler, :exceptions => 1)
|
142
|
+
end
|
143
|
+
|
144
|
+
test "exceptions raised from message processing should be ignored" do
|
145
|
+
header = header_with_params({})
|
146
|
+
Message.any_instance.expects(:process).raises(Exception.new)
|
147
|
+
assert_nothing_raised { @callback.call(header, 'foo') }
|
148
|
+
end
|
149
|
+
|
150
|
+
test "should call recover on the server when processing the handler returns true on recover?" do
|
151
|
+
header = header_with_params({})
|
152
|
+
result = mock("result")
|
153
|
+
result.expects(:recover?).returns(true)
|
154
|
+
Message.any_instance.expects(:process).returns(result)
|
155
|
+
@sub.expects(:sleep).with(1)
|
156
|
+
mq = mock("MQ")
|
157
|
+
mq.expects(:recover)
|
158
|
+
@sub.expects(:mq).with(@sub.server).returns(mq)
|
159
|
+
@callback.call(header, 'foo')
|
160
|
+
end
|
161
|
+
|
162
|
+
test "should sent a reply with status OK if the message reply_to header is set and processing the handler succeeds" do
|
163
|
+
header = header_with_params(:reply_to => "tmp-queue")
|
164
|
+
result = RC::OK
|
165
|
+
Message.any_instance.expects(:process).returns(result)
|
166
|
+
Message.any_instance.expects(:handler_result).returns("response-data")
|
167
|
+
mq = mock("MQ")
|
168
|
+
@sub.expects(:mq).with(@sub.server).returns(mq)
|
169
|
+
exchange = mock("exchange")
|
170
|
+
exchange.expects(:publish).with("response-data", :headers => {:status => "OK"})
|
171
|
+
MQ::Exchange.expects(:new).with(mq, :direct, "", :key => "tmp-queue").returns(exchange)
|
172
|
+
@callback.call(header, 'foo')
|
173
|
+
end
|
174
|
+
|
175
|
+
test "should sent a reply with status FAILED if the message reply_to header is set and processing the handler fails" do
|
176
|
+
header = header_with_params(:reply_to => "tmp-queue")
|
177
|
+
result = RC::AttemptsLimitReached
|
178
|
+
Message.any_instance.expects(:process).returns(result)
|
179
|
+
Message.any_instance.expects(:handler_result).returns(nil)
|
180
|
+
mq = mock("MQ")
|
181
|
+
@sub.expects(:mq).with(@sub.server).returns(mq)
|
182
|
+
exchange = mock("exchange")
|
183
|
+
exchange.expects(:publish).with("", :headers => {:status => "FAILED"})
|
184
|
+
MQ::Exchange.expects(:new).with(mq, :direct, "", :key => "tmp-queue").returns(exchange)
|
185
|
+
@callback.call(header, 'foo')
|
186
|
+
end
|
187
|
+
|
188
|
+
end
|
189
|
+
|
190
|
+
class SubscriptionTest < Test::Unit::TestCase
|
191
|
+
def setup
|
192
|
+
@client = Client.new
|
193
|
+
@sub = @client.send(:subscriber)
|
194
|
+
end
|
195
|
+
|
196
|
+
test "subscribe should create subscriptions on all queues for all servers" do
|
197
|
+
@sub.servers << "localhost:7777"
|
198
|
+
@client.register_message(:a)
|
199
|
+
@client.register_message(:b)
|
200
|
+
@client.register_queue(:a)
|
201
|
+
@client.register_queue(:b)
|
202
|
+
@client.register_handler(%W(a b)){}
|
203
|
+
@sub.expects(:subscribe).with("a").times(2)
|
204
|
+
@sub.expects(:subscribe).with("b").times(2)
|
205
|
+
@sub.send(:subscribe_queues, %W(a b))
|
206
|
+
end
|
207
|
+
|
208
|
+
test "subscribe should subscribe with a subscription callback created from the registered block" do
|
209
|
+
@client.register_queue(:some_queue, :exchange => "some_exchange", :key => "some_key")
|
210
|
+
server = @sub.server
|
211
|
+
header = header_with_params({})
|
212
|
+
header.expects(:ack)
|
213
|
+
block_called = false
|
214
|
+
proc = lambda do |m|
|
215
|
+
block_called = true
|
216
|
+
assert_equal header, m.header
|
217
|
+
assert_equal "data", m.data
|
218
|
+
assert_equal server, m.server
|
219
|
+
end
|
220
|
+
@sub.register_handler("some_queue", &proc)
|
221
|
+
q = mock("QUEUE")
|
222
|
+
q.expects(:subscribe).with({:ack => true, :key => "#"}).yields(header, 'foo')
|
223
|
+
@sub.expects(:queues).returns({"some_queue" => q})
|
224
|
+
@sub.send(:subscribe, "some_queue")
|
225
|
+
assert block_called
|
226
|
+
end
|
227
|
+
|
228
|
+
test "subscribe should fail if no handler exists for given message" do
|
229
|
+
assert_raises(Error){ @sub.send(:subscribe, "some_queue") }
|
230
|
+
end
|
231
|
+
|
232
|
+
test "listening should use eventmachine. create exchanges. bind queues. install subscribers. and yield." do
|
233
|
+
@client.register_queue(:a)
|
234
|
+
@client.register_message(:a)
|
235
|
+
EM.expects(:run).yields
|
236
|
+
@sub.expects(:create_exchanges).with(["a"])
|
237
|
+
@sub.expects(:bind_queues).with(["a"])
|
238
|
+
@sub.expects(:subscribe_queues).with(["a"])
|
239
|
+
@sub.listen(["a"]) {}
|
240
|
+
end
|
241
|
+
end
|
242
|
+
|
243
|
+
class HandlersTest < Test::Unit::TestCase
|
244
|
+
def setup
|
245
|
+
@client = Client.new
|
246
|
+
@sub = @client.send(:subscriber)
|
247
|
+
end
|
248
|
+
|
249
|
+
test "initially we should have no handlers" do
|
250
|
+
assert_equal({}, @sub.instance_variable_get("@handlers"))
|
251
|
+
end
|
252
|
+
|
253
|
+
test "registering a handler for a queue should store it in the configuration with symbolized option keys" do
|
254
|
+
opts = {"ack" => true}
|
255
|
+
@sub.register_handler("some_queue", opts){ |*args| 42 }
|
256
|
+
opts, block = @sub.instance_variable_get("@handlers")["some_queue"]
|
257
|
+
assert_equal({:ack => true}, opts)
|
258
|
+
assert_equal 42, block.call(1)
|
259
|
+
end
|
260
|
+
|
261
|
+
end
|
262
|
+
|
263
|
+
end
|