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.
Files changed (59) hide show
  1. checksums.yaml +15 -0
  2. data/LICENSE.txt +20 -0
  3. data/README.md +260 -0
  4. data/Rakefile +32 -0
  5. data/bin/chore +34 -0
  6. data/chore-core.gemspec +46 -0
  7. data/lib/chore/cli.rb +232 -0
  8. data/lib/chore/configuration.rb +13 -0
  9. data/lib/chore/consumer.rb +52 -0
  10. data/lib/chore/duplicate_detector.rb +56 -0
  11. data/lib/chore/fetcher.rb +31 -0
  12. data/lib/chore/hooks.rb +25 -0
  13. data/lib/chore/job.rb +103 -0
  14. data/lib/chore/json_encoder.rb +18 -0
  15. data/lib/chore/manager.rb +47 -0
  16. data/lib/chore/publisher.rb +29 -0
  17. data/lib/chore/queues/filesystem/consumer.rb +128 -0
  18. data/lib/chore/queues/filesystem/filesystem_queue.rb +49 -0
  19. data/lib/chore/queues/filesystem/publisher.rb +45 -0
  20. data/lib/chore/queues/sqs/consumer.rb +121 -0
  21. data/lib/chore/queues/sqs/publisher.rb +55 -0
  22. data/lib/chore/queues/sqs.rb +38 -0
  23. data/lib/chore/railtie.rb +18 -0
  24. data/lib/chore/signal.rb +175 -0
  25. data/lib/chore/strategies/consumer/batcher.rb +76 -0
  26. data/lib/chore/strategies/consumer/single_consumer_strategy.rb +34 -0
  27. data/lib/chore/strategies/consumer/threaded_consumer_strategy.rb +81 -0
  28. data/lib/chore/strategies/worker/forked_worker_strategy.rb +221 -0
  29. data/lib/chore/strategies/worker/single_worker_strategy.rb +39 -0
  30. data/lib/chore/tasks/queues.task +11 -0
  31. data/lib/chore/unit_of_work.rb +17 -0
  32. data/lib/chore/util.rb +18 -0
  33. data/lib/chore/version.rb +9 -0
  34. data/lib/chore/worker.rb +117 -0
  35. data/lib/chore-core.rb +1 -0
  36. data/lib/chore.rb +218 -0
  37. data/spec/chore/cli_spec.rb +182 -0
  38. data/spec/chore/consumer_spec.rb +36 -0
  39. data/spec/chore/duplicate_detector_spec.rb +62 -0
  40. data/spec/chore/fetcher_spec.rb +38 -0
  41. data/spec/chore/hooks_spec.rb +44 -0
  42. data/spec/chore/job_spec.rb +80 -0
  43. data/spec/chore/json_encoder_spec.rb +11 -0
  44. data/spec/chore/manager_spec.rb +39 -0
  45. data/spec/chore/queues/filesystem/filesystem_consumer_spec.rb +71 -0
  46. data/spec/chore/queues/sqs/consumer_spec.rb +136 -0
  47. data/spec/chore/queues/sqs/publisher_spec.rb +74 -0
  48. data/spec/chore/queues/sqs_spec.rb +37 -0
  49. data/spec/chore/signal_spec.rb +244 -0
  50. data/spec/chore/strategies/consumer/batcher_spec.rb +93 -0
  51. data/spec/chore/strategies/consumer/single_consumer_strategy_spec.rb +23 -0
  52. data/spec/chore/strategies/consumer/threaded_consumer_strategy_spec.rb +105 -0
  53. data/spec/chore/strategies/worker/forked_worker_strategy_spec.rb +281 -0
  54. data/spec/chore/strategies/worker/single_worker_strategy_spec.rb +36 -0
  55. data/spec/chore/worker_spec.rb +134 -0
  56. data/spec/chore_spec.rb +108 -0
  57. data/spec/spec_helper.rb +58 -0
  58. data/spec/test_job.rb +7 -0
  59. metadata +194 -0
