beetle 0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +5 -0
- data/MIT-LICENSE +20 -0
- data/README.rdoc +82 -0
- data/Rakefile +114 -0
- data/TODO +7 -0
- data/beetle.gemspec +127 -0
- data/etc/redis-master.conf +189 -0
- data/etc/redis-slave.conf +189 -0
- data/examples/README.rdoc +14 -0
- data/examples/attempts.rb +66 -0
- data/examples/handler_class.rb +64 -0
- data/examples/handling_exceptions.rb +73 -0
- data/examples/multiple_exchanges.rb +48 -0
- data/examples/multiple_queues.rb +43 -0
- data/examples/redis_failover.rb +65 -0
- data/examples/redundant.rb +65 -0
- data/examples/rpc.rb +45 -0
- data/examples/simple.rb +39 -0
- data/lib/beetle.rb +57 -0
- data/lib/beetle/base.rb +78 -0
- data/lib/beetle/client.rb +252 -0
- data/lib/beetle/configuration.rb +31 -0
- data/lib/beetle/deduplication_store.rb +152 -0
- data/lib/beetle/handler.rb +95 -0
- data/lib/beetle/message.rb +336 -0
- data/lib/beetle/publisher.rb +187 -0
- data/lib/beetle/r_c.rb +40 -0
- data/lib/beetle/subscriber.rb +144 -0
- data/script/start_rabbit +29 -0
- data/snafu.rb +55 -0
- data/test/beetle.yml +81 -0
- data/test/beetle/base_test.rb +52 -0
- data/test/beetle/bla.rb +0 -0
- data/test/beetle/client_test.rb +305 -0
- data/test/beetle/configuration_test.rb +5 -0
- data/test/beetle/deduplication_store_test.rb +90 -0
- data/test/beetle/handler_test.rb +105 -0
- data/test/beetle/message_test.rb +744 -0
- data/test/beetle/publisher_test.rb +407 -0
- data/test/beetle/r_c_test.rb +9 -0
- data/test/beetle/subscriber_test.rb +263 -0
- data/test/beetle_test.rb +5 -0
- data/test/test_helper.rb +20 -0
- data/tmp/master/.gitignore +2 -0
- data/tmp/slave/.gitignore +3 -0
- metadata +192 -0
@@ -0,0 +1,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
|
+
|