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.
- data/lib/tom_queue.rb +56 -0
- data/lib/tom_queue/deferred_work_manager.rb +233 -0
- data/lib/tom_queue/deferred_work_set.rb +165 -0
- data/lib/tom_queue/delayed_job.rb +33 -0
- data/lib/tom_queue/delayed_job/external_messages.rb +56 -0
- data/lib/tom_queue/delayed_job/job.rb +365 -0
- data/lib/tom_queue/external_consumer.rb +136 -0
- data/lib/tom_queue/logging_helper.rb +19 -0
- data/lib/tom_queue/queue_manager.rb +264 -0
- data/lib/tom_queue/sorted_array.rb +69 -0
- data/lib/tom_queue/work.rb +62 -0
- data/spec/database.yml +14 -0
- data/spec/helper.rb +75 -0
- data/spec/tom_queue/deferred_work/deferred_work_manager_integration_spec.rb +186 -0
- data/spec/tom_queue/deferred_work/deferred_work_manager_spec.rb +134 -0
- data/spec/tom_queue/deferred_work/deferred_work_set_spec.rb +134 -0
- data/spec/tom_queue/delayed_job/delayed_job_integration_spec.rb +155 -0
- data/spec/tom_queue/delayed_job/delayed_job_spec.rb +818 -0
- data/spec/tom_queue/external_consumer_integration_spec.rb +225 -0
- data/spec/tom_queue/helper.rb +91 -0
- data/spec/tom_queue/logging_helper_spec.rb +152 -0
- data/spec/tom_queue/queue_manager_spec.rb +218 -0
- data/spec/tom_queue/sorted_array_spec.rb +160 -0
- data/spec/tom_queue/tom_queue_integration_spec.rb +296 -0
- data/spec/tom_queue/tom_queue_spec.rb +30 -0
- data/spec/tom_queue/work_spec.rb +35 -0
- data/tom_queue.gemspec +21 -0
- metadata +137 -0
@@ -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
|