delayed_job 2.0.4 → 2.0.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/CHANGELOG +7 -0
- data/README.textile +3 -2
- data/VERSION +1 -1
- data/delayed_job.gemspec +2 -2
- data/generators/delayed_job/templates/migration.rb +1 -0
- data/lib/delayed/backend/active_record.rb +27 -5
- data/lib/delayed/backend/base.rb +22 -4
- data/lib/delayed/command.rb +3 -2
- data/lib/delayed/recipes.rb +18 -3
- data/lib/delayed/worker.rb +9 -17
- data/spec/backend/shared_backend_spec.rb +55 -0
- data/spec/sample_jobs.rb +1 -0
- data/spec/worker_spec.rb +19 -1
- metadata +4 -4
data/CHANGELOG
CHANGED
@@ -1,3 +1,10 @@
|
|
1
|
+
2.0.5 - 2010-12-01
|
2
|
+
* Added #reschedule_at hook on payload to determine when the job should be rescheduled [backported from 2.1]
|
3
|
+
* Added --sleep-delay command line option [backported from 2.1]
|
4
|
+
* Added 'delayed_job_server_role' Capistrano variable to allow delayed_job to run on its own worker server
|
5
|
+
set :delayed_job_server_role, :worker
|
6
|
+
* Changed AR backend to reserve jobs using an UPDATE query to reduce worker contention [backported from 2.1]
|
7
|
+
|
1
8
|
2.0.4 - 2010-11-14
|
2
9
|
* Fix issue where dirty tracking prevented job from being properly unlocked
|
3
10
|
* Add delayed_job_args variable for Capistrano recipe to allow configuration of started workers (e.g. "-n 2 --max-priority 10")
|
data/README.textile
CHANGED
@@ -19,13 +19,14 @@ This version is for Rails 2.x only. For rails 3 support, install delayed_job 2.
|
|
19
19
|
To install as a gem, add the following to @config/environment.rb@:
|
20
20
|
|
21
21
|
<pre>
|
22
|
-
config.gem 'delayed_job'
|
22
|
+
config.gem 'delayed_job', :version => '~>2.0.4'
|
23
23
|
</pre>
|
24
24
|
|
25
25
|
Rake tasks are not automatically loaded from gems, so you'll need to add the following to your Rakefile:
|
26
26
|
|
27
27
|
<pre>
|
28
28
|
begin
|
29
|
+
gem 'delayed_job', '~>2.0.4'
|
29
30
|
require 'delayed/tasks'
|
30
31
|
rescue LoadError
|
31
32
|
STDERR.puts "Run `rake gems:install` to install delayed_job"
|
@@ -35,7 +36,7 @@ end
|
|
35
36
|
To install as a plugin:
|
36
37
|
|
37
38
|
<pre>
|
38
|
-
script/plugin install git://github.com/collectiveidea/delayed_job.git
|
39
|
+
script/plugin install git://github.com/collectiveidea/delayed_job.git -r v2.0
|
39
40
|
</pre>
|
40
41
|
|
41
42
|
After delayed_job is installed, you will need to setup the backend.
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
2.0.
|
1
|
+
2.0.5
|
data/delayed_job.gemspec
CHANGED
@@ -5,11 +5,11 @@
|
|
5
5
|
|
6
6
|
Gem::Specification.new do |s|
|
7
7
|
s.name = %q{delayed_job}
|
8
|
-
s.version = "2.0.
|
8
|
+
s.version = "2.0.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{2010-
|
12
|
+
s.date = %q{2010-12-01}
|
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
|
|
15
15
|
This gem is collectiveidea's fork (http://github.com/collectiveidea/delayed_job).}
|
@@ -30,6 +30,10 @@ module Delayed
|
|
30
30
|
}
|
31
31
|
named_scope :by_priority, :order => 'priority ASC, run_at ASC'
|
32
32
|
|
33
|
+
named_scope :locked_by_worker, lambda{|worker_name, max_run_time|
|
34
|
+
{:conditions => ['locked_by = ? AND locked_at > ?', worker_name, db_time_now - max_run_time]}
|
35
|
+
}
|
36
|
+
|
33
37
|
def self.after_fork
|
34
38
|
::ActiveRecord::Base.connection.reconnect!
|
35
39
|
end
|
@@ -38,15 +42,33 @@ module Delayed
|
|
38
42
|
def self.clear_locks!(worker_name)
|
39
43
|
update_all("locked_by = null, locked_at = null", ["locked_by = ?", worker_name])
|
40
44
|
end
|
41
|
-
|
42
|
-
|
43
|
-
def self.find_available(worker_name, limit = 5, max_run_time = Worker.max_run_time)
|
45
|
+
|
46
|
+
def self.jobs_available_to_worker(worker_name, max_run_time)
|
44
47
|
scope = self.ready_to_run(worker_name, max_run_time)
|
45
48
|
scope = scope.scoped(:conditions => ['priority >= ?', Worker.min_priority]) if Worker.min_priority
|
46
49
|
scope = scope.scoped(:conditions => ['priority <= ?', Worker.max_priority]) if Worker.max_priority
|
47
|
-
|
50
|
+
scope.by_priority
|
51
|
+
end
|
52
|
+
|
53
|
+
# Reserve a single job in a single update query. This causes workers to serialize on the
|
54
|
+
# database and avoids contention.
|
55
|
+
def self.reserve(worker, max_run_time = Worker.max_run_time)
|
56
|
+
affected_rows = 0
|
57
|
+
::ActiveRecord::Base.silence do
|
58
|
+
affected_rows = update_all(["locked_at = ?, locked_by = ?", db_time_now, worker.name], jobs_available_to_worker(worker.name, max_run_time).scope(:find)[:conditions], :limit => 1)
|
59
|
+
end
|
60
|
+
|
61
|
+
if affected_rows == 1
|
62
|
+
locked_by_worker(worker.name, max_run_time).first
|
63
|
+
else
|
64
|
+
nil
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
# Find a few candidate jobs to run (in case some immediately get locked by others).
|
69
|
+
def self.find_available(worker_name, limit = 5, max_run_time = Worker.max_run_time)
|
48
70
|
::ActiveRecord::Base.silence do
|
49
|
-
|
71
|
+
jobs_available_to_worker(worker_name, max_run_time).all(:limit => limit)
|
50
72
|
end
|
51
73
|
end
|
52
74
|
|
data/lib/delayed/backend/base.rb
CHANGED
@@ -20,7 +20,15 @@ module Delayed
|
|
20
20
|
run_at = args[1]
|
21
21
|
self.create(:payload_object => object, :priority => priority.to_i, :run_at => run_at)
|
22
22
|
end
|
23
|
-
|
23
|
+
|
24
|
+
def reserve(worker, max_run_time = Worker.max_run_time)
|
25
|
+
# We get up to 5 jobs from the db. In case we cannot get exclusive access to a job we try the next.
|
26
|
+
# this leads to a more even distribution of jobs across the worker processes
|
27
|
+
find_available(worker.name, 5, max_run_time).detect do |job|
|
28
|
+
job.lock_exclusively!(max_run_time, worker.name)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
24
32
|
# Hook method that is called before a new worker is forked
|
25
33
|
def before_fork
|
26
34
|
end
|
@@ -71,6 +79,16 @@ module Delayed
|
|
71
79
|
self.locked_at = nil
|
72
80
|
self.locked_by = nil
|
73
81
|
end
|
82
|
+
|
83
|
+
def reschedule_at
|
84
|
+
payload_object.respond_to?(:reschedule_at) ?
|
85
|
+
payload_object.reschedule_at(self.class.db_time_now, attempts) :
|
86
|
+
self.class.db_time_now + (attempts ** 4) + 5
|
87
|
+
end
|
88
|
+
|
89
|
+
def max_attempts
|
90
|
+
payload_object.max_attempts if payload_object.respond_to?(:max_attempts)
|
91
|
+
end
|
74
92
|
|
75
93
|
private
|
76
94
|
|
@@ -99,13 +117,13 @@ module Delayed
|
|
99
117
|
def attempt_to_load(klass)
|
100
118
|
klass.constantize
|
101
119
|
end
|
102
|
-
|
120
|
+
|
103
121
|
protected
|
104
122
|
|
105
123
|
def set_default_run_at
|
106
124
|
self.run_at ||= self.class.db_time_now
|
107
|
-
end
|
108
|
-
|
125
|
+
end
|
126
|
+
|
109
127
|
end
|
110
128
|
end
|
111
129
|
end
|
data/lib/delayed/command.rb
CHANGED
@@ -44,8 +44,9 @@ module Delayed
|
|
44
44
|
opts.on('-m', '--monitor', 'Start monitor process.') do
|
45
45
|
@monitor = true
|
46
46
|
end
|
47
|
-
|
48
|
-
|
47
|
+
opts.on('--sleep-delay N', "Amount of time to sleep when no jobs are found") do |n|
|
48
|
+
@options[:sleep_delay] = n
|
49
|
+
end
|
49
50
|
end
|
50
51
|
@args = opts.parse!(args)
|
51
52
|
end
|
data/lib/delayed/recipes.rb
CHANGED
@@ -6,6 +6,17 @@
|
|
6
6
|
# after "deploy:stop", "delayed_job:stop"
|
7
7
|
# after "deploy:start", "delayed_job:start"
|
8
8
|
# after "deploy:restart", "delayed_job:restart"
|
9
|
+
#
|
10
|
+
# If you want to use command line options, for example to start multiple workers,
|
11
|
+
# define a Capistrano variable delayed_job_args:
|
12
|
+
#
|
13
|
+
# set :delayed_jobs_args, "-n 2"
|
14
|
+
#
|
15
|
+
# If you've got delayed_job workers running on a servers, you can also specify
|
16
|
+
# which servers have delayed_job running and should be restarted after deploy.
|
17
|
+
#
|
18
|
+
# set :delayed_job_server_role, :worker
|
19
|
+
#
|
9
20
|
|
10
21
|
Capistrano::Configuration.instance.load do
|
11
22
|
namespace :delayed_job do
|
@@ -17,18 +28,22 @@ Capistrano::Configuration.instance.load do
|
|
17
28
|
fetch(:delayed_job_args, "")
|
18
29
|
end
|
19
30
|
|
31
|
+
def roles
|
32
|
+
fetch(:delayed_job_server_role, :app)
|
33
|
+
end
|
34
|
+
|
20
35
|
desc "Stop the delayed_job process"
|
21
|
-
task :stop, :roles =>
|
36
|
+
task :stop, :roles => lambda { roles } do
|
22
37
|
run "cd #{current_path};#{rails_env} script/delayed_job stop"
|
23
38
|
end
|
24
39
|
|
25
40
|
desc "Start the delayed_job process"
|
26
|
-
task :start, :roles =>
|
41
|
+
task :start, :roles => lambda { roles } do
|
27
42
|
run "cd #{current_path};#{rails_env} script/delayed_job start #{args}"
|
28
43
|
end
|
29
44
|
|
30
45
|
desc "Restart the delayed_job process"
|
31
|
-
task :restart, :roles =>
|
46
|
+
task :restart, :roles => lambda { roles } do
|
32
47
|
run "cd #{current_path};#{rails_env} script/delayed_job restart #{args}"
|
33
48
|
end
|
34
49
|
end
|
data/lib/delayed/worker.rb
CHANGED
@@ -49,6 +49,7 @@ module Delayed
|
|
49
49
|
@quiet = options[:quiet]
|
50
50
|
self.class.min_priority = options[:min_priority] if options.has_key?(:min_priority)
|
51
51
|
self.class.max_priority = options[:max_priority] if options.has_key?(:max_priority)
|
52
|
+
self.class.sleep_delay = options[:sleep_delay] if options.has_key?(:sleep_delay)
|
52
53
|
end
|
53
54
|
|
54
55
|
# Every worker has a unique name which by default is the pid of the process. There are some
|
@@ -84,7 +85,7 @@ module Delayed
|
|
84
85
|
break if $exit
|
85
86
|
|
86
87
|
if count.zero?
|
87
|
-
sleep(
|
88
|
+
sleep(self.class.sleep_delay)
|
88
89
|
else
|
89
90
|
say "#{count} jobs processed at %.4f j/s, %d failed ..." % [count / realtime, result.last]
|
90
91
|
end
|
@@ -131,9 +132,8 @@ module Delayed
|
|
131
132
|
# Reschedule the job in the future (when a job fails).
|
132
133
|
# Uses an exponential scale depending on the number of failed attempts.
|
133
134
|
def reschedule(job, time = nil)
|
134
|
-
if (job.attempts += 1) <
|
135
|
-
|
136
|
-
job.run_at = time
|
135
|
+
if (job.attempts += 1) < max_attempts(job)
|
136
|
+
job.run_at = time || job.reschedule_at
|
137
137
|
job.unlock
|
138
138
|
job.save!
|
139
139
|
else
|
@@ -154,6 +154,10 @@ module Delayed
|
|
154
154
|
logger.add level, "#{Time.now.strftime('%FT%T%z')}: #{text}" if logger
|
155
155
|
end
|
156
156
|
|
157
|
+
def max_attempts(job)
|
158
|
+
job.max_attempts || self.class.max_attempts
|
159
|
+
end
|
160
|
+
|
157
161
|
protected
|
158
162
|
|
159
163
|
def handle_failed_job(job, error)
|
@@ -165,19 +169,7 @@ module Delayed
|
|
165
169
|
# Run the next job we can get an exclusive lock on.
|
166
170
|
# If no jobs are left we return nil
|
167
171
|
def reserve_and_run_one_job
|
168
|
-
|
169
|
-
# We get up to 5 jobs from the db. In case we cannot get exclusive access to a job we try the next.
|
170
|
-
# this leads to a more even distribution of jobs across the worker processes
|
171
|
-
job = Delayed::Job.find_available(name, 5, self.class.max_run_time).detect do |job|
|
172
|
-
if job.lock_exclusively!(self.class.max_run_time, name)
|
173
|
-
say "acquired lock on #{job.name}"
|
174
|
-
true
|
175
|
-
else
|
176
|
-
say "failed to acquire exclusive lock for #{job.name}", Logger::WARN
|
177
|
-
false
|
178
|
-
end
|
179
|
-
end
|
180
|
-
|
172
|
+
job = Delayed::Job.reserve(self)
|
181
173
|
run(job) if job
|
182
174
|
end
|
183
175
|
end
|
@@ -173,6 +173,46 @@ shared_examples_for 'a backend' do
|
|
173
173
|
end
|
174
174
|
end
|
175
175
|
|
176
|
+
describe "reserve" do
|
177
|
+
before do
|
178
|
+
Delayed::Worker.max_run_time = 2.minutes
|
179
|
+
@worker = Delayed::Worker.new(:quiet => true)
|
180
|
+
end
|
181
|
+
|
182
|
+
it "should not reserve failed jobs" do
|
183
|
+
create_job :attempts => 50, :failed_at => described_class.db_time_now
|
184
|
+
described_class.reserve(@worker).should be_nil
|
185
|
+
end
|
186
|
+
|
187
|
+
it "should not reserve jobs scheduled for the future" do
|
188
|
+
create_job :run_at => (described_class.db_time_now + 1.minute)
|
189
|
+
described_class.reserve(@worker).should be_nil
|
190
|
+
end
|
191
|
+
|
192
|
+
it "should lock the job so other workers can't reserve it" do
|
193
|
+
job = create_job
|
194
|
+
described_class.reserve(@worker).should == job
|
195
|
+
new_worker = Delayed::Worker.new(:quiet => true)
|
196
|
+
new_worker.name = 'worker2'
|
197
|
+
described_class.reserve(new_worker).should be_nil
|
198
|
+
end
|
199
|
+
|
200
|
+
it "should reserve open jobs" do
|
201
|
+
job = create_job
|
202
|
+
described_class.reserve(@worker).should == job
|
203
|
+
end
|
204
|
+
|
205
|
+
it "should reserve expired jobs" do
|
206
|
+
job = create_job(:locked_by => @worker.name, :locked_at => described_class.db_time_now - 3.minutes)
|
207
|
+
described_class.reserve(@worker).should == job
|
208
|
+
end
|
209
|
+
|
210
|
+
it "should reserve own jobs" do
|
211
|
+
job = create_job(:locked_by => @worker.name, :locked_at => (described_class.db_time_now - 1.minutes))
|
212
|
+
described_class.reserve(@worker).should == job
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
176
216
|
context "#name" do
|
177
217
|
it "should be the class name of the job that was enqueued" do
|
178
218
|
@backend.create(:payload_object => ErrorJob.new ).name.should == 'ErrorJob'
|
@@ -259,4 +299,19 @@ shared_examples_for 'a backend' do
|
|
259
299
|
@job.id.should_not be_nil
|
260
300
|
end
|
261
301
|
end
|
302
|
+
|
303
|
+
context "max_attempts" do
|
304
|
+
before(:each) do
|
305
|
+
@job = described_class.enqueue SimpleJob.new
|
306
|
+
end
|
307
|
+
|
308
|
+
it 'should not be defined' do
|
309
|
+
@job.max_attempts.should be_nil
|
310
|
+
end
|
311
|
+
|
312
|
+
it 'should use the max_retries value on the payload when defined' do
|
313
|
+
@job.payload_object.stub!(:max_attempts).and_return(99)
|
314
|
+
@job.max_attempts.should == 99
|
315
|
+
end
|
316
|
+
end
|
262
317
|
end
|
data/spec/sample_jobs.rb
CHANGED
data/spec/worker_spec.rb
CHANGED
@@ -131,6 +131,24 @@ describe Delayed::Worker do
|
|
131
131
|
@job.locked_at.should be_nil
|
132
132
|
@job.locked_by.should be_nil
|
133
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
|
134
152
|
end
|
135
153
|
|
136
154
|
context "reschedule" do
|
@@ -147,7 +165,7 @@ describe Delayed::Worker do
|
|
147
165
|
|
148
166
|
it "should run that hook" do
|
149
167
|
@job.payload_object.should_receive :on_permanent_failure
|
150
|
-
|
168
|
+
@worker.reschedule(@job)
|
151
169
|
end
|
152
170
|
end
|
153
171
|
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: delayed_job
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 5
|
5
5
|
prerelease: false
|
6
6
|
segments:
|
7
7
|
- 2
|
8
8
|
- 0
|
9
|
-
-
|
10
|
-
version: 2.0.
|
9
|
+
- 5
|
10
|
+
version: 2.0.5
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- Brandon Keepers
|
@@ -16,7 +16,7 @@ autorequire:
|
|
16
16
|
bindir: bin
|
17
17
|
cert_chain: []
|
18
18
|
|
19
|
-
date: 2010-
|
19
|
+
date: 2010-12-01 00:00:00 -05:00
|
20
20
|
default_executable:
|
21
21
|
dependencies:
|
22
22
|
- !ruby/object:Gem::Dependency
|