chore-core 1.5.2
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.
- checksums.yaml +15 -0
- data/LICENSE.txt +20 -0
- data/README.md +260 -0
- data/Rakefile +32 -0
- data/bin/chore +34 -0
- data/chore-core.gemspec +46 -0
- data/lib/chore/cli.rb +232 -0
- data/lib/chore/configuration.rb +13 -0
- data/lib/chore/consumer.rb +52 -0
- data/lib/chore/duplicate_detector.rb +56 -0
- data/lib/chore/fetcher.rb +31 -0
- data/lib/chore/hooks.rb +25 -0
- data/lib/chore/job.rb +103 -0
- data/lib/chore/json_encoder.rb +18 -0
- data/lib/chore/manager.rb +47 -0
- data/lib/chore/publisher.rb +29 -0
- data/lib/chore/queues/filesystem/consumer.rb +128 -0
- data/lib/chore/queues/filesystem/filesystem_queue.rb +49 -0
- data/lib/chore/queues/filesystem/publisher.rb +45 -0
- data/lib/chore/queues/sqs/consumer.rb +121 -0
- data/lib/chore/queues/sqs/publisher.rb +55 -0
- data/lib/chore/queues/sqs.rb +38 -0
- data/lib/chore/railtie.rb +18 -0
- data/lib/chore/signal.rb +175 -0
- data/lib/chore/strategies/consumer/batcher.rb +76 -0
- data/lib/chore/strategies/consumer/single_consumer_strategy.rb +34 -0
- data/lib/chore/strategies/consumer/threaded_consumer_strategy.rb +81 -0
- data/lib/chore/strategies/worker/forked_worker_strategy.rb +221 -0
- data/lib/chore/strategies/worker/single_worker_strategy.rb +39 -0
- data/lib/chore/tasks/queues.task +11 -0
- data/lib/chore/unit_of_work.rb +17 -0
- data/lib/chore/util.rb +18 -0
- data/lib/chore/version.rb +9 -0
- data/lib/chore/worker.rb +117 -0
- data/lib/chore-core.rb +1 -0
- data/lib/chore.rb +218 -0
- data/spec/chore/cli_spec.rb +182 -0
- data/spec/chore/consumer_spec.rb +36 -0
- data/spec/chore/duplicate_detector_spec.rb +62 -0
- data/spec/chore/fetcher_spec.rb +38 -0
- data/spec/chore/hooks_spec.rb +44 -0
- data/spec/chore/job_spec.rb +80 -0
- data/spec/chore/json_encoder_spec.rb +11 -0
- data/spec/chore/manager_spec.rb +39 -0
- data/spec/chore/queues/filesystem/filesystem_consumer_spec.rb +71 -0
- data/spec/chore/queues/sqs/consumer_spec.rb +136 -0
- data/spec/chore/queues/sqs/publisher_spec.rb +74 -0
- data/spec/chore/queues/sqs_spec.rb +37 -0
- data/spec/chore/signal_spec.rb +244 -0
- data/spec/chore/strategies/consumer/batcher_spec.rb +93 -0
- data/spec/chore/strategies/consumer/single_consumer_strategy_spec.rb +23 -0
- data/spec/chore/strategies/consumer/threaded_consumer_strategy_spec.rb +105 -0
- data/spec/chore/strategies/worker/forked_worker_strategy_spec.rb +281 -0
- data/spec/chore/strategies/worker/single_worker_strategy_spec.rb +36 -0
- data/spec/chore/worker_spec.rb +134 -0
- data/spec/chore_spec.rb +108 -0
- data/spec/spec_helper.rb +58 -0
- data/spec/test_job.rb +7 -0
- metadata +194 -0
@@ -0,0 +1,136 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Chore::Queues::SQS::Consumer do
|
4
|
+
let(:queue_name) { "test" }
|
5
|
+
let(:queue_url) { "test_url" }
|
6
|
+
let(:queues) { double("queues") }
|
7
|
+
let(:queue) { double("test_queue") }
|
8
|
+
let(:options) { {} }
|
9
|
+
let(:consumer) { Chore::Queues::SQS::Consumer.new(queue_name) }
|
10
|
+
let(:message) { TestMessage.new("handle",queue_name,"message body", 1) }
|
11
|
+
let(:pool) { double("pool") }
|
12
|
+
let(:sqs) { double('AWS::SQS') }
|
13
|
+
|
14
|
+
before do
|
15
|
+
AWS::SQS.stub(:new).and_return(sqs)
|
16
|
+
sqs.stub(:queues).and_return { queues }
|
17
|
+
|
18
|
+
queues.stub(:url_for) { queue_url }
|
19
|
+
queues.stub(:[]) { queue }
|
20
|
+
queue.stub(:receive_message) { message }
|
21
|
+
queue.stub(:visibility_timeout) { 10 }
|
22
|
+
pool.stub(:empty!) { nil }
|
23
|
+
end
|
24
|
+
|
25
|
+
describe "consuming messages" do
|
26
|
+
let!(:consumer_run_for_one_message) { consumer.stub(:running?).and_return(true, false) }
|
27
|
+
let!(:messages_be_unique) { Chore::DuplicateDetector.any_instance.stub(:found_duplicate?).and_return(false) }
|
28
|
+
let!(:queue_contain_messages) { queue.stub(:receive_messages).and_return(message) }
|
29
|
+
|
30
|
+
it 'should configure sqs' do
|
31
|
+
Chore.config.stub(:aws_access_key).and_return('key')
|
32
|
+
Chore.config.stub(:aws_secret_key).and_return('secret')
|
33
|
+
|
34
|
+
AWS::SQS.should_receive(:new).with(
|
35
|
+
:access_key_id => 'key',
|
36
|
+
:secret_access_key => 'secret',
|
37
|
+
:logger => Chore.logger,
|
38
|
+
:log_level => :debug
|
39
|
+
).and_return(sqs)
|
40
|
+
consumer.consume
|
41
|
+
end
|
42
|
+
|
43
|
+
it 'should not configure sqs multiple times' do
|
44
|
+
consumer.stub(:running?).and_return(true, true, false)
|
45
|
+
|
46
|
+
AWS::SQS.should_receive(:new).once.and_return(sqs)
|
47
|
+
consumer.consume
|
48
|
+
end
|
49
|
+
|
50
|
+
it 'should look up the queue url based on the queue name' do
|
51
|
+
queues.should_receive(:url_for).with('test').and_return(queue_url)
|
52
|
+
consumer.consume
|
53
|
+
end
|
54
|
+
|
55
|
+
it 'should look up the queue based on the queue url' do
|
56
|
+
queues.should_receive(:[]).with(queue_url).and_return(queue)
|
57
|
+
consumer.consume
|
58
|
+
end
|
59
|
+
|
60
|
+
context "should receive a message from the queue" do
|
61
|
+
|
62
|
+
it 'should use the default size of 10 when no queue_polling_size is specified' do
|
63
|
+
queue.should_receive(:receive_messages).with(:limit => 10, :attributes => [:receive_count])
|
64
|
+
consumer.consume
|
65
|
+
end
|
66
|
+
|
67
|
+
it 'should respect the queue_polling_size when specified' do
|
68
|
+
Chore.config.stub(:queue_polling_size).and_return(5)
|
69
|
+
queue.should_receive(:receive_messages).with(:limit => 5, :attributes => [:receive_count])
|
70
|
+
consumer.consume
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
it "should check the uniqueness of the message" do
|
75
|
+
Chore::DuplicateDetector.any_instance.should_receive(:found_duplicate?).with(message).and_return(false)
|
76
|
+
consumer.consume
|
77
|
+
end
|
78
|
+
|
79
|
+
it "should yield the message to the handler block" do
|
80
|
+
expect { |b| consumer.consume(&b) }.to yield_with_args('handle', queue_name, 10, 'message body', 0)
|
81
|
+
end
|
82
|
+
|
83
|
+
it 'should not yield for a dupe message' do
|
84
|
+
Chore::DuplicateDetector.any_instance.should_receive(:found_duplicate?).with(message).and_return(true)
|
85
|
+
expect {|b| consumer.consume(&b) }.not_to yield_control
|
86
|
+
end
|
87
|
+
|
88
|
+
context 'with no messages' do
|
89
|
+
let!(:consumer_run_for_one_message) { consumer.stub(:running?).and_return(true, true, false) }
|
90
|
+
let!(:queue_contain_messages) { queue.stub(:receive_messages).and_return(message, nil) }
|
91
|
+
|
92
|
+
it 'should sleep' do
|
93
|
+
consumer.should_receive(:sleep).with(1)
|
94
|
+
consumer.consume
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
context 'with messages' do
|
99
|
+
let!(:consumer_run_for_one_message) { consumer.stub(:running?).and_return(true, true, false) }
|
100
|
+
let!(:queue_contain_messages) { queue.stub(:receive_messages).and_return(message, message) }
|
101
|
+
|
102
|
+
it 'should not sleep' do
|
103
|
+
consumer.should_not_receive(:sleep)
|
104
|
+
consumer.consume
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
describe '#reset_connection!' do
|
110
|
+
it 'should reset the connection after a call to reset_connection!' do
|
111
|
+
AWS::Core::Http::ConnectionPool.stub(:pools).and_return([pool])
|
112
|
+
pool.should_receive(:empty!)
|
113
|
+
Chore::Queues::SQS::Consumer.reset_connection!
|
114
|
+
consumer.send(:queue)
|
115
|
+
end
|
116
|
+
|
117
|
+
it 'should not reset the connection between calls' do
|
118
|
+
sqs = consumer.send(:queue)
|
119
|
+
sqs.should be consumer.send(:queue)
|
120
|
+
end
|
121
|
+
|
122
|
+
it 'should reconfigure sqs' do
|
123
|
+
consumer.stub(:running?).and_return(true, false)
|
124
|
+
Chore::DuplicateDetector.any_instance.stub(:found_duplicate?).and_return(false)
|
125
|
+
|
126
|
+
queue.stub(:receive_messages).and_return(message)
|
127
|
+
consumer.consume
|
128
|
+
|
129
|
+
Chore::Queues::SQS::Consumer.reset_connection!
|
130
|
+
AWS::SQS.should_receive(:new).and_return(sqs)
|
131
|
+
|
132
|
+
consumer.stub(:running?).and_return(true, false)
|
133
|
+
consumer.consume
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module Chore
|
4
|
+
describe Queues::SQS::Publisher do
|
5
|
+
let(:job) { {'class' => 'TestJob', 'args'=>[1,2,'3']}}
|
6
|
+
let(:queue_name) { 'test_queue' }
|
7
|
+
let(:queue_url) {"http://www.queue_url.com/test_queue"}
|
8
|
+
let(:queue) { double('queue', :send_message => nil) }
|
9
|
+
let(:sqs) do
|
10
|
+
double('sqs', :queues => double('queues', :named => queue, :url_for => queue_url, :[] => queue))
|
11
|
+
end
|
12
|
+
let(:publisher) { Queues::SQS::Publisher.new }
|
13
|
+
let(:pool) { double("pool") }
|
14
|
+
|
15
|
+
before(:each) do
|
16
|
+
AWS::SQS.stub(:new).and_return(sqs)
|
17
|
+
end
|
18
|
+
|
19
|
+
it 'should configure sqs' do
|
20
|
+
Chore.config.stub(:aws_access_key).and_return('key')
|
21
|
+
Chore.config.stub(:aws_secret_key).and_return('secret')
|
22
|
+
|
23
|
+
AWS::SQS.should_receive(:new).with(
|
24
|
+
:access_key_id => 'key',
|
25
|
+
:secret_access_key => 'secret',
|
26
|
+
:logger => Chore.logger,
|
27
|
+
:log_level => :debug
|
28
|
+
)
|
29
|
+
publisher.publish(queue_name,job)
|
30
|
+
end
|
31
|
+
|
32
|
+
it 'should create send an encoded message to the specified queue' do
|
33
|
+
queue.should_receive(:send_message).with(job.to_json)
|
34
|
+
publisher.publish(queue_name,job)
|
35
|
+
end
|
36
|
+
|
37
|
+
it 'should lookup the queue when publishing' do
|
38
|
+
sqs.queues.should_receive(:url_for).with('test_queue')
|
39
|
+
publisher.publish('test_queue', job)
|
40
|
+
end
|
41
|
+
|
42
|
+
it 'should lookup multiple queues if specified' do
|
43
|
+
sqs.queues.should_receive(:url_for).with('test_queue')
|
44
|
+
sqs.queues.should_receive(:url_for).with('test_queue2')
|
45
|
+
publisher.publish('test_queue', job)
|
46
|
+
publisher.publish('test_queue2', job)
|
47
|
+
end
|
48
|
+
|
49
|
+
it 'should only lookup a named queue once' do
|
50
|
+
sqs.queues.should_receive(:url_for).with('test_queue').once
|
51
|
+
2.times { publisher.publish('test_queue', job) }
|
52
|
+
end
|
53
|
+
|
54
|
+
describe '#reset_connection!' do
|
55
|
+
it 'should reset the connection after a call to reset_connection!' do
|
56
|
+
AWS::Core::Http::ConnectionPool.stub(:pools).and_return([pool])
|
57
|
+
pool.should_receive(:empty!)
|
58
|
+
Chore::Queues::SQS::Publisher.reset_connection!
|
59
|
+
publisher.queue(queue_name)
|
60
|
+
end
|
61
|
+
|
62
|
+
it 'should not reset the connection between calls' do
|
63
|
+
sqs = publisher.queue(queue_name)
|
64
|
+
sqs.should be publisher.queue(queue_name)
|
65
|
+
end
|
66
|
+
|
67
|
+
it 'should reconfigure sqs' do
|
68
|
+
Chore::Queues::SQS::Publisher.reset_connection!
|
69
|
+
AWS::SQS.should_receive(:new)
|
70
|
+
publisher.queue(queue_name)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module Chore
|
2
|
+
describe Queues::SQS do
|
3
|
+
context "when managing queues" do
|
4
|
+
let(:fake_sqs) {double(Object)}
|
5
|
+
let(:fake_queue_collection) {double(Object)}
|
6
|
+
let(:queue_name) {"test"}
|
7
|
+
let(:queue_url) {"http://amazon.sqs.url/queues/#{queue_name}"}
|
8
|
+
let(:fake_queue) {double(Object)}
|
9
|
+
|
10
|
+
before(:each) do
|
11
|
+
AWS::SQS.stub(:new).and_return(fake_sqs)
|
12
|
+
Chore.stub(:prefixed_queue_names) {[queue_name]}
|
13
|
+
fake_queue.stub(:delete)
|
14
|
+
|
15
|
+
fake_queue_collection.stub(:[]) do |key|
|
16
|
+
fake_queue
|
17
|
+
end
|
18
|
+
|
19
|
+
fake_queue_collection.stub(:create)
|
20
|
+
fake_sqs.stub(:queues).and_return(fake_queue_collection)
|
21
|
+
fake_queue_collection.stub(:url_for).with(queue_name).and_return(queue_url)
|
22
|
+
end
|
23
|
+
|
24
|
+
it 'should create queues that are defined in its internal job name list' do
|
25
|
+
#Only one job defined in the spec suite
|
26
|
+
fake_queue_collection.should_receive(:create)
|
27
|
+
Chore::Queues::SQS.create_queues!
|
28
|
+
end
|
29
|
+
|
30
|
+
it 'should delete queues that are defined in its internal job name list' do
|
31
|
+
#Only one job defined in the spec suite
|
32
|
+
fake_queue.should_receive(:delete)
|
33
|
+
Chore::Queues::SQS.delete_queues!
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,244 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
|
2
|
+
|
3
|
+
describe Chore::Signal do
|
4
|
+
let(:signal_handlers) {{}}
|
5
|
+
before :each do
|
6
|
+
::Signal.stub(:trap) do |signal, command=nil, &block|
|
7
|
+
if command
|
8
|
+
signal_handlers.delete(signal)
|
9
|
+
else
|
10
|
+
signal_handlers[signal] = block
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
Process.stub(:kill) do |signal, process_id|
|
15
|
+
signal_handlers[signal].call if signal_handlers[signal]
|
16
|
+
sleep 0.1
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
after(:each) do
|
21
|
+
described_class.reset
|
22
|
+
end
|
23
|
+
|
24
|
+
describe 'trap' do
|
25
|
+
context 'without any handlers' do
|
26
|
+
it 'should not be intercepted by Chore::Signal' do
|
27
|
+
described_class.should_not_receive(:process).with("SIG1")
|
28
|
+
lambda { Process.kill('SIG1', Process.pid) }
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
context 'with a command' do
|
33
|
+
before(:each) do
|
34
|
+
described_class.trap('SIG1', 'DEFAULT')
|
35
|
+
end
|
36
|
+
|
37
|
+
it 'should result in Rubys default behavior for the signal' do
|
38
|
+
lambda { Process.kill('SIG1', Process.pid) }.should_not raise_error(SignalException)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
context 'with a command and handler' do
|
43
|
+
before(:each) do
|
44
|
+
@callbacks = []
|
45
|
+
described_class.trap('SIG1', 'DEFAULT') do
|
46
|
+
@callbacks << :usr2
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
it 'should result in Rubys default behavior for the signal' do
|
51
|
+
lambda { Process.kill('SIG1', Process.pid) }.should_not raise_error(SignalException)
|
52
|
+
end
|
53
|
+
|
54
|
+
it 'should not call the handler' do
|
55
|
+
begin
|
56
|
+
Process.kill('SIG1', Process.pid)
|
57
|
+
rescue SignalException => e
|
58
|
+
|
59
|
+
end
|
60
|
+
@callbacks.should match_array([])
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
context 'with a single handler' do
|
65
|
+
before(:each) do
|
66
|
+
@callbacks = []
|
67
|
+
end
|
68
|
+
|
69
|
+
context 'without exceptions' do
|
70
|
+
before(:each) do
|
71
|
+
described_class.trap('SIG1') do
|
72
|
+
@callbacks << :sig1
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
it 'should not call the handler if signal does not match' do
|
77
|
+
lambda { Process.kill('SIG2', Process.pid) }.should_not raise_error(SignalException)
|
78
|
+
@callbacks.should match_array([])
|
79
|
+
end
|
80
|
+
|
81
|
+
it 'should call the handler if the signal matches' do
|
82
|
+
Process.kill('SIG1', Process.pid)
|
83
|
+
@callbacks.should match_array([:sig1])
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
context 'with exceptions' do
|
88
|
+
before(:each) do
|
89
|
+
@count = 0
|
90
|
+
@callbacks = []
|
91
|
+
described_class.trap('SIG1') do
|
92
|
+
@count += 1
|
93
|
+
raise ArgumentError if @count == 1
|
94
|
+
@callbacks << :sig1
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
it 'should not retry the callback' do
|
99
|
+
Process.kill('SIG1', Process.pid)
|
100
|
+
@callbacks.should match_array([])
|
101
|
+
end
|
102
|
+
|
103
|
+
it 'should still continue processing' do
|
104
|
+
2.times { Process.kill('SIG1', Process.pid) }
|
105
|
+
@callbacks.should match_array([:sig1])
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
context 'with multiple handlers' do
|
111
|
+
before(:each) do
|
112
|
+
@callbacks = []
|
113
|
+
described_class.trap('SIG1') do
|
114
|
+
@callbacks << :first
|
115
|
+
end
|
116
|
+
described_class.trap('SIG1') do
|
117
|
+
@callbacks << :second
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
it 'should only call the last recorded handler' do
|
122
|
+
Process.kill('SIG1', Process.pid)
|
123
|
+
@callbacks.should match_array([:second])
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
context 'with reset handler' do
|
128
|
+
before(:each) do
|
129
|
+
@callbacks = []
|
130
|
+
described_class.trap('SIG1') do
|
131
|
+
@callbacks << :first
|
132
|
+
end
|
133
|
+
described_class.trap('SIG1', 'DEFAULT')
|
134
|
+
end
|
135
|
+
|
136
|
+
it 'should not call the original handler' do
|
137
|
+
Process.kill('SIG1', Process.pid)
|
138
|
+
@callbacks.should match_array([])
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
context 'with multiple signals' do
|
143
|
+
before(:each) do
|
144
|
+
@callbacks = []
|
145
|
+
described_class.trap('SIG1') do
|
146
|
+
@callbacks << :sig1
|
147
|
+
end
|
148
|
+
described_class.trap('SIG2') do
|
149
|
+
@callbacks << :sig2
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
it 'should handle each one' do
|
154
|
+
Process.kill('SIG1', Process.pid)
|
155
|
+
Process.kill('SIG2', Process.pid)
|
156
|
+
@callbacks.should match_array([:sig1, :sig2])
|
157
|
+
end
|
158
|
+
|
159
|
+
it 'should process most recent signals first' do
|
160
|
+
mutex = Mutex.new
|
161
|
+
described_class.trap('SIG1') do
|
162
|
+
@callbacks << :sig1
|
163
|
+
mutex.lock
|
164
|
+
end
|
165
|
+
described_class.trap('SIG3') do
|
166
|
+
@callbacks << :sig3
|
167
|
+
end
|
168
|
+
|
169
|
+
mutex.lock
|
170
|
+
Process.kill('SIG1', Process.pid)
|
171
|
+
Process.kill('SIG1', Process.pid)
|
172
|
+
Process.kill('SIG2', Process.pid)
|
173
|
+
Process.kill('SIG3', Process.pid)
|
174
|
+
mutex.unlock
|
175
|
+
sleep 0.1
|
176
|
+
|
177
|
+
@callbacks.should match_array([:sig1, :sig3, :sig2, :sig1])
|
178
|
+
end
|
179
|
+
|
180
|
+
it 'should process primary signals first' do
|
181
|
+
mutex = Mutex.new
|
182
|
+
described_class.trap('SIG1') do
|
183
|
+
@callbacks << :sig1
|
184
|
+
mutex.lock
|
185
|
+
end
|
186
|
+
described_class.trap('SIG3') do
|
187
|
+
@callbacks << :sig3
|
188
|
+
end
|
189
|
+
|
190
|
+
mutex.lock
|
191
|
+
Process.kill('SIG1', Process.pid)
|
192
|
+
Process.kill('SIG2', Process.pid)
|
193
|
+
Process.kill('SIG3', Process.pid)
|
194
|
+
mutex.unlock
|
195
|
+
sleep 0.1
|
196
|
+
|
197
|
+
@callbacks.should match_array([:sig1, :sig2, :sig3])
|
198
|
+
end
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
describe 'reset' do
|
203
|
+
it 'should clear existing handlers' do
|
204
|
+
callbacks = []
|
205
|
+
described_class.trap('SIG1') do
|
206
|
+
callbacks << :sig1
|
207
|
+
end
|
208
|
+
described_class.reset
|
209
|
+
Process.kill('SIG1', Process.pid)
|
210
|
+
callbacks.should match_array([])
|
211
|
+
end
|
212
|
+
|
213
|
+
it 'should not run unprocessed signals' do
|
214
|
+
callbacks = []
|
215
|
+
mutex = Mutex.new
|
216
|
+
described_class.trap('SIG1') do
|
217
|
+
callbacks << :sig1
|
218
|
+
mutex.lock
|
219
|
+
end
|
220
|
+
described_class.trap('SIG2') do
|
221
|
+
callbacks << :sig2
|
222
|
+
end
|
223
|
+
mutex.lock
|
224
|
+
Process.kill('SIG1', Process.pid)
|
225
|
+
Process.kill('SIG2', Process.pid)
|
226
|
+
described_class.reset
|
227
|
+
mutex.unlock
|
228
|
+
callbacks.should match_array([:sig1])
|
229
|
+
end
|
230
|
+
|
231
|
+
it 'should still listen for new traps' do
|
232
|
+
callbacks = []
|
233
|
+
described_class.trap('SIG1') do
|
234
|
+
callbacks << :sig1
|
235
|
+
end
|
236
|
+
described_class.reset
|
237
|
+
described_class.trap('SIG1') do
|
238
|
+
callbacks << :sig1
|
239
|
+
end
|
240
|
+
Process.kill('SIG1', Process.pid)
|
241
|
+
callbacks.should match_array([:sig1])
|
242
|
+
end
|
243
|
+
end
|
244
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Chore::Strategy::Batcher do
|
4
|
+
let(:batch_size) { 5 }
|
5
|
+
let(:callback) { double("callback") }
|
6
|
+
subject do
|
7
|
+
batcher = Chore::Strategy::Batcher.new(batch_size)
|
8
|
+
batcher.callback = callback
|
9
|
+
batcher
|
10
|
+
end
|
11
|
+
|
12
|
+
context 'with no items' do
|
13
|
+
it 'should not be ready' do
|
14
|
+
subject.should_not be_ready
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'should not invoke the callback when executed' do
|
18
|
+
callback.should_not_receive(:call)
|
19
|
+
subject.execute
|
20
|
+
end
|
21
|
+
|
22
|
+
it 'should not invoke the callback when force-executed' do
|
23
|
+
callback.should_not_receive(:call)
|
24
|
+
subject.execute(true)
|
25
|
+
end
|
26
|
+
|
27
|
+
it 'should not invoke callback when adding a new item' do
|
28
|
+
callback.should_not_receive(:call)
|
29
|
+
subject.add('test')
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
context 'with partial batch completed' do
|
34
|
+
let(:batch) { ['test'] * 3 }
|
35
|
+
|
36
|
+
before(:each) do
|
37
|
+
subject.batch = batch.dup
|
38
|
+
end
|
39
|
+
|
40
|
+
it 'should not be ready' do
|
41
|
+
subject.should_not be_ready
|
42
|
+
end
|
43
|
+
|
44
|
+
it 'should not invoke callback when executed' do
|
45
|
+
callback.should_not_receive(:call)
|
46
|
+
subject.execute
|
47
|
+
end
|
48
|
+
|
49
|
+
it 'should invoke callback when force-executed' do
|
50
|
+
callback.should_receive(:call).with(batch)
|
51
|
+
subject.execute(true)
|
52
|
+
end
|
53
|
+
|
54
|
+
it 'should invoke callback when add completes the batch' do
|
55
|
+
subject.add('test')
|
56
|
+
callback.should_receive(:call).with(['test'] * 5)
|
57
|
+
subject.add('test')
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
context 'with batch completed' do
|
62
|
+
let(:batch) { ['test'] * 5 }
|
63
|
+
|
64
|
+
before(:each) do
|
65
|
+
subject.batch = batch.dup
|
66
|
+
end
|
67
|
+
|
68
|
+
it 'should be ready' do
|
69
|
+
subject.should be_ready
|
70
|
+
end
|
71
|
+
|
72
|
+
it 'should invoke callback when executed' do
|
73
|
+
callback.should_receive(:call).with(batch)
|
74
|
+
subject.execute
|
75
|
+
end
|
76
|
+
|
77
|
+
it 'should invoke callback when force-executed' do
|
78
|
+
callback.should_receive(:call).with(batch)
|
79
|
+
subject.execute(true)
|
80
|
+
end
|
81
|
+
|
82
|
+
it 'should invoke callback with subset-only when added to' do
|
83
|
+
callback.should_receive(:call).with(['test'] * 5)
|
84
|
+
subject.add('test')
|
85
|
+
end
|
86
|
+
|
87
|
+
it 'should leave remaining batch when added to' do
|
88
|
+
callback.stub(:call)
|
89
|
+
subject.add('test')
|
90
|
+
subject.batch.should == ['test']
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Chore::Strategy::SingleConsumerStrategy do
|
4
|
+
let(:fetcher) { double("fetcher") }
|
5
|
+
let(:manager) { double("manager") }
|
6
|
+
let(:consumer) { double("consumer") }
|
7
|
+
let(:test_queues) { ["test-queue"] }
|
8
|
+
let(:strategy) { Chore::Strategy::SingleConsumerStrategy.new(fetcher) }
|
9
|
+
|
10
|
+
before do
|
11
|
+
fetcher.stub(:manager) { manager }
|
12
|
+
Chore.config.stub(:queues).and_return(test_queues)
|
13
|
+
Chore.config.stub(:consumer).and_return(consumer)
|
14
|
+
|
15
|
+
end
|
16
|
+
|
17
|
+
it "should consume and then assign a message" do
|
18
|
+
consumer.should_receive(:new).with(test_queues.first).and_return(consumer)
|
19
|
+
consumer.should_receive(:consume).and_yield(1, 'test-queue', 60, "test", 0)
|
20
|
+
manager.should_receive(:assign).with(Chore::UnitOfWork.new(1, 'test-queue', 60, "test", 0, consumer))
|
21
|
+
strategy.fetch
|
22
|
+
end
|
23
|
+
end
|