delayed_job 2.0.8 → 2.1.0.pre
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/.gitignore +2 -0
- data/README.textile +14 -58
- data/Rakefile +53 -0
- data/VERSION +1 -0
- data/benchmarks.rb +33 -0
- data/delayed_job.gemspec +125 -0
- data/init.rb +1 -0
- data/lib/delayed/backend/active_record.rb +11 -13
- data/lib/delayed/backend/base.rb +14 -58
- data/lib/delayed/backend/couch_rest.rb +109 -0
- data/lib/delayed/backend/data_mapper.rb +8 -12
- data/lib/delayed/backend/mongo_mapper.rb +8 -12
- data/lib/delayed/command.rb +3 -8
- data/lib/delayed/message_sending.rb +10 -19
- data/lib/delayed/performable_method.rb +5 -48
- data/lib/delayed/railtie.rb +4 -0
- data/lib/delayed/recipes.rb +5 -24
- data/lib/delayed/worker.rb +26 -27
- data/lib/delayed/yaml_ext.rb +40 -0
- data/lib/delayed_job.rb +1 -1
- data/lib/generators/delayed_job/delayed_job_generator.rb +34 -0
- data/lib/generators/delayed_job/templates/migration.rb +21 -0
- data/lib/generators/delayed_job/templates/script +5 -0
- data/spec/autoloaded/clazz.rb +7 -0
- data/spec/autoloaded/struct.rb +7 -0
- data/spec/backend/couch_rest_job_spec.rb +15 -0
- data/spec/backend/mongo_mapper_job_spec.rb +11 -11
- data/spec/backend/shared_backend_spec.rb +41 -109
- data/spec/message_sending_spec.rb +1 -46
- data/spec/performable_method_spec.rb +22 -45
- data/spec/sample_jobs.rb +0 -1
- data/spec/setup/couch_rest.rb +7 -0
- data/spec/spec_helper.rb +6 -3
- data/spec/worker_spec.rb +6 -29
- metadata +174 -260
- data/lib/delayed/deserialization_error.rb +0 -4
- data/spec/delayed_method_spec.rb +0 -46
- data/spec/story_spec.rb +0 -17
@@ -0,0 +1,40 @@
|
|
1
|
+
# These extensions allow properly serializing and autoloading of
|
2
|
+
# Classes, Modules and Structs
|
3
|
+
|
4
|
+
require 'yaml'
|
5
|
+
|
6
|
+
class Module
|
7
|
+
yaml_as "tag:ruby.yaml.org,2002:module"
|
8
|
+
|
9
|
+
def self.yaml_new(klass, tag, val)
|
10
|
+
val.constantize
|
11
|
+
end
|
12
|
+
|
13
|
+
def to_yaml( opts = {} )
|
14
|
+
YAML::quick_emit( nil, opts ) { |out|
|
15
|
+
out.scalar(taguri, self.name, :plain)
|
16
|
+
}
|
17
|
+
end
|
18
|
+
|
19
|
+
def yaml_tag_read_class(name)
|
20
|
+
# Constantize the object so that ActiveSupport can attempt
|
21
|
+
# its auto loading magic. Will raise LoadError if not successful.
|
22
|
+
name.constantize
|
23
|
+
name
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
|
28
|
+
class Class
|
29
|
+
yaml_as "tag:ruby.yaml.org,2002:class"
|
30
|
+
remove_method :to_yaml # use Module's to_yaml
|
31
|
+
end
|
32
|
+
|
33
|
+
class Struct
|
34
|
+
def self.yaml_tag_read_class(name)
|
35
|
+
# Constantize the object so that ActiveSupport can attempt
|
36
|
+
# its auto loading magic. Will raise LoadError if not successful.
|
37
|
+
name.constantize
|
38
|
+
"Struct::#{ name }"
|
39
|
+
end
|
40
|
+
end
|
data/lib/delayed_job.rb
CHANGED
@@ -2,9 +2,9 @@ require 'active_support'
|
|
2
2
|
|
3
3
|
require File.dirname(__FILE__) + '/delayed/message_sending'
|
4
4
|
require File.dirname(__FILE__) + '/delayed/performable_method'
|
5
|
+
require File.dirname(__FILE__) + '/delayed/yaml_ext'
|
5
6
|
require File.dirname(__FILE__) + '/delayed/backend/base'
|
6
7
|
require File.dirname(__FILE__) + '/delayed/worker'
|
7
|
-
require File.dirname(__FILE__) + '/delayed/deserialization_error'
|
8
8
|
require File.dirname(__FILE__) + '/delayed/railtie' if defined?(::Rails::Railtie)
|
9
9
|
|
10
10
|
Object.send(:include, Delayed::MessageSending)
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'rails/generators'
|
2
|
+
require 'rails/generators/migration'
|
3
|
+
|
4
|
+
class DelayedJobGenerator < Rails::Generators::Base
|
5
|
+
|
6
|
+
include Rails::Generators::Migration
|
7
|
+
|
8
|
+
def self.source_root
|
9
|
+
@source_root ||= File.join(File.dirname(__FILE__), 'templates')
|
10
|
+
end
|
11
|
+
|
12
|
+
# Implement the required interface for Rails::Generators::Migration.
|
13
|
+
#
|
14
|
+
def self.next_migration_number(dirname) #:nodoc:
|
15
|
+
next_migration_number = current_migration_number(dirname) + 1
|
16
|
+
if ActiveRecord::Base.timestamped_migrations
|
17
|
+
[Time.now.utc.strftime("%Y%m%d%H%M%S"), "%.14d" % next_migration_number].max
|
18
|
+
else
|
19
|
+
"%.3d" % next_migration_number
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def create_script_file
|
24
|
+
template 'script', 'script/delayed_job'
|
25
|
+
chmod 'script/delayed_job', 0755
|
26
|
+
end
|
27
|
+
|
28
|
+
def create_migration_file
|
29
|
+
if defined?(ActiveRecord)
|
30
|
+
migration_template 'migration.rb', 'db/migrate/create_delayed_jobs.rb'
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
class CreateDelayedJobs < ActiveRecord::Migration
|
2
|
+
def self.up
|
3
|
+
create_table :delayed_jobs, :force => true do |table|
|
4
|
+
table.integer :priority, :default => 0 # Allows some jobs to jump to the front of the queue
|
5
|
+
table.integer :attempts, :default => 0 # Provides for retries, but still fail eventually.
|
6
|
+
table.text :handler # YAML-encoded string of the object that will do work
|
7
|
+
table.text :last_error # reason for last failure (See Note below)
|
8
|
+
table.datetime :run_at # When to run. Could be Time.zone.now for immediately, or sometime in the future.
|
9
|
+
table.datetime :locked_at # Set when a client is working on this object
|
10
|
+
table.datetime :failed_at # Set when all retries have failed (actually, by default, the record is deleted instead)
|
11
|
+
table.string :locked_by # Who is working on this object (if locked)
|
12
|
+
table.timestamps
|
13
|
+
end
|
14
|
+
|
15
|
+
add_index :delayed_jobs, [:priority, :run_at], :name => 'delayed_jobs_priority'
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.down
|
19
|
+
drop_table :delayed_jobs
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'backend/shared_backend_spec'
|
3
|
+
require 'delayed/backend/couch_rest'
|
4
|
+
|
5
|
+
describe Delayed::Backend::CouchRest::Job do
|
6
|
+
before(:all) do
|
7
|
+
@backend = Delayed::Backend::CouchRest::Job
|
8
|
+
end
|
9
|
+
|
10
|
+
before(:each) do
|
11
|
+
@backend.delete_all
|
12
|
+
end
|
13
|
+
|
14
|
+
it_should_behave_like 'a backend'
|
15
|
+
end
|
@@ -6,11 +6,11 @@ describe Delayed::Backend::MongoMapper::Job do
|
|
6
6
|
before(:all) do
|
7
7
|
@backend = Delayed::Backend::MongoMapper::Job
|
8
8
|
end
|
9
|
-
|
9
|
+
|
10
10
|
before(:each) do
|
11
11
|
MongoMapper.database.collections.each(&:remove)
|
12
12
|
end
|
13
|
-
|
13
|
+
|
14
14
|
it_should_behave_like 'a backend'
|
15
15
|
|
16
16
|
describe "indexes" do
|
@@ -22,23 +22,23 @@ describe Delayed::Backend::MongoMapper::Job do
|
|
22
22
|
@backend.collection.index_information.detect { |index| index[0] == 'locked_by_1' }.should_not be_nil
|
23
23
|
end
|
24
24
|
end
|
25
|
-
|
25
|
+
|
26
26
|
describe "delayed method" do
|
27
27
|
class MongoStoryReader
|
28
28
|
def read(story)
|
29
29
|
"Epilog: #{story.tell}"
|
30
30
|
end
|
31
31
|
end
|
32
|
-
|
32
|
+
|
33
33
|
class MongoStory
|
34
34
|
include ::MongoMapper::Document
|
35
35
|
key :text, String
|
36
|
-
|
36
|
+
|
37
37
|
def tell
|
38
38
|
text
|
39
39
|
end
|
40
40
|
end
|
41
|
-
|
41
|
+
|
42
42
|
it "should ignore not found errors because they are permanent" do
|
43
43
|
story = MongoStory.create :text => 'Once upon a time...'
|
44
44
|
job = story.delay.tell
|
@@ -66,12 +66,12 @@ describe Delayed::Backend::MongoMapper::Job do
|
|
66
66
|
job.payload_object.perform.should == 'Epilog: Once upon a time...'
|
67
67
|
end
|
68
68
|
end
|
69
|
-
|
69
|
+
|
70
70
|
describe "before_fork" do
|
71
71
|
after do
|
72
|
-
MongoMapper.connection.
|
72
|
+
MongoMapper.connection.connect_to_master
|
73
73
|
end
|
74
|
-
|
74
|
+
|
75
75
|
it "should disconnect" do
|
76
76
|
lambda do
|
77
77
|
Delayed::Backend::MongoMapper::Job.before_fork
|
@@ -83,12 +83,12 @@ describe Delayed::Backend::MongoMapper::Job do
|
|
83
83
|
before do
|
84
84
|
MongoMapper.connection.close
|
85
85
|
end
|
86
|
-
|
86
|
+
|
87
87
|
it "should call reconnect" do
|
88
88
|
lambda do
|
89
89
|
Delayed::Backend::MongoMapper::Job.after_fork
|
90
90
|
end.should change { !!MongoMapper.connection.connected? }.from(false).to(true)
|
91
91
|
end
|
92
92
|
end
|
93
|
-
|
93
|
+
|
94
94
|
end
|
@@ -1,3 +1,9 @@
|
|
1
|
+
class NamedJob < Struct.new(:perform)
|
2
|
+
def display_name
|
3
|
+
'named_job'
|
4
|
+
end
|
5
|
+
end
|
6
|
+
|
1
7
|
shared_examples_for 'a backend' do
|
2
8
|
def create_job(opts = {})
|
3
9
|
@backend.create(opts.merge(:payload_object => SimpleJob.new))
|
@@ -6,7 +12,6 @@ shared_examples_for 'a backend' do
|
|
6
12
|
before do
|
7
13
|
Delayed::Worker.max_priority = nil
|
8
14
|
Delayed::Worker.min_priority = nil
|
9
|
-
Delayed::Worker.default_priority = 99
|
10
15
|
SimpleJob.runs = 0
|
11
16
|
end
|
12
17
|
|
@@ -33,11 +38,6 @@ shared_examples_for 'a backend' do
|
|
33
38
|
@job.priority.should == 5
|
34
39
|
end
|
35
40
|
|
36
|
-
it "should use default priority when it is not set" do
|
37
|
-
@job = @backend.enqueue SimpleJob.new
|
38
|
-
@job.priority.should == 99
|
39
|
-
end
|
40
|
-
|
41
41
|
it "should be able to set run_at when enqueuing items" do
|
42
42
|
later = @backend.db_time_now + 5.minutes
|
43
43
|
@job = @backend.enqueue SimpleJob.new, 5, later
|
@@ -49,39 +49,27 @@ shared_examples_for 'a backend' do
|
|
49
49
|
job = @backend.enqueue M::ModuleJob.new
|
50
50
|
lambda { job.invoke_job }.should change { M::ModuleJob.runs }.from(0).to(1)
|
51
51
|
end
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
it "should raise an DeserializationError when the job is badly encoded" do
|
59
|
-
job = @backend.new :handler => "--- !ruby/object:SimpleJob {"
|
60
|
-
lambda { job.payload_object.perform }.should raise_error(Delayed::DeserializationError)
|
61
|
-
end
|
62
|
-
|
63
|
-
it "should try to load the class when it is unknown at the time of the deserialization" do
|
64
|
-
job = @backend.new :handler => "--- !ruby/object:JobThatDoesNotExist {}"
|
65
|
-
job.should_receive(:attempt_to_load).with('JobThatDoesNotExist').and_return(true)
|
66
|
-
lambda { job.payload_object.perform }.should raise_error(Delayed::DeserializationError)
|
67
|
-
end
|
68
|
-
|
69
|
-
it "should try include the namespace when loading unknown objects" do
|
70
|
-
job = @backend.new :handler => "--- !ruby/object:Delayed::JobThatDoesNotExist {}"
|
71
|
-
job.should_receive(:attempt_to_load).with('Delayed::JobThatDoesNotExist').and_return(true)
|
72
|
-
lambda { job.payload_object.perform }.should raise_error(Delayed::DeserializationError)
|
73
|
-
end
|
52
|
+
|
53
|
+
describe "payload_object" do
|
54
|
+
it "should raise a DeserializationError when the job class is totally unknown" do
|
55
|
+
job = @backend.new :handler => "--- !ruby/object:JobThatDoesNotExist {}"
|
56
|
+
lambda { job.payload_object }.should raise_error(Delayed::Backend::DeserializationError)
|
57
|
+
end
|
74
58
|
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
59
|
+
it "should raise a DeserializationError when the job struct is totally unknown" do
|
60
|
+
job = @backend.new :handler => "--- !ruby/struct:StructThatDoesNotExist {}"
|
61
|
+
lambda { job.payload_object }.should raise_error(Delayed::Backend::DeserializationError)
|
62
|
+
end
|
63
|
+
|
64
|
+
it "should autoload classes that are unknown at runtime" do
|
65
|
+
job = @backend.new :handler => "--- !ruby/object:Autoloaded::Clazz {}"
|
66
|
+
lambda { job.payload_object }.should_not raise_error(Delayed::Backend::DeserializationError)
|
67
|
+
end
|
80
68
|
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
69
|
+
it "should autoload structs that are unknown at runtime" do
|
70
|
+
job = @backend.new :handler => "--- !ruby/struct:Autoloaded::Struct {}"
|
71
|
+
lambda { job.payload_object }.should_not raise_error(Delayed::Backend::DeserializationError)
|
72
|
+
end
|
85
73
|
end
|
86
74
|
|
87
75
|
describe "find_available" do
|
@@ -177,55 +165,15 @@ shared_examples_for 'a backend' do
|
|
177
165
|
@job_copy_for_worker_2.lock_exclusively!(4.hours, 'worker2').should == false
|
178
166
|
end
|
179
167
|
end
|
180
|
-
|
181
|
-
describe "reserve" do
|
182
|
-
before do
|
183
|
-
Delayed::Worker.max_run_time = 2.minutes
|
184
|
-
@worker = Delayed::Worker.new(:quiet => true)
|
185
|
-
end
|
186
|
-
|
187
|
-
it "should not reserve failed jobs" do
|
188
|
-
create_job :attempts => 50, :failed_at => described_class.db_time_now
|
189
|
-
described_class.reserve(@worker).should be_nil
|
190
|
-
end
|
191
168
|
|
192
|
-
it "should not reserve jobs scheduled for the future" do
|
193
|
-
create_job :run_at => (described_class.db_time_now + 1.minute)
|
194
|
-
described_class.reserve(@worker).should be_nil
|
195
|
-
end
|
196
|
-
|
197
|
-
it "should lock the job so other workers can't reserve it" do
|
198
|
-
job = create_job
|
199
|
-
described_class.reserve(@worker).should == job
|
200
|
-
new_worker = Delayed::Worker.new(:quiet => true)
|
201
|
-
new_worker.name = 'worker2'
|
202
|
-
described_class.reserve(new_worker).should be_nil
|
203
|
-
end
|
204
|
-
|
205
|
-
it "should reserve open jobs" do
|
206
|
-
job = create_job
|
207
|
-
described_class.reserve(@worker).should == job
|
208
|
-
end
|
209
|
-
|
210
|
-
it "should reserve expired jobs" do
|
211
|
-
job = create_job(:locked_by => @worker.name, :locked_at => described_class.db_time_now - 3.minutes)
|
212
|
-
described_class.reserve(@worker).should == job
|
213
|
-
end
|
214
|
-
|
215
|
-
it "should reserve own jobs" do
|
216
|
-
job = create_job(:locked_by => @worker.name, :locked_at => (described_class.db_time_now - 1.minutes))
|
217
|
-
described_class.reserve(@worker).should == job
|
218
|
-
end
|
219
|
-
end
|
220
|
-
|
221
169
|
context "#name" do
|
222
170
|
it "should be the class name of the job that was enqueued" do
|
223
171
|
@backend.create(:payload_object => ErrorJob.new ).name.should == 'ErrorJob'
|
224
172
|
end
|
225
|
-
|
173
|
+
|
226
174
|
it "should be the method that will be called if its a performable method object" do
|
227
|
-
|
228
|
-
|
175
|
+
job = @backend.new(:payload_object => NamedJob.new)
|
176
|
+
job.name.should == 'named_job'
|
229
177
|
end
|
230
178
|
|
231
179
|
it "should be the instance method that will be called if its a performable method object" do
|
@@ -304,38 +252,22 @@ shared_examples_for 'a backend' do
|
|
304
252
|
@job.id.should_not be_nil
|
305
253
|
end
|
306
254
|
end
|
307
|
-
|
308
|
-
context "max_attempts" do
|
309
|
-
before(:each) do
|
310
|
-
@job = described_class.enqueue SimpleJob.new
|
311
|
-
end
|
312
|
-
|
313
|
-
it 'should not be defined' do
|
314
|
-
@job.max_attempts.should be_nil
|
315
|
-
end
|
316
|
-
|
317
|
-
it 'should use the max_retries value on the payload when defined' do
|
318
|
-
@job.payload_object.stub!(:max_attempts).and_return(99)
|
319
|
-
@job.max_attempts.should == 99
|
320
|
-
end
|
321
|
-
end
|
322
255
|
|
323
|
-
describe "
|
324
|
-
|
325
|
-
|
256
|
+
describe "yaml serialization" do
|
257
|
+
it "should reload changed attributes" do
|
258
|
+
job = @backend.enqueue SimpleJob.new
|
259
|
+
yaml = job.to_yaml
|
260
|
+
job.priority = 99
|
261
|
+
job.save
|
262
|
+
YAML.load(yaml).priority.should == 99
|
326
263
|
end
|
327
264
|
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
job = described_class.create! :handler => "--- !ruby/object:JobThatDoesNotExist {}"
|
334
|
-
@worker.work_off
|
335
|
-
job.reload
|
336
|
-
job.failed_at.should_not be_nil
|
337
|
-
end
|
338
|
-
end
|
265
|
+
it "should ignore destroyed records" do
|
266
|
+
job = @backend.enqueue SimpleJob.new
|
267
|
+
yaml = job.to_yaml
|
268
|
+
job.destroy
|
269
|
+
lambda { YAML.load(yaml).should be_nil }.should_not raise_error
|
339
270
|
end
|
340
271
|
end
|
272
|
+
|
341
273
|
end
|
@@ -22,44 +22,6 @@ describe Delayed::MessageSending do
|
|
22
22
|
job.payload_object.args.should == [1]
|
23
23
|
}.should change { Delayed::Job.count }
|
24
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
25
|
end
|
64
26
|
|
65
27
|
context "delay" do
|
@@ -71,14 +33,7 @@ describe Delayed::MessageSending do
|
|
71
33
|
job.payload_object.args.should == ['l']
|
72
34
|
}.should change { Delayed::Job.count }.by(1)
|
73
35
|
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
|
-
|
36
|
+
|
82
37
|
it "should set job options" do
|
83
38
|
run_at = Time.parse('2010-05-03 12:55 AM')
|
84
39
|
job = Object.delay(:priority => 20, :run_at => run_at).to_s
|