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.
Files changed (46) hide show
  1. data/.gitignore +5 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.rdoc +82 -0
  4. data/Rakefile +114 -0
  5. data/TODO +7 -0
  6. data/beetle.gemspec +127 -0
  7. data/etc/redis-master.conf +189 -0
  8. data/etc/redis-slave.conf +189 -0
  9. data/examples/README.rdoc +14 -0
  10. data/examples/attempts.rb +66 -0
  11. data/examples/handler_class.rb +64 -0
  12. data/examples/handling_exceptions.rb +73 -0
  13. data/examples/multiple_exchanges.rb +48 -0
  14. data/examples/multiple_queues.rb +43 -0
  15. data/examples/redis_failover.rb +65 -0
  16. data/examples/redundant.rb +65 -0
  17. data/examples/rpc.rb +45 -0
  18. data/examples/simple.rb +39 -0
  19. data/lib/beetle.rb +57 -0
  20. data/lib/beetle/base.rb +78 -0
  21. data/lib/beetle/client.rb +252 -0
  22. data/lib/beetle/configuration.rb +31 -0
  23. data/lib/beetle/deduplication_store.rb +152 -0
  24. data/lib/beetle/handler.rb +95 -0
  25. data/lib/beetle/message.rb +336 -0
  26. data/lib/beetle/publisher.rb +187 -0
  27. data/lib/beetle/r_c.rb +40 -0
  28. data/lib/beetle/subscriber.rb +144 -0
  29. data/script/start_rabbit +29 -0
  30. data/snafu.rb +55 -0
  31. data/test/beetle.yml +81 -0
  32. data/test/beetle/base_test.rb +52 -0
  33. data/test/beetle/bla.rb +0 -0
  34. data/test/beetle/client_test.rb +305 -0
  35. data/test/beetle/configuration_test.rb +5 -0
  36. data/test/beetle/deduplication_store_test.rb +90 -0
  37. data/test/beetle/handler_test.rb +105 -0
  38. data/test/beetle/message_test.rb +744 -0
  39. data/test/beetle/publisher_test.rb +407 -0
  40. data/test/beetle/r_c_test.rb +9 -0
  41. data/test/beetle/subscriber_test.rb +263 -0
  42. data/test/beetle_test.rb +5 -0
  43. data/test/test_helper.rb +20 -0
  44. data/tmp/master/.gitignore +2 -0
  45. data/tmp/slave/.gitignore +3 -0
  46. 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