beetle 0.1

Sign up to get free protection for your applications and to get access to all the features.
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
+