delayed_job 2.1.0.pre2 → 2.1.1
Sign up to get free protection for your applications and to get access to all the features.
- data/README.textile +78 -43
- data/lib/delayed/backend/active_record.rb +14 -29
- data/lib/delayed/backend/base.rb +66 -37
- data/lib/delayed/backend/shared_spec.rb +188 -191
- data/lib/delayed/command.rb +3 -2
- data/lib/delayed/deserialization_error.rb +4 -0
- data/lib/delayed/message_sending.rb +19 -11
- data/lib/delayed/performable_mailer.rb +21 -0
- data/lib/delayed/performable_method.rb +19 -15
- data/lib/delayed/recipes.rb +6 -2
- data/lib/delayed/serialization/active_record.rb +13 -0
- data/lib/delayed/worker.rb +31 -40
- data/lib/delayed_job.rb +4 -2
- data/spec/active_record_job_spec.rb +4 -4
- data/spec/database.yml +4 -0
- data/spec/message_sending_spec.rb +42 -4
- data/spec/performable_mailer_spec.rb +46 -0
- data/spec/performable_method_spec.rb +27 -11
- data/spec/sample_jobs.rb +27 -19
- data/spec/spec_helper.rb +10 -8
- metadata +57 -21
- data/generators/delayed_job/delayed_job_generator.rb +0 -22
- data/generators/delayed_job/templates/migration.rb +0 -21
- data/generators/delayed_job/templates/script +0 -5
- data/init.rb +0 -1
- data/rails/init.rb +0 -5
- data/tasks/jobs.rake +0 -1
data/README.textile
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
h1. Delayed::Job
|
2
2
|
|
3
|
-
Delated_job (or DJ) encapsulates the common pattern of asynchronously executing longer tasks in the background.
|
3
|
+
Delated_job (or DJ) encapsulates the common pattern of asynchronously executing longer tasks in the background.
|
4
4
|
|
5
5
|
It is a direct extraction from Shopify where the job table is responsible for a multitude of core tasks. Amongst those tasks are:
|
6
6
|
|
@@ -9,31 +9,17 @@ It is a direct extraction from Shopify where the job table is responsible for a
|
|
9
9
|
* http downloads
|
10
10
|
* updating smart collections
|
11
11
|
* updating solr, our search server, after product changes
|
12
|
-
* batch imports
|
13
|
-
* spam checks
|
12
|
+
* batch imports
|
13
|
+
* spam checks
|
14
14
|
|
15
15
|
h2. Installation
|
16
16
|
|
17
|
-
|
17
|
+
delayed_job 2.1 only supports Rails 3.0+. See the "2.0 branch":https://github.com/collectiveidea/delayed_job/tree/v2.0 for Rails 2.
|
18
18
|
|
19
|
-
|
20
|
-
config.gem 'delayed_job'
|
21
|
-
</pre>
|
22
|
-
|
23
|
-
Rake tasks are not automatically loaded from gems, so you'll need to add the following to your Rakefile:
|
19
|
+
To install, add delayed_job to your @Gemfile@ and run `bundle install`:
|
24
20
|
|
25
21
|
<pre>
|
26
|
-
|
27
|
-
require 'delayed/tasks'
|
28
|
-
rescue LoadError
|
29
|
-
STDERR.puts "Run `rake gems:install` to install delayed_job"
|
30
|
-
end
|
31
|
-
</pre>
|
32
|
-
|
33
|
-
To install as a plugin:
|
34
|
-
|
35
|
-
<pre>
|
36
|
-
script/plugin install git://github.com/collectiveidea/delayed_job.git
|
22
|
+
gem 'delayed_job'
|
37
23
|
</pre>
|
38
24
|
|
39
25
|
After delayed_job is installed, you will need to setup the backend.
|
@@ -42,12 +28,10 @@ h2. Backends
|
|
42
28
|
|
43
29
|
delayed_job supports multiple backends for storing the job queue. "See the wiki for other backends":http://wiki.github.com/collectiveidea/delayed_job/backends besides Active Record.
|
44
30
|
|
45
|
-
h3. Active Record
|
46
|
-
|
47
31
|
The default is Active Record, which requires a jobs table.
|
48
32
|
|
49
33
|
<pre>
|
50
|
-
$ script/generate delayed_job
|
34
|
+
$ script/rails generate delayed_job
|
51
35
|
$ rake db:migrate
|
52
36
|
</pre>
|
53
37
|
|
@@ -60,7 +44,7 @@ Call @.delay.method(params)@ on any object and it will be processed in the backg
|
|
60
44
|
Notifier.deliver_signup(@user)
|
61
45
|
|
62
46
|
# with delayed_job
|
63
|
-
Notifier.delay.
|
47
|
+
Notifier.delay.signup(@user)
|
64
48
|
</pre>
|
65
49
|
|
66
50
|
If a method should always be run in the background, you can call @#handle_asynchronously@ after the method declaration:
|
@@ -77,6 +61,39 @@ device = Device.new
|
|
77
61
|
device.deliver
|
78
62
|
</pre>
|
79
63
|
|
64
|
+
handle_asynchronously can take as options anything you can pass to delay. In addition the values can be Proc objects allowing call time evaluation of the value. For some examples:
|
65
|
+
|
66
|
+
<pre>
|
67
|
+
class LongTasks
|
68
|
+
def send_mailer
|
69
|
+
# Some other code
|
70
|
+
end
|
71
|
+
handle_asynchronously :send_mailer, :priority => 20
|
72
|
+
|
73
|
+
def in_the_future
|
74
|
+
# Some other code
|
75
|
+
end
|
76
|
+
# 5.minutes.from_now will be evaluated when in_the_future is called
|
77
|
+
handle_asynchronously :in_the_future, :run_at => Proc.new { 5.minutes.from_now }
|
78
|
+
|
79
|
+
def self.when_to_run
|
80
|
+
2.hours.from_now
|
81
|
+
end
|
82
|
+
|
83
|
+
def call_a_class_method
|
84
|
+
# Some other code
|
85
|
+
end
|
86
|
+
handle_asynchronously :call_a_class_method, :run_at => Proc.new { when_to_run }
|
87
|
+
|
88
|
+
attr_reader :how_important
|
89
|
+
|
90
|
+
def call_an_instance_method
|
91
|
+
# Some other code
|
92
|
+
end
|
93
|
+
handle_asynchronously :call_an_instance_method, :priority => Proc.new {|i| i.how_important }
|
94
|
+
end
|
95
|
+
</pre>
|
96
|
+
|
80
97
|
h2. Running Jobs
|
81
98
|
|
82
99
|
@script/delayed_job@ can be used to manage a background process which will start working off jobs. Make sure you've run `script/generate delayed_job`.
|
@@ -92,39 +109,61 @@ $ RAILS_ENV=production script/delayed_job stop
|
|
92
109
|
|
93
110
|
Workers can be running on any computer, as long as they have access to the database and their clock is in sync. Keep in mind that each worker will check the database at least every 5 seconds.
|
94
111
|
|
95
|
-
You can also invoke @rake jobs:work@ which will start working off jobs. You can cancel the rake task with @CTRL-C@.
|
112
|
+
You can also invoke @rake jobs:work@ which will start working off jobs. You can cancel the rake task with @CTRL-C@.
|
96
113
|
|
97
114
|
h2. Custom Jobs
|
98
115
|
|
99
|
-
Jobs are simple ruby objects with a method called perform. Any object which responds to perform can be stuffed into the jobs table. Job objects are serialized to yaml so that they can later be resurrected by the job runner.
|
116
|
+
Jobs are simple ruby objects with a method called perform. Any object which responds to perform can be stuffed into the jobs table. Job objects are serialized to yaml so that they can later be resurrected by the job runner.
|
100
117
|
|
101
118
|
<pre>
|
102
119
|
class NewsletterJob < Struct.new(:text, :emails)
|
103
120
|
def perform
|
104
121
|
emails.each { |e| NewsletterMailer.deliver_text_to_email(text, e) }
|
105
|
-
end
|
106
|
-
end
|
107
|
-
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
108
125
|
Delayed::Job.enqueue NewsletterJob.new('lorem ipsum...', Customers.find(:all).collect(&:email))
|
109
126
|
</pre>
|
110
127
|
|
111
|
-
|
128
|
+
h2. Hooks
|
129
|
+
|
130
|
+
You can define hooks on your job that will be called at different stages in the process:
|
112
131
|
|
113
132
|
<pre>
|
114
133
|
class ParanoidNewsletterJob < NewsletterJob
|
134
|
+
def enqueue(job)
|
135
|
+
record_stat 'newsletter_job/enqueue'
|
136
|
+
end
|
137
|
+
|
115
138
|
def perform
|
116
139
|
emails.each { |e| NewsletterMailer.deliver_text_to_email(text, e) }
|
117
|
-
end
|
140
|
+
end
|
141
|
+
|
142
|
+
def before(job)
|
143
|
+
record_stat 'newsletter_job/start'
|
144
|
+
end
|
118
145
|
|
119
|
-
def
|
146
|
+
def after(job)
|
147
|
+
record_stat 'newsletter_job/after'
|
148
|
+
end
|
149
|
+
|
150
|
+
def success(job)
|
151
|
+
record_stat 'newsletter_job/success'
|
152
|
+
end
|
153
|
+
|
154
|
+
def error(job, exception)
|
155
|
+
notify_hoptoad(exception)
|
156
|
+
end
|
157
|
+
|
158
|
+
def failure
|
120
159
|
page_sysadmin_in_the_middle_of_the_night
|
121
160
|
end
|
122
|
-
end
|
161
|
+
end
|
123
162
|
</pre>
|
124
163
|
|
125
164
|
h2. Gory Details
|
126
165
|
|
127
|
-
The library evolves around a delayed_jobs table which looks as follows:
|
166
|
+
The library evolves around a delayed_jobs table which looks as follows:
|
128
167
|
|
129
168
|
<pre>
|
130
169
|
create_table :delayed_jobs, :force => true do |table|
|
@@ -151,6 +190,8 @@ make sure your job doesn't exceed this time. You should set this to the longest
|
|
151
190
|
By default, it will delete failed jobs (and it always deletes successful jobs). If you want to keep failed jobs, set
|
152
191
|
Delayed::Worker.destroy_failed_jobs = false. The failed jobs will be marked with non-null failed_at.
|
153
192
|
|
193
|
+
By default all jobs are scheduled with priority = 0, which is top priority. You can change this by setting Delayed::Worker.default_priority to something else. Lower numbers have higher priority.
|
194
|
+
|
154
195
|
Here is an example of changing job parameters in Rails:
|
155
196
|
|
156
197
|
<pre>
|
@@ -173,19 +214,13 @@ h2. How to contribute
|
|
173
214
|
|
174
215
|
If you find what looks like a bug:
|
175
216
|
|
176
|
-
#
|
177
|
-
|
217
|
+
# Search the "mailing list":http://groups.google.com/group/delayed_job to see if anyone else had the same issue.
|
218
|
+
# Check the "GitHub issue tracker":http://github.com/collectiveidea/delayed_job/issues/ to see if anyone else has reported issue.
|
178
219
|
# If you don't see anything, create an issue with information on how to reproduce it.
|
179
220
|
|
180
221
|
If you want to contribute an enhancement or a fix:
|
181
222
|
|
182
223
|
# Fork the project on github.
|
183
|
-
http://github.com/collectiveidea/delayed_job/
|
184
224
|
# Make your changes with tests.
|
185
|
-
# Commit the changes without making changes to the Rakefile
|
225
|
+
# Commit the changes without making changes to the Rakefile or any other files that aren't related to your enhancement or fix
|
186
226
|
# Send a pull request.
|
187
|
-
|
188
|
-
h3. Changelog
|
189
|
-
|
190
|
-
See http://wiki.github.com/collectiveidea/delayed_job/changelog for a list of changes.
|
191
|
-
|
@@ -1,19 +1,5 @@
|
|
1
1
|
require 'active_record'
|
2
2
|
|
3
|
-
class ActiveRecord::Base
|
4
|
-
yaml_as "tag:ruby.yaml.org,2002:ActiveRecord"
|
5
|
-
|
6
|
-
def self.yaml_new(klass, tag, val)
|
7
|
-
klass.find(val['attributes']['id'])
|
8
|
-
rescue ActiveRecord::RecordNotFound
|
9
|
-
nil
|
10
|
-
end
|
11
|
-
|
12
|
-
def to_yaml_properties
|
13
|
-
['@attributes']
|
14
|
-
end
|
15
|
-
end
|
16
|
-
|
17
3
|
module Delayed
|
18
4
|
module Backend
|
19
5
|
module ActiveRecord
|
@@ -22,23 +8,20 @@ module Delayed
|
|
22
8
|
class Job < ::ActiveRecord::Base
|
23
9
|
include Delayed::Backend::Base
|
24
10
|
set_table_name :delayed_jobs
|
25
|
-
|
11
|
+
|
26
12
|
before_save :set_default_run_at
|
27
13
|
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
{:conditions => ['(run_at <= ? AND (locked_at IS NULL OR locked_at < ?) OR locked_by = ?) AND failed_at IS NULL', db_time_now, db_time_now - max_run_time, worker_name]}
|
36
|
-
}
|
37
|
-
named_scope :by_priority, :order => 'priority ASC, run_at ASC'
|
14
|
+
scope :ready_to_run, lambda {|worker_name, max_run_time|
|
15
|
+
where(['(run_at <= ? AND (locked_at IS NULL OR locked_at < ?) OR locked_by = ?) AND failed_at IS NULL', db_time_now, db_time_now - max_run_time, worker_name])
|
16
|
+
}
|
17
|
+
scope :by_priority, order('priority ASC, run_at ASC')
|
18
|
+
|
19
|
+
def self.before_fork
|
20
|
+
::ActiveRecord::Base.clear_all_connections!
|
38
21
|
end
|
39
22
|
|
40
23
|
def self.after_fork
|
41
|
-
::ActiveRecord::Base.
|
24
|
+
::ActiveRecord::Base.establish_connection
|
42
25
|
end
|
43
26
|
|
44
27
|
# When a worker is exiting, make sure we don't have any locked jobs.
|
@@ -51,7 +34,7 @@ module Delayed
|
|
51
34
|
scope = self.ready_to_run(worker_name, max_run_time)
|
52
35
|
scope = scope.scoped(:conditions => ['priority >= ?', Worker.min_priority]) if Worker.min_priority
|
53
36
|
scope = scope.scoped(:conditions => ['priority <= ?', Worker.max_priority]) if Worker.max_priority
|
54
|
-
|
37
|
+
|
55
38
|
::ActiveRecord::Base.silence do
|
56
39
|
scope.by_priority.all(:limit => limit)
|
57
40
|
end
|
@@ -70,8 +53,10 @@ module Delayed
|
|
70
53
|
self.class.update_all(["locked_at = ?", now], ["id = ? and locked_by = ?", id, worker])
|
71
54
|
end
|
72
55
|
if affected_rows == 1
|
73
|
-
self.locked_at
|
74
|
-
self.locked_by
|
56
|
+
self.locked_at = now
|
57
|
+
self.locked_by = worker
|
58
|
+
self.locked_at_will_change!
|
59
|
+
self.locked_by_will_change!
|
75
60
|
return true
|
76
61
|
else
|
77
62
|
return false
|
data/lib/delayed/backend/base.rb
CHANGED
@@ -1,91 +1,120 @@
|
|
1
1
|
module Delayed
|
2
2
|
module Backend
|
3
|
-
class DeserializationError < StandardError
|
4
|
-
end
|
5
|
-
|
6
3
|
module Base
|
7
4
|
def self.included(base)
|
8
5
|
base.extend ClassMethods
|
9
6
|
end
|
10
|
-
|
7
|
+
|
11
8
|
module ClassMethods
|
12
9
|
# Add a job to the queue
|
13
10
|
def enqueue(*args)
|
14
|
-
|
15
|
-
|
11
|
+
options = {
|
12
|
+
:priority => Delayed::Worker.default_priority
|
13
|
+
}
|
14
|
+
|
15
|
+
if args.size == 1 && args.first.is_a?(Hash)
|
16
|
+
options.merge!(args.first)
|
17
|
+
else
|
18
|
+
options[:payload_object] = args.shift
|
19
|
+
options[:priority] = args.first || options[:priority]
|
20
|
+
options[:run_at] = args[1]
|
21
|
+
end
|
22
|
+
|
23
|
+
unless options[:payload_object].respond_to?(:perform)
|
16
24
|
raise ArgumentError, 'Cannot enqueue items which do not respond to perform'
|
17
25
|
end
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
26
|
+
|
27
|
+
self.create(options).tap do |job|
|
28
|
+
job.hook(:enqueue)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def reserve(worker, max_run_time = Worker.max_run_time)
|
33
|
+
# We get up to 5 jobs from the db. In case we cannot get exclusive access to a job we try the next.
|
34
|
+
# this leads to a more even distribution of jobs across the worker processes
|
35
|
+
find_available(worker.name, 5, max_run_time).detect do |job|
|
36
|
+
job.lock_exclusively!(max_run_time, worker.name)
|
37
|
+
end
|
22
38
|
end
|
23
|
-
|
39
|
+
|
24
40
|
# Hook method that is called before a new worker is forked
|
25
41
|
def before_fork
|
26
42
|
end
|
27
|
-
|
43
|
+
|
28
44
|
# Hook method that is called after a new worker is forked
|
29
45
|
def after_fork
|
30
46
|
end
|
31
|
-
|
47
|
+
|
32
48
|
def work_off(num = 100)
|
33
49
|
warn "[DEPRECATION] `Delayed::Job.work_off` is deprecated. Use `Delayed::Worker.new.work_off instead."
|
34
50
|
Delayed::Worker.new.work_off(num)
|
35
51
|
end
|
36
52
|
end
|
37
|
-
|
38
|
-
ParseObjectFromYaml = /\!ruby\/\w+\:([^\s]+)/
|
39
53
|
|
40
54
|
def failed?
|
41
55
|
failed_at
|
42
56
|
end
|
43
57
|
alias_method :failed, :failed?
|
44
58
|
|
59
|
+
ParseObjectFromYaml = /\!ruby\/\w+\:([^\s]+)/
|
60
|
+
|
45
61
|
def name
|
46
|
-
@name ||=
|
47
|
-
|
48
|
-
|
49
|
-
|
62
|
+
@name ||= payload_object.respond_to?(:display_name) ?
|
63
|
+
payload_object.display_name :
|
64
|
+
payload_object.class.name
|
65
|
+
rescue DeserializationError
|
66
|
+
ParseObjectFromYaml.match(handler)[1]
|
50
67
|
end
|
51
68
|
|
52
69
|
def payload_object=(object)
|
70
|
+
@payload_object = object
|
53
71
|
self.handler = object.to_yaml
|
54
72
|
end
|
55
|
-
|
73
|
+
|
56
74
|
def payload_object
|
57
75
|
@payload_object ||= YAML.load(self.handler)
|
58
|
-
rescue TypeError, LoadError, NameError => e
|
59
|
-
|
60
|
-
|
76
|
+
rescue TypeError, LoadError, NameError, ArgumentError => e
|
77
|
+
raise DeserializationError,
|
78
|
+
"Job failed to load: #{e.message}. Handler: #{handler.inspect}"
|
61
79
|
end
|
62
80
|
|
63
|
-
# Moved into its own method so that new_relic can trace it.
|
64
81
|
def invoke_job
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
payload_object.after(self) if payload_object.respond_to?(:after)
|
74
|
-
end
|
82
|
+
hook :before
|
83
|
+
payload_object.perform
|
84
|
+
hook :success
|
85
|
+
rescue Exception => e
|
86
|
+
hook :error, e
|
87
|
+
raise e
|
88
|
+
ensure
|
89
|
+
hook :after
|
75
90
|
end
|
76
|
-
|
91
|
+
|
77
92
|
# Unlock this job (note: not saved to DB)
|
78
93
|
def unlock
|
79
94
|
self.locked_at = nil
|
80
95
|
self.locked_by = nil
|
81
96
|
end
|
82
|
-
|
97
|
+
|
98
|
+
def hook(name, *args)
|
99
|
+
if payload_object.respond_to?(name)
|
100
|
+
method = payload_object.method(name)
|
101
|
+
method.arity == 0 ? method.call : method.call(self, *args)
|
102
|
+
end
|
103
|
+
rescue DeserializationError
|
104
|
+
# do nothing
|
105
|
+
end
|
106
|
+
|
107
|
+
def reschedule_at
|
108
|
+
payload_object.respond_to?(:reschedule_at) ?
|
109
|
+
payload_object.reschedule_at(self.class.db_time_now, attempts) :
|
110
|
+
self.class.db_time_now + (attempts ** 4) + 5
|
111
|
+
end
|
112
|
+
|
83
113
|
protected
|
84
114
|
|
85
115
|
def set_default_run_at
|
86
116
|
self.run_at ||= self.class.db_time_now
|
87
117
|
end
|
88
|
-
|
89
118
|
end
|
90
119
|
end
|
91
120
|
end
|
@@ -12,156 +12,168 @@ shared_examples_for 'a delayed_job backend' do
|
|
12
12
|
SimpleJob.runs = 0
|
13
13
|
described_class.delete_all
|
14
14
|
end
|
15
|
-
|
15
|
+
|
16
16
|
it "should set run_at automatically if not set" do
|
17
17
|
described_class.create(:payload_object => ErrorJob.new ).run_at.should_not be_nil
|
18
18
|
end
|
19
19
|
|
20
20
|
it "should not set run_at automatically if already set" do
|
21
21
|
later = described_class.db_time_now + 5.minutes
|
22
|
-
described_class.create(:payload_object => ErrorJob.new, :run_at => later)
|
22
|
+
job = described_class.create(:payload_object => ErrorJob.new, :run_at => later)
|
23
|
+
job.run_at.should be_within(1).of(later)
|
23
24
|
end
|
24
|
-
|
25
|
+
|
25
26
|
describe "enqueue" do
|
26
|
-
|
27
|
-
|
28
|
-
|
27
|
+
context "with a hash" do
|
28
|
+
it "should raise ArgumentError when handler doesn't respond_to :perform" do
|
29
|
+
lambda { described_class.enqueue(:payload_object => Object.new) }.should raise_error(ArgumentError)
|
30
|
+
end
|
29
31
|
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
32
|
+
it "should be able to set priority" do
|
33
|
+
job = described_class.enqueue :payload_object => SimpleJob.new, :priority => 5
|
34
|
+
job.priority.should == 5
|
35
|
+
end
|
34
36
|
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
37
|
+
it "should use default priority" do
|
38
|
+
job = described_class.enqueue :payload_object => SimpleJob.new
|
39
|
+
job.priority.should == 99
|
40
|
+
end
|
39
41
|
|
40
|
-
|
41
|
-
|
42
|
-
|
42
|
+
it "should be able to set run_at" do
|
43
|
+
later = described_class.db_time_now + 5.minutes
|
44
|
+
job = described_class.enqueue :payload_object => SimpleJob.new, :run_at => later
|
45
|
+
job.run_at.should be_within(1).of(later)
|
46
|
+
end
|
43
47
|
end
|
44
48
|
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
end
|
49
|
+
context "with multiple arguments" do
|
50
|
+
it "should raise ArgumentError when handler doesn't respond_to :perform" do
|
51
|
+
lambda { described_class.enqueue(Object.new) }.should raise_error(ArgumentError)
|
52
|
+
end
|
50
53
|
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
54
|
+
it "should increase count after enqueuing items" do
|
55
|
+
described_class.enqueue SimpleJob.new
|
56
|
+
described_class.count.should == 1
|
57
|
+
end
|
58
|
+
|
59
|
+
it "should be able to set priority" do
|
60
|
+
@job = described_class.enqueue SimpleJob.new, 5
|
61
|
+
@job.priority.should == 5
|
62
|
+
end
|
63
|
+
|
64
|
+
it "should use default priority when it is not set" do
|
65
|
+
@job = described_class.enqueue SimpleJob.new
|
66
|
+
@job.priority.should == 99
|
67
|
+
end
|
68
|
+
|
69
|
+
it "should be able to set run_at" do
|
70
|
+
later = described_class.db_time_now + 5.minutes
|
71
|
+
@job = described_class.enqueue SimpleJob.new, 5, later
|
72
|
+
@job.run_at.should be_within(1).of(later)
|
73
|
+
end
|
74
|
+
|
75
|
+
it "should work with jobs in modules" do
|
76
|
+
M::ModuleJob.runs = 0
|
77
|
+
job = described_class.enqueue M::ModuleJob.new
|
78
|
+
lambda { job.invoke_job }.should change { M::ModuleJob.runs }.from(0).to(1)
|
79
|
+
end
|
55
80
|
end
|
56
81
|
end
|
57
|
-
|
82
|
+
|
58
83
|
describe "callbacks" do
|
59
84
|
before(:each) do
|
60
|
-
|
61
|
-
FailureCallbackJob.messages = []
|
85
|
+
CallbackJob.messages = []
|
62
86
|
end
|
63
|
-
|
87
|
+
|
88
|
+
%w(before success after).each do |callback|
|
89
|
+
it "should call #{callback} with job" do
|
90
|
+
job = described_class.enqueue(CallbackJob.new)
|
91
|
+
job.payload_object.should_receive(callback).with(job)
|
92
|
+
job.invoke_job
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
64
96
|
it "should call before and after callbacks" do
|
65
|
-
job = described_class.enqueue(
|
97
|
+
job = described_class.enqueue(CallbackJob.new)
|
98
|
+
CallbackJob.messages.should == ["enqueue"]
|
66
99
|
job.invoke_job
|
67
|
-
|
100
|
+
CallbackJob.messages.should == ["enqueue", "before", "perform", "success", "after"]
|
68
101
|
end
|
69
102
|
|
70
103
|
it "should call the after callback with an error" do
|
71
|
-
job = described_class.enqueue(
|
72
|
-
|
73
|
-
|
104
|
+
job = described_class.enqueue(CallbackJob.new)
|
105
|
+
job.payload_object.should_receive(:perform).and_raise(RuntimeError.new("fail"))
|
106
|
+
|
107
|
+
lambda { job.invoke_job }.should raise_error
|
108
|
+
CallbackJob.messages.should == ["enqueue", "before", "error: RuntimeError", "after"]
|
109
|
+
end
|
110
|
+
|
111
|
+
it "should call error when before raises an error" do
|
112
|
+
job = described_class.enqueue(CallbackJob.new)
|
113
|
+
job.payload_object.should_receive(:before).and_raise(RuntimeError.new("fail"))
|
114
|
+
lambda { job.invoke_job }.should raise_error(RuntimeError)
|
115
|
+
CallbackJob.messages.should == ["enqueue", "error: RuntimeError", "after"]
|
74
116
|
end
|
75
|
-
|
76
117
|
end
|
77
|
-
|
118
|
+
|
78
119
|
describe "payload_object" do
|
79
120
|
it "should raise a DeserializationError when the job class is totally unknown" do
|
80
121
|
job = described_class.new :handler => "--- !ruby/object:JobThatDoesNotExist {}"
|
81
|
-
lambda { job.payload_object }.should raise_error(Delayed::
|
122
|
+
lambda { job.payload_object }.should raise_error(Delayed::DeserializationError)
|
82
123
|
end
|
83
124
|
|
84
125
|
it "should raise a DeserializationError when the job struct is totally unknown" do
|
85
126
|
job = described_class.new :handler => "--- !ruby/struct:StructThatDoesNotExist {}"
|
86
|
-
lambda { job.payload_object }.should raise_error(Delayed::
|
127
|
+
lambda { job.payload_object }.should raise_error(Delayed::DeserializationError)
|
87
128
|
end
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
described_class.find_available('worker', 5, 1.second).should_not include(@job)
|
94
|
-
end
|
95
|
-
|
96
|
-
it "should not find jobs scheduled for the future" do
|
97
|
-
@job = create_job :run_at => (described_class.db_time_now + 1.minute)
|
98
|
-
described_class.find_available('worker', 5, 4.hours).should_not include(@job)
|
99
|
-
end
|
100
|
-
|
101
|
-
it "should not find jobs locked by another worker" do
|
102
|
-
@job = create_job(:locked_by => 'other_worker', :locked_at => described_class.db_time_now - 1.minute)
|
103
|
-
described_class.find_available('worker', 5, 4.hours).should_not include(@job)
|
104
|
-
end
|
105
|
-
|
106
|
-
it "should find open jobs" do
|
107
|
-
@job = create_job
|
108
|
-
described_class.find_available('worker', 5, 4.hours).should include(@job)
|
109
|
-
end
|
110
|
-
|
111
|
-
it "should find expired jobs" do
|
112
|
-
@job = create_job(:locked_by => 'worker', :locked_at => described_class.db_time_now - 2.minutes)
|
113
|
-
described_class.find_available('worker', 5, 1.minute).should include(@job)
|
114
|
-
end
|
115
|
-
|
116
|
-
it "should find own jobs" do
|
117
|
-
@job = create_job(:locked_by => 'worker', :locked_at => (described_class.db_time_now - 1.minutes))
|
118
|
-
described_class.find_available('worker', 5, 4.hours).should include(@job)
|
119
|
-
end
|
120
|
-
|
121
|
-
it "should find only the right amount of jobs" do
|
122
|
-
10.times { create_job }
|
123
|
-
described_class.find_available('worker', 7, 4.hours).should have(7).jobs
|
129
|
+
|
130
|
+
it "should raise a DeserializationError when the YAML.load raises argument error" do
|
131
|
+
job = described_class.find(create_job.id)
|
132
|
+
YAML.should_receive(:load).and_raise(ArgumentError)
|
133
|
+
lambda { job.payload_object }.should raise_error(Delayed::DeserializationError)
|
124
134
|
end
|
125
135
|
end
|
126
|
-
|
127
|
-
|
128
|
-
before
|
129
|
-
|
136
|
+
|
137
|
+
describe "reserve" do
|
138
|
+
before do
|
139
|
+
Delayed::Worker.max_run_time = 2.minutes
|
140
|
+
@worker = Delayed::Worker.new(:quiet => true)
|
130
141
|
end
|
131
142
|
|
132
|
-
it "should not
|
133
|
-
|
143
|
+
it "should not reserve failed jobs" do
|
144
|
+
create_job :attempts => 50, :failed_at => described_class.db_time_now
|
145
|
+
described_class.reserve(@worker).should be_nil
|
134
146
|
end
|
135
147
|
|
136
|
-
it "should
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
it "should be able to get access to the task if it was started more then max_age ago" do
|
141
|
-
@job.locked_at = described_class.db_time_now - 5.hours
|
142
|
-
@job.save
|
148
|
+
it "should not reserve jobs scheduled for the future" do
|
149
|
+
create_job :run_at => (described_class.db_time_now + 1.minute)
|
150
|
+
described_class.reserve(@worker).should be_nil
|
151
|
+
end
|
143
152
|
|
144
|
-
|
145
|
-
|
146
|
-
@
|
147
|
-
|
153
|
+
it "should lock the job so other workers can't reserve it" do
|
154
|
+
job = create_job
|
155
|
+
described_class.reserve(@worker).should == job
|
156
|
+
new_worker = Delayed::Worker.new(:quiet => true)
|
157
|
+
new_worker.name = 'worker2'
|
158
|
+
described_class.reserve(new_worker).should be_nil
|
148
159
|
end
|
149
160
|
|
150
|
-
it "should
|
151
|
-
|
161
|
+
it "should reserve open jobs" do
|
162
|
+
job = create_job
|
163
|
+
described_class.reserve(@worker).should == job
|
152
164
|
end
|
153
165
|
|
154
|
-
it "should
|
155
|
-
|
166
|
+
it "should reserve expired jobs" do
|
167
|
+
job = create_job(:locked_by => @worker.name, :locked_at => described_class.db_time_now - 3.minutes)
|
168
|
+
described_class.reserve(@worker).should == job
|
156
169
|
end
|
157
170
|
|
158
|
-
it "should
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
end
|
171
|
+
it "should reserve own jobs" do
|
172
|
+
job = create_job(:locked_by => @worker.name, :locked_at => (described_class.db_time_now - 1.minutes))
|
173
|
+
described_class.reserve(@worker).should == job
|
174
|
+
end
|
163
175
|
end
|
164
|
-
|
176
|
+
|
165
177
|
context "when another worker has worked on a task since the job was found to be available, it" do
|
166
178
|
|
167
179
|
before :each do
|
@@ -184,7 +196,7 @@ shared_examples_for 'a delayed_job backend' do
|
|
184
196
|
it "should be the class name of the job that was enqueued" do
|
185
197
|
described_class.create(:payload_object => ErrorJob.new ).name.should == 'ErrorJob'
|
186
198
|
end
|
187
|
-
|
199
|
+
|
188
200
|
it "should be the method that will be called if its a performable method object" do
|
189
201
|
job = described_class.new(:payload_object => NamedJob.new)
|
190
202
|
job.name.should == 'named_job'
|
@@ -194,8 +206,15 @@ shared_examples_for 'a delayed_job backend' do
|
|
194
206
|
@job = Story.create(:text => "...").delay.save
|
195
207
|
@job.name.should == 'Story#save'
|
196
208
|
end
|
209
|
+
|
210
|
+
it "should parse from handler on deserialization error" do
|
211
|
+
job = Story.create(:text => "...").delay.text
|
212
|
+
job.payload_object.object.destroy
|
213
|
+
job = described_class.find(job.id)
|
214
|
+
job.name.should == 'Delayed::PerformableMethod'
|
215
|
+
end
|
197
216
|
end
|
198
|
-
|
217
|
+
|
199
218
|
context "worker prioritization" do
|
200
219
|
before(:each) do
|
201
220
|
Delayed::Worker.max_priority = nil
|
@@ -206,7 +225,7 @@ shared_examples_for 'a delayed_job backend' do
|
|
206
225
|
10.times { described_class.enqueue SimpleJob.new, rand(10) }
|
207
226
|
jobs = described_class.find_available('worker', 10)
|
208
227
|
jobs.size.should == 10
|
209
|
-
jobs.each_cons(2) do |a, b|
|
228
|
+
jobs.each_cons(2) do |a, b|
|
210
229
|
a.priority.should <= b.priority
|
211
230
|
end
|
212
231
|
end
|
@@ -227,23 +246,23 @@ shared_examples_for 'a delayed_job backend' do
|
|
227
246
|
jobs.each {|job| job.priority.should <= max}
|
228
247
|
end
|
229
248
|
end
|
230
|
-
|
249
|
+
|
231
250
|
context "clear_locks!" do
|
232
251
|
before do
|
233
252
|
@job = create_job(:locked_by => 'worker', :locked_at => described_class.db_time_now)
|
234
253
|
end
|
235
|
-
|
254
|
+
|
236
255
|
it "should clear locks for the given worker" do
|
237
256
|
described_class.clear_locks!('worker')
|
238
257
|
described_class.find_available('worker2', 5, 1.minute).should include(@job)
|
239
258
|
end
|
240
|
-
|
259
|
+
|
241
260
|
it "should not clear locks for other workers" do
|
242
261
|
described_class.clear_locks!('worker1')
|
243
262
|
described_class.find_available('worker1', 5, 1.minute).should_not include(@job)
|
244
263
|
end
|
245
264
|
end
|
246
|
-
|
265
|
+
|
247
266
|
context "unlock" do
|
248
267
|
before do
|
249
268
|
@job = create_job(:locked_by => 'worker', :locked_at => described_class.db_time_now)
|
@@ -255,13 +274,13 @@ shared_examples_for 'a delayed_job backend' do
|
|
255
274
|
@job.locked_at.should be_nil
|
256
275
|
end
|
257
276
|
end
|
258
|
-
|
277
|
+
|
259
278
|
context "large handler" do
|
260
279
|
before do
|
261
280
|
text = "Lorem ipsum dolor sit amet. " * 1000
|
262
281
|
@job = described_class.enqueue Delayed::PerformableMethod.new(text, :length, {})
|
263
282
|
end
|
264
|
-
|
283
|
+
|
265
284
|
it "should have an id" do
|
266
285
|
@job.id.should_not be_nil
|
267
286
|
end
|
@@ -269,18 +288,19 @@ shared_examples_for 'a delayed_job backend' do
|
|
269
288
|
|
270
289
|
describe "yaml serialization" do
|
271
290
|
it "should reload changed attributes" do
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
job.
|
276
|
-
YAML.load(yaml).priority.should == 99
|
291
|
+
story = Story.create(:text => 'hello')
|
292
|
+
job = story.delay.tell
|
293
|
+
story.update_attributes :text => 'goodbye'
|
294
|
+
described_class.find(job.id).payload_object.object.text.should == 'goodbye'
|
277
295
|
end
|
278
296
|
|
279
|
-
it "should
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
lambda {
|
297
|
+
it "should raise deserialization error for destroyed records" do
|
298
|
+
story = Story.create(:text => 'hello')
|
299
|
+
job = story.delay.tell
|
300
|
+
story.destroy
|
301
|
+
lambda {
|
302
|
+
described_class.find(job.id).payload_object
|
303
|
+
}.should raise_error(Delayed::DeserializationError)
|
284
304
|
end
|
285
305
|
end
|
286
306
|
|
@@ -306,55 +326,15 @@ shared_examples_for 'a delayed_job backend' do
|
|
306
326
|
Delayed::Worker.max_run_time = old_max_run_time
|
307
327
|
end
|
308
328
|
end
|
309
|
-
end
|
310
|
-
|
311
|
-
context "worker prioritization" do
|
312
|
-
before(:each) do
|
313
|
-
@worker = Delayed::Worker.new(:max_priority => 5, :min_priority => -5, :quiet => true)
|
314
|
-
end
|
315
|
-
|
316
|
-
it "should only work_off jobs that are >= min_priority" do
|
317
|
-
create_job(:priority => -10)
|
318
|
-
create_job(:priority => 0)
|
319
|
-
@worker.work_off
|
320
|
-
|
321
|
-
SimpleJob.runs.should == 1
|
322
|
-
end
|
323
|
-
|
324
|
-
it "should only work_off jobs that are <= max_priority" do
|
325
|
-
create_job(:priority => 10)
|
326
|
-
create_job(:priority => 0)
|
327
|
-
|
328
|
-
@worker.work_off
|
329
|
-
|
330
|
-
SimpleJob.runs.should == 1
|
331
|
-
end
|
332
|
-
end
|
333
329
|
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
end
|
343
|
-
|
344
|
-
it "should run open jobs" do
|
345
|
-
create_job
|
346
|
-
lambda { @worker.work_off }.should change { SimpleJob.runs }.from(0).to(1)
|
347
|
-
end
|
348
|
-
|
349
|
-
it "should run expired jobs" do
|
350
|
-
expired_time = Delayed::Job.db_time_now - (1.minutes + Delayed::Worker.max_run_time)
|
351
|
-
create_job(:locked_by => 'other_worker', :locked_at => expired_time)
|
352
|
-
lambda { @worker.work_off }.should change { SimpleJob.runs }.from(0).to(1)
|
353
|
-
end
|
354
|
-
|
355
|
-
it "should run own jobs" do
|
356
|
-
create_job(:locked_by => @worker.name, :locked_at => (Delayed::Job.db_time_now - 1.minutes))
|
357
|
-
lambda { @worker.work_off }.should change { SimpleJob.runs }.from(0).to(1)
|
330
|
+
context "when the job raises a deserialization error" do
|
331
|
+
it "should mark the job as failed" do
|
332
|
+
Delayed::Worker.destroy_failed_jobs = false
|
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
|
358
338
|
end
|
359
339
|
end
|
360
340
|
|
@@ -364,7 +344,7 @@ shared_examples_for 'a delayed_job backend' do
|
|
364
344
|
Delayed::Worker.destroy_failed_jobs = true
|
365
345
|
Delayed::Worker.max_attempts = 25
|
366
346
|
|
367
|
-
@job = Delayed::Job.enqueue
|
347
|
+
@job = Delayed::Job.enqueue(ErrorJob.new)
|
368
348
|
end
|
369
349
|
|
370
350
|
it "should record last_error when destroy_failed_jobs = false, max_attempts = 1" do
|
@@ -376,15 +356,32 @@ shared_examples_for 'a delayed_job backend' do
|
|
376
356
|
@job.attempts.should == 1
|
377
357
|
@job.failed_at.should_not be_nil
|
378
358
|
end
|
379
|
-
|
359
|
+
|
380
360
|
it "should re-schedule jobs after failing" do
|
381
|
-
@worker.
|
361
|
+
@worker.work_off
|
382
362
|
@job.reload
|
383
363
|
@job.last_error.should =~ /did not work/
|
384
364
|
@job.last_error.should =~ /sample_jobs.rb:\d+:in `perform'/
|
385
365
|
@job.attempts.should == 1
|
386
366
|
@job.run_at.should > Delayed::Job.db_time_now - 10.minutes
|
387
367
|
@job.run_at.should < Delayed::Job.db_time_now + 10.minutes
|
368
|
+
@job.locked_by.should be_nil
|
369
|
+
@job.locked_at.should be_nil
|
370
|
+
end
|
371
|
+
|
372
|
+
it 'should re-schedule with handler provided time if present' do
|
373
|
+
@job = Delayed::Job.enqueue(CustomRescheduleJob.new(99.minutes))
|
374
|
+
@worker.run(@job)
|
375
|
+
@job.reload
|
376
|
+
|
377
|
+
(Delayed::Job.db_time_now + 99.minutes - @job.run_at).abs.should < 1
|
378
|
+
end
|
379
|
+
|
380
|
+
it "should not fail when the triggered error doesn't have a message" do
|
381
|
+
error_with_nil_message = StandardError.new
|
382
|
+
error_with_nil_message.stub!(:message).and_return nil
|
383
|
+
@job.stub!(:invoke_job).and_raise error_with_nil_message
|
384
|
+
lambda{@worker.run(@job)}.should_not raise_error
|
388
385
|
end
|
389
386
|
end
|
390
387
|
|
@@ -392,35 +389,35 @@ shared_examples_for 'a delayed_job backend' do
|
|
392
389
|
before do
|
393
390
|
@job = Delayed::Job.create :payload_object => SimpleJob.new
|
394
391
|
end
|
395
|
-
|
392
|
+
|
396
393
|
share_examples_for "any failure more than Worker.max_attempts times" do
|
397
|
-
context "when the job's payload has
|
394
|
+
context "when the job's payload has a #failure hook" do
|
398
395
|
before do
|
399
396
|
@job = Delayed::Job.create :payload_object => OnPermanentFailureJob.new
|
400
|
-
@job.payload_object.should respond_to :
|
397
|
+
@job.payload_object.should respond_to :failure
|
401
398
|
end
|
402
399
|
|
403
400
|
it "should run that hook" do
|
404
|
-
@job.payload_object.should_receive :
|
401
|
+
@job.payload_object.should_receive :failure
|
405
402
|
Delayed::Worker.max_attempts.times { @worker.reschedule(@job) }
|
406
403
|
end
|
407
404
|
end
|
408
405
|
|
409
|
-
context "when the job's payload has no #
|
410
|
-
# It's a little tricky to test this in a straightforward way,
|
411
|
-
# because putting a should_not_receive expectation on
|
412
|
-
# @job.payload_object.
|
413
|
-
# incorrectly return true to
|
414
|
-
# payload_object.respond_to? :
|
415
|
-
# reschedule uses to decide whether to call
|
416
|
-
# So instead, we just make sure that the payload_object as it
|
417
|
-
# already stands doesn't respond_to?
|
406
|
+
context "when the job's payload has no #failure hook" do
|
407
|
+
# It's a little tricky to test this in a straightforward way,
|
408
|
+
# because putting a should_not_receive expectation on
|
409
|
+
# @job.payload_object.failure makes that object
|
410
|
+
# incorrectly return true to
|
411
|
+
# payload_object.respond_to? :failure, which is what
|
412
|
+
# reschedule uses to decide whether to call failure.
|
413
|
+
# So instead, we just make sure that the payload_object as it
|
414
|
+
# already stands doesn't respond_to? failure, then
|
418
415
|
# shove it through the iterated reschedule loop and make sure we
|
419
416
|
# don't get a NoMethodError (caused by calling that nonexistent
|
420
|
-
#
|
421
|
-
|
417
|
+
# failure method).
|
418
|
+
|
422
419
|
before do
|
423
|
-
@job.payload_object.should_not respond_to(:
|
420
|
+
@job.payload_object.should_not respond_to(:failure)
|
424
421
|
end
|
425
422
|
|
426
423
|
it "should not try to run that hook" do
|
@@ -442,18 +439,18 @@ shared_examples_for 'a delayed_job backend' do
|
|
442
439
|
@job.should_receive(:destroy)
|
443
440
|
Delayed::Worker.max_attempts.times { @worker.reschedule(@job) }
|
444
441
|
end
|
445
|
-
|
442
|
+
|
446
443
|
it "should not be destroyed if failed fewer than Worker.max_attempts times" do
|
447
444
|
@job.should_not_receive(:destroy)
|
448
445
|
(Delayed::Worker.max_attempts - 1).times { @worker.reschedule(@job) }
|
449
446
|
end
|
450
447
|
end
|
451
|
-
|
448
|
+
|
452
449
|
context "and we don't want to destroy jobs" do
|
453
450
|
before do
|
454
451
|
Delayed::Worker.destroy_failed_jobs = false
|
455
452
|
end
|
456
|
-
|
453
|
+
|
457
454
|
it_should_behave_like "any failure more than Worker.max_attempts times"
|
458
455
|
|
459
456
|
it "should be failed if it failed more than Worker.max_attempts times" do
|