delayed_job_hooked 2.1.5
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/MIT-LICENSE +20 -0
- data/README.textile +243 -0
- data/contrib/delayed_job.monitrc +14 -0
- data/contrib/delayed_job_multiple.monitrc +23 -0
- data/lib/delayed/backend/active_record.rb +82 -0
- data/lib/delayed/backend/base.rb +128 -0
- data/lib/delayed/backend/shared_spec.rb +485 -0
- data/lib/delayed/command.rb +112 -0
- data/lib/delayed/deserialization_error.rb +4 -0
- data/lib/delayed/message_sending.rb +54 -0
- data/lib/delayed/performable_mailer.rb +21 -0
- data/lib/delayed/performable_method.rb +31 -0
- data/lib/delayed/railtie.rb +18 -0
- data/lib/delayed/recipes.rb +50 -0
- data/lib/delayed/serialization/active_record.rb +13 -0
- data/lib/delayed/tasks.rb +11 -0
- data/lib/delayed/worker.rb +182 -0
- data/lib/delayed/yaml_ext.rb +41 -0
- data/lib/delayed_job.rb +13 -0
- 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/recipes/delayed_job.rb +1 -0
- data/spec/active_record_job_spec.rb +36 -0
- data/spec/autoloaded/clazz.rb +7 -0
- data/spec/autoloaded/struct.rb +7 -0
- data/spec/database.yml +4 -0
- data/spec/message_sending_spec.rb +116 -0
- data/spec/performable_mailer_spec.rb +46 -0
- data/spec/performable_method_spec.rb +64 -0
- data/spec/sample_jobs.rb +73 -0
- data/spec/spec_helper.rb +58 -0
- data/spec/worker_spec.rb +37 -0
- data/spec/yaml_ext_spec.rb +31 -0
- metadata +221 -0
@@ -0,0 +1,41 @@
|
|
1
|
+
# These extensions allow properly serializing and autoloading of
|
2
|
+
# Classes, Modules and Structs
|
3
|
+
|
4
|
+
require 'yaml'
|
5
|
+
YAML::ENGINE.yamler = "syck" if defined?(YAML::ENGINE)
|
6
|
+
|
7
|
+
class Module
|
8
|
+
yaml_as "tag:ruby.yaml.org,2002:module"
|
9
|
+
|
10
|
+
def self.yaml_new(klass, tag, val)
|
11
|
+
val.constantize
|
12
|
+
end
|
13
|
+
|
14
|
+
def to_yaml( opts = {} )
|
15
|
+
YAML::quick_emit( nil, opts ) { |out|
|
16
|
+
out.scalar(taguri, self.name, :plain)
|
17
|
+
}
|
18
|
+
end
|
19
|
+
|
20
|
+
def yaml_tag_read_class(name)
|
21
|
+
# Constantize the object so that ActiveSupport can attempt
|
22
|
+
# its auto loading magic. Will raise LoadError if not successful.
|
23
|
+
name.constantize
|
24
|
+
name
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
|
29
|
+
class Class
|
30
|
+
yaml_as "tag:ruby.yaml.org,2002:class"
|
31
|
+
remove_method :to_yaml if respond_to?(:to_yaml) && method(:to_yaml).owner == Class # use Module's to_yaml
|
32
|
+
end
|
33
|
+
|
34
|
+
class Struct
|
35
|
+
def self.yaml_tag_read_class(name)
|
36
|
+
# Constantize the object so that ActiveSupport can attempt
|
37
|
+
# its auto loading magic. Will raise LoadError if not successful.
|
38
|
+
name.constantize
|
39
|
+
"Struct::#{ name }"
|
40
|
+
end
|
41
|
+
end
|
data/lib/delayed_job.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
require 'active_support'
|
2
|
+
|
3
|
+
require File.dirname(__FILE__) + '/delayed/message_sending'
|
4
|
+
require File.dirname(__FILE__) + '/delayed/performable_method'
|
5
|
+
require File.dirname(__FILE__) + '/delayed/performable_mailer' if defined?(ActionMailer)
|
6
|
+
require File.dirname(__FILE__) + '/delayed/yaml_ext'
|
7
|
+
require File.dirname(__FILE__) + '/delayed/backend/base'
|
8
|
+
require File.dirname(__FILE__) + '/delayed/worker'
|
9
|
+
require File.dirname(__FILE__) + '/delayed/deserialization_error'
|
10
|
+
require File.dirname(__FILE__) + '/delayed/railtie' if defined?(Rails::Railtie)
|
11
|
+
|
12
|
+
Object.send(:include, Delayed::MessageSending)
|
13
|
+
Module.send(:include, Delayed::MessageSending::ClassMethods)
|
@@ -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 @@
|
|
1
|
+
require File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib', 'delayed', 'recipes'))
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'delayed/backend/active_record'
|
3
|
+
|
4
|
+
describe Delayed::Backend::ActiveRecord::Job do
|
5
|
+
after do
|
6
|
+
Time.zone = nil
|
7
|
+
end
|
8
|
+
|
9
|
+
it_should_behave_like 'a delayed_job backend'
|
10
|
+
|
11
|
+
context "db_time_now" do
|
12
|
+
it "should return time in current time zone if set" do
|
13
|
+
Time.zone = 'Eastern Time (US & Canada)'
|
14
|
+
%w(EST EDT).should include(Delayed::Job.db_time_now.zone)
|
15
|
+
end
|
16
|
+
|
17
|
+
it "should return UTC time if that is the AR default" do
|
18
|
+
Time.zone = nil
|
19
|
+
ActiveRecord::Base.default_timezone = :utc
|
20
|
+
Delayed::Backend::ActiveRecord::Job.db_time_now.zone.should == 'UTC'
|
21
|
+
end
|
22
|
+
|
23
|
+
it "should return local time if that is the AR default" do
|
24
|
+
Time.zone = 'Central Time (US & Canada)'
|
25
|
+
ActiveRecord::Base.default_timezone = :local
|
26
|
+
%w(CST CDT).should include(Delayed::Backend::ActiveRecord::Job.db_time_now.zone)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
describe "after_fork" do
|
31
|
+
it "should call reconnect on the connection" do
|
32
|
+
ActiveRecord::Base.should_receive(:establish_connection)
|
33
|
+
Delayed::Backend::ActiveRecord::Job.after_fork
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
data/spec/database.yml
ADDED
@@ -0,0 +1,116 @@
|
|
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_name.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_name.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
|
+
|
89
|
+
class FairyTail
|
90
|
+
attr_accessor :happy_ending
|
91
|
+
def tell
|
92
|
+
@happy_ending = true
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
it "should not delay the job when delay_jobs is false" do
|
97
|
+
Delayed::Worker.delay_jobs = false
|
98
|
+
fairy_tail = FairyTail.new
|
99
|
+
lambda {
|
100
|
+
lambda {
|
101
|
+
fairy_tail.delay.tell
|
102
|
+
}.should change(fairy_tail, :happy_ending).from(nil).to(true)
|
103
|
+
}.should_not change { Delayed::Job.count }
|
104
|
+
end
|
105
|
+
|
106
|
+
it "should delay the job when delay_jobs is true" do
|
107
|
+
Delayed::Worker.delay_jobs = true
|
108
|
+
fairy_tail = FairyTail.new
|
109
|
+
lambda {
|
110
|
+
lambda {
|
111
|
+
fairy_tail.delay.tell
|
112
|
+
}.should_not change(fairy_tail, :happy_ending)
|
113
|
+
}.should change { Delayed::Job.count }.by(1)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
require 'action_mailer'
|
4
|
+
class MyMailer < ActionMailer::Base
|
5
|
+
def signup(email)
|
6
|
+
mail :to => email, :subject => "Delaying Emails"
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
describe ActionMailer::Base do
|
11
|
+
describe "delay" do
|
12
|
+
it "should enqueue a PerformableEmail job" do
|
13
|
+
lambda {
|
14
|
+
job = MyMailer.delay.signup('john@example.com')
|
15
|
+
job.payload_object.class.should == Delayed::PerformableMailer
|
16
|
+
job.payload_object.method_name.should == :signup
|
17
|
+
job.payload_object.args.should == ['john@example.com']
|
18
|
+
}.should change { Delayed::Job.count }.by(1)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
describe "delay on a mail object" do
|
23
|
+
it "should raise an exception" do
|
24
|
+
lambda {
|
25
|
+
MyMailer.signup('john@example.com').delay
|
26
|
+
}.should raise_error(RuntimeError)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
describe Delayed::PerformableMailer do
|
31
|
+
describe "perform" do
|
32
|
+
before do
|
33
|
+
@email = mock('email', :deliver => true)
|
34
|
+
@mailer_class = mock('MailerClass', :signup => @email)
|
35
|
+
@mailer = Delayed::PerformableMailer.new(@mailer_class, :signup, ['john@example.com'])
|
36
|
+
end
|
37
|
+
|
38
|
+
it "should call the method and #deliver on the mailer" do
|
39
|
+
@mailer_class.should_receive(:signup).with('john@example.com')
|
40
|
+
@email.should_receive(:deliver)
|
41
|
+
@mailer.perform
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Delayed::PerformableMethod do
|
4
|
+
describe "perform" do
|
5
|
+
before do
|
6
|
+
@method = Delayed::PerformableMethod.new("foo", :count, ['o'])
|
7
|
+
end
|
8
|
+
|
9
|
+
context "with the persisted record cannot be found" do
|
10
|
+
before do
|
11
|
+
@method.object = nil
|
12
|
+
end
|
13
|
+
|
14
|
+
it "should be a no-op if object is nil" do
|
15
|
+
lambda { @method.perform }.should_not raise_error
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
it "should call the method on the object" do
|
20
|
+
@method.object.should_receive(:count).with('o')
|
21
|
+
@method.perform
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
it "should raise a NoMethodError if target method doesn't exist" do
|
26
|
+
lambda {
|
27
|
+
Delayed::PerformableMethod.new(Object, :method_that_does_not_exist, [])
|
28
|
+
}.should raise_error(NoMethodError)
|
29
|
+
end
|
30
|
+
|
31
|
+
it "should not raise NoMethodError if target method is private" do
|
32
|
+
clazz = Class.new do
|
33
|
+
def private_method
|
34
|
+
end
|
35
|
+
private :private_method
|
36
|
+
end
|
37
|
+
lambda {
|
38
|
+
Delayed::PerformableMethod.new(clazz.new, :private_method, [])
|
39
|
+
}.should_not raise_error(NoMethodError)
|
40
|
+
end
|
41
|
+
|
42
|
+
describe "hooks" do
|
43
|
+
%w(enqueue before after success).each do |hook|
|
44
|
+
it "should delegate #{hook} hook to object" do
|
45
|
+
story = Story.new
|
46
|
+
story.should_receive(hook).with(an_instance_of(Delayed::Job))
|
47
|
+
story.delay.tell.invoke_job
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
it "should delegate error hook to object" do
|
52
|
+
story = Story.new
|
53
|
+
story.should_receive(:error).with(an_instance_of(Delayed::Job), an_instance_of(RuntimeError))
|
54
|
+
story.should_receive(:tell).and_raise(RuntimeError)
|
55
|
+
lambda { story.delay.tell.invoke_job }.should raise_error
|
56
|
+
end
|
57
|
+
|
58
|
+
it "should delegate failure hook to object" do
|
59
|
+
method = Delayed::PerformableMethod.new("object", :size, [])
|
60
|
+
method.object.should_receive(:failure)
|
61
|
+
method.failure
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
data/spec/sample_jobs.rb
ADDED
@@ -0,0 +1,73 @@
|
|
1
|
+
class NamedJob < Struct.new(:perform)
|
2
|
+
def display_name
|
3
|
+
'named_job'
|
4
|
+
end
|
5
|
+
end
|
6
|
+
|
7
|
+
class SimpleJob
|
8
|
+
cattr_accessor :runs; self.runs = 0
|
9
|
+
def perform; @@runs += 1; end
|
10
|
+
end
|
11
|
+
|
12
|
+
class ErrorJob
|
13
|
+
cattr_accessor :runs; self.runs = 0
|
14
|
+
def perform; raise 'did not work'; end
|
15
|
+
end
|
16
|
+
|
17
|
+
class CustomRescheduleJob < Struct.new(:offset)
|
18
|
+
cattr_accessor :runs; self.runs = 0
|
19
|
+
def perform; raise 'did not work'; end
|
20
|
+
def reschedule_at(time, attempts); time + offset; end
|
21
|
+
end
|
22
|
+
|
23
|
+
class LongRunningJob
|
24
|
+
def perform; sleep 250; end
|
25
|
+
end
|
26
|
+
|
27
|
+
class OnPermanentFailureJob < SimpleJob
|
28
|
+
def failure; end
|
29
|
+
def max_attempts; 1; end
|
30
|
+
end
|
31
|
+
|
32
|
+
module M
|
33
|
+
class ModuleJob
|
34
|
+
cattr_accessor :runs; self.runs = 0
|
35
|
+
def perform; @@runs += 1; end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
class CallbackJob
|
40
|
+
cattr_accessor :messages
|
41
|
+
|
42
|
+
def enqueue(job)
|
43
|
+
self.class.messages << 'enqueue'
|
44
|
+
end
|
45
|
+
|
46
|
+
def before(job)
|
47
|
+
self.class.messages << 'before'
|
48
|
+
end
|
49
|
+
|
50
|
+
def perform
|
51
|
+
self.class.messages << 'perform'
|
52
|
+
end
|
53
|
+
|
54
|
+
def after(job)
|
55
|
+
self.class.messages << 'after'
|
56
|
+
end
|
57
|
+
|
58
|
+
def success(job)
|
59
|
+
self.class.messages << 'success'
|
60
|
+
end
|
61
|
+
|
62
|
+
def error(job, error)
|
63
|
+
self.class.messages << "error: #{error.class}"
|
64
|
+
end
|
65
|
+
|
66
|
+
def failure(job)
|
67
|
+
self.class.messages << 'failure'
|
68
|
+
end
|
69
|
+
|
70
|
+
def completed(job)
|
71
|
+
self.class.messages << 'completed'
|
72
|
+
end
|
73
|
+
end
|