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,105 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
class TestConsumer < Chore::Consumer
|
4
|
+
def initialize(queue_name, opts={})
|
5
|
+
end
|
6
|
+
|
7
|
+
def consume
|
8
|
+
# just something that looks like an SQS message
|
9
|
+
msg = OpenStruct.new( :id => 1, :body => "test" )
|
10
|
+
yield msg if block_given?
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
class NoQueueConsumer < Chore::Consumer
|
15
|
+
def initialize(queue_name, opts={})
|
16
|
+
raise Chore::TerribleMistake
|
17
|
+
end
|
18
|
+
|
19
|
+
def consume
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
describe Chore::Strategy::ThreadedConsumerStrategy do
|
24
|
+
let(:fetcher) { double("fetcher") }
|
25
|
+
let(:manager) { double("manager") }
|
26
|
+
let(:consumer) { TestConsumer }
|
27
|
+
let(:strategy) { Chore::Strategy::ThreadedConsumerStrategy.new(fetcher) }
|
28
|
+
|
29
|
+
before(:each) do
|
30
|
+
fetcher.stub(:consumers) { [consumer] }
|
31
|
+
fetcher.stub(:manager) { manager }
|
32
|
+
Chore.configure do |c|
|
33
|
+
c.queues = ['test']
|
34
|
+
c.consumer = consumer
|
35
|
+
c.batch_size = batch_size
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
describe "unfilled batch" do
|
40
|
+
let(:batch_size) { 2 }
|
41
|
+
|
42
|
+
it "should queue but not assign the message" do
|
43
|
+
consumer.any_instance.should_receive(:consume).and_yield(1, 'test-queue', 60, "test", 0)
|
44
|
+
strategy.fetch
|
45
|
+
strategy.batcher.batch.size.should == 1
|
46
|
+
|
47
|
+
work = strategy.batcher.batch[0]
|
48
|
+
work.id.should == 1
|
49
|
+
work.queue_name.should == 'test-queue'
|
50
|
+
work.queue_timeout.should == 60
|
51
|
+
work.message.should == "test"
|
52
|
+
work.previous_attempts.should == 0
|
53
|
+
work.current_attempt.should == 1
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
describe "full batch" do
|
58
|
+
let(:batch_size) { 1 }
|
59
|
+
|
60
|
+
it "should assign the batch" do
|
61
|
+
manager.should_receive(:assign)
|
62
|
+
consumer.any_instance.should_receive(:consume).and_yield(1, 'test-queue', 60, "test", 0)
|
63
|
+
strategy.fetch
|
64
|
+
strategy.batcher.batch.size.should == 0
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
describe "2 threads per queue" do
|
69
|
+
let(:batch_size) { 2 }
|
70
|
+
let(:thread) { double('thread') }
|
71
|
+
|
72
|
+
before do
|
73
|
+
Chore.config.threads_per_queue = 2
|
74
|
+
thread.stub(:join)
|
75
|
+
end
|
76
|
+
|
77
|
+
it "should spawn two threads" do
|
78
|
+
# two for threads per queue and one for batcher#schedule
|
79
|
+
Thread.should_receive(:new).exactly(3).times { thread }
|
80
|
+
strategy.fetch
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
describe "non-existent queue" do
|
85
|
+
let(:bad_consumer) { NoQueueConsumer }
|
86
|
+
let(:fetcher) { double("fetcher") }
|
87
|
+
let(:strategy) { Chore::Strategy::ThreadedConsumerStrategy.new(fetcher) }
|
88
|
+
let(:batch_size) { 2 }
|
89
|
+
|
90
|
+
before do
|
91
|
+
fetcher.stub(:consumers) { [bad_consumer] }
|
92
|
+
Chore.configure do |c|
|
93
|
+
c.queues = ['test']
|
94
|
+
c.consumer = bad_consumer
|
95
|
+
c.batch_size = batch_size
|
96
|
+
c.threads_per_queue = 1
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
it "should shut down when a queue doesn't exist" do
|
101
|
+
manager.should_receive(:shutdown!)
|
102
|
+
strategy.fetch
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
@@ -0,0 +1,281 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'securerandom'
|
3
|
+
|
4
|
+
describe Chore::Strategy::ForkedWorkerStrategy do
|
5
|
+
let(:manager) { double('manager') }
|
6
|
+
let(:forker) do
|
7
|
+
strategy = Chore::Strategy::ForkedWorkerStrategy.new(manager)
|
8
|
+
strategy.stub(:exit!)
|
9
|
+
strategy
|
10
|
+
end
|
11
|
+
let(:job_timeout) { 60 }
|
12
|
+
let(:job) { Chore::UnitOfWork.new(SecureRandom.uuid, 'test', job_timeout, Chore::JsonEncoder.encode(TestJob.job_hash([1,2,"3"])), 0) }
|
13
|
+
let!(:worker) { Chore::Worker.new(job) }
|
14
|
+
let(:pid) { Random.rand(2048) }
|
15
|
+
|
16
|
+
after(:each) do
|
17
|
+
Process.stub(:kill => nil, :wait => pid)
|
18
|
+
forker.stop!
|
19
|
+
end
|
20
|
+
|
21
|
+
context "signal handling" do
|
22
|
+
it 'should trap signals from terminating children and reap them' do
|
23
|
+
Chore::Signal.should_receive(:trap).with('CHLD').and_yield
|
24
|
+
Chore::Strategy::ForkedWorkerStrategy.any_instance.should_receive(:reap_terminated_workers!)
|
25
|
+
forker
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
context '#assign' do
|
30
|
+
before(:each) do
|
31
|
+
forker.stub(:fork).and_yield.and_return(pid, pid + 1)
|
32
|
+
forker.stub(:after_fork)
|
33
|
+
end
|
34
|
+
after(:each) do
|
35
|
+
Chore.clear_hooks!
|
36
|
+
end
|
37
|
+
|
38
|
+
it 'should pop off the worker queue when assignd a job' do
|
39
|
+
Queue.any_instance.should_receive(:pop)
|
40
|
+
forker.assign(job)
|
41
|
+
end
|
42
|
+
|
43
|
+
it 'should assign a job to a new worker' do
|
44
|
+
Chore::Worker.should_receive(:new).with(job).and_return(worker)
|
45
|
+
worker.should_receive(:start)
|
46
|
+
forker.assign(job)
|
47
|
+
end
|
48
|
+
|
49
|
+
it 'should add an assigned worker to the worker list' do
|
50
|
+
forker.workers.should_receive(:[]=).with(pid,kind_of(Chore::Worker))
|
51
|
+
forker.assign(job)
|
52
|
+
end
|
53
|
+
|
54
|
+
it 'should fork a child for each new worker' do
|
55
|
+
forker.should_receive(:fork).and_yield.and_return(pid)
|
56
|
+
forker.assign(job)
|
57
|
+
end
|
58
|
+
|
59
|
+
it 'should remove the worker from the list when it has completed' do
|
60
|
+
forker.assign(job)
|
61
|
+
|
62
|
+
Process.should_receive(:wait).with(pid, Process::WNOHANG).and_return(pid)
|
63
|
+
forker.send(:reap_terminated_workers!)
|
64
|
+
|
65
|
+
forker.workers.should_not include(pid)
|
66
|
+
end
|
67
|
+
|
68
|
+
it 'should not remove the worker from the list if it has not yet completed' do
|
69
|
+
forker.assign(job)
|
70
|
+
|
71
|
+
Process.stub(:wait).and_return(nil)
|
72
|
+
forker.send(:reap_terminated_workers!)
|
73
|
+
|
74
|
+
forker.workers.should include(pid)
|
75
|
+
end
|
76
|
+
|
77
|
+
it 'should add the worker back to the queue when it has completed' do
|
78
|
+
2.times { forker.assign(job) }
|
79
|
+
|
80
|
+
Queue.any_instance.should_receive(:<<).twice.with(:worker)
|
81
|
+
|
82
|
+
Process.stub(:wait).and_return(pid, pid + 1)
|
83
|
+
forker.send(:reap_terminated_workers!)
|
84
|
+
end
|
85
|
+
|
86
|
+
it 'should only release a worker once if reaped twice' do
|
87
|
+
forker.assign(job)
|
88
|
+
reaped = false
|
89
|
+
|
90
|
+
forker.should_receive(:release_worker).once
|
91
|
+
|
92
|
+
Process.should_receive(:wait).twice.with(pid, anything).and_return do
|
93
|
+
if !reaped
|
94
|
+
reaped = true
|
95
|
+
forker.send(:reap_terminated_workers!)
|
96
|
+
end
|
97
|
+
|
98
|
+
pid
|
99
|
+
end
|
100
|
+
forker.send(:reap_terminated_workers!)
|
101
|
+
end
|
102
|
+
|
103
|
+
it 'should continue to allow reaping after an exception occurs' do
|
104
|
+
2.times { forker.assign(job) }
|
105
|
+
|
106
|
+
Process.should_receive(:wait).and_raise(Errno::ECHILD)
|
107
|
+
Process.should_receive(:wait).and_return(pid + 1)
|
108
|
+
forker.send(:reap_terminated_workers!)
|
109
|
+
|
110
|
+
forker.workers.should be_empty
|
111
|
+
end
|
112
|
+
|
113
|
+
[:before_fork, :after_fork, :within_fork, :before_fork_shutdown].each do |hook|
|
114
|
+
it "should run #{hook} hooks" do
|
115
|
+
hook_called = false
|
116
|
+
Chore.add_hook(hook) { hook_called = true }
|
117
|
+
forker.assign(job)
|
118
|
+
hook_called.should be_true
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
it 'should run around_fork hooks' do
|
123
|
+
hook_called = false
|
124
|
+
Chore.add_hook(:around_fork) {|&blk| hook_called = true; blk.call }
|
125
|
+
forker.assign(job)
|
126
|
+
hook_called.should be_true
|
127
|
+
end
|
128
|
+
|
129
|
+
it 'should run before_fork_shutdown hooks even if job errors' do
|
130
|
+
Chore::Worker.stub(:new).and_return(worker)
|
131
|
+
worker.stub(:start).and_raise(ArgumentError)
|
132
|
+
|
133
|
+
hook_called = false
|
134
|
+
Chore.add_hook(:before_fork_shutdown) { hook_called = true }
|
135
|
+
|
136
|
+
begin
|
137
|
+
forker.assign(job)
|
138
|
+
rescue ArgumentError => ex
|
139
|
+
end
|
140
|
+
|
141
|
+
hook_called.should be_true
|
142
|
+
end
|
143
|
+
|
144
|
+
it 'should exit the process without running at_exit handlers' do
|
145
|
+
forker.should_receive(:exit!).with(true)
|
146
|
+
forker.assign(job)
|
147
|
+
end
|
148
|
+
|
149
|
+
context 'long-lived work' do
|
150
|
+
let(:job_timeout) { 0.1 }
|
151
|
+
|
152
|
+
before(:each) do
|
153
|
+
Process.stub(:kill)
|
154
|
+
Chore::Worker.stub(:new).and_return(worker)
|
155
|
+
end
|
156
|
+
|
157
|
+
it 'should kill the process if it expires' do
|
158
|
+
Process.should_receive(:kill).with('KILL', pid)
|
159
|
+
forker.assign(job)
|
160
|
+
sleep 2
|
161
|
+
end
|
162
|
+
|
163
|
+
it 'should run the on_failure callback hook' do
|
164
|
+
forker.assign(job)
|
165
|
+
Chore.should_receive(:run_hooks_for).with(:on_failure, anything, instance_of(Chore::TimeoutError))
|
166
|
+
sleep 2
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
context 'short-lived work' do
|
171
|
+
let(:job_timeout) { 0.1 }
|
172
|
+
|
173
|
+
before(:each) do
|
174
|
+
Chore::Worker.stub(:new).and_return(worker)
|
175
|
+
end
|
176
|
+
|
177
|
+
it 'should not kill the process if does not expire' do
|
178
|
+
Process.should_not_receive(:kill)
|
179
|
+
|
180
|
+
forker.assign(job)
|
181
|
+
Process.stub(:wait).and_return(pid)
|
182
|
+
forker.send(:reap_terminated_workers!)
|
183
|
+
sleep 2
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
context '#before_fork' do
|
189
|
+
before(:each) do
|
190
|
+
Chore::Worker.stub(:new).and_return(worker)
|
191
|
+
end
|
192
|
+
after(:each) do
|
193
|
+
Chore.clear_hooks!
|
194
|
+
end
|
195
|
+
|
196
|
+
it 'should release the worker if an exception occurs' do
|
197
|
+
Chore.add_hook(:before_fork) { raise ArgumentError }
|
198
|
+
forker.should_receive(:release_worker)
|
199
|
+
forker.assign(job)
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
context '#around_fork' do
|
204
|
+
before(:each) do
|
205
|
+
Chore::Worker.stub(:new).and_return(worker)
|
206
|
+
end
|
207
|
+
after(:each) do
|
208
|
+
Chore.clear_hooks!
|
209
|
+
end
|
210
|
+
|
211
|
+
it 'should release the worker if an exception occurs' do
|
212
|
+
Chore.add_hook(:around_fork) {|worker, &block| raise ArgumentError}
|
213
|
+
forker.should_receive(:release_worker)
|
214
|
+
forker.assign(job)
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
218
|
+
context '#after_fork' do
|
219
|
+
let(:worker) { double('worker') }
|
220
|
+
|
221
|
+
it 'should clear signals' do
|
222
|
+
forker.should_receive(:clear_child_signals)
|
223
|
+
forker.send(:after_fork,worker)
|
224
|
+
end
|
225
|
+
|
226
|
+
it 'should trap signals' do
|
227
|
+
forker.should_receive(:trap_child_signals)
|
228
|
+
forker.send(:after_fork,worker)
|
229
|
+
end
|
230
|
+
|
231
|
+
it 'should set the procline' do
|
232
|
+
forker.should_receive(:procline)
|
233
|
+
forker.send(:after_fork,worker)
|
234
|
+
end
|
235
|
+
end
|
236
|
+
|
237
|
+
context '#stop!' do
|
238
|
+
before(:each) do
|
239
|
+
Process.stub(:kill)
|
240
|
+
|
241
|
+
forker.stub(:fork).and_yield.and_return(pid)
|
242
|
+
forker.stub(:after_fork)
|
243
|
+
forker.assign(job)
|
244
|
+
end
|
245
|
+
|
246
|
+
it 'should send a quit signal to each child' do
|
247
|
+
Process.should_receive(:kill).once.with('QUIT', pid)
|
248
|
+
Process.stub(:wait).and_return(pid, nil)
|
249
|
+
forker.stop!
|
250
|
+
end
|
251
|
+
|
252
|
+
it 'should reap each worker' do
|
253
|
+
Process.should_receive(:wait).and_return(pid)
|
254
|
+
forker.stop!
|
255
|
+
forker.workers.should be_empty
|
256
|
+
end
|
257
|
+
|
258
|
+
it 'should resend quit signal to children if workers are not reaped' do
|
259
|
+
Process.should_receive(:kill).twice.with('QUIT', pid)
|
260
|
+
Process.stub(:wait).and_return(nil, pid, nil)
|
261
|
+
forker.stop!
|
262
|
+
end
|
263
|
+
|
264
|
+
it 'should send kill signal to children if timeout is exceeded' do
|
265
|
+
Chore.config.stub(:shutdown_timeout).and_return(0.05)
|
266
|
+
Process.should_receive(:kill).once.with('QUIT', pid)
|
267
|
+
Process.stub(:wait).and_return(nil)
|
268
|
+
Process.should_receive(:kill).once.with('KILL', pid)
|
269
|
+
forker.stop!
|
270
|
+
end
|
271
|
+
|
272
|
+
it 'should not allow more work to be assigned' do
|
273
|
+
Process.stub(:wait).and_return(pid, nil)
|
274
|
+
forker.stop!
|
275
|
+
|
276
|
+
Chore::Worker.should_not_receive(:new)
|
277
|
+
forker.assign(job)
|
278
|
+
end
|
279
|
+
end
|
280
|
+
end
|
281
|
+
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Chore::Strategy::SingleWorkerStrategy do
|
4
|
+
let(:manager) { mock('Manager') }
|
5
|
+
subject { described_class.new(manager) }
|
6
|
+
|
7
|
+
describe '#stop!' do
|
8
|
+
before(:each) do
|
9
|
+
subject.stub(:worker).and_return worker
|
10
|
+
end
|
11
|
+
|
12
|
+
context 'given there is no current worker' do
|
13
|
+
let(:worker) { nil }
|
14
|
+
|
15
|
+
it 'does nothing' do
|
16
|
+
allow_message_expectations_on_nil
|
17
|
+
|
18
|
+
worker.should_not_receive(:stop!)
|
19
|
+
subject.stop!
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
context 'given there is a current worker' do
|
24
|
+
let(:worker) { mock('Worker') }
|
25
|
+
before(:each) do
|
26
|
+
subject.stub(:worker).and_return worker
|
27
|
+
end
|
28
|
+
|
29
|
+
it 'stops the worker' do
|
30
|
+
worker.should_receive(:stop!)
|
31
|
+
subject.stop!
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
@@ -0,0 +1,134 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
|
2
|
+
|
3
|
+
describe Chore::Worker do
|
4
|
+
|
5
|
+
class SimpleJob
|
6
|
+
include Chore::Job
|
7
|
+
queue_options :name => 'test', :publisher => FakePublisher, :max_attempts => 100
|
8
|
+
|
9
|
+
def perform(*args)
|
10
|
+
return args
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
let(:consumer) { double('consumer', :complete => nil) }
|
15
|
+
let(:job_args) { [1,2,'3'] }
|
16
|
+
let(:job) { SimpleJob.job_hash(job_args) }
|
17
|
+
|
18
|
+
it 'should use a default encoder' do
|
19
|
+
worker = Chore::Worker.new
|
20
|
+
worker.options[:encoder].should == Chore::JsonEncoder
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'should process a single job' do
|
24
|
+
work = Chore::UnitOfWork.new('1', 'test', 60, Chore::JsonEncoder.encode(job), 0, consumer)
|
25
|
+
SimpleJob.should_receive(:perform).with(*job_args)
|
26
|
+
consumer.should_receive(:complete).with('1')
|
27
|
+
w = Chore::Worker.new(work)
|
28
|
+
w.start
|
29
|
+
end
|
30
|
+
|
31
|
+
it 'should process multiple jobs' do
|
32
|
+
work = []
|
33
|
+
10.times do |i|
|
34
|
+
work << Chore::UnitOfWork.new(i, 'test', 60, Chore::JsonEncoder.encode(job), 0, consumer)
|
35
|
+
end
|
36
|
+
SimpleJob.should_receive(:perform).exactly(10).times
|
37
|
+
consumer.should_receive(:complete).exactly(10).times
|
38
|
+
Chore::Worker.start(work)
|
39
|
+
end
|
40
|
+
|
41
|
+
describe 'expired?' do
|
42
|
+
let(:now) { Time.now }
|
43
|
+
let(:queue_timeouts) { [10, 20, 30] }
|
44
|
+
let(:work) do
|
45
|
+
queue_timeouts.map do |queue_timeout|
|
46
|
+
Chore::UnitOfWork.new('1', 'test', queue_timeout, Chore::JsonEncoder.encode(job), 0, consumer)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
let(:worker) do
|
50
|
+
Timecop.freeze(now) do
|
51
|
+
Chore::Worker.new(work)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
it 'should not be expired when before total timeout' do
|
56
|
+
worker.should_not be_expired
|
57
|
+
end
|
58
|
+
|
59
|
+
it 'should not be expired when at total timeout' do
|
60
|
+
Timecop.freeze(now + 60) do
|
61
|
+
worker.should_not be_expired
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
it 'should be expired when past total timeout' do
|
66
|
+
Timecop.freeze(now + 61) do
|
67
|
+
worker.should be_expired
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
describe 'with errors' do
|
73
|
+
context 'on parse' do
|
74
|
+
let(:job) { "Not-A-Valid-Json-String" }
|
75
|
+
|
76
|
+
it 'should fail cleanly' do
|
77
|
+
work = Chore::UnitOfWork.new(2,'test',60,job,0,consumer)
|
78
|
+
consumer.should_not_receive(:complete)
|
79
|
+
Chore.should_receive(:run_hooks_for).with(:on_failure, job, anything())
|
80
|
+
Chore::Worker.start(work)
|
81
|
+
end
|
82
|
+
|
83
|
+
context 'more than the maximum allowed times' do
|
84
|
+
before(:each) do
|
85
|
+
Chore.config.stub(:max_attempts).and_return(10)
|
86
|
+
end
|
87
|
+
|
88
|
+
it 'should permanently fail' do
|
89
|
+
work = Chore::UnitOfWork.new(2,'test',60,job,9,consumer)
|
90
|
+
Chore.should_receive(:run_hooks_for).with(:on_permanent_failure, 'test', job, anything())
|
91
|
+
Chore::Worker.start(work)
|
92
|
+
end
|
93
|
+
|
94
|
+
it 'should mark the item as completed' do
|
95
|
+
work = Chore::UnitOfWork.new(2,'test',60,job,9,consumer)
|
96
|
+
consumer.should_receive(:complete).with(2)
|
97
|
+
Chore::Worker.start(work)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
context 'on perform' do
|
103
|
+
let(:encoded_job) { Chore::JsonEncoder.encode(job) }
|
104
|
+
let(:parsed_job) { JSON.parse(encoded_job) }
|
105
|
+
|
106
|
+
before(:each) do
|
107
|
+
SimpleJob.stub(:perform).and_raise(ArgumentError)
|
108
|
+
SimpleJob.stub(:run_hooks_for).and_return(true)
|
109
|
+
end
|
110
|
+
|
111
|
+
it 'should fail cleanly' do
|
112
|
+
work = Chore::UnitOfWork.new(2,'test',60,encoded_job,0,consumer)
|
113
|
+
consumer.should_not_receive(:complete)
|
114
|
+
SimpleJob.should_receive(:run_hooks_for).with(:on_failure, parsed_job, anything())
|
115
|
+
|
116
|
+
Chore::Worker.start(work)
|
117
|
+
end
|
118
|
+
|
119
|
+
context 'more than the maximum allowed times' do
|
120
|
+
it 'should permanently fail' do
|
121
|
+
work = Chore::UnitOfWork.new(2,'test',60,encoded_job,999,consumer)
|
122
|
+
SimpleJob.should_receive(:run_hooks_for).with(:on_permanent_failure, 'test', parsed_job, anything())
|
123
|
+
Chore::Worker.start(work)
|
124
|
+
end
|
125
|
+
|
126
|
+
it 'should mark the item as completed' do
|
127
|
+
work = Chore::UnitOfWork.new(2,'test',60,encoded_job,999,consumer)
|
128
|
+
consumer.should_receive(:complete).with(2)
|
129
|
+
Chore::Worker.start(work)
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
data/spec/chore_spec.rb
ADDED
@@ -0,0 +1,108 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
|
2
|
+
|
3
|
+
describe Chore do
|
4
|
+
before(:each) do
|
5
|
+
Chore.clear_hooks!
|
6
|
+
end
|
7
|
+
it 'should allow you to add a hook' do
|
8
|
+
blk = proc { true }
|
9
|
+
Chore.add_hook(:before_perform,&blk)
|
10
|
+
Chore.hooks_for(:before_perform).first.should == blk
|
11
|
+
end
|
12
|
+
|
13
|
+
it 'should call a hook if it exists' do
|
14
|
+
blk = proc { raise StandardError }
|
15
|
+
Chore.add_hook(:before_perform,&blk)
|
16
|
+
expect { Chore.run_hooks_for(:before_perform) }.to raise_error
|
17
|
+
end
|
18
|
+
|
19
|
+
it 'should not call a hook if it doesn\'t exist' do
|
20
|
+
blk = proc { raise StandardError }
|
21
|
+
expect { Chore.run_hooks_for(:before_perform) }.to_not raise_error
|
22
|
+
end
|
23
|
+
|
24
|
+
it 'should pass args to the block if present' do
|
25
|
+
blk = proc {|*args| true }
|
26
|
+
blk.should_receive(:call).with('1','2',3)
|
27
|
+
Chore.add_hook(:an_event,&blk)
|
28
|
+
Chore.run_hooks_for(:an_event, '1','2',3)
|
29
|
+
end
|
30
|
+
|
31
|
+
it 'should support multiple hooks for an event' do
|
32
|
+
blk = proc { true }
|
33
|
+
blk.should_receive(:call).twice
|
34
|
+
Chore.add_hook(:before_perform,&blk)
|
35
|
+
Chore.add_hook(:before_perform,&blk)
|
36
|
+
|
37
|
+
Chore.run_hooks_for(:before_perform)
|
38
|
+
end
|
39
|
+
|
40
|
+
it 'should support passing blocks' do
|
41
|
+
runner = proc { }
|
42
|
+
|
43
|
+
blk = proc { true }
|
44
|
+
blk.should_receive(:call) do |&arg1|
|
45
|
+
arg1.should_not be_nil
|
46
|
+
end
|
47
|
+
Chore.add_hook(:around_perform,&blk)
|
48
|
+
|
49
|
+
Chore.run_hooks_for(:around_perform, &runner)
|
50
|
+
end
|
51
|
+
|
52
|
+
it 'should call passed block' do
|
53
|
+
Chore.add_hook(:around_perform) do |&blk|
|
54
|
+
blk.call
|
55
|
+
end
|
56
|
+
|
57
|
+
run = false
|
58
|
+
Chore.run_hooks_for(:around_perform) { run = true }
|
59
|
+
run.should be_true
|
60
|
+
end
|
61
|
+
|
62
|
+
it 'should call passed blocks even if there are no hooks' do
|
63
|
+
run = false
|
64
|
+
Chore.run_hooks_for(:around_perform) { run = true }
|
65
|
+
run.should be_true
|
66
|
+
end
|
67
|
+
|
68
|
+
it 'should set configuration' do
|
69
|
+
Chore.configure {|c| c.test_config_option = 'howdy' }
|
70
|
+
Chore.config.test_config_option.should == 'howdy'
|
71
|
+
end
|
72
|
+
|
73
|
+
describe 'reopen_logs' do
|
74
|
+
let(:open_files) do
|
75
|
+
[
|
76
|
+
mock('file', :closed? => false, :reopen => nil, :sync= => nil, :path => '/a'),
|
77
|
+
mock('file2', :closed? => false, :reopen => nil, :sync= => nil, :path => '/b')
|
78
|
+
]
|
79
|
+
end
|
80
|
+
let(:closed_files) do
|
81
|
+
[mock('file3', :closed? => true)]
|
82
|
+
end
|
83
|
+
let(:files) { open_files + closed_files }
|
84
|
+
|
85
|
+
before(:each) do
|
86
|
+
ObjectSpace.stub(:each_object).and_yield(open_files[0]).and_yield(open_files[1])
|
87
|
+
end
|
88
|
+
|
89
|
+
it 'should look up all instances of files' do
|
90
|
+
ObjectSpace.should_receive(:each_object).with(File)
|
91
|
+
Chore.reopen_logs
|
92
|
+
end
|
93
|
+
|
94
|
+
it 'should reopen files that are not closed' do
|
95
|
+
open_files.each do |file|
|
96
|
+
file.should_receive(:reopen).with(file.path, 'a+')
|
97
|
+
end
|
98
|
+
Chore.reopen_logs
|
99
|
+
end
|
100
|
+
|
101
|
+
it 'should sync files' do
|
102
|
+
open_files.each do |file|
|
103
|
+
file.should_receive(:sync=).with(true)
|
104
|
+
end
|
105
|
+
Chore.reopen_logs
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|