@@ -0,0 +1,182 @@
1
+ require 'spec_helper'
2
+
3
+ class TestJob2
4
+ include Chore::Job
5
+ end
6
+
7
+ describe Chore::CLI do
8
+ let(:cli) { Chore::CLI.send(:new) }
9
+
10
+ describe ".register_option" do
11
+ let(:cli) { Chore::CLI.instance }
12
+
13
+ it 'should allow configuration options to be registered externally' do
14
+ args = ['some','args']
15
+ Chore::CLI.register_option('option_name',*args)
16
+ cli.registered_opts['option_name'].should == {:args => args}
17
+ end
18
+
19
+ it 'should allow configuration options to come from a file' do
20
+ file = StringIO.new("--key-name=some_value")
21
+ File.stub(:read).and_return(file.read)
22
+
23
+ args = ['-k', '--key-name SOME_VALUE', "Some description"]
24
+ Chore::CLI.register_option "key_name", *args
25
+ options = cli.parse_config_file(file)
26
+ cli.registered_opts['key_name'].should == {:args => args}
27
+ options[:key_name].should == 'some_value'
28
+ end
29
+
30
+ it 'should handle ERB tags in a config file' do
31
+ file = StringIO.new("--key-name=<%= 'erb_inserted_value' %>\n--other-key=<%= 'second_val' %>")
32
+ File.stub(:read).and_return(file.read)
33
+
34
+ Chore::CLI.register_option "key_name", '-k', '--key-name SOME_VALUE', "Some description"
35
+ Chore::CLI.register_option "other_key", '-o', '--other-key SOME_VALUE', "Some description"
36
+ options = cli.parse_config_file(file)
37
+ options[:key_name].should == 'erb_inserted_value'
38
+ options[:other_key].should == 'second_val'
39
+ end
40
+ end
41
+
42
+ context 'queue mananagement' do
43
+ before(:each) do
44
+ TestJob.queue_options :name => 'test_queue', :publisher => Chore::Publisher
45
+ TestJob2.queue_options :name => 'test2', :publisher => Chore::Publisher
46
+ cli.send(:options).delete(:queues)
47
+ cli.stub(:validate!)
48
+ cli.stub(:boot_system)
49
+ end
50
+
51
+ after :all do
52
+ #Removing the prefix due to spec idempotency issues in job spec
53
+ Chore.config.queue_prefix = nil
54
+ end
55
+
56
+ it 'should detect queues based on included jobs' do
57
+ cli.parse([])
58
+ Chore.config.queues.should include('test_queue')
59
+ end
60
+
61
+ it 'should honor --except when processing all queues' do
62
+ cli.parse(['--except=test_queue'])
63
+ Chore.config.queues.should_not include('test_queue')
64
+ end
65
+
66
+ it 'should honor --queue-prefix when processing all queues' do
67
+ cli.parse(['--queue-prefix=prefixey'])
68
+ Chore.config.queues.should include('prefixey_test2')
69
+ end
70
+
71
+ context 'when provided duplicate queues' do
72
+ let(:queue_options) {['--queues=test2,test2']}
73
+ before :each do
74
+ cli.parse(queue_options)
75
+ end
76
+
77
+ it 'should not have duplicate queues' do
78
+ Chore.config.queues.count.should == 1
79
+ end
80
+ end
81
+
82
+ context 'when both --queue_prefix and --queues have been provided' do
83
+ let(:queue_options) {['--queue-prefix=prefixy', '--queues=test2']}
84
+ before :each do
85
+ cli.parse(queue_options)
86
+ end
87
+
88
+ it 'should honor --queue_prefix' do
89
+ total_queues = Chore.config.queues.count
90
+ prefixed_queues = Chore.config.queues.count {|item| item.start_with?("prefixy_")}
91
+ prefixed_queues.should == total_queues
92
+ end
93
+
94
+ it 'should prefix the names of the specified queues' do
95
+ Chore.config.queues.should include('prefixy_test2')
96
+ end
97
+
98
+ it 'should not prefix the names of queues that were not specified' do
99
+ Chore.config.queues.should_not include('prefixy_test_queue')
100
+ end
101
+
102
+ it 'should not have a queue without the prefix' do
103
+ Chore.config.queues.should_not include('test2')
104
+ end
105
+
106
+ it 'should not have a queue that was not specified' do
107
+ Chore.config.queues.should_not include('test_queue')
108
+ end
109
+ end
110
+
111
+ it 'should raise an exception if both --queues and --except are specified' do
112
+ expect { cli.parse(['--except=something','--queues=something,else']) }.to raise_error(ArgumentError)
113
+ end
114
+
115
+ it 'should raise an exception if no queues are found' do
116
+ Chore::Job.job_classes.clear
117
+ expect { cli.parse([]) }.to raise_error(ArgumentError)
118
+ end
119
+ end
120
+
121
+ describe "#parse" do
122
+ let(:cli) do
123
+ Chore::CLI.send(:new).tap do |cli|
124
+ cli.send(:options).clear
125
+ cli.stub(:validate!)
126
+ cli.stub(:boot_system)
127
+ cli.stub(:detect_queues)
128
+ end
129
+ end
130
+
131
+ let(:config) { cli.parse(command); Chore.config }
132
+
133
+ context "--consumer-strategy" do
134
+ let(:command) { ["--consumer-strategy=Chore::Strategy::SingleConsumerStrategy"] }
135
+
136
+ it "should set the consumer class" do
137
+ config.consumer_strategy.should == Chore::Strategy::SingleConsumerStrategy
138
+ end
139
+ end
140
+
141
+ describe '--shutdown-timeout' do
142
+ let(:command) { ["--shutdown-timeout=#{amount}"] }
143
+ subject { config.shutdown_timeout }
144
+
145
+ context 'given a numeric value' do
146
+ let(:amount) { '10.0' }
147
+
148
+ it 'is that amount' do
149
+ subject.should == amount.to_f
150
+ end
151
+ end
152
+
153
+ context 'given no value' do
154
+ let(:command) { [] }
155
+ it 'is the default value, 120 seconds' do
156
+ subject.should == 120.0
157
+ end
158
+ end
159
+ end
160
+
161
+ describe '--max-attempts' do
162
+ let(:command) { ["--max-attempts=#{amount}"] }
163
+ subject { config.max_attempts }
164
+
165
+ context 'given a numeric value' do
166
+ let(:amount) { '10' }
167
+
168
+ it 'is that amount' do
169
+ subject.should == amount.to_i
170
+ end
171
+ end
172
+
173
+ context 'given no value' do
174
+ let(:command) { [] }
175
+ it 'is the default value, infinity' do
176
+ subject.should == 1.0 / 0.0
177
+ end
178
+ end
179
+ end
180
+ end
181
+
182
+ end
@@ -0,0 +1,36 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
+
3
+ describe Chore::Consumer do
4
+ let(:queue) { "test" }
5
+ let(:options) { {} }
6
+ let(:consumer) { Chore::Consumer.new(queue) }
7
+ let(:message) { "message" }
8
+
9
+ it 'should have a consume method' do
10
+ consumer.should respond_to :consume
11
+ end
12
+
13
+ it 'should have a reject method' do
14
+ consumer.should respond_to :reject
15
+ end
16
+
17
+ it 'should have a complete method' do
18
+ consumer.should respond_to :complete
19
+ end
20
+
21
+ it 'should have a class level reset_connection method' do
22
+ Chore::Consumer.should respond_to :reset_connection!
23
+ end
24
+
25
+ it 'should not have an implemented consume method' do
26
+ expect { consumer.consume }.to raise_error(NotImplementedError)
27
+ end
28
+
29
+ it 'should not have an implemented reject method' do
30
+ expect { consumer.reject(message) }.to raise_error(NotImplementedError)
31
+ end
32
+
33
+ it 'should not have an implemented complete method' do
34
+ expect { consumer.complete(message) }.to raise_error(NotImplementedError)
35
+ end
36
+ end
@@ -0,0 +1,62 @@
1
+ require 'spec_helper'
2
+ require 'securerandom'
3
+
4
+ describe Chore::DuplicateDetector do
5
+ let(:memcache) { double("memcache") }
6
+ let(:dupe_on_cache_failure) { false }
7
+ let(:dedupe_params) { { :memcached_client => memcache, :dupe_on_cache_failure => dupe_on_cache_failure } }
8
+ let(:dedupe) { Chore::DuplicateDetector.new(dedupe_params)}
9
+ let(:message) { double('message') }
10
+ let(:timeout) { 2 }
11
+ let(:queue_url) {"queue://bogus/url"}
12
+ let(:queue) { (q = double('queue')).stub(:visibility_timeout).and_return(timeout); q.stub(:url).and_return(queue_url); q }
13
+ let(:id) { SecureRandom.uuid }
14
+
15
+ before(:each) do
16
+ message.stub(:id).and_return(id)
17
+ message.stub(:queue).and_return(queue)
18
+ end
19
+
20
+ describe "#found_duplicate" do
21
+ it 'should not return true if the message has not already been seen' do
22
+ memcache.should_receive(:add).and_return(true)
23
+ dedupe.found_duplicate?(message).should_not be_true
24
+ end
25
+
26
+ it 'should return true if the message has already been seen' do
27
+ memcache.should_receive(:add).and_return(false)
28
+ dedupe.found_duplicate?(message).should be_true
29
+ end
30
+
31
+ it 'should return false if given an invalid message' do
32
+ dedupe.found_duplicate?(double()).should be_false
33
+ end
34
+
35
+ it "should return false when identity store errors" do
36
+ memcache.should_receive(:add).and_raise("no")
37
+ dedupe.found_duplicate?(message).should be_false
38
+ end
39
+
40
+ it "should set the timeout to be the queue's " do
41
+ memcache.should_receive(:add).with(id,"1",timeout).and_return(true)
42
+ dedupe.found_duplicate?(message).should be_false
43
+ end
44
+
45
+ it "should call #visibility_timeout once and only once" do
46
+ queue.should_receive(:visibility_timeout).once
47
+ memcache.should_receive(:add).at_least(3).times.and_return(true)
48
+ 3.times { dedupe.found_duplicate?(message) }
49
+ end
50
+
51
+ context 'when a memecached connection error occurs' do
52
+ context 'and when Chore.config.dedupe_strategy is set to :strict' do
53
+ let(:dupe_on_cache_failure) { true }
54
+
55
+ it "returns true" do
56
+ memcache.should_receive(:add).and_raise
57
+ dedupe.found_duplicate?(message).should be_true
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,38 @@
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
+ describe Chore::Fetcher do
15
+ let(:manager) { double("manager") }
16
+ let(:consumer) { TestConsumer }
17
+ let(:fetcher) { Chore::Fetcher.new(manager) }
18
+
19
+ before(:each) do
20
+ Chore.configure do |c|
21
+ c.queues = ['test']
22
+ c.consumer = consumer
23
+ c.batch_size = 1
24
+ end
25
+ end
26
+
27
+
28
+ it "should have a start function" do
29
+ fetcher.should respond_to :start
30
+ end
31
+
32
+ describe "fetching messages" do
33
+ it "should assign its message" do
34
+ manager.should_receive(:assign)
35
+ fetcher.start
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,44 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
+
3
+ class TestHooks
4
+ include Chore::Hooks
5
+ end
6
+
7
+ describe Chore::Hooks do
8
+ let(:test_instance){ TestHooks.new }
9
+ it 'should respond_to run_hooks_for' do
10
+ test_instance.should respond_to(:run_hooks_for)
11
+ end
12
+
13
+ it 'should call a defined hook' do
14
+ test_instance.should_receive(:before_perform_test).and_return(true)
15
+ test_instance.run_hooks_for(:before_perform)
16
+ end
17
+
18
+ it 'should call multiple defined hooks' do
19
+ 3.times do |i|
20
+ test_instance.should_receive(:"before_perform_test#{i}").and_return(true)
21
+ end
22
+ test_instance.run_hooks_for(:before_perform)
23
+ end
24
+
25
+ it 'should bubble up raised exceptions' do
26
+ test_instance.should_receive(:"before_perform_raise").and_raise(RuntimeError)
27
+ expect { test_instance.run_hooks_for(:before_perform) }.to raise_error(RuntimeError)
28
+ end
29
+
30
+ describe 'global hooks' do
31
+ before(:each) do
32
+ Chore.clear_hooks!
33
+ Chore.add_hook(:test_hook,&callback)
34
+ end
35
+
36
+ let(:callback) { proc { true } }
37
+
38
+ it 'should call a global hook' do
39
+ callback.should_receive(:call).once
40
+ test_instance.run_hooks_for(:test_hook)
41
+ end
42
+
43
+ end
44
+ end
@@ -0,0 +1,80 @@
1
+ require 'spec_helper'
2
+
3
+ describe Chore::Job do
4
+ let(:args) { [1,2, { :a => :hash }] }
5
+ let(:config) { { :name => 'test_queue', :publisher => Chore::Publisher } }
6
+
7
+ before(:each) do
8
+ TestJob.queue_options config
9
+ end
10
+
11
+ after(:each) do
12
+ TestJob.queue_options config
13
+ end
14
+
15
+ it 'should have an perform_async method' do
16
+ TestJob.should respond_to :perform_async
17
+ end
18
+
19
+ it 'should have a perform method' do
20
+ TestJob.should respond_to :perform
21
+ end
22
+
23
+ it 'should require a queue when configuring' do
24
+ expect { TestJob.queue_options(:name => nil) }.to raise_error(ArgumentError)
25
+ end
26
+
27
+ it 'should require a publisher when configuring' do
28
+ expect { TestJob.queue_options(:publisher => nil) }.to raise_error(ArgumentError)
29
+ end
30
+
31
+ it 'should take params via perform' do
32
+ TestJob.any_instance.should_receive(:perform).with(*args)
33
+ TestJob.perform(*args)
34
+ end
35
+
36
+ it 'should store class level configuration' do
37
+ TestJob.queue_options(:name => 'test_queue')
38
+ TestJob.options[:name].should == 'test_queue'
39
+ end
40
+
41
+ describe(:perform_async) do
42
+ it 'should call an instance of the queue_options publisher' do
43
+ args = [1,2,{:h => 'ash'}]
44
+ TestJob.queue_options(:publisher => Chore::Publisher)
45
+ Chore::Publisher.any_instance.should_receive(:publish).with('test_queue',{:class => 'TestJob',:args => args}).and_return(true)
46
+ TestJob.perform_async(*args)
47
+ end
48
+ end
49
+
50
+ describe 'publisher configured via Chore.configure' do
51
+ before do
52
+ Chore.configure do |c|
53
+ c.publisher = Chore::Publisher
54
+ end
55
+
56
+ class NoPublisherJob
57
+ include Chore::Job
58
+ queue_options :name => "test_queue"
59
+
60
+ def perform
61
+ end
62
+ end
63
+ end
64
+
65
+ it 'should have the default publisher' do
66
+ NoPublisherJob.options[:publisher].should == Chore::Publisher
67
+ end
68
+
69
+ describe 'global publisher can be overridden' do
70
+ before do
71
+ TestJob.queue_options config.merge(:publisher => FakePublisher)
72
+ end
73
+
74
+ it 'should override publisher' do
75
+ TestJob.options[:publisher].should == FakePublisher
76
+ TestJob.options[:publisher].should_not == Chore::Publisher
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,11 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
+
3
+ describe Chore::JsonEncoder do
4
+ it 'should have an encode method' do
5
+ subject.should respond_to :encode
6
+ end
7
+
8
+ it 'should have a decode method' do
9
+ subject.should respond_to :decode
10
+ end
11
+ end
@@ -0,0 +1,39 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
+ require 'timeout'
3
+
4
+ describe Chore::Manager do
5
+
6
+ let(:fetcher) { mock(:start => nil) }
7
+ let(:opts) { { :num_workers => 4, :other_opt => 'hi', :fetcher => fetcher } }
8
+
9
+ before(:each) do
10
+ Chore.configure {|c| c.fetcher = fetcher; c.worker_strategy = Chore::Strategy::SingleWorkerStrategy }
11
+ fetcher.should_receive(:new).and_return(fetcher)
12
+ end
13
+
14
+ it 'should call create an instance of the defined fetcher' do
15
+ manager = Chore::Manager.new
16
+ end
17
+
18
+ describe 'running the manager' do
19
+
20
+ let(:manager) { Chore::Manager.new}
21
+ let(:work) { Chore::UnitOfWork.new(Chore::JsonEncoder.encode({:class => 'MyClass',:args => []}),mock()) }
22
+
23
+ it 'should start the fetcher when starting the manager' do
24
+ fetcher.should_receive(:start)
25
+ manager.start
26
+ end
27
+
28
+ describe 'assigning messages' do
29
+ it 'should create a worker if one is available' do
30
+ worker = mock()
31
+ Chore::Worker.should_receive(:new).with(work).and_return(worker)
32
+ worker.should_receive(:start).with()
33
+ manager.assign(work)
34
+ end
35
+ end
36
+
37
+ end
38
+
39
+ end
@@ -0,0 +1,71 @@
1
+ require 'spec_helper'
2
+
3
+ # This test is actually testing both the publisher and the consumer behavior but what we
4
+ # really want to validate is that they can pass messages off to each other. Hard coding in
5
+ # the behavior of each in two separate tests was becoming a mess and would be hard to maintain.
6
+ describe Chore::Queues::Filesystem::Consumer do
7
+ let(:consumer) { Chore::Queues::Filesystem::Consumer.new(test_queue) }
8
+ let(:publisher) { Chore::Queues::Filesystem::Publisher.new }
9
+ let(:test_queues_dir) { "test-queues" }
10
+ let(:test_queue) { "test-queue" }
11
+
12
+ before do
13
+ Chore.config.fs_queue_root = test_queues_dir
14
+ Chore.config.stub(:default_queue_timeout).and_return(60)
15
+ consumer.stub(:sleep)
16
+ end
17
+
18
+ after do
19
+ FileUtils.rm_rf(test_queues_dir)
20
+ end
21
+
22
+ let!(:consumer_run_for_one_message) { consumer.stub(:running?).and_return(true, false) }
23
+ let(:test_job_hash) {{:class => "TestClass", :args => "test-args"}}
24
+
25
+ context "founding a published job" do
26
+ before do
27
+ publisher.publish(test_queue, test_job_hash)
28
+ end
29
+
30
+ it "should consume a published job and yield the job to the handler block" do
31
+ expect { |b| consumer.consume(&b) }.to yield_with_args(anything, 'test-queue', 60, test_job_hash.to_json, 0)
32
+ end
33
+
34
+ context "rejecting a job" do
35
+ let!(:consumer_run_for_two_messages) { consumer.stub(:running?).and_return(true, false,true,false) }
36
+
37
+ it "should requeue a job that gets rejected" do
38
+ rejected = false
39
+ consumer.consume do |job_id, queue_name, job_hash|
40
+ consumer.reject(job_id)
41
+ rejected = true
42
+ end
43
+ rejected.should be_true
44
+
45
+ expect { |b| consumer.consume(&b) }.to yield_with_args(anything, 'test-queue', 60, test_job_hash.to_json, 1)
46
+ end
47
+ end
48
+
49
+ context "completing a job" do
50
+ let!(:consumer_run_for_two_messages) { consumer.stub(:running?).and_return(true, false,true,false) }
51
+
52
+ it "should remove job on completion" do
53
+ completed = false
54
+ consumer.consume do |job_id, queue_name, job_hash|
55
+ consumer.complete(job_id)
56
+ completed = true
57
+ end
58
+ completed.should be_true
59
+
60
+ expect { |b| consumer.consume(&b) }.to_not yield_control
61
+ end
62
+ end
63
+ end
64
+
65
+ context "not finding a published job" do
66
+ it "should consume a published job and yield the job to the handler block" do
67
+ expect { |b| consumer.consume(&b) }.to_not yield_control
68
+ end
69
+ end
70
+ end
71
+