delayed_job 2.0.4 → 2.0.5

Sign up to get free protection for your applications and to get access to all the features.
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