delayed_job 1.8.4 → 1.8.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/README.textile +7 -7
- data/Rakefile +1 -1
- data/VERSION +1 -1
- data/contrib/delayed_job.monitrc +2 -2
- data/delayed_job.gemspec +5 -4
- data/generators/delayed_job/templates/migration.rb +1 -1
- data/lib/delayed/command.rb +4 -1
- data/lib/delayed/job.rb +31 -11
- data/lib/delayed/performable_method.rb +1 -1
- data/lib/delayed/recipes.rb +7 -3
- data/lib/delayed/worker.rb +4 -2
- data/spec/database.rb +1 -0
- data/spec/job_spec.rb +26 -13
- metadata +2 -2
data/README.textile
CHANGED
@@ -17,8 +17,7 @@ h2. Installation
|
|
17
17
|
To install as a gem, add the following to @config/environment.rb@:
|
18
18
|
|
19
19
|
<pre>
|
20
|
-
config.gem '
|
21
|
-
:source => 'http://gems.github.com'
|
20
|
+
config.gem 'delayed_job'
|
22
21
|
</pre>
|
23
22
|
|
24
23
|
Rake tasks are not automatically loaded from gems, so you'll need to add the following to your Rakefile:
|
@@ -118,7 +117,7 @@ The library evolves around a delayed_jobs table which looks as follows:
|
|
118
117
|
table.integer :attempts, :default => 0 # Provides for retries, but still fail eventually.
|
119
118
|
table.text :handler # YAML-encoded string of the object that will do work
|
120
119
|
table.text :last_error # reason for last failure (See Note below)
|
121
|
-
table.datetime :run_at # When to run. Could be Time.now for immediately, or sometime in the future.
|
120
|
+
table.datetime :run_at # When to run. Could be Time.zone.now for immediately, or sometime in the future.
|
122
121
|
table.datetime :locked_at # Set when a client is working on this object
|
123
122
|
table.datetime :failed_at # Set when all retries have failed (actually, by default, the record is deleted instead)
|
124
123
|
table.string :locked_by # Who is working on this object (if locked)
|
@@ -127,10 +126,10 @@ The library evolves around a delayed_jobs table which looks as follows:
|
|
127
126
|
|
128
127
|
On failure, the job is scheduled again in 5 seconds + N ** 4, where N is the number of retries.
|
129
128
|
|
130
|
-
The default
|
129
|
+
The default Job::max_attempts is 25. After this, the job either deleted (default), or left in the database with "failed_at" set.
|
131
130
|
With the default of 25 attempts, the last retry will be 20 days later, with the last interval being almost 100 hours.
|
132
131
|
|
133
|
-
The default
|
132
|
+
The default Job::max_run_time is 4.hours. If your job takes longer than that, another computer could pick it up. It's up to you to
|
134
133
|
make sure your job doesn't exceed this time. You should set this to the longest time you think the job could take.
|
135
134
|
|
136
135
|
By default, it will delete failed jobs (and it always deletes successful jobs). If you want to keep failed jobs, set
|
@@ -142,8 +141,9 @@ Here is an example of changing job parameters in Rails:
|
|
142
141
|
# config/initializers/delayed_job_config.rb
|
143
142
|
Delayed::Job.destroy_failed_jobs = false
|
144
143
|
silence_warnings do
|
145
|
-
Delayed::
|
146
|
-
Delayed::Job
|
144
|
+
Delayed::Worker::sleep_delay = 60
|
145
|
+
Delayed::Job::max_attempts = 3
|
146
|
+
Delayed::Job::max_run_time = 5.minutes
|
147
147
|
end
|
148
148
|
</pre>
|
149
149
|
|
data/Rakefile
CHANGED
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
1.8.
|
1
|
+
1.8.5
|
data/contrib/delayed_job.monitrc
CHANGED
@@ -10,5 +10,5 @@
|
|
10
10
|
|
11
11
|
check process delayed_job
|
12
12
|
with pidfile /var/www/apps/{app_name}/shared/pids/delayed_job.pid
|
13
|
-
start program = "RAILS_ENV=production /var/www/apps/{app_name}/current/script/delayed_job start"
|
14
|
-
stop program = "RAILS_ENV=production /var/www/apps/{app_name}/current/script/delayed_job stop"
|
13
|
+
start program = "/usr/bin/env RAILS_ENV=production /var/www/apps/{app_name}/current/script/delayed_job start"
|
14
|
+
stop program = "/usr/bin/env RAILS_ENV=production /var/www/apps/{app_name}/current/script/delayed_job stop"
|
data/delayed_job.gemspec
CHANGED
@@ -1,15 +1,15 @@
|
|
1
1
|
# Generated by jeweler
|
2
|
-
# DO NOT EDIT THIS FILE
|
3
|
-
# Instead, edit Jeweler::Tasks in Rakefile, and run
|
2
|
+
# DO NOT EDIT THIS FILE DIRECTLY
|
3
|
+
# Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
|
4
4
|
# -*- encoding: utf-8 -*-
|
5
5
|
|
6
6
|
Gem::Specification.new do |s|
|
7
7
|
s.name = %q{delayed_job}
|
8
|
-
s.version = "1.8.
|
8
|
+
s.version = "1.8.5"
|
9
9
|
|
10
10
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
11
|
s.authors = ["Brandon Keepers", "Tobias L\303\274tke"]
|
12
|
-
s.date = %q{
|
12
|
+
s.date = %q{2010-03-15}
|
13
13
|
s.description = %q{Delayed_job (or DJ) encapsulates the common pattern of asynchronously executing longer tasks in the background. It is a direct extraction from Shopify where the job table is responsible for a multitude of core tasks.}
|
14
14
|
s.email = %q{tobi@leetsoft.com}
|
15
15
|
s.extra_rdoc_files = [
|
@@ -64,3 +64,4 @@ Gem::Specification.new do |s|
|
|
64
64
|
else
|
65
65
|
end
|
66
66
|
end
|
67
|
+
|
@@ -5,7 +5,7 @@ class CreateDelayedJobs < ActiveRecord::Migration
|
|
5
5
|
table.integer :attempts, :default => 0 # Provides for retries, but still fail eventually.
|
6
6
|
table.text :handler # YAML-encoded string of the object that will do work
|
7
7
|
table.text :last_error # reason for last failure (See Note below)
|
8
|
-
table.datetime :run_at # When to run. Could be Time.now for immediately, or sometime in the future.
|
8
|
+
table.datetime :run_at # When to run. Could be Time.zone.now for immediately, or sometime in the future.
|
9
9
|
table.datetime :locked_at # Set when a client is working on this object
|
10
10
|
table.datetime :failed_at # Set when all retries have failed (actually, by default, the record is deleted instead)
|
11
11
|
table.string :locked_by # Who is working on this object (if locked)
|
data/lib/delayed/command.rb
CHANGED
@@ -54,13 +54,16 @@ module Delayed
|
|
54
54
|
# Re-open file handles
|
55
55
|
@files_to_reopen.each do |file|
|
56
56
|
begin
|
57
|
-
file.reopen File.join(RAILS_ROOT, 'log', 'delayed_job.log'), '
|
57
|
+
file.reopen File.join(RAILS_ROOT, 'log', 'delayed_job.log'), 'a+'
|
58
58
|
file.sync = true
|
59
59
|
rescue ::Exception
|
60
60
|
end
|
61
61
|
end
|
62
62
|
|
63
63
|
Delayed::Worker.logger = Rails.logger
|
64
|
+
if Delayed::Worker.logger.respond_to? :auto_flushing=
|
65
|
+
Delayed::Worker.logger.auto_flushing = true
|
66
|
+
end
|
64
67
|
ActiveRecord::Base.connection.reconnect!
|
65
68
|
|
66
69
|
Delayed::Job.worker_name = "#{worker_name} #{Delayed::Job.worker_name}"
|
data/lib/delayed/job.rb
CHANGED
@@ -8,8 +8,11 @@ module Delayed
|
|
8
8
|
# A job object that is persisted to the database.
|
9
9
|
# Contains the work object as a YAML field.
|
10
10
|
class Job < ActiveRecord::Base
|
11
|
-
|
12
|
-
|
11
|
+
@@max_attempts = 25
|
12
|
+
@@max_run_time = 4.hours
|
13
|
+
|
14
|
+
cattr_accessor :max_attempts, :max_run_time
|
15
|
+
|
13
16
|
set_table_name :delayed_jobs
|
14
17
|
|
15
18
|
# By default failed jobs are destroyed after too many attempts.
|
@@ -21,8 +24,24 @@ module Delayed
|
|
21
24
|
# Every worker has a unique name which by default is the pid of the process.
|
22
25
|
# There are some advantages to overriding this with something which survives worker retarts:
|
23
26
|
# Workers can safely resume working on tasks which are locked by themselves. The worker will assume that it crashed before.
|
24
|
-
|
25
|
-
|
27
|
+
@@worker_name = nil
|
28
|
+
|
29
|
+
def self.worker_name
|
30
|
+
return @@worker_name unless @@worker_name.nil?
|
31
|
+
"host:#{Socket.gethostname} pid:#{Process.pid}" rescue "pid:#{Process.pid}"
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.worker_name=(val)
|
35
|
+
@@worker_name = val
|
36
|
+
end
|
37
|
+
|
38
|
+
def worker_name
|
39
|
+
self.class.worker_name
|
40
|
+
end
|
41
|
+
|
42
|
+
def worker_name=(val)
|
43
|
+
@@worker_name = val
|
44
|
+
end
|
26
45
|
|
27
46
|
NextTaskSQL = '(run_at <= ? AND (locked_at IS NULL OR locked_at < ?) OR (locked_by = ?)) AND failed_at IS NULL'
|
28
47
|
NextTaskOrder = 'priority DESC, run_at ASC'
|
@@ -65,16 +84,17 @@ module Delayed
|
|
65
84
|
# Reschedule the job in the future (when a job fails).
|
66
85
|
# Uses an exponential scale depending on the number of failed attempts.
|
67
86
|
def reschedule(message, backtrace = [], time = nil)
|
68
|
-
|
87
|
+
self.last_error = message + "\n" + backtrace.join("\n")
|
88
|
+
|
89
|
+
if (self.attempts += 1) < max_attempts
|
69
90
|
time ||= Job.db_time_now + (attempts ** 4) + 5
|
70
91
|
|
71
92
|
self.run_at = time
|
72
|
-
self.last_error = message + "\n" + backtrace.join("\n")
|
73
93
|
self.unlock
|
74
94
|
save!
|
75
95
|
else
|
76
|
-
logger.info "* [JOB] PERMANENTLY removing #{self.name} because of #{attempts}
|
77
|
-
destroy_failed_jobs ? destroy : update_attribute(:failed_at,
|
96
|
+
logger.info "* [JOB] PERMANENTLY removing #{self.name} because of #{attempts} consecutive failures."
|
97
|
+
destroy_failed_jobs ? destroy : update_attribute(:failed_at, Delayed::Job.db_time_now)
|
78
98
|
end
|
79
99
|
end
|
80
100
|
|
@@ -118,7 +138,7 @@ module Delayed
|
|
118
138
|
end
|
119
139
|
|
120
140
|
# Find a few candidate jobs to run (in case some immediately get locked by others).
|
121
|
-
def self.find_available(limit = 5, max_run_time =
|
141
|
+
def self.find_available(limit = 5, max_run_time = max_run_time)
|
122
142
|
|
123
143
|
time_now = db_time_now
|
124
144
|
|
@@ -145,7 +165,7 @@ module Delayed
|
|
145
165
|
|
146
166
|
# Run the next job we can get an exclusive lock on.
|
147
167
|
# If no jobs are left we return nil
|
148
|
-
def self.reserve_and_run_one_job(max_run_time =
|
168
|
+
def self.reserve_and_run_one_job(max_run_time = max_run_time)
|
149
169
|
|
150
170
|
# We get up to 5 jobs from the db. In case we cannot get exclusive access to a job we try the next.
|
151
171
|
# this leads to a more even distribution of jobs across the worker processes
|
@@ -247,7 +267,7 @@ module Delayed
|
|
247
267
|
# Note: This does not ping the DB to get the time, so all your clients
|
248
268
|
# must have syncronized clocks.
|
249
269
|
def self.db_time_now
|
250
|
-
(ActiveRecord::Base.default_timezone == :utc) ? Time.now.utc : Time.now
|
270
|
+
(ActiveRecord::Base.default_timezone == :utc) ? Time.now.utc : Time.zone.now
|
251
271
|
end
|
252
272
|
|
253
273
|
protected
|
@@ -4,7 +4,7 @@ module Delayed
|
|
4
4
|
AR_STRING_FORMAT = /^AR\:([A-Z][\w\:]+)\:(\d+)$/
|
5
5
|
|
6
6
|
def initialize(object, method, args)
|
7
|
-
raise NoMethodError, "undefined method `#{method}' for #{
|
7
|
+
raise NoMethodError, "undefined method `#{method}' for #{object.inspect}" unless object.respond_to?(method)
|
8
8
|
|
9
9
|
self.object = dump(object)
|
10
10
|
self.args = args.map { |a| dump(a) }
|
data/lib/delayed/recipes.rb
CHANGED
@@ -9,19 +9,23 @@
|
|
9
9
|
|
10
10
|
Capistrano::Configuration.instance.load do
|
11
11
|
namespace :delayed_job do
|
12
|
+
def rails_env
|
13
|
+
fetch(:rails_env, false) ? "RAILS_ENV=#{fetch(:rails_env)}" : ''
|
14
|
+
end
|
15
|
+
|
12
16
|
desc "Stop the delayed_job process"
|
13
17
|
task :stop, :roles => :app do
|
14
|
-
run "cd #{current_path}
|
18
|
+
run "cd #{current_path};#{rails_env} script/delayed_job stop"
|
15
19
|
end
|
16
20
|
|
17
21
|
desc "Start the delayed_job process"
|
18
22
|
task :start, :roles => :app do
|
19
|
-
run "cd #{current_path}
|
23
|
+
run "cd #{current_path};#{rails_env} script/delayed_job start"
|
20
24
|
end
|
21
25
|
|
22
26
|
desc "Restart the delayed_job process"
|
23
27
|
task :restart, :roles => :app do
|
24
|
-
run "cd #{current_path}
|
28
|
+
run "cd #{current_path};#{rails_env} script/delayed_job restart"
|
25
29
|
end
|
26
30
|
end
|
27
31
|
end
|
data/lib/delayed/worker.rb
CHANGED
@@ -1,6 +1,8 @@
|
|
1
1
|
module Delayed
|
2
2
|
class Worker
|
3
|
-
|
3
|
+
@@sleep_delay = 5
|
4
|
+
|
5
|
+
cattr_accessor :sleep_delay
|
4
6
|
|
5
7
|
cattr_accessor :logger
|
6
8
|
self.logger = if defined?(Merb::Logger)
|
@@ -33,7 +35,7 @@ module Delayed
|
|
33
35
|
break if $exit
|
34
36
|
|
35
37
|
if count.zero?
|
36
|
-
sleep(
|
38
|
+
sleep(@@sleep_delay)
|
37
39
|
else
|
38
40
|
say "#{count} jobs processed at %.4f j/s, %d failed ..." % [count / realtime, result.last]
|
39
41
|
end
|
data/spec/database.rb
CHANGED
@@ -11,6 +11,7 @@ require 'spec'
|
|
11
11
|
ActiveRecord::Base.logger = Logger.new('/tmp/dj.log')
|
12
12
|
ActiveRecord::Base.establish_connection(:adapter => 'sqlite3', :database => '/tmp/jobs.sqlite')
|
13
13
|
ActiveRecord::Migration.verbose = false
|
14
|
+
ActiveRecord::Base.default_timezone = :utc if Time.zone.nil?
|
14
15
|
|
15
16
|
ActiveRecord::Schema.define do
|
16
17
|
|
data/spec/job_spec.rb
CHANGED
@@ -58,7 +58,7 @@ describe Delayed::Job do
|
|
58
58
|
end
|
59
59
|
|
60
60
|
it "should be able to set run_at when enqueuing items" do
|
61
|
-
later = 5.minutes
|
61
|
+
later = (Delayed::Job.db_time_now+5.minutes)
|
62
62
|
Delayed::Job.enqueue SimpleJob.new, 5, later
|
63
63
|
|
64
64
|
# use be close rather than equal to because millisecond values cn be lost in DB round trip
|
@@ -111,6 +111,19 @@ describe Delayed::Job do
|
|
111
111
|
job.run_at.should < Delayed::Job.db_time_now + 10.minutes
|
112
112
|
end
|
113
113
|
|
114
|
+
it "should record last_error when destroy_failed_jobs = false, max_attempts = 1" do
|
115
|
+
Delayed::Job.destroy_failed_jobs = false
|
116
|
+
Delayed::Job::max_attempts = 1
|
117
|
+
job = Delayed::Job.enqueue ErrorJob.new
|
118
|
+
Delayed::Job.work_off
|
119
|
+
job.reload
|
120
|
+
job.last_error.should =~ /did not work/
|
121
|
+
job.last_error.should =~ /job_spec.rb/
|
122
|
+
job.attempts.should == 1
|
123
|
+
|
124
|
+
job.failed_at.should_not == nil
|
125
|
+
end
|
126
|
+
|
114
127
|
it "should raise an DeserializationError when the job class is totally unknown" do
|
115
128
|
|
116
129
|
job = Delayed::Job.new
|
@@ -162,14 +175,14 @@ describe Delayed::Job do
|
|
162
175
|
Delayed::Job.destroy_failed_jobs = true
|
163
176
|
end
|
164
177
|
|
165
|
-
it "should be destroyed if it failed more than
|
178
|
+
it "should be destroyed if it failed more than Job::max_attempts times" do
|
166
179
|
@job.should_receive(:destroy)
|
167
|
-
Delayed::Job::
|
180
|
+
Delayed::Job::max_attempts.times { @job.reschedule 'FAIL' }
|
168
181
|
end
|
169
182
|
|
170
|
-
it "should not be destroyed if failed fewer than
|
183
|
+
it "should not be destroyed if failed fewer than Job::max_attempts times" do
|
171
184
|
@job.should_not_receive(:destroy)
|
172
|
-
(Delayed::Job::
|
185
|
+
(Delayed::Job::max_attempts - 1).times { @job.reschedule 'FAIL' }
|
173
186
|
end
|
174
187
|
end
|
175
188
|
|
@@ -178,21 +191,21 @@ describe Delayed::Job do
|
|
178
191
|
Delayed::Job.destroy_failed_jobs = false
|
179
192
|
end
|
180
193
|
|
181
|
-
it "should be failed if it failed more than
|
194
|
+
it "should be failed if it failed more than Job::max_attempts times" do
|
182
195
|
@job.reload.failed_at.should == nil
|
183
|
-
Delayed::Job::
|
196
|
+
Delayed::Job::max_attempts.times { @job.reschedule 'FAIL' }
|
184
197
|
@job.reload.failed_at.should_not == nil
|
185
198
|
end
|
186
199
|
|
187
|
-
it "should not be failed if it failed fewer than
|
188
|
-
(Delayed::Job::
|
200
|
+
it "should not be failed if it failed fewer than Job::max_attempts times" do
|
201
|
+
(Delayed::Job::max_attempts - 1).times { @job.reschedule 'FAIL' }
|
189
202
|
@job.reload.failed_at.should == nil
|
190
203
|
end
|
191
204
|
|
192
205
|
end
|
193
206
|
end
|
194
207
|
|
195
|
-
it "should fail after
|
208
|
+
it "should fail after Job::max_run_time" do
|
196
209
|
@job = Delayed::Job.create :payload_object => LongRunningJob.new
|
197
210
|
Delayed::Job.reserve_and_run_one_job(1.second)
|
198
211
|
@job.reload.last_error.should =~ /expired/
|
@@ -200,7 +213,7 @@ describe Delayed::Job do
|
|
200
213
|
end
|
201
214
|
|
202
215
|
it "should never find failed jobs" do
|
203
|
-
@job = Delayed::Job.create :payload_object => SimpleJob.new, :attempts => 50, :failed_at =>
|
216
|
+
@job = Delayed::Job.create :payload_object => SimpleJob.new, :attempts => 50, :failed_at => Delayed::Job.db_time_now
|
204
217
|
Delayed::Job.find_available(1).length.should == 0
|
205
218
|
end
|
206
219
|
|
@@ -262,7 +275,7 @@ describe Delayed::Job do
|
|
262
275
|
end
|
263
276
|
|
264
277
|
it "should not allow a second worker to get exclusive access if failed to be processed by worker1 and run_at time is now in future (due to backing off behaviour)" do
|
265
|
-
@job.update_attributes(:attempts => 1, :run_at =>
|
278
|
+
@job.update_attributes(:attempts => 1, :run_at => 1.day.from_now)
|
266
279
|
@job_copy_for_worker_2.lock_exclusively!(4.hours, 'worker2').should == false
|
267
280
|
end
|
268
281
|
end
|
@@ -379,7 +392,7 @@ describe Delayed::Job do
|
|
379
392
|
context "while running with locked and expired jobs, it" do
|
380
393
|
before(:each) do
|
381
394
|
Delayed::Job.worker_name = 'worker1'
|
382
|
-
exp_time = Delayed::Job.db_time_now - (1.minutes + Delayed::Job::
|
395
|
+
exp_time = Delayed::Job.db_time_now - (1.minutes + Delayed::Job::max_run_time)
|
383
396
|
Delayed::Job.create(:payload_object => SimpleJob.new, :locked_by => 'worker1', :locked_at => exp_time)
|
384
397
|
Delayed::Job.create(:payload_object => SimpleJob.new, :locked_by => 'worker2', :locked_at => (Delayed::Job.db_time_now - 1.minutes))
|
385
398
|
Delayed::Job.create(:payload_object => SimpleJob.new)
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: delayed_job
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.8.
|
4
|
+
version: 1.8.5
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Brandon Keepers
|
@@ -10,7 +10,7 @@ autorequire:
|
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
12
|
|
13
|
-
date:
|
13
|
+
date: 2010-03-15 00:00:00 -05:00
|
14
14
|
default_executable:
|
15
15
|
dependencies: []
|
16
16
|
|