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,105 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../test_helper')
2
+
3
+ module Beetle
4
+
5
+ class Foobar < Handler
6
+ def process
7
+ end
8
+ end
9
+
10
+ class SubFoobar < Foobar
11
+ def process
12
+ raise RuntimeError
13
+ end
14
+ end
15
+
16
+ class HandlerTest < Test::Unit::TestCase
17
+
18
+ test "should allow using a block as a callback" do
19
+ test_var = false
20
+ handler = Handler.create(lambda {|message| test_var = message})
21
+ handler.call(true)
22
+ assert test_var
23
+ end
24
+
25
+ test "should allow using a subclass of a handler as a callback" do
26
+ handler = Handler.create(Foobar)
27
+ Foobar.any_instance.expects(:process)
28
+ handler.call('some_message')
29
+ end
30
+
31
+ test "should allow using an instance of a subclass of handler as a callback" do
32
+ handler = Handler.create(Foobar.new)
33
+ Foobar.any_instance.expects(:process)
34
+ handler.call('some_message')
35
+ end
36
+
37
+ test "should set the instance variable message to the received message" do
38
+ handler = Handler.create(Foobar)
39
+ assert_nil handler.message
40
+ handler.call("message received")
41
+ assert_equal "message received", handler.message
42
+ end
43
+
44
+ test "should call the error method with the exception if no error callback has been given" do
45
+ handler = Handler.create(SubFoobar)
46
+ e = Exception.new
47
+ handler.expects(:error).with(e)
48
+ handler.process_exception(e)
49
+ end
50
+
51
+ test "should call the given error callback with the exception" do
52
+ mock = mock('error handler')
53
+ e = Exception.new
54
+ mock.expects(:call).with('message', e)
55
+ handler = Handler.create(lambda {}, :errback => mock)
56
+ handler.instance_variable_set(:@message, 'message')
57
+ handler.process_exception(e)
58
+ end
59
+
60
+ test "should call the failure method with the exception if no failure callback has been given" do
61
+ handler = Handler.create(SubFoobar)
62
+ handler.expects(:failure).with(1)
63
+ handler.process_failure(1)
64
+ end
65
+
66
+ test "should call the given failure callback with the result" do
67
+ mock = mock('failure handler')
68
+ mock.expects(:call).with('message', 1)
69
+ handler = Handler.create(lambda {}, {:failback => mock})
70
+ handler.instance_variable_set(:@message, 'message')
71
+ handler.process_failure(1)
72
+ end
73
+
74
+ test "logger should point to the Beetle.config.logger" do
75
+ handler = Handler.create(Foobar)
76
+ assert_equal Beetle.config.logger, handler.logger
77
+ assert_equal Beetle.config.logger, Handler.logger
78
+ end
79
+
80
+ test "default implementation of error and process and failure should not crash" do
81
+ handler = Handler.create(lambda {})
82
+ handler.process
83
+ handler.error('barfoo')
84
+ handler.failure('razzmatazz')
85
+ end
86
+
87
+ test "should silently rescue exceptions in the process_exception call" do
88
+ mock = mock('error handler')
89
+ e = Exception.new
90
+ mock.expects(:call).with('message', e).raises(RuntimeError)
91
+ handler = Handler.create(lambda {}, :errback => mock)
92
+ handler.instance_variable_set(:@message, 'message')
93
+ assert_nothing_raised {handler.process_exception(e)}
94
+ end
95
+
96
+ test "should silently rescue exceptions in the process_failure call" do
97
+ mock = mock('failure handler')
98
+ mock.expects(:call).with('message', 1).raises(RuntimeError)
99
+ handler = Handler.create(lambda {}, :failback => mock)
100
+ handler.instance_variable_set(:@message, 'message')
101
+ assert_nothing_raised {handler.process_failure(1)}
102
+ end
103
+
104
+ end
105
+ end
@@ -0,0 +1,744 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../test_helper')
2
+
3
+
4
+ module Beetle
5
+
6
+ class EncodingTest < Test::Unit::TestCase
7
+
8
+ test "a message should encode/decode the message format version correctly" do
9
+ header = header_with_params({})
10
+ m = Message.new("queue", header, 'foo')
11
+ assert_equal Message::FORMAT_VERSION, m.format_version
12
+ end
13
+
14
+ test "a redundantly encoded message should have the redundant flag set on delivery" do
15
+ header = header_with_params(:redundant => true)
16
+ m = Message.new("queue", header, 'foo')
17
+ assert m.redundant?
18
+ assert_equal(Message::FLAG_REDUNDANT, m.flags & Message::FLAG_REDUNDANT)
19
+ end
20
+
21
+ test "encoding a message with a specfied time to live should set an expiration time" do
22
+ Message.expects(:now).returns(25)
23
+ header = header_with_params(:ttl => 17)
24
+ m = Message.new("queue", header, 'foo')
25
+ assert_equal 42, m.expires_at
26
+ end
27
+
28
+ test "encoding a message should set the default expiration date if none is provided in the call to encode" do
29
+ Message.expects(:now).returns(1)
30
+ header = header_with_params({})
31
+ m = Message.new("queue", header, 'foo')
32
+ assert_equal 1 + Message::DEFAULT_TTL, m.expires_at
33
+ end
34
+
35
+ test "the publishing options should include both the beetle headers and the amqp params" do
36
+ key = 'fookey'
37
+ options = Message.publishing_options(:redundant => true, :key => key, :mandatory => true, :immediate => true, :persistent => true)
38
+
39
+ assert options[:mandatory]
40
+ assert options[:immediate]
41
+ assert options[:persistent]
42
+ assert_equal key, options[:key]
43
+ assert_equal "1", options[:headers][:flags]
44
+ end
45
+
46
+ test "the publishing options should silently ignore other parameters than the valid publishing keys" do
47
+ options = Message.publishing_options(:redundant => true, :mandatory => true, :bogus => true)
48
+ assert_equal "1", options[:headers][:flags]
49
+ assert options[:mandatory]
50
+ assert_nil options[:bogus]
51
+ end
52
+
53
+ test "the publishing options for a redundant message should include the uuid" do
54
+ uuid = 'wadduyouwantfromme'
55
+ Message.expects(:generate_uuid).returns(uuid)
56
+ options = Message.publishing_options(:redundant => true)
57
+ assert_equal uuid, options[:message_id]
58
+ end
59
+
60
+ test "the publishing options must only include string values" do
61
+ options = Message.publishing_options(:redundant => true, :mandatory => true, :bogus => true)
62
+ assert options[:headers].all? {|_, param| param.is_a?(String)}
63
+ end
64
+
65
+ end
66
+
67
+ class KeyManagementTest < Test::Unit::TestCase
68
+ def setup
69
+ @store = DeduplicationStore.new
70
+ @store.flushdb
71
+ end
72
+
73
+ test "should be able to extract msg_id from any key" do
74
+ header = header_with_params({})
75
+ message = Message.new("somequeue", header, 'foo', :store => @store)
76
+ @store.keys(message.msg_id).each do |key|
77
+ assert_equal message.msg_id, @store.msg_id(key)
78
+ end
79
+ end
80
+
81
+ test "should be able to garbage collect expired keys" do
82
+ Beetle.config.expects(:gc_threshold).returns(0)
83
+ header = header_with_params({:ttl => 0})
84
+ message = Message.new("somequeue", header, 'foo', :store => @store)
85
+ assert !message.key_exists?
86
+ assert message.key_exists?
87
+ @store.redis.expects(:del).with(@store.keys(message.msg_id))
88
+ @store.garbage_collect_keys(Time.now.to_i+1)
89
+ end
90
+
91
+ test "should not garbage collect not yet expired keys" do
92
+ Beetle.config.expects(:gc_threshold).returns(0)
93
+ header = header_with_params({:ttl => 0})
94
+ message = Message.new("somequeue", header, 'foo', :store => @store)
95
+ assert !message.key_exists?
96
+ assert message.key_exists?
97
+ @store.redis.expects(:del).never
98
+ @store.garbage_collect_keys(Time.now.to_i-1)
99
+ end
100
+
101
+ test "successful processing of a non redundant message should delete all keys from the database" do
102
+ header = header_with_params({})
103
+ header.expects(:ack)
104
+ message = Message.new("somequeue", header, 'foo', :store => @store)
105
+
106
+ assert !message.expired?
107
+ assert !message.redundant?
108
+
109
+ message.process(lambda {|*args|})
110
+
111
+ @store.keys(message.msg_id).each do |key|
112
+ assert !@store.redis.exists(key)
113
+ end
114
+ end
115
+
116
+ test "succesful processing of a redundant message twice should delete all keys from the database" do
117
+ header = header_with_params({:redundant => true})
118
+ header.expects(:ack).twice
119
+ message = Message.new("somequeue", header, 'foo', :store => @store)
120
+
121
+ assert !message.expired?
122
+ assert message.redundant?
123
+
124
+ message.process(lambda {|*args|})
125
+ message.process(lambda {|*args|})
126
+
127
+ @store.keys(message.msg_id).each do |key|
128
+ assert !@store.redis.exists(key)
129
+ end
130
+ end
131
+
132
+ test "successful processing of a redundant message once should insert all but the delay key and the exception count key into the database" do
133
+ header = header_with_params({:redundant => true})
134
+ header.expects(:ack)
135
+ message = Message.new("somequeue", header, 'foo', :store => @store)
136
+
137
+ assert !message.expired?
138
+ assert message.redundant?
139
+
140
+ message.process(lambda {|*args|})
141
+
142
+ assert @store.exists(message.msg_id, :status)
143
+ assert @store.exists(message.msg_id, :expires)
144
+ assert @store.exists(message.msg_id, :attempts)
145
+ assert @store.exists(message.msg_id, :timeout)
146
+ assert @store.exists(message.msg_id, :ack_count)
147
+ assert !@store.exists(message.msg_id, :delay)
148
+ assert !@store.exists(message.msg_id, :exceptions)
149
+ end
150
+ end
151
+
152
+ class AckingTest < Test::Unit::TestCase
153
+
154
+ def setup
155
+ @store = DeduplicationStore.new
156
+ @store.flushdb
157
+ end
158
+
159
+ test "an expired message should be acked without calling the handler" do
160
+ header = header_with_params(:ttl => -1)
161
+ header.expects(:ack)
162
+ message = Message.new("somequeue", header, 'foo', :store => @store)
163
+ assert message.expired?
164
+
165
+ processed = :no
166
+ message.process(lambda {|*args| processed = true})
167
+ assert_equal :no, processed
168
+ end
169
+
170
+ test "a delayed message should not be acked and the handler should not be called" do
171
+ header = header_with_params()
172
+ header.expects(:ack).never
173
+ message = Message.new("somequeue", header, 'foo', :attempts => 2, :store => @store)
174
+ message.set_delay!
175
+ assert !message.key_exists?
176
+ assert message.delayed?
177
+
178
+ processed = :no
179
+ message.process(lambda {|*args| processed = true})
180
+ assert_equal :no, processed
181
+ end
182
+
183
+ test "acking a non redundant message should remove the ack_count key" do
184
+ header = header_with_params({})
185
+ header.expects(:ack)
186
+ message = Message.new("somequeue", header, 'foo', :store => @store)
187
+
188
+ message.process(lambda {|*args|})
189
+ assert !message.redundant?
190
+ assert !@store.exists(message.msg_id, :ack_count)
191
+ end
192
+
193
+ test "a redundant message should be acked after calling the handler" do
194
+ header = header_with_params({:redundant => true})
195
+ message = Message.new("somequeue", header, 'foo', :store => @store)
196
+
197
+ message.expects(:ack!)
198
+ assert message.redundant?
199
+ message.process(lambda {|*args|})
200
+ end
201
+
202
+ test "acking a redundant message should increment the ack_count key" do
203
+ header = header_with_params({:redundant => true})
204
+ header.expects(:ack)
205
+ message = Message.new("somequeue", header, 'foo', :store => @store)
206
+
207
+ assert_equal nil, @store.get(message.msg_id, :ack_count)
208
+ message.process(lambda {|*args|})
209
+ assert message.redundant?
210
+ assert_equal "1", @store.get(message.msg_id, :ack_count)
211
+ end
212
+
213
+ test "acking a redundant message twice should remove the ack_count key" do
214
+ header = header_with_params({:redundant => true})
215
+ header.expects(:ack).twice
216
+ message = Message.new("somequeue", header, 'foo', :store => @store)
217
+
218
+ message.process(lambda {|*args|})
219
+ message.process(lambda {|*args|})
220
+ assert message.redundant?
221
+ assert !@store.exists(message.msg_id, :ack_count)
222
+ end
223
+
224
+ end
225
+
226
+ class FreshMessageTest < Test::Unit::TestCase
227
+ def setup
228
+ @store = DeduplicationStore.new
229
+ @store.flushdb
230
+ end
231
+
232
+ test "processing a fresh message sucessfully should first run the handler and then ack it" do
233
+ header = header_with_params({})
234
+ message = Message.new("somequeue", header, 'foo', :attempts => 2, :store => @store)
235
+ assert !message.attempts_limit_reached?
236
+
237
+ proc = mock("proc")
238
+ s = sequence("s")
239
+ proc.expects(:call).in_sequence(s)
240
+ header.expects(:ack).in_sequence(s)
241
+ assert_equal RC::OK, message.process(proc)
242
+ end
243
+
244
+ test "after processing a redundant fresh message successfully the ack count should be 1 and the status should be completed" do
245
+ header = header_with_params({:redundant => true})
246
+ message = Message.new("somequeue", header, 'foo', :timeout => 10.seconds, :store => @store)
247
+ assert !message.attempts_limit_reached?
248
+ assert message.redundant?
249
+
250
+ proc = mock("proc")
251
+ s = sequence("s")
252
+ proc.expects(:call).in_sequence(s)
253
+ message.expects(:completed!).in_sequence(s)
254
+ header.expects(:ack).in_sequence(s)
255
+ assert_equal RC::OK, message.__send__(:process_internal, proc)
256
+ assert_equal "1", @store.get(message.msg_id, :ack_count)
257
+ end
258
+
259
+ end
260
+
261
+ class SimpleMessageTest < Test::Unit::TestCase
262
+ def setup
263
+ @store = DeduplicationStore.new
264
+ @store.flushdb
265
+ @store.expects(:redis).never
266
+ end
267
+
268
+ test "when processing a simple message, ack should precede calling the handler" do
269
+ header = header_with_params({})
270
+ message = Message.new("somequeue", header, 'foo', :attempts => 1, :store => @store)
271
+
272
+ proc = mock("proc")
273
+ s = sequence("s")
274
+ header.expects(:ack).in_sequence(s)
275
+ proc.expects(:call).in_sequence(s)
276
+ assert_equal RC::OK, message.process(proc)
277
+ end
278
+
279
+ test "when processing a simple message, RC::AttemptsLimitReached should be returned if the handler crashes" do
280
+ header = header_with_params({})
281
+ message = Message.new("somequeue", header, 'foo', :attempts => 1, :store => @store)
282
+
283
+ proc = mock("proc")
284
+ s = sequence("s")
285
+ header.expects(:ack).in_sequence(s)
286
+ e = Exception.new("ohoh")
287
+ proc.expects(:call).in_sequence(s).raises(e)
288
+ proc.expects(:process_exception).with(e).in_sequence(s)
289
+ proc.expects(:process_failure).with(RC::AttemptsLimitReached).in_sequence(s)
290
+ assert_equal RC::AttemptsLimitReached, message.process(proc)
291
+ end
292
+
293
+ end
294
+
295
+ class HandlerCrashTest < Test::Unit::TestCase
296
+ def setup
297
+ @store = DeduplicationStore.new
298
+ @store.flushdb
299
+ end
300
+
301
+ test "a message should not be acked if the handler crashes and the exception limit has not been reached" do
302
+ header = header_with_params({})
303
+ message = Message.new("somequeue", header, 'foo', :delay => 42, :timeout => 10.seconds, :exceptions => 1, :store => @store)
304
+ assert !message.attempts_limit_reached?
305
+ assert !message.exceptions_limit_reached?
306
+ assert !message.timed_out?
307
+
308
+ proc = lambda {|*args| raise "crash"}
309
+ message.stubs(:now).returns(10)
310
+ message.expects(:completed!).never
311
+ header.expects(:ack).never
312
+ assert_equal RC::HandlerCrash, message.__send__(:process_internal, proc)
313
+ assert !message.completed?
314
+ assert_equal "1", @store.get(message.msg_id, :exceptions)
315
+ assert_equal "0", @store.get(message.msg_id, :timeout)
316
+ assert_equal "52", @store.get(message.msg_id, :delay)
317
+ end
318
+
319
+ test "a message should delete the mutex before resetting the timer if attempts and exception limits havn't been reached" do
320
+ Message.stubs(:now).returns(9)
321
+ header = header_with_params({})
322
+ message = Message.new("somequeue", header, 'foo', :delay => 42, :timeout => 10.seconds, :exceptions => 1, :store => @store)
323
+ assert !message.attempts_limit_reached?
324
+ assert !message.exceptions_limit_reached?
325
+ assert !@store.get(message.msg_id, :mutex)
326
+ assert !message.timed_out?
327
+
328
+ proc = lambda {|*args| raise "crash"}
329
+ message.expects(:delete_mutex!)
330
+ message.stubs(:now).returns(10)
331
+ message.expects(:completed!).never
332
+ header.expects(:ack).never
333
+ assert_equal RC::HandlerCrash, message.__send__(:process_internal, proc)
334
+ end
335
+
336
+ test "a message should be acked if the handler crashes and the exception limit has been reached" do
337
+ header = header_with_params({})
338
+ message = Message.new("somequeue", header, 'foo', :timeout => 10.seconds, :attempts => 2, :store => @store)
339
+ assert !message.attempts_limit_reached?
340
+ assert !message.exceptions_limit_reached?
341
+ assert !message.timed_out?
342
+ assert !message.simple?
343
+
344
+ proc = lambda {|*args| raise "crash"}
345
+ s = sequence("s")
346
+ message.expects(:completed!).never
347
+ header.expects(:ack)
348
+ assert_equal RC::ExceptionsLimitReached, message.__send__(:process_internal, proc)
349
+ end
350
+
351
+ test "a message should be acked if the handler crashes and the attempts limit has been reached" do
352
+ header = header_with_params({})
353
+ message = Message.new("somequeue", header, 'foo', :timeout => 10.seconds, :attempts => 2, :store => @store)
354
+ message.increment_execution_attempts!
355
+ assert !message.attempts_limit_reached?
356
+ assert !message.exceptions_limit_reached?
357
+ assert !message.timed_out?
358
+
359
+ proc = lambda {|*args| raise "crash"}
360
+ s = sequence("s")
361
+ message.expects(:completed!).never
362
+ header.expects(:ack)
363
+ assert_equal RC::AttemptsLimitReached, message.__send__(:process_internal, proc)
364
+ end
365
+
366
+ end
367
+
368
+ class SeenMessageTest < Test::Unit::TestCase
369
+ def setup
370
+ @store = DeduplicationStore.new
371
+ @store.flushdb
372
+ end
373
+
374
+ test "a completed existing message should be just acked and not run the handler" do
375
+ header = header_with_params({})
376
+ message = Message.new("somequeue", header, 'foo', :attempts => 2, :store => @store)
377
+ assert !message.key_exists?
378
+ message.completed!
379
+ assert message.completed?
380
+
381
+ proc = mock("proc")
382
+ s = sequence("s")
383
+ header.expects(:ack)
384
+ proc.expects(:call).never
385
+ assert_equal RC::OK, message.__send__(:process_internal, proc)
386
+ end
387
+
388
+ test "an incomplete, delayed existing message should be processed later" do
389
+ header = header_with_params({})
390
+ message = Message.new("somequeue", header, 'foo', :delay => 10.seconds, :attempts => 2, :store => @store)
391
+ assert !message.key_exists?
392
+ assert !message.completed?
393
+ message.set_delay!
394
+ assert message.delayed?
395
+
396
+ proc = mock("proc")
397
+ s = sequence("s")
398
+ header.expects(:ack).never
399
+ proc.expects(:call).never
400
+ assert_equal RC::Delayed, message.__send__(:process_internal, proc)
401
+ assert message.delayed?
402
+ assert !message.completed?
403
+ end
404
+
405
+ test "an incomplete, undelayed, not yet timed out, existing message should be processed later" do
406
+ header = header_with_params({})
407
+ message = Message.new("somequeue", header, 'foo', :timeout => 10.seconds, :attempts => 2, :store => @store)
408
+ assert !message.key_exists?
409
+ assert !message.completed?
410
+ assert !message.delayed?
411
+ message.set_timeout!
412
+ assert !message.timed_out?
413
+
414
+ proc = mock("proc")
415
+ s = sequence("s")
416
+ header.expects(:ack).never
417
+ proc.expects(:call).never
418
+ assert_equal RC::HandlerNotYetTimedOut, message.__send__(:process_internal, proc)
419
+ assert !message.delayed?
420
+ assert !message.completed?
421
+ assert !message.timed_out?
422
+ end
423
+
424
+ test "an incomplete, undelayed, not yet timed out, existing message which has reached the handler execution attempts limit should be acked and not run the handler" do
425
+ header = header_with_params({})
426
+ message = Message.new("somequeue", header, 'foo', :attempts => 2, :store => @store)
427
+ message.increment_execution_attempts!
428
+ assert !message.key_exists?
429
+ assert !message.completed?
430
+ assert !message.delayed?
431
+ message.timed_out!
432
+ assert message.timed_out?
433
+
434
+ assert !message.attempts_limit_reached?
435
+ message.attempts_limit.times {message.increment_execution_attempts!}
436
+ assert message.attempts_limit_reached?
437
+
438
+ proc = mock("proc")
439
+ header.expects(:ack)
440
+ proc.expects(:call).never
441
+ assert_equal RC::AttemptsLimitReached, message.send(:process_internal, proc)
442
+ end
443
+
444
+ test "an incomplete, undelayed, timed out, existing message which has reached the exceptions limit should be acked and not run the handler" do
445
+ header = header_with_params({})
446
+ message = Message.new("somequeue", header, 'foo', :attempts => 2, :store => @store)
447
+ message.increment_execution_attempts!
448
+ assert !message.key_exists?
449
+ assert !message.completed?
450
+ assert !message.delayed?
451
+ message.timed_out!
452
+ assert message.timed_out?
453
+ assert !message.attempts_limit_reached?
454
+ message.increment_exception_count!
455
+ assert message.exceptions_limit_reached?
456
+
457
+ proc = mock("proc")
458
+ header.expects(:ack)
459
+ proc.expects(:call).never
460
+ assert_equal RC::ExceptionsLimitReached, message.send(:process_internal, proc)
461
+ end
462
+
463
+ test "an incomplete, undelayed, timed out, existing message should be processed again if the mutex can be aquired" do
464
+ header = header_with_params({:redundant => true})
465
+ message = Message.new("somequeue", header, 'foo', :store => @store)
466
+ assert !message.key_exists?
467
+ assert !message.completed?
468
+ assert !message.delayed?
469
+ message.timed_out!
470
+ assert message.timed_out?
471
+ assert !message.attempts_limit_reached?
472
+ assert !message.exceptions_limit_reached?
473
+
474
+ proc = mock("proc")
475
+ s = sequence("s")
476
+ message.expects(:set_timeout!).in_sequence(s)
477
+ proc.expects(:call).in_sequence(s)
478
+ header.expects(:ack).in_sequence(s)
479
+ assert_equal RC::OK, message.__send__(:process_internal, proc)
480
+ assert message.completed?
481
+ end
482
+
483
+ test "an incomplete, undelayed, timed out, existing message should not be processed again if the mutex cannot be aquired" do
484
+ header = header_with_params({:redundant => true})
485
+ message = Message.new("somequeue", header, 'foo', :store => @store)
486
+ assert !message.key_exists?
487
+ assert !message.completed?
488
+ assert !message.delayed?
489
+ message.timed_out!
490
+ assert message.timed_out?
491
+ assert !message.attempts_limit_reached?
492
+ assert !message.exceptions_limit_reached?
493
+ message.aquire_mutex!
494
+ assert @store.exists(message.msg_id, :mutex)
495
+
496
+ proc = mock("proc")
497
+ proc.expects(:call).never
498
+ header.expects(:ack).never
499
+ assert_equal RC::MutexLocked, message.__send__(:process_internal, proc)
500
+ assert !message.completed?
501
+ assert !@store.exists(message.msg_id, :mutex)
502
+ end
503
+
504
+ end
505
+
506
+ class ProcessingTest < Test::Unit::TestCase
507
+ def setup
508
+ @store = DeduplicationStore.new
509
+ @store.flushdb
510
+ end
511
+
512
+ test "processing a message catches internal exceptions risen by process_internal and returns an internal error" do
513
+ header = header_with_params({})
514
+ message = Message.new("somequeue", header, 'foo', :store => @store)
515
+ message.expects(:process_internal).raises(Exception.new)
516
+ handler = Handler.new
517
+ handler.expects(:process_exception).never
518
+ handler.expects(:process_failure).never
519
+ assert_equal RC::InternalError, message.process(1)
520
+ end
521
+
522
+ test "processing a message with a crashing processor calls the processors exception handler and returns an internal error" do
523
+ header = header_with_params({})
524
+ message = Message.new("somequeue", header, 'foo', :exceptions => 1, :store => @store)
525
+ errback = lambda{|*args|}
526
+ exception = Exception.new
527
+ action = lambda{|*args| raise exception}
528
+ handler = Handler.create(action, :errback => errback)
529
+ handler.expects(:process_exception).with(exception).once
530
+ handler.expects(:process_failure).never
531
+ result = message.process(handler)
532
+ assert_equal RC::HandlerCrash, result
533
+ assert result.recover?
534
+ assert !result.failure?
535
+ end
536
+
537
+ test "processing a message with a crashing processor calls the processors exception handler and failure handler if the attempts limit has been reached" do
538
+ header = header_with_params({})
539
+ header.expects(:ack)
540
+ message = Message.new("somequeue", header, 'foo', :attempts => 2, :store => @store)
541
+ message.increment_execution_attempts!
542
+ errback = mock("errback")
543
+ failback = mock("failback")
544
+ exception = Exception.new
545
+ action = lambda{|*args| raise exception}
546
+ handler = Handler.create(action, :errback => errback, :failback => failback)
547
+ errback.expects(:call).once
548
+ failback.expects(:call).once
549
+ result = message.process(handler)
550
+ assert_equal RC::AttemptsLimitReached, result
551
+ assert !result.recover?
552
+ assert result.failure?
553
+ end
554
+
555
+ test "processing a message with a crashing processor calls the processors exception handler and failure handler if the exceptions limit has been reached" do
556
+ header = header_with_params({})
557
+ header.expects(:ack)
558
+ message = Message.new("somequeue", header, 'foo', :attempts => 2, :store => @store)
559
+ errback = mock("errback")
560
+ failback = mock("failback")
561
+ exception = Exception.new
562
+ action = lambda{|*args| raise exception}
563
+ handler = Handler.create(action, :errback => errback, :failback => failback)
564
+ errback.expects(:call).once
565
+ failback.expects(:call).once
566
+ result = message.process(handler)
567
+ assert_equal RC::ExceptionsLimitReached, result
568
+ assert !result.recover?
569
+ assert result.failure?
570
+ end
571
+
572
+ end
573
+
574
+ class HandlerTimeoutTest < Test::Unit::TestCase
575
+ def setup
576
+ @store = DeduplicationStore.new
577
+ @store.flushdb
578
+ end
579
+
580
+ test "a handler running longer than the specified timeout should be aborted" do
581
+ header = header_with_params({})
582
+ header.expects(:ack)
583
+ message = Message.new("somequeue", header, 'foo', :timeout => 0.1, :attempts => 2, :store => @store)
584
+ action = lambda{|*args| while true; end}
585
+ handler = Handler.create(action)
586
+ result = message.process(handler)
587
+ assert_equal RC::ExceptionsLimitReached, result
588
+ end
589
+ end
590
+
591
+ class SettingsTest < Test::Unit::TestCase
592
+ def setup
593
+ @store = DeduplicationStore.new
594
+ @store.flushdb
595
+ end
596
+
597
+ test "completed! should store the status 'complete' in the database" do
598
+ header = header_with_params({})
599
+ message = Message.new("somequeue", header, 'foo', :store => @store)
600
+ assert !message.completed?
601
+ message.completed!
602
+ assert message.completed?
603
+ assert_equal "completed", @store.get(message.msg_id, :status)
604
+ end
605
+
606
+ test "set_delay! should store the current time plus the number of delayed seconds in the database" do
607
+ header = header_with_params({})
608
+ message = Message.new("somequeue", header, 'foo', :delay => 1, :store => @store)
609
+ message.expects(:now).returns(1)
610
+ message.set_delay!
611
+ assert_equal "2", @store.get(message.msg_id, :delay)
612
+ message.expects(:now).returns(2)
613
+ assert !message.delayed?
614
+ message.expects(:now).returns(0)
615
+ assert message.delayed?
616
+ end
617
+
618
+ test "set_delay! should use the default delay if the delay hasn't been set on the message instance" do
619
+ header = header_with_params({})
620
+ message = Message.new("somequeue", header, 'foo', :store => @store)
621
+ message.expects(:now).returns(0)
622
+ message.set_delay!
623
+ assert_equal "#{Message::DEFAULT_HANDLER_EXECUTION_ATTEMPTS_DELAY}", @store.get(message.msg_id, :delay)
624
+ message.expects(:now).returns(message.delay)
625
+ assert !message.delayed?
626
+ message.expects(:now).returns(0)
627
+ assert message.delayed?
628
+ end
629
+
630
+ test "set_timeout! should store the current time plus the number of timeout seconds in the database" do
631
+ header = header_with_params({})
632
+ message = Message.new("somequeue", header, 'foo', :timeout => 1, :store => @store)
633
+ message.expects(:now).returns(1)
634
+ message.set_timeout!
635
+ assert_equal "2", @store.get(message.msg_id, :timeout)
636
+ message.expects(:now).returns(2)
637
+ assert !message.timed_out?
638
+ message.expects(:now).returns(3)
639
+ assert message.timed_out?
640
+ end
641
+
642
+ test "set_timeout! should use the default timeout if the timeout hasn't been set on the message instance" do
643
+ header = header_with_params({})
644
+ message = Message.new("somequeue", header, 'foo', :store => @store)
645
+ message.expects(:now).returns(0)
646
+ message.set_timeout!
647
+ assert_equal "#{Message::DEFAULT_HANDLER_TIMEOUT}", @store.get(message.msg_id, :timeout)
648
+ message.expects(:now).returns(message.timeout)
649
+ assert !message.timed_out?
650
+ message.expects(:now).returns(Message::DEFAULT_HANDLER_TIMEOUT+1)
651
+ assert message.timed_out?
652
+ end
653
+
654
+ test "incrementing execution attempts should increment by 1" do
655
+ header = header_with_params({})
656
+ message = Message.new("somequeue", header, 'foo', :store => @store)
657
+ assert_equal 1, message.increment_execution_attempts!
658
+ assert_equal 2, message.increment_execution_attempts!
659
+ assert_equal 3, message.increment_execution_attempts!
660
+ end
661
+
662
+ test "accessing execution attempts should return the number of execution attempts made so far" do
663
+ header = header_with_params({})
664
+ message = Message.new("somequeue", header, 'foo', :store => @store)
665
+ assert_equal 0, message.attempts
666
+ message.increment_execution_attempts!
667
+ assert_equal 1, message.attempts
668
+ message.increment_execution_attempts!
669
+ assert_equal 2, message.attempts
670
+ message.increment_execution_attempts!
671
+ assert_equal 3, message.attempts
672
+ end
673
+
674
+ test "accessing execution attempts should return 0 if none were made" do
675
+ header = header_with_params({})
676
+ message = Message.new("somequeue", header, 'foo', :store => @store)
677
+ assert_equal 0, message.attempts
678
+ end
679
+
680
+
681
+ test "attempts limit should be set exception limit + 1 iff the configured attempts limit is equal to or smaller than the exceptions limit" do
682
+ header = header_with_params({})
683
+ message = Message.new("somequeue", header, 'foo', :exceptions => 1, :store => @store)
684
+ assert_equal 2, message.attempts_limit
685
+ assert_equal 1, message.exceptions_limit
686
+ message = Message.new("somequeue", header, 'foo', :exceptions => 2, :store => @store)
687
+ assert_equal 3, message.attempts_limit
688
+ assert_equal 2, message.exceptions_limit
689
+ message = Message.new("somequeue", header, 'foo', :attempts => 5, :exceptions => 2, :store => @store)
690
+ assert_equal 5, message.attempts_limit
691
+ assert_equal 2, message.exceptions_limit
692
+ end
693
+
694
+ test "attempts limit should be reached after incrementing the attempt limit counter 'attempts limit' times" do
695
+ header = header_with_params({})
696
+ message = Message.new("somequeue", header, 'foo', :attempts =>2, :store => @store)
697
+ assert !message.attempts_limit_reached?
698
+ message.increment_execution_attempts!
699
+ assert !message.attempts_limit_reached?
700
+ message.increment_execution_attempts!
701
+ assert message.attempts_limit_reached?
702
+ message.increment_execution_attempts!
703
+ assert message.attempts_limit_reached?
704
+ end
705
+
706
+ test "incrementing exception counts should increment by 1" do
707
+ header = header_with_params({})
708
+ message = Message.new("somequeue", header, 'foo', :store => @store)
709
+ assert_equal 1, message.increment_exception_count!
710
+ assert_equal 2, message.increment_exception_count!
711
+ assert_equal 3, message.increment_exception_count!
712
+ end
713
+
714
+ test "default exceptions limit should be reached after incrementing the attempt limit counter 1 time" do
715
+ header = header_with_params({})
716
+ message = Message.new("somequeue", header, 'foo', :store => @store)
717
+ assert !message.exceptions_limit_reached?
718
+ message.increment_exception_count!
719
+ assert message.exceptions_limit_reached?
720
+ end
721
+
722
+ test "exceptions limit should be reached after incrementing the attempt limit counter 'exceptions limit + 1' times" do
723
+ header = header_with_params({})
724
+ message = Message.new("somequeue", header, 'foo', :exceptions => 1, :store => @store)
725
+ assert !message.exceptions_limit_reached?
726
+ message.increment_exception_count!
727
+ assert !message.exceptions_limit_reached?
728
+ message.increment_exception_count!
729
+ assert message.exceptions_limit_reached?
730
+ message.increment_exception_count!
731
+ assert message.exceptions_limit_reached?
732
+ end
733
+
734
+ test "failure to aquire a mutex should delete it from the database" do
735
+ header = header_with_params({})
736
+ message = Message.new("somequeue", header, 'foo', :store => @store)
737
+ assert message.aquire_mutex!
738
+ assert !message.aquire_mutex!
739
+ assert !@store.exists(message.msg_id, :mutex)
740
+ end
741
+ end
742
+
743
+ end
744
+