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 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.4
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.4"
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-11-14}
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).}
@@ -13,6 +13,7 @@ class CreateDelayedJobs < ActiveRecord::Migration
13
13
  end
14
14
 
15
15
  add_index :delayed_jobs, [:priority, :run_at], :name => 'delayed_jobs_priority'
16
+ add_index :delayed_jobs, :locked_by, :name => 'delayed_jobs_locked_by'
16
17
  end
17
18
 
18
19
  def self.down
@@ -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
- # Find a few candidate jobs to run (in case some immediately get locked by others).
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
- scope.by_priority.all(:limit => limit)
71
+ jobs_available_to_worker(worker_name, max_run_time).all(:limit => limit)
50
72
  end
51
73
  end
52
74
 
@@ -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
@@ -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
@@ -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 => :app do
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 => :app do
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 => :app do
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
@@ -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(@@sleep_delay)
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) < self.class.max_attempts
135
- time ||= Job.db_time_now + (job.attempts ** 4) + 5
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
@@ -15,6 +15,7 @@ end
15
15
  class OnPermanentFailureJob < SimpleJob
16
16
  def on_permanent_failure
17
17
  end
18
+ def max_attempts; 1; end
18
19
  end
19
20
 
20
21
  module M
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
- Delayed::Worker.max_attempts.times { @worker.reschedule(@job) }
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: 7
4
+ hash: 5
5
5
  prerelease: false
6
6
  segments:
7
7
  - 2
8
8
  - 0
9
- - 4
10
- version: 2.0.4
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-11-14 00:00:00 -06:00
19
+ date: 2010-12-01 00:00:00 -05:00
20
20
  default_executable:
21
21
  dependencies:
22
22
  - !ruby/object:Gem::Dependency