tom_queue 0.0.1.dev

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.
@@ -0,0 +1,218 @@
1
+ require 'tom_queue/helper'
2
+
3
+ describe TomQueue::QueueManager do
4
+
5
+ let(:manager) { TomQueue::QueueManager.new("test-#{Time.now.to_f}") }
6
+ let(:channel) { TomQueue.bunny.create_channel }
7
+
8
+ describe "basic creation" do
9
+
10
+ it "should be a thing" do
11
+ defined?(TomQueue::QueueManager).should be_true
12
+ end
13
+
14
+ it "should be created with a name-prefix" do
15
+ manager.prefix.should =~ /^test-[\d.]+$/
16
+ end
17
+
18
+ it "should default the prefix to TomQueue.default_prefix if available" do
19
+ TomQueue.default_prefix = "test-#{Time.now.to_f}"
20
+ TomQueue::QueueManager.new.prefix.should == TomQueue.default_prefix
21
+ end
22
+
23
+ it "should raise an ArgumentError if no prefix is specified and no default is available" do
24
+ TomQueue.default_prefix = nil
25
+ lambda {
26
+ TomQueue::QueueManager.new
27
+ }.should raise_exception(ArgumentError, /prefix is required/)
28
+ end
29
+
30
+ it "should use the TomQueue.bunny object" do
31
+ manager.bunny.should == TomQueue.bunny
32
+ end
33
+
34
+ it "should stick to the same bunny object, even if TomQueue.bunny changes" do
35
+ manager
36
+ TomQueue.bunny = "A FAKE RABBIT"
37
+ manager.bunny.should be_a(Bunny::Session)
38
+ end
39
+ end
40
+
41
+ describe "AMQP configuration" do
42
+
43
+ TomQueue::PRIORITIES.each do |priority|
44
+ it "should create a queue for '#{priority}' priority" do
45
+ manager.queues[priority].name.should == "#{manager.prefix}.balance.#{priority}"
46
+ # Declare the queue, if the parameters don't match the brokers existing channel, then bunny will throw an
47
+ # exception.
48
+ channel.queue("#{manager.prefix}.balance.#{priority}", :durable => true, :auto_delete => false, :exclusive => false)
49
+ end
50
+ end
51
+
52
+ it "should create a single durable topic exchange" do
53
+ manager.exchange.name.should == "#{manager.prefix}.work"
54
+ # Now we declare it again on the broker, which will raise an exception if the parameters don't match
55
+ channel.topic("#{manager.prefix}.work", :durable => true, :auto_delete => false)
56
+ end
57
+
58
+ end
59
+
60
+ describe "QueueManager message publishing" do
61
+
62
+ it "should forward the payload directly" do
63
+ manager.publish("foobar")
64
+ manager.pop.ack!.payload.should == "foobar"
65
+ end
66
+
67
+ it "should return nil" do
68
+ manager.publish("some work").should be_nil
69
+ end
70
+
71
+ it "should raise an exception if the payload isn't a string" do
72
+ lambda {
73
+ manager.publish({"some" => {"structured_data" => true}})
74
+ }.should raise_exception(ArgumentError, /must be a string/)
75
+ end
76
+
77
+ describe "deferred execution" do
78
+
79
+ it "should allow a run-at time to be specified" do
80
+ manager.publish("future", :run_at => Time.now + 2.2)
81
+ end
82
+
83
+ it "should throw an ArgumentError exception if :run_at isn't a Time object" do
84
+ lambda {
85
+ manager.publish("future", :run_at => "around 10pm ?")
86
+ }.should raise_exception(ArgumentError, /must be a Time object/)
87
+ end
88
+
89
+ it "should write the run_at time in the message headers as an ISO-8601 timestamp, with 4-digits of decimal precision" do
90
+ execution_time = Time.now - 1.0
91
+ manager.publish("future", :run_at => execution_time)
92
+ manager.pop.ack!.headers[:headers]['run_at'].should == execution_time.iso8601(4)
93
+ end
94
+
95
+ it "should default to :run_at the current time" do
96
+ manager.publish("future")
97
+ future_time = Time.now
98
+ Time.parse(manager.pop.ack!.headers[:headers]['run_at']).should < future_time
99
+ end
100
+ end
101
+
102
+ describe "message priorities" do
103
+ it "should have an array of priorities, in the correct order" do
104
+ TomQueue::PRIORITIES.should be_a(Array)
105
+ TomQueue::PRIORITIES.should == [
106
+ TomQueue::HIGH_PRIORITY,
107
+ TomQueue::NORMAL_PRIORITY,
108
+ TomQueue::LOW_PRIORITY,
109
+ TomQueue::BULK_PRIORITY
110
+ ]
111
+ end
112
+
113
+ it "should allow the message priority to be set" do
114
+ manager.publish("foobar", :priority => TomQueue::BULK_PRIORITY)
115
+ end
116
+
117
+ it "should throw an ArgumentError if an unknown priority value is used" do
118
+ lambda {
119
+ manager.publish("foobar", :priority => "VERY BLOODY IMPORTANT")
120
+ }.should raise_exception(ArgumentError, /unknown priority level/)
121
+ end
122
+
123
+ it "should write the priority in the message header as 'job_priority'" do
124
+ manager.publish("foobar", :priority => TomQueue::BULK_PRIORITY)
125
+ manager.pop.ack!.headers[:headers]['job_priority'].should == TomQueue::BULK_PRIORITY
126
+ end
127
+
128
+ it "should default to normal priority" do
129
+ manager.publish("foobar")
130
+ manager.pop.ack!.headers[:headers]['job_priority'].should == TomQueue::NORMAL_PRIORITY
131
+ end
132
+ end
133
+
134
+ TomQueue::PRIORITIES.each do |priority|
135
+ it "should publish #{priority} priority messages to the single exchange, with routing key set to '#{priority}'" do
136
+ manager.publish("foo", :priority => priority)
137
+ manager.pop.ack!.response.tap do |resp|
138
+ resp.exchange.should == "#{manager.prefix}.work"
139
+ resp.routing_key.should == priority
140
+ end
141
+ end
142
+ end
143
+
144
+ end
145
+
146
+
147
+ describe "QueueManager - deferred message handling" do
148
+
149
+ describe "when popping a message" do
150
+ it "should ensure a deferred manager with the same prefix is running" do
151
+ manager.publish("work")
152
+ TomQueue::DeferredWorkManager.instance(manager.prefix).should_receive(:ensure_running)
153
+ manager.pop
154
+ end
155
+ end
156
+
157
+ describe "when publishing a deferred message" do
158
+ it "should not publish to the normal AMQP queue" do
159
+ manager.publish("work", :run_at => Time.now + 0.1)
160
+ manager.queues.values.find { |q| channel.basic_get(q.name).first }.should be_nil
161
+ end
162
+ it "should call #handle_deferred on the appropriate deferred work manager" do
163
+ TomQueue::DeferredWorkManager.instance(manager.prefix).should_receive(:handle_deferred)
164
+ manager.publish("work", :run_at => Time.now + 0.1)
165
+ end
166
+ it "should pass the original payload" do
167
+ TomQueue::DeferredWorkManager.instance(manager.prefix).should_receive(:handle_deferred).with("work", anything)
168
+ manager.publish("work", :run_at => Time.now + 0.1)
169
+ end
170
+ it "should pass the original options" do
171
+ run_time = Time.now + 0.5
172
+ TomQueue::DeferredWorkManager.instance(manager.prefix).should_receive(:handle_deferred).with(anything, hash_including(:priority => TomQueue::NORMAL_PRIORITY, :run_at => run_time))
173
+ manager.publish("work", :run_at => run_time)
174
+ end
175
+ end
176
+
177
+ end
178
+
179
+ describe "QueueManager#pop - work popping" do
180
+ before do
181
+ manager.publish("foo")
182
+ manager.publish("bar")
183
+ end
184
+
185
+ it "should not have setup a consumer before the first call" do
186
+ manager.queues.values.each do |queue|
187
+ queue.status[:consumer_count].should == 0
188
+ end
189
+ end
190
+
191
+ it "should not leave any running consumers for immediate messages" do
192
+ manager.pop.ack!
193
+ manager.queues.values.each do |queue|
194
+ queue.status[:consumer_count].should == 0
195
+ end
196
+ end
197
+
198
+ it "should not leave any running consumers after it has waited for a message " do
199
+ manager.pop.ack!
200
+ manager.pop.ack!
201
+ Thread.new { sleep 0.1; manager.publish("baz") }
202
+ manager.pop.ack!
203
+ manager.queues.values.each do |queue|
204
+ queue.status[:consumer_count].should == 0
205
+ end
206
+ end
207
+
208
+ it "should return a QueueManager::Work instance" do
209
+ manager.pop.ack!.should be_a(TomQueue::Work)
210
+ end
211
+
212
+ it "should return the message at the head of the queue" do
213
+ manager.pop.ack!.payload.should == "foo"
214
+ manager.pop.ack!.payload.should == "bar"
215
+ end
216
+ end
217
+
218
+ end
@@ -0,0 +1,160 @@
1
+ require 'tom_queue/helper'
2
+
3
+ describe Range, 'tomqueue_binary_search' do
4
+
5
+ it "should return nil for an empty range" do
6
+ (0...0).tomqueue_binary_search.should be_nil
7
+ end
8
+
9
+ describe "for a single item range" do
10
+ let(:range) { 5...6 }
11
+
12
+ it "should yield the index" do
13
+ range.tomqueue_binary_search do |index|
14
+ @index = index
15
+ end
16
+ @index.should == 5
17
+ end
18
+
19
+ it "should return 0 if the yield returned -1" do
20
+ range.tomqueue_binary_search { |index| -1 }.should == 5
21
+ end
22
+
23
+ it "should return 1 if the yield returned +1" do
24
+ range.tomqueue_binary_search { |index| +1 }.should == 6
25
+ end
26
+
27
+ it "should return 0 if the yield returned 0" do
28
+ range.tomqueue_binary_search { |index| 0 }.should == 5
29
+ end
30
+ end
31
+
32
+ describe "for two item range" do
33
+ let(:range) { 7..8 }
34
+
35
+ it "should yield the lower number" do
36
+ range.tomqueue_binary_search do |index|
37
+ @index = index
38
+ 0
39
+ end
40
+ @index.should == 7
41
+ end
42
+
43
+ it "should return the lower number if the block returns -1" do
44
+ range.tomqueue_binary_search { |i| -1 }.should == 7
45
+ end
46
+ it "should return the lower number if the block returns 0" do
47
+ range.tomqueue_binary_search { |i| 0 }.should == 7
48
+ end
49
+
50
+ it "should yield the second number if the block returns +1" do
51
+ range.tomqueue_binary_search do |i|
52
+ if i == 7
53
+ 1
54
+ elsif i == 8
55
+ @yielded = true
56
+ 0
57
+ end
58
+ end
59
+ @yielded.should be_true
60
+ end
61
+ end
62
+
63
+ describe "for a three item range" do
64
+ let(:range) { 7..9 }
65
+
66
+ it "should yield the mid-point" do
67
+ range.tomqueue_binary_search do |index|
68
+ @index = index
69
+ 0
70
+ end
71
+ @index.should == 8
72
+ end
73
+
74
+ it "should return the mid-point if the block returns 0" do
75
+ range.tomqueue_binary_search { |index| 0 }.should == 8
76
+ end
77
+
78
+ it "should recurse to the right on +1" do
79
+ @yielded = []
80
+ range.tomqueue_binary_search { |index| @yielded << index; 1 }.should == 10
81
+ @yielded.should == [8,9]
82
+ end
83
+
84
+ it "should recurse to the left on -1" do
85
+ @yielded = []
86
+ range.tomqueue_binary_search { |index| @yielded << index; -1 }.should == 7
87
+ @yielded.should == [8,7]
88
+ end
89
+
90
+ end
91
+
92
+ describe "acceptance 1" do
93
+ let(:range) { 0...100 }
94
+ let(:value) { 43 }
95
+
96
+ before do
97
+ @yielded = []
98
+ @result = range.tomqueue_binary_search { |i| @yielded << i; value <=> i }
99
+ end
100
+
101
+ it "should get the correct result" do
102
+ @result.should == value
103
+ end
104
+
105
+ it "should yield the correct values" do
106
+ @yielded.should == [49, 24, 36, 42, 45, 43]
107
+ end
108
+ end
109
+
110
+ describe "acceptance 2" do
111
+ let(:range) { 0..3 }
112
+ let(:value) { 3 }
113
+
114
+ before do
115
+ @yielded = []
116
+ @result = range.tomqueue_binary_search { |i| @yielded << i; value <=> i }
117
+ end
118
+
119
+ it "should get the correct result" do
120
+ @result.should == value
121
+ end
122
+
123
+ it "should yield the correct values" do
124
+ @yielded.should == [1,2,3]
125
+ end
126
+ end
127
+
128
+
129
+ end
130
+
131
+
132
+
133
+ describe TomQueue::SortedArray do
134
+
135
+ let(:array) { TomQueue::SortedArray.new }
136
+
137
+ it "should insert in sorted order" do
138
+ array << 4
139
+ array << 5
140
+ array << 2
141
+ array << 1
142
+ array << 3
143
+ array.should == [1,2,3,4,5]
144
+ end
145
+
146
+ it "should work for all permutations of insertion" do
147
+ numbers = [0,1,2,3,4,5,6]
148
+ numbers.permutation.each do |permutation|
149
+ array = TomQueue::SortedArray.new
150
+ permutation.each do |i|
151
+ array << i
152
+ end
153
+ array.should == numbers
154
+ end
155
+ end
156
+
157
+ it "should return itself when inserting" do
158
+ (array << 3).should == array
159
+ end
160
+ end
@@ -0,0 +1,296 @@
1
+
2
+ require 'net/http'
3
+ require 'tom_queue/helper'
4
+
5
+ describe TomQueue::QueueManager, "simple publish / pop" do
6
+
7
+ let(:manager) { TomQueue::QueueManager.new("test-#{Time.now.to_f}", 'manager') }
8
+ let(:consumer) { TomQueue::QueueManager.new(manager.prefix, 'consumer1') }
9
+ let(:consumer2) { TomQueue::QueueManager.new(manager.prefix, 'consumer2') }
10
+
11
+ it "should pop a previously published message" do
12
+ manager.publish('some work')
13
+ manager.pop.payload.should == 'some work'
14
+ end
15
+
16
+ it "should block on #pop until work is published" do
17
+ manager
18
+
19
+ Thread.new do
20
+ sleep 0.1
21
+ manager.publish('some work')
22
+ end
23
+
24
+ consumer.pop.payload.should == 'some work'
25
+ end
26
+
27
+ it "should work between objects (hello, rabbitmq)" do
28
+ manager.publish "work"
29
+ consumer.pop.payload.should == "work"
30
+ end
31
+
32
+ it "should load-balance work between multiple consumers" do
33
+ manager.publish "foo"
34
+ manager.publish "bar"
35
+
36
+ consumer.pop.payload.should == "foo"
37
+ consumer2.pop.payload.should == "bar"
38
+ end
39
+
40
+ it "should work for more than one message!" do
41
+ input, output = [], []
42
+ (0..9).each do |i|
43
+ input << i.to_s
44
+ manager.publish i.to_s
45
+ end
46
+
47
+ (input.size / 2).times do
48
+ a = consumer.pop
49
+ b = consumer2.pop
50
+ output << a.ack!.payload
51
+ output << b.ack!.payload
52
+ end
53
+ output.should == input
54
+ end
55
+
56
+ it "should not drop messages when two different priorities arrive" do
57
+ manager.publish("1", :priority => TomQueue::BULK_PRIORITY)
58
+ manager.publish("2", :priority => TomQueue::NORMAL_PRIORITY)
59
+ manager.publish("3", :priority => TomQueue::HIGH_PRIORITY)
60
+ out = []
61
+ out << consumer.pop.ack!.payload
62
+ out << consumer.pop.ack!.payload
63
+ out << consumer.pop.ack!.payload
64
+ out.sort.should == ["1", "2", "3"]
65
+ end
66
+
67
+ it "should handle priority queueing, maintaining per-priority FIFO ordering" do
68
+ manager.publish("1", :priority => TomQueue::BULK_PRIORITY)
69
+ manager.publish("2", :priority => TomQueue::NORMAL_PRIORITY)
70
+ manager.publish("3", :priority => TomQueue::HIGH_PRIORITY)
71
+
72
+ # 1,2,3 in the queue - 3 wins as it's highest priority
73
+ consumer.pop.ack!.payload.should == "3"
74
+
75
+ manager.publish("4", :priority => TomQueue::NORMAL_PRIORITY)
76
+
77
+ # 1,2,4 in the queue - 2 wins as it's highest (NORMAL) and first in
78
+ consumer.pop.ack!.payload.should == "2"
79
+
80
+ manager.publish("5", :priority => TomQueue::BULK_PRIORITY)
81
+
82
+ # 1,4,5 in the queue - we'd expect 4 (highest), 1 (first bulk), 5 (second bulk)
83
+ consumer.pop.ack!.payload.should == "4"
84
+ consumer.pop.ack!.payload.should == "1"
85
+ consumer.pop.ack!.payload.should == "5"
86
+ end
87
+
88
+ it "should handle priority queueing across two consumers" do
89
+ manager.publish("1", :priority => TomQueue::BULK_PRIORITY)
90
+ manager.publish("2", :priority => TomQueue::HIGH_PRIORITY)
91
+ manager.publish("3", :priority => TomQueue::NORMAL_PRIORITY)
92
+
93
+
94
+ # 1,2,3 in the queue - 3 wins as it's highest priority
95
+ order = []
96
+ order << consumer.pop.ack!.payload
97
+
98
+ manager.publish("4", :priority => TomQueue::NORMAL_PRIORITY)
99
+
100
+ # 1,2,4 in the queue - 2 wins as it's highest (NORMAL) and first in
101
+ order << consumer.pop.ack!.payload
102
+
103
+ manager.publish("5", :priority => TomQueue::BULK_PRIORITY)
104
+
105
+ # 1,4,5 in the queue - we'd expect 4 (highest), 1 (first bulk), 5 (second bulk)
106
+ order << consumer.pop.ack!.payload
107
+ order << consumer2.pop.ack!.payload
108
+ order << consumer.pop.ack!.payload
109
+
110
+ order.should == ["2","3","4","1","5"]
111
+ end
112
+
113
+ it "should immediately run a high priority task, when there are lots of bulks" do
114
+ 100.times do |i|
115
+ manager.publish("stuff #{i}", :priority => TomQueue::BULK_PRIORITY)
116
+ end
117
+
118
+ consumer.pop.ack!.payload.should == "stuff 0"
119
+ consumer2.pop.ack!.payload.should == "stuff 1"
120
+ consumer.pop.payload.should == "stuff 2"
121
+
122
+ manager.publish("HIGH1", :priority => TomQueue::HIGH_PRIORITY)
123
+ manager.publish("NORMAL1", :priority => TomQueue::NORMAL_PRIORITY)
124
+ manager.publish("HIGH2", :priority => TomQueue::HIGH_PRIORITY)
125
+ manager.publish("NORMAL2", :priority => TomQueue::NORMAL_PRIORITY)
126
+
127
+ consumer.pop.ack!.payload.should == "HIGH1"
128
+ consumer.pop.ack!.payload.should == "HIGH2"
129
+ consumer2.pop.ack!.payload.should == "NORMAL1"
130
+ consumer.pop.ack!.payload.should == "NORMAL2"
131
+
132
+ consumer2.pop.ack!.payload.should == "stuff 3"
133
+ end
134
+
135
+ it "should allow a message to be deferred for future execution" do
136
+ execution_time = Time.now + 0.2
137
+ manager.publish("future-work", :run_at => execution_time )
138
+
139
+ consumer.pop.ack!
140
+ Time.now.to_f.should > execution_time.to_f
141
+ end
142
+
143
+ describe "slow tests", :timeout => 100 do
144
+
145
+ class QueueConsumerThread
146
+
147
+ class WorkObject < Struct.new(:payload, :received_at, :run_at)
148
+ def <=>(other)
149
+ self.received_at.to_f <=> other.received_at.to_f
150
+ end
151
+ end
152
+
153
+ attr_reader :work, :thread
154
+
155
+ def initialize(manager, &work_proc)
156
+ @manager = manager
157
+ @work_proc = work_proc
158
+ @work = []
159
+ end
160
+
161
+ def thread_main
162
+ loop do
163
+ begin
164
+ work = @manager.pop
165
+ recv_time = Time.now
166
+
167
+ Thread.exit if work.payload == "done"
168
+
169
+ work_obj = WorkObject.new(work.payload, recv_time, Time.parse(work.headers[:headers]['run_at']))
170
+ @work << work_obj
171
+ @work_proc && @work_proc.call(work_obj)
172
+
173
+ work.ack!
174
+ rescue
175
+ p $!
176
+ end
177
+ end
178
+
179
+ end
180
+
181
+ def signal_shutdown(time=nil)
182
+ time ||= Time.now
183
+ @manager.publish("done", :run_at => time)
184
+ end
185
+ def start!
186
+ @thread ||= Thread.new(&method(:thread_main))
187
+ self
188
+ end
189
+ end
190
+
191
+
192
+ it "should work with lots of messages, without dropping and deliver FIFO" do
193
+ source_order = []
194
+
195
+ # Run both consumers, in parallel threads, so in some cases,
196
+ # there should be a thread waiting for work
197
+ consumers = 16.times.collect do |i|
198
+ consumer = TomQueue::QueueManager.new(manager.prefix, "thread-#{i}")
199
+ QueueConsumerThread.new(consumer) { |work| sleep rand(0.5) }.start!
200
+ end
201
+
202
+ # Now publish some work
203
+ 250.times do |i|
204
+ work = "work #{i}"
205
+ source_order << work
206
+ manager.publish(work)
207
+ end
208
+
209
+ # Now publish a bunch of messages to cause the threads to exit the loop
210
+ consumers.each { |c| c.signal_shutdown }
211
+ consumers.each { |c| c.thread.join }
212
+
213
+ # Merge the list of received work together sorted by received time,
214
+ # and compare to the source list
215
+ sink_order = consumers.map { |c| c.work }.flatten.sort.map { |a| a.payload }
216
+
217
+ source_order.should == sink_order
218
+ end
219
+
220
+ it "should be able to drain the queue, block and resume when new work arrives" do
221
+ source_order = []
222
+
223
+ # Run both consumers, in parallel threads, so in some cases,
224
+ # there should be a thread waiting for work
225
+ consumers = 10.times.collect do |i|
226
+ consumer = TomQueue::QueueManager.new(manager.prefix, "thread-#{i}")
227
+ QueueConsumerThread.new(consumer) { |work| sleep rand(0.5) }.start!
228
+ end
229
+
230
+ # This sleep gives the workers enough time to block on the first call to pop
231
+ sleep 0.1 until manager.queues[TomQueue::NORMAL_PRIORITY].status[:consumer_count] == consumers.size
232
+
233
+ # Now publish some work
234
+ 50.times do |i|
235
+ work = "work #{i}"
236
+ source_order << work
237
+ manager.publish(work)
238
+ end
239
+
240
+ # Rough and ready - wait for the queue to empty
241
+ sleep 0.1 until manager.queues[TomQueue::NORMAL_PRIORITY].status[:message_count] == 0
242
+
243
+ # Now publish some more work
244
+ 50.times do |i|
245
+ work = "work 2-#{i}"
246
+ source_order << work
247
+ manager.publish(work)
248
+ end
249
+
250
+ # Now publish a bunch of messages to cause the threads to exit the loop
251
+ consumers.each { |c| c.signal_shutdown }
252
+ consumers.each { |c| c.thread.join }
253
+
254
+ # Now merge all the consumers internal work arrays into one
255
+ # sorted by the received_at timestamps
256
+ sink_order = consumers.map { |c| c.work }.flatten.sort.map { |a| a.payload }
257
+
258
+ # Compare what the publisher did to what the workers did.
259
+ sink_order.should == source_order
260
+ end
261
+
262
+ it "should work with lots of deferred work on the queue, and still schedule all messages" do
263
+ # sit in a loop to pop it all off again
264
+ consumers = 5.times.collect do |i|
265
+ consumer = TomQueue::QueueManager.new(manager.prefix, "thread-#{i}")
266
+ QueueConsumerThread.new(consumer).start!
267
+ end
268
+
269
+ # Generate some work
270
+ max_run_at = Time.now
271
+ 200.times do |i|
272
+ run_at = Time.now + (rand * 6.0)
273
+ max_run_at = [max_run_at, run_at].max
274
+ manager.publish(JSON.dump(:id => i), :run_at => run_at)
275
+ end
276
+
277
+ # Shutdown the consumers again
278
+ consumers.each do |c|
279
+ c.signal_shutdown(max_run_at + 1.0)
280
+ end
281
+ consumers.each { |c| c.thread.join }
282
+
283
+ # Now make sure none of the messages were delivered too late!
284
+ total_size = 0
285
+ consumers.each do |c|
286
+ total_size += c.work.size
287
+ c.work.each do |work|
288
+ work.received_at.should < (work.run_at + 1.0)
289
+ end
290
+ end
291
+
292
+ total_size.should == 200
293
+ end
294
+ end
295
+
296
+ end