delayed-job-ajaycb 2.0.10

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. data/MIT-LICENSE +20 -0
  2. data/README.textile +250 -0
  3. data/contrib/delayed_job.monitrc +14 -0
  4. data/contrib/delayed_job_multiple.monitrc +23 -0
  5. data/generators/delayed_job/delayed_job_generator.rb +28 -0
  6. data/generators/delayed_job/templates/migration.rb +22 -0
  7. data/generators/delayed_job/templates/migration_queue_name.rb +12 -0
  8. data/generators/delayed_job/templates/script +5 -0
  9. data/lib/delayed/backend/active_record.rb +97 -0
  10. data/lib/delayed/backend/base.rb +126 -0
  11. data/lib/delayed/backend/data_mapper.rb +125 -0
  12. data/lib/delayed/backend/mongo_mapper.rb +110 -0
  13. data/lib/delayed/command.rb +116 -0
  14. data/lib/delayed/deserialization_error.rb +4 -0
  15. data/lib/delayed/message_sending.rb +53 -0
  16. data/lib/delayed/performable_method.rb +62 -0
  17. data/lib/delayed/railtie.rb +10 -0
  18. data/lib/delayed/recipes.rb +50 -0
  19. data/lib/delayed/tasks.rb +15 -0
  20. data/lib/delayed/worker.rb +190 -0
  21. data/lib/delayed_job.rb +15 -0
  22. data/rails/init.rb +5 -0
  23. data/recipes/delayed_job.rb +1 -0
  24. data/spec/backend/active_record_job_spec.rb +70 -0
  25. data/spec/backend/data_mapper_job_spec.rb +16 -0
  26. data/spec/backend/mongo_mapper_job_spec.rb +94 -0
  27. data/spec/backend/shared_backend_spec.rb +342 -0
  28. data/spec/delayed_method_spec.rb +46 -0
  29. data/spec/message_sending_spec.rb +89 -0
  30. data/spec/performable_method_spec.rb +53 -0
  31. data/spec/sample_jobs.rb +26 -0
  32. data/spec/setup/active_record.rb +34 -0
  33. data/spec/setup/data_mapper.rb +8 -0
  34. data/spec/setup/mongo_mapper.rb +17 -0
  35. data/spec/spec_helper.rb +28 -0
  36. data/spec/story_spec.rb +17 -0
  37. data/spec/worker_spec.rb +237 -0
  38. data/tasks/jobs.rake +1 -0
  39. metadata +329 -0
@@ -0,0 +1,46 @@
1
+ require 'spec_helper'
2
+
3
+ describe Object do
4
+ before { Delayed::Job.delete_all }
5
+
6
+ it "should call #delay on methods which are wrapped with handle_asynchronously" do
7
+ story = Story.create :text => 'Once upon...'
8
+
9
+ Delayed::Job.count.should == 0
10
+
11
+ story.whatever(1, 5)
12
+
13
+ Delayed::Job.count.should == 1
14
+ job = Delayed::Job.first
15
+ job.payload_object.class.should == Delayed::PerformableMethod
16
+ job.payload_object.method.should == :whatever_without_delay
17
+ job.payload_object.args.should == [1, 5]
18
+ job.payload_object.perform.should == 'Once upon...'
19
+ end
20
+
21
+ context "delay" do
22
+ it "should raise a ArgumentError if target method doesn't exist" do
23
+ lambda { Object.new.delay.method_that_does_not_exist }.should raise_error(NoMethodError)
24
+ end
25
+
26
+ it "should add a new entry to the job table when delay is called on it" do
27
+ lambda { Object.new.delay.to_s }.should change { Delayed::Job.count }.by(1)
28
+ end
29
+
30
+ it "should add a new entry to the job table when delay is called on the class" do
31
+ lambda { Object.delay.to_s }.should change { Delayed::Job.count }.by(1)
32
+ end
33
+
34
+ it "should set job options" do
35
+ run_at = 1.day.from_now
36
+ job = Object.delay(:priority => 20, :run_at => run_at).to_s
37
+ job.run_at.should == run_at
38
+ job.priority.should == 20
39
+ end
40
+
41
+ it "should save args for original method" do
42
+ job = 3.delay.+(5)
43
+ job.payload_object.args.should == [5]
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,89 @@
1
+ require 'spec_helper'
2
+
3
+ describe Delayed::MessageSending do
4
+ describe "handle_asynchronously" do
5
+ class Story < ActiveRecord::Base
6
+ def tell!(arg)
7
+ end
8
+ handle_asynchronously :tell!
9
+ end
10
+
11
+ it "should alias original method" do
12
+ Story.new.should respond_to(:tell_without_delay!)
13
+ Story.new.should respond_to(:tell_with_delay!)
14
+ end
15
+
16
+ it "should create a PerformableMethod" do
17
+ story = Story.create!
18
+ lambda {
19
+ job = story.tell!(1)
20
+ job.payload_object.class.should == Delayed::PerformableMethod
21
+ job.payload_object.method.should == :tell_without_delay!
22
+ job.payload_object.args.should == [1]
23
+ }.should change { Delayed::Job.count }
24
+ end
25
+
26
+ describe 'with options' do
27
+ class Fable
28
+ class << self
29
+ attr_accessor :importance
30
+ end
31
+ def tell
32
+ end
33
+ handle_asynchronously :tell, :priority => Proc.new { self.importance }
34
+ end
35
+
36
+ it 'should set the priority based on the Fable importance' do
37
+ Fable.importance = 10
38
+ job = Fable.new.tell
39
+ job.priority.should == 10
40
+
41
+ Fable.importance = 20
42
+ job = Fable.new.tell
43
+ job.priority.should == 20
44
+ end
45
+
46
+ describe 'using a proc with parament' do
47
+ class Yarn
48
+ attr_accessor :importance
49
+ def spin
50
+ end
51
+ handle_asynchronously :spin, :priority => Proc.new {|y| y.importance }
52
+ end
53
+
54
+ it 'should set the priority based on the Fable importance' do
55
+ job = Yarn.new.tap {|y| y.importance = 10 }.spin
56
+ job.priority.should == 10
57
+
58
+ job = Yarn.new.tap {|y| y.importance = 20 }.spin
59
+ job.priority.should == 20
60
+ end
61
+ end
62
+ end
63
+ end
64
+
65
+ context "delay" do
66
+ it "should create a new PerformableMethod job" do
67
+ lambda {
68
+ job = "hello".delay.count('l')
69
+ job.payload_object.class.should == Delayed::PerformableMethod
70
+ job.payload_object.method.should == :count
71
+ job.payload_object.args.should == ['l']
72
+ }.should change { Delayed::Job.count }.by(1)
73
+ end
74
+
75
+ it "should set default priority" do
76
+ Delayed::Worker.default_priority = 99
77
+ job = Object.delay.to_s
78
+ job.priority.should == 99
79
+ Delayed::Worker.default_priority = 0
80
+ end
81
+
82
+ it "should set job options" do
83
+ run_at = Time.parse('2010-05-03 12:55 AM')
84
+ job = Object.delay(:priority => 20, :run_at => run_at).to_s
85
+ job.run_at.should == run_at
86
+ job.priority.should == 20
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,53 @@
1
+ require 'spec_helper'
2
+
3
+ class StoryReader
4
+ def read(story)
5
+ "Epilog: #{story.tell}"
6
+ end
7
+ end
8
+
9
+ describe Delayed::PerformableMethod do
10
+
11
+ it "should ignore ActiveRecord::RecordNotFound errors because they are permanent" do
12
+ story = Story.create :text => 'Once upon...'
13
+ p = Delayed::PerformableMethod.new(story, :tell, [])
14
+ story.destroy
15
+ lambda { p.perform }.should_not raise_error
16
+ end
17
+
18
+ it "should store the object as string if its an active record" do
19
+ story = Story.create :text => 'Once upon...'
20
+ p = Delayed::PerformableMethod.new(story, :tell, [])
21
+ p.class.should == Delayed::PerformableMethod
22
+ p.object.should == "LOAD;Story;#{story.id}"
23
+ p.method.should == :tell
24
+ p.args.should == []
25
+ p.perform.should == 'Once upon...'
26
+ end
27
+
28
+ it "should allow class methods to be called on ActiveRecord models" do
29
+ p = Delayed::PerformableMethod.new(Story, :count, [])
30
+ lambda { p.send(:load, p.object) }.should_not raise_error
31
+ end
32
+
33
+ it "should store arguments as string if they are active record objects" do
34
+ story = Story.create :text => 'Once upon...'
35
+ reader = StoryReader.new
36
+ p = Delayed::PerformableMethod.new(reader, :read, [story])
37
+ p.class.should == Delayed::PerformableMethod
38
+ p.method.should == :read
39
+ p.args.should == ["LOAD;Story;#{story.id}"]
40
+ p.perform.should == 'Epilog: Once upon...'
41
+ end
42
+
43
+ it "should not raise NoMethodError if target method is private" do
44
+ clazz = Class.new do
45
+ def private_method
46
+ end
47
+ private :private_method
48
+ end
49
+ lambda {
50
+ Delayed::PerformableMethod.new(clazz.new, :private_method, [])
51
+ }.should_not raise_error(NoMethodError)
52
+ end
53
+ end
@@ -0,0 +1,26 @@
1
+ class SimpleJob
2
+ cattr_accessor :runs; self.runs = 0
3
+ def perform; @@runs += 1; end
4
+ end
5
+
6
+ class ErrorJob
7
+ cattr_accessor :runs; self.runs = 0
8
+ def perform; raise 'did not work'; end
9
+ end
10
+
11
+ class LongRunningJob
12
+ def perform; sleep 250; end
13
+ end
14
+
15
+ class OnPermanentFailureJob < SimpleJob
16
+ def on_permanent_failure
17
+ end
18
+ def max_attempts; 1; end
19
+ end
20
+
21
+ module M
22
+ class ModuleJob
23
+ cattr_accessor :runs; self.runs = 0
24
+ def perform; @@runs += 1; end
25
+ end
26
+ end
@@ -0,0 +1,34 @@
1
+ require 'active_record'
2
+
3
+ ActiveRecord::Base.establish_connection(:adapter => 'sqlite3', :database => ':memory:')
4
+ ActiveRecord::Base.logger = Delayed::Worker.logger
5
+ ActiveRecord::Migration.verbose = false
6
+
7
+ ActiveRecord::Schema.define do
8
+ create_table :delayed_jobs, :force => true do |table|
9
+ table.integer :priority, :default => 0
10
+ table.integer :attempts, :default => 0
11
+ table.text :handler
12
+ table.text :last_error
13
+ table.datetime :run_at
14
+ table.datetime :locked_at
15
+ table.datetime :failed_at
16
+ table.string :locked_by
17
+ table.string :queue_name, :default=>"default"
18
+ table.timestamps
19
+ end
20
+
21
+ add_index :delayed_jobs, [:queue_name, :priority, :run_at], :name => 'delayed_jobs_priority'
22
+
23
+ create_table :stories, :force => true do |table|
24
+ table.string :text
25
+ end
26
+ end
27
+
28
+ # Purely useful for test cases...
29
+ class Story < ActiveRecord::Base
30
+ def tell; text; end
31
+ def whatever(n, _); tell*n; end
32
+
33
+ handle_asynchronously :whatever
34
+ end
@@ -0,0 +1,8 @@
1
+ require 'dm-core'
2
+ require 'dm-validations'
3
+
4
+ require 'delayed/backend/data_mapper'
5
+
6
+ DataMapper.logger = Delayed::Worker.logger
7
+ DataMapper.setup(:default, "sqlite3::memory:")
8
+ DataMapper.auto_migrate!
@@ -0,0 +1,17 @@
1
+ require 'mongo_mapper'
2
+
3
+ MongoMapper.config = {
4
+ RAILS_ENV => {'database' => 'delayed_job'}
5
+ }
6
+ MongoMapper.connect RAILS_ENV
7
+
8
+ unless defined?(Story)
9
+ class Story
10
+ include ::MongoMapper::Document
11
+ def tell; text; end
12
+ def whatever(n, _); tell*n; end
13
+ def self.count; end
14
+
15
+ handle_asynchronously :whatever
16
+ end
17
+ end
@@ -0,0 +1,28 @@
1
+ $:.unshift(File.dirname(__FILE__) + '/../lib')
2
+
3
+ require 'rubygems'
4
+ require 'spec'
5
+ require 'logger'
6
+
7
+ gem 'rails', '~>2.3.5'
8
+
9
+ require 'delayed_job'
10
+ require 'sample_jobs'
11
+
12
+ Delayed::Worker.logger = Logger.new('/tmp/dj.log')
13
+ RAILS_ENV = 'test'
14
+
15
+ # determine the available backends
16
+ BACKENDS = []
17
+ Dir.glob("#{File.dirname(__FILE__)}/setup/*.rb") do |backend|
18
+ begin
19
+ backend = File.basename(backend, '.rb')
20
+ require "setup/#{backend}"
21
+ require "backend/#{backend}_job_spec"
22
+ BACKENDS << backend.to_sym
23
+ rescue LoadError, Exception
24
+ puts "Unable to load #{backend} backend! #{$!}"
25
+ end
26
+ end
27
+
28
+ Delayed::Worker.backend = BACKENDS.first
@@ -0,0 +1,17 @@
1
+ require 'spec_helper'
2
+
3
+ describe "A story" do
4
+
5
+ before(:all) do
6
+ @story = Story.create :text => "Once upon a time..."
7
+ end
8
+
9
+ it "should be shared" do
10
+ @story.tell.should == 'Once upon a time...'
11
+ end
12
+
13
+ it "should not return its result if it storytelling is delayed" do
14
+ @story.delay.tell.should_not == 'Once upon a time...'
15
+ end
16
+
17
+ end
@@ -0,0 +1,237 @@
1
+ require 'spec_helper'
2
+
3
+ describe Delayed::Worker do
4
+ def job_create(opts = {})
5
+ Delayed::Job.create(opts.merge(:payload_object => SimpleJob.new))
6
+ end
7
+
8
+ describe "backend=" do
9
+ it "should set the Delayed::Job constant to the backend" do
10
+ @clazz = Class.new
11
+ Delayed::Worker.backend = @clazz
12
+ Delayed::Job.should == @clazz
13
+ end
14
+
15
+ it "should set backend with a symbol" do
16
+ Delayed::Worker.backend = Class.new
17
+ Delayed::Worker.backend = :active_record
18
+ Delayed::Worker.backend.should == Delayed::Backend::ActiveRecord::Job
19
+ end
20
+ end
21
+
22
+ BACKENDS.each do |backend|
23
+ describe "with the #{backend} backend" do
24
+ before do
25
+ Delayed::Worker.backend = backend
26
+ Delayed::Job.delete_all
27
+
28
+ @worker = Delayed::Worker.new(:max_priority => nil, :min_priority => nil, :quiet => true)
29
+
30
+ SimpleJob.runs = 0
31
+ end
32
+
33
+ describe "running a job" do
34
+ it "should fail after Worker.max_run_time" do
35
+ begin
36
+ old_max_run_time = Delayed::Worker.max_run_time
37
+ Delayed::Worker.max_run_time = 1.second
38
+ @job = Delayed::Job.create :payload_object => LongRunningJob.new
39
+ @worker.run(@job)
40
+ @job.reload.last_error.should =~ /expired/
41
+ @job.attempts.should == 1
42
+ ensure
43
+ Delayed::Worker.max_run_time = old_max_run_time
44
+ end
45
+ end
46
+ end
47
+
48
+ context "worker prioritization" do
49
+ before(:each) do
50
+ @worker = Delayed::Worker.new(:max_priority => 5, :min_priority => -5, :quiet => true)
51
+ end
52
+
53
+ it "should only work_off jobs that are >= min_priority" do
54
+ SimpleJob.runs.should == 0
55
+
56
+ job_create(:priority => -10)
57
+ job_create(:priority => 0)
58
+ @worker.work_off
59
+
60
+ SimpleJob.runs.should == 1
61
+ end
62
+
63
+ it "should only work_off jobs that are <= max_priority" do
64
+ SimpleJob.runs.should == 0
65
+
66
+ job_create(:priority => 10)
67
+ job_create(:priority => 0)
68
+
69
+ @worker.work_off
70
+
71
+ SimpleJob.runs.should == 1
72
+ end
73
+ end
74
+
75
+ context "while running with locked and expired jobs" do
76
+ before(:each) do
77
+ @worker.name = 'worker1'
78
+ end
79
+
80
+ it "should not run jobs locked by another worker" do
81
+ job_create(:locked_by => 'other_worker', :locked_at => (Delayed::Job.db_time_now - 1.minutes))
82
+ lambda { @worker.work_off }.should_not change { SimpleJob.runs }
83
+ end
84
+
85
+ it "should run open jobs" do
86
+ job_create
87
+ lambda { @worker.work_off }.should change { SimpleJob.runs }.from(0).to(1)
88
+ end
89
+
90
+ it "should run expired jobs" do
91
+ expired_time = Delayed::Job.db_time_now - (1.minutes + Delayed::Worker.max_run_time)
92
+ job_create(:locked_by => 'other_worker', :locked_at => expired_time)
93
+ lambda { @worker.work_off }.should change { SimpleJob.runs }.from(0).to(1)
94
+ end
95
+
96
+ it "should run own jobs" do
97
+ job_create(:locked_by => @worker.name, :locked_at => (Delayed::Job.db_time_now - 1.minutes))
98
+ lambda { @worker.work_off }.should change { SimpleJob.runs }.from(0).to(1)
99
+ end
100
+ end
101
+
102
+ describe "failed jobs" do
103
+ before do
104
+ # reset defaults
105
+ Delayed::Worker.destroy_failed_jobs = true
106
+ Delayed::Worker.max_attempts = 25
107
+ Delayed::Job.delete_all
108
+
109
+ @job = Delayed::Job.enqueue ErrorJob.new
110
+ end
111
+
112
+ it "should record last_error when destroy_failed_jobs = false, max_attempts = 1" do
113
+ Delayed::Worker.destroy_failed_jobs = false
114
+ Delayed::Worker.max_attempts = 1
115
+ @worker.run(@job)
116
+ @job.reload
117
+ @job.last_error.should =~ /did not work/
118
+ @job.last_error.should =~ /worker_spec.rb/
119
+ @job.attempts.should == 1
120
+ @job.failed_at.should_not be_nil
121
+ end
122
+
123
+ it "should re-schedule jobs after failing" do
124
+ @worker.work_off
125
+ @job.reload
126
+ @job.last_error.should =~ /did not work/
127
+ @job.last_error.should =~ /sample_jobs.rb:8:in `perform'/
128
+ @job.attempts.should == 1
129
+ @job.run_at.should > Delayed::Job.db_time_now - 10.minutes
130
+ @job.run_at.should < Delayed::Job.db_time_now + 10.minutes
131
+ @job.locked_at.should be_nil
132
+ @job.locked_by.should be_nil
133
+ end
134
+
135
+ context "when the job's payload implements #reschedule_at" do
136
+ before(:each) do
137
+ @reschedule_at = Time.current + 7.hours
138
+ @job.payload_object.stub!(:reschedule_at).and_return(@reschedule_at)
139
+ end
140
+
141
+ it 'should invoke the strategy to re-schedule' do
142
+ @job.payload_object.should_receive(:reschedule_at) do |time, attempts|
143
+ (Delayed::Job.db_time_now - time).should < 2
144
+ attempts.should == 1
145
+
146
+ Delayed::Job.db_time_now + 5
147
+ end
148
+
149
+ @worker.run(@job)
150
+ end
151
+ end
152
+ end
153
+
154
+ context "reschedule" do
155
+ before do
156
+ @job = Delayed::Job.create :payload_object => SimpleJob.new
157
+ end
158
+
159
+ share_examples_for "any failure more than Worker.max_attempts times" do
160
+ context "when the job's payload has an #on_permanent_failure hook" do
161
+ before do
162
+ @job = Delayed::Job.create :payload_object => OnPermanentFailureJob.new
163
+ @job.payload_object.should respond_to :on_permanent_failure
164
+ end
165
+
166
+ it "should run that hook" do
167
+ @job.payload_object.should_receive :on_permanent_failure
168
+ @worker.reschedule(@job)
169
+ end
170
+ end
171
+
172
+ context "when the job's payload has no #on_permanent_failure hook" do
173
+ # It's a little tricky to test this in a straightforward way,
174
+ # because putting a should_not_receive expectation on
175
+ # @job.payload_object.on_permanent_failure makes that object
176
+ # incorrectly return true to
177
+ # payload_object.respond_to? :on_permanent_failure, which is what
178
+ # reschedule uses to decide whether to call on_permanent_failure.
179
+ # So instead, we just make sure that the payload_object as it
180
+ # already stands doesn't respond_to? on_permanent_failure, then
181
+ # shove it through the iterated reschedule loop and make sure we
182
+ # don't get a NoMethodError (caused by calling that nonexistent
183
+ # on_permanent_failure method).
184
+
185
+ before do
186
+ @job.payload_object.should_not respond_to(:on_permanent_failure)
187
+ end
188
+
189
+ it "should not try to run that hook" do
190
+ lambda do
191
+ Delayed::Worker.max_attempts.times { @worker.reschedule(@job) }
192
+ end.should_not raise_exception(NoMethodError)
193
+ end
194
+ end
195
+ end
196
+
197
+ context "and we want to destroy jobs" do
198
+ before do
199
+ Delayed::Worker.destroy_failed_jobs = true
200
+ end
201
+
202
+ it_should_behave_like "any failure more than Worker.max_attempts times"
203
+
204
+ it "should be destroyed if it failed more than Worker.max_attempts times" do
205
+ @job.should_receive(:destroy)
206
+ Delayed::Worker.max_attempts.times { @worker.reschedule(@job) }
207
+ end
208
+
209
+ it "should not be destroyed if failed fewer than Worker.max_attempts times" do
210
+ @job.should_not_receive(:destroy)
211
+ (Delayed::Worker.max_attempts - 1).times { @worker.reschedule(@job) }
212
+ end
213
+ end
214
+
215
+ context "and we don't want to destroy jobs" do
216
+ before do
217
+ Delayed::Worker.destroy_failed_jobs = false
218
+ end
219
+
220
+ it_should_behave_like "any failure more than Worker.max_attempts times"
221
+
222
+ it "should be failed if it failed more than Worker.max_attempts times" do
223
+ @job.reload.failed_at.should == nil
224
+ Delayed::Worker.max_attempts.times { @worker.reschedule(@job) }
225
+ @job.reload.failed_at.should_not == nil
226
+ end
227
+
228
+ it "should not be failed if it failed fewer than Worker.max_attempts times" do
229
+ (Delayed::Worker.max_attempts - 1).times { @worker.reschedule(@job) }
230
+ @job.reload.failed_at.should == nil
231
+ end
232
+ end
233
+ end
234
+ end
235
+ end
236
+
237
+ end