delayed_job 2.1.4 → 3.0.0.pre

Sign up to get free protection for your applications and to get access to all the features.
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
+ Delayed_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
 
@@ -12,6 +12,9 @@ It is a direct extraction from Shopify where the job table is responsible for a
12
12
  * batch imports
13
13
  * spam checks
14
14
 
15
+ "Follow us on Twitter":https://twitter.com/delayedjob to get updates and notices about new releases.
16
+
17
+
15
18
  h2. Installation
16
19
 
17
20
  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.
@@ -23,13 +23,18 @@ module Delayed
23
23
  unless options[:payload_object].respond_to?(:perform)
24
24
  raise ArgumentError, 'Cannot enqueue items which do not respond to perform'
25
25
  end
26
-
26
+
27
27
  if Delayed::Worker.delay_jobs
28
- self.create(options).tap do |job|
29
- job.hook(:enqueue)
28
+ self.new(options).tap do |job|
29
+ Delayed::Worker.lifecycle.run_callbacks(:enqueue, job) do
30
+ job.hook(:enqueue)
31
+ job.save
32
+ end
30
33
  end
31
34
  else
32
- options[:payload_object].perform
35
+ Delayed::Job.new(:payload_object => options[:payload_object]).tap do |job|
36
+ job.invoke_job
37
+ end
33
38
  end
34
39
  end
35
40
 
@@ -83,14 +88,18 @@ module Delayed
83
88
  end
84
89
 
85
90
  def invoke_job
86
- hook :before
87
- payload_object.perform
88
- hook :success
89
- rescue Exception => e
90
- hook :error, e
91
- raise e
92
- ensure
93
- hook :after
91
+ Delayed::Worker.lifecycle.run_callbacks(:invoke_job, self) do
92
+ begin
93
+ hook :before
94
+ payload_object.perform
95
+ hook :success
96
+ rescue Exception => e
97
+ hook :error, e
98
+ raise e
99
+ ensure
100
+ hook :after
101
+ end
102
+ end
94
103
  end
95
104
 
96
105
  # Unlock this job (note: not saved to DB)
@@ -113,16 +122,25 @@ module Delayed
113
122
  payload_object.reschedule_at(self.class.db_time_now, attempts) :
114
123
  self.class.db_time_now + (attempts ** 4) + 5
115
124
  end
116
-
125
+
117
126
  def max_attempts
118
127
  payload_object.max_attempts if payload_object.respond_to?(:max_attempts)
119
128
  end
120
-
129
+
130
+ def fail!
131
+ update_attributes(:failed_at => self.class.db_time_now)
132
+ end
133
+
121
134
  protected
122
135
 
123
136
  def set_default_run_at
124
137
  self.run_at ||= self.class.db_time_now
125
138
  end
139
+
140
+ # Call during reload operation to clear out internal state
141
+ def reset
142
+ @payload_object = nil
143
+ end
126
144
  end
127
145
  end
128
146
  end
@@ -1,5 +1,7 @@
1
1
  require File.expand_path('../../../../spec/sample_jobs', __FILE__)
2
2
 
3
+ require 'active_support/core_ext'
4
+
3
5
  shared_examples_for 'a delayed_job backend' do
4
6
  let(:worker) { Delayed::Worker.new }
5
7
 
@@ -26,6 +28,13 @@ shared_examples_for 'a delayed_job backend' do
26
28
  job.run_at.should be_within(1).of(later)
27
29
  end
28
30
 
31
+ describe "#reload" do
32
+ it 'should cause the payload to be reloaded' do
33
+ job = described_class.enqueue :payload_object => SimpleJob.new
34
+ job.payload_object.object_id.should_not == job.reload.payload_object.object_id
35
+ end
36
+ end
37
+
29
38
  describe "enqueue" do
30
39
  context "with a hash" do
31
40
  it "should raise ArgumentError when handler doesn't respond_to :perform" do
@@ -47,6 +56,11 @@ shared_examples_for 'a delayed_job backend' do
47
56
  job = described_class.enqueue :payload_object => SimpleJob.new, :run_at => later
48
57
  job.run_at.should be_within(1).of(later)
49
58
  end
59
+
60
+ it "should be able to set queue" do
61
+ job = described_class.enqueue :payload_object => SimpleJob.new, :queue => 'tracking'
62
+ job.queue.should == 'tracking'
63
+ end
50
64
  end
51
65
 
52
66
  context "with multiple arguments" do
@@ -58,12 +72,6 @@ shared_examples_for 'a delayed_job backend' do
58
72
  described_class.enqueue SimpleJob.new
59
73
  described_class.count.should == 1
60
74
  end
61
-
62
- it "should not increase count after enqueuing items when delay_jobs is false" do
63
- Delayed::Worker.delay_jobs = false
64
- described_class.enqueue SimpleJob.new
65
- described_class.count.should == 0
66
- end
67
75
 
68
76
  it "should be able to set priority [DEPRECATED]" do
69
77
  silence_warnings do
@@ -91,6 +99,27 @@ shared_examples_for 'a delayed_job backend' do
91
99
  lambda { job.invoke_job }.should change { M::ModuleJob.runs }.from(0).to(1)
92
100
  end
93
101
  end
102
+
103
+ context "with delay_jobs = false" do
104
+ before(:each) do
105
+ Delayed::Worker.delay_jobs = false
106
+ end
107
+
108
+ it "should not increase count after enqueuing items" do
109
+ described_class.enqueue SimpleJob.new
110
+ described_class.count.should == 0
111
+ end
112
+
113
+ it 'should invoke the enqueued job' do
114
+ job = SimpleJob.new
115
+ job.should_receive(:perform)
116
+ described_class.enqueue job
117
+ end
118
+
119
+ it 'should return a job, not the result of invocation' do
120
+ described_class.enqueue(SimpleJob.new).should be_instance_of(described_class)
121
+ end
122
+ end
94
123
  end
95
124
 
96
125
  describe "callbacks" do
@@ -141,7 +170,7 @@ shared_examples_for 'a delayed_job backend' do
141
170
  end
142
171
 
143
172
  it "should raise a DeserializationError when the YAML.load raises argument error" do
144
- job = described_class.find(create_job.id)
173
+ job = described_class.new :handler => "--- !ruby/struct:GoingToRaiseArgError {}"
145
174
  YAML.should_receive(:load).and_raise(ArgumentError)
146
175
  lambda { job.payload_object }.should raise_error(Delayed::DeserializationError)
147
176
  end
@@ -169,7 +198,7 @@ shared_examples_for 'a delayed_job backend' do
169
198
 
170
199
  it "should reserve jobs scheduled for the past when time zones are involved" do
171
200
  Time.zone = 'US/Eastern'
172
- job = create_job :run_at => described_class.db_time_now - 1.minute.ago.in_time_zone
201
+ job = create_job :run_at => described_class.db_time_now - 1.minute
173
202
  described_class.reserve(worker).should == job
174
203
  end
175
204
 
@@ -187,7 +216,7 @@ shared_examples_for 'a delayed_job backend' do
187
216
  end
188
217
 
189
218
  it "should reserve expired jobs" do
190
- job = create_job(:locked_by => worker.name, :locked_at => described_class.db_time_now - 3.minutes)
219
+ job = create_job(:locked_by => 'some other worker', :locked_at => described_class.db_time_now - Delayed::Worker.max_run_time - 1.minute)
191
220
  described_class.reserve(worker).should == job
192
221
  end
193
222
 
@@ -215,8 +244,7 @@ shared_examples_for 'a delayed_job backend' do
215
244
  it "should parse from handler on deserialization error" do
216
245
  job = Story.create(:text => "...").delay.text
217
246
  job.payload_object.object.destroy
218
- job = described_class.find(job.id)
219
- job.name.should == 'Delayed::PerformableMethod'
247
+ job.reload.name.should == 'Delayed::PerformableMethod'
220
248
  end
221
249
  end
222
250
 
@@ -289,20 +317,73 @@ shared_examples_for 'a delayed_job backend' do
289
317
  @job.id.should_not be_nil
290
318
  end
291
319
  end
292
-
320
+
321
+ context "named queues" do
322
+ context "when worker has one queue set" do
323
+ before(:each) do
324
+ worker.queues = ['large']
325
+ end
326
+
327
+ it "should only work off jobs which are from its queue" do
328
+ SimpleJob.runs.should == 0
329
+
330
+ create_job(:queue => "large")
331
+ create_job(:queue => "small")
332
+ worker.work_off
333
+
334
+ SimpleJob.runs.should == 1
335
+ end
336
+ end
337
+
338
+ context "when worker has two queue set" do
339
+ before(:each) do
340
+ worker.queues = ['large', 'small']
341
+ end
342
+
343
+ it "should only work off jobs which are from its queue" do
344
+ SimpleJob.runs.should == 0
345
+
346
+ create_job(:queue => "large")
347
+ create_job(:queue => "small")
348
+ create_job(:queue => "medium")
349
+ create_job
350
+ worker.work_off
351
+
352
+ SimpleJob.runs.should == 2
353
+ end
354
+ end
355
+
356
+ context "when worker does not have queue set" do
357
+ before(:each) do
358
+ worker.queues = []
359
+ end
360
+
361
+ it "should work off all jobs" do
362
+ SimpleJob.runs.should == 0
363
+
364
+ create_job(:queue => "one")
365
+ create_job(:queue => "two")
366
+ create_job
367
+ worker.work_off
368
+
369
+ SimpleJob.runs.should == 3
370
+ end
371
+ end
372
+ end
373
+
293
374
  context "max_attempts" do
294
375
  before(:each) do
295
376
  @job = described_class.enqueue SimpleJob.new
296
377
  end
297
-
378
+
298
379
  it 'should not be defined' do
299
380
  @job.max_attempts.should be_nil
300
381
  end
301
-
382
+
302
383
  it 'should use the max_retries value on the payload when defined' do
303
384
  @job.payload_object.stub!(:max_attempts).and_return(99)
304
385
  @job.max_attempts.should == 99
305
- end
386
+ end
306
387
  end
307
388
 
308
389
  describe "yaml serialization" do
@@ -310,7 +391,7 @@ shared_examples_for 'a delayed_job backend' do
310
391
  story = Story.create(:text => 'hello')
311
392
  job = story.delay.tell
312
393
  story.update_attributes :text => 'goodbye'
313
- described_class.find(job.id).payload_object.object.text.should == 'goodbye'
394
+ job.reload.payload_object.object.text.should == 'goodbye'
314
395
  end
315
396
 
316
397
  it "should raise deserialization error for destroyed records" do
@@ -318,7 +399,7 @@ shared_examples_for 'a delayed_job backend' do
318
399
  job = story.delay.tell
319
400
  story.destroy
320
401
  lambda {
321
- described_class.find(job.id).payload_object
402
+ job.reload.payload_object
322
403
  }.should raise_error(Delayed::DeserializationError)
323
404
  end
324
405
  end
@@ -5,17 +5,16 @@ require 'optparse'
5
5
  module Delayed
6
6
  class Command
7
7
  attr_accessor :worker_count
8
-
8
+
9
9
  def initialize(args)
10
- @files_to_reopen = []
11
10
  @options = {
12
11
  :quiet => true,
13
12
  :pid_dir => "#{Rails.root}/tmp/pids"
14
13
  }
15
-
14
+
16
15
  @worker_count = 1
17
16
  @monitor = false
18
-
17
+
19
18
  opts = OptionParser.new do |opts|
20
19
  opts.banner = "Usage: #{File.basename($0)} [options] start|stop|restart|run"
21
20
 
@@ -50,20 +49,20 @@ module Delayed
50
49
  opts.on('-p', '--prefix NAME', "String to be prefixed to worker process names") do |prefix|
51
50
  @options[:prefix] = prefix
52
51
  end
52
+ opts.on('--queues=queues', "Specify which queue DJ must look up for jobs") do |queues|
53
+ @options[:queues] = queues.split(',')
54
+ end
55
+ opts.on('--queue=queue', "Specify which queue DJ must look up for jobs") do |queue|
56
+ @options[:queues] = queue.split(',')
57
+ end
53
58
  end
54
59
  @args = opts.parse!(args)
55
60
  end
56
-
57
- def daemonize
58
- Delayed::Worker.backend.before_fork
59
61
 
60
- ObjectSpace.each_object(File) do |file|
61
- @files_to_reopen << file unless file.closed?
62
- end
63
-
62
+ def daemonize
64
63
  dir = @options[:pid_dir]
65
64
  Dir.mkdir(dir) unless File.exists?(dir)
66
-
65
+
67
66
  if @worker_count > 1 && @options[:identifier]
68
67
  raise ArgumentError, 'Cannot specify both --number-of-workers and --identifier'
69
68
  elsif @worker_count == 1 && @options[:identifier]
@@ -76,29 +75,21 @@ module Delayed
76
75
  end
77
76
  end
78
77
  end
79
-
78
+
80
79
  def run_process(process_name, dir)
80
+ Delayed::Worker.before_fork
81
81
  Daemons.run_proc(process_name, :dir => dir, :dir_mode => :normal, :monitor => @monitor, :ARGV => @args) do |*args|
82
82
  $0 = File.join(@options[:prefix], process_name) if @options[:prefix]
83
83
  run process_name
84
84
  end
85
85
  end
86
-
86
+
87
87
  def run(worker_name = nil)
88
88
  Dir.chdir(Rails.root)
89
-
90
- # Re-open file handles
91
- @files_to_reopen.each do |file|
92
- begin
93
- file.reopen file.path, "a+"
94
- file.sync = true
95
- rescue ::Exception
96
- end
97
- end
98
-
89
+
90
+ Delayed::Worker.after_fork
99
91
  Delayed::Worker.logger = Logger.new(File.join(Rails.root, 'log', 'delayed_job.log'))
100
- Delayed::Worker.backend.after_fork
101
-
92
+
102
93
  worker = Delayed::Worker.new(@options)
103
94
  worker.name_prefix = "#{worker_name} "
104
95
  worker.start
@@ -107,6 +98,5 @@ module Delayed
107
98
  STDERR.puts e.message
108
99
  exit 1
109
100
  end
110
-
111
101
  end
112
102
  end
@@ -0,0 +1,84 @@
1
+ module Delayed
2
+ class InvalidCallback < Exception; end
3
+
4
+ class Lifecycle
5
+ EVENTS = {
6
+ :enqueue => [:job],
7
+ :execute => [:worker],
8
+ :loop => [:worker],
9
+ :perform => [:worker, :job],
10
+ :error => [:worker, :job],
11
+ :failure => [:worker, :job],
12
+ :invoke_job => [:job]
13
+ }
14
+
15
+ def initialize
16
+ @callbacks = EVENTS.keys.inject({}) { |hash, e| hash[e] = Callback.new; hash }
17
+ end
18
+
19
+ def before(event, &block)
20
+ add(:before, event, &block)
21
+ end
22
+
23
+ def after(event, &block)
24
+ add(:after, event, &block)
25
+ end
26
+
27
+ def around(event, &block)
28
+ add(:around, event, &block)
29
+ end
30
+
31
+ def run_callbacks(event, *args, &block)
32
+ missing_callback(event) unless @callbacks.has_key?(event)
33
+
34
+ unless EVENTS[event].size == args.size
35
+ raise ArgumentError, "Callback #{event} expects #{EVENTS[event].size} parameter(s): #{EVENTS[event].join(', ')}"
36
+ end
37
+
38
+ @callbacks[event].execute(*args, &block)
39
+ end
40
+
41
+ private
42
+
43
+ def add(type, event, &block)
44
+ missing_callback(event) unless @callbacks.has_key?(event)
45
+
46
+ @callbacks[event].add(type, &block)
47
+ end
48
+
49
+ def missing_callback(event)
50
+ raise InvalidCallback, "Unknown callback event: #{event}"
51
+ end
52
+ end
53
+
54
+ class Callback
55
+ def initialize
56
+ @before = []
57
+ @after = []
58
+
59
+ # Identity proc. Avoids special cases when there is no existing around chain.
60
+ @around = lambda { |*args, &block| block.call(*args) }
61
+ end
62
+
63
+ def execute(*args, &block)
64
+ @before.each { |c| c.call(*args) }
65
+ result = @around.call(*args, &block)
66
+ @after.each { |c| c.call(*args) }
67
+ result
68
+ end
69
+
70
+ def add(type, &callback)
71
+ case type
72
+ when :before
73
+ @before << callback
74
+ when :after
75
+ @after << callback
76
+ when :around
77
+ chain = @around # use a local variable so that the current chain is closed over in the following lambda
78
+ @around = lambda { |*a, &block| chain.call(*a) { |*b| callback.call(*b, &block) } }
79
+ else
80
+ raise InvalidCallback, "Invalid callback type: #{type}"
81
+ end
82
+ end
83
+ end
84
+ end
@@ -1,7 +1,9 @@
1
1
  require 'active_support/core_ext/module/delegation'
2
2
 
3
3
  module Delayed
4
- class PerformableMethod < Struct.new(:object, :method_name, :args)
4
+ class PerformableMethod
5
+ attr_accessor :object, :method_name, :args
6
+
5
7
  delegate :method, :to => :object
6
8
 
7
9
  def initialize(object, method_name, args)
@@ -0,0 +1,13 @@
1
+ module Delayed
2
+ class Plugin
3
+ class_attribute :callback_block
4
+
5
+ def self.callbacks(&block)
6
+ self.callback_block = block
7
+ end
8
+
9
+ def initialize
10
+ self.class.callback_block.call(Delayed::Worker.lifecycle) if self.class.callback_block
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,15 @@
1
+ module Delayed
2
+ module Plugins
3
+ class ClearLocks < Plugin
4
+ callbacks do |lifecycle|
5
+ lifecycle.around(:execute) do |worker, &block|
6
+ begin
7
+ block.call(worker)
8
+ ensure
9
+ Delayed::Job.clear_locks!(worker.name)
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,65 @@
1
+ class ActiveRecord::Base
2
+ # serialize to YAML
3
+ def encode_with(coder)
4
+ coder["attributes"] = @attributes
5
+ coder.tag = ['!ruby/ActiveRecord', self.class.name].join(':')
6
+ end
7
+ end
8
+
9
+ class Delayed::PerformableMethod
10
+ # serialize to YAML
11
+ def encode_with(coder)
12
+ coder.map = {
13
+ "object" => object,
14
+ "method_name" => method_name,
15
+ "args" => args
16
+ }
17
+ end
18
+ end
19
+
20
+ module Psych
21
+ module Visitors
22
+ class YAMLTree
23
+ def visit_Class(klass)
24
+ tag = ['!ruby/class', klass.name].join(':')
25
+ register(klass, @emitter.start_mapping(nil, tag, false, Nodes::Mapping::BLOCK))
26
+ @emitter.end_mapping
27
+ end
28
+ end
29
+
30
+ class ToRuby
31
+ def visit_Psych_Nodes_Mapping_with_class(object)
32
+ return revive(Psych.load_tags[object.tag], object) if Psych.load_tags[object.tag]
33
+
34
+ case object.tag
35
+ when /^!ruby\/class:?(.*)?$/
36
+ resolve_class $1
37
+ when /^!ruby\/ActiveRecord:(.+)$/
38
+ klass = resolve_class($1)
39
+ payload = Hash[*object.children.map { |c| accept c }]
40
+ id = payload["attributes"][klass.primary_key]
41
+ begin
42
+ if ActiveRecord::VERSION::MAJOR == 3
43
+ klass.unscoped.find(id)
44
+ else # Rails 2
45
+ klass.with_exclusive_scope { klass.find(id) }
46
+ end
47
+ rescue ActiveRecord::RecordNotFound
48
+ raise Delayed::DeserializationError
49
+ end
50
+ else
51
+ visit_Psych_Nodes_Mapping_without_class(object)
52
+ end
53
+ end
54
+ alias_method_chain :visit_Psych_Nodes_Mapping, :class
55
+
56
+ def resolve_class_with_constantize(klass_name)
57
+ klass_name.constantize
58
+ rescue
59
+ resolve_class_without_constantize(klass_name)
60
+ end
61
+ alias_method_chain :resolve_class, :constantize
62
+ end
63
+ end
64
+ end
65
+
@@ -4,8 +4,6 @@ require 'rails'
4
4
  module Delayed
5
5
  class Railtie < Rails::Railtie
6
6
  initializer :after_initialize do
7
- Delayed::Worker.guess_backend
8
-
9
7
  ActiveSupport.on_load(:action_mailer) do
10
8
  ActionMailer::Base.send(:extend, Delayed::DelayMail)
11
9
  end
@@ -2,7 +2,11 @@ class ActiveRecord::Base
2
2
  yaml_as "tag:ruby.yaml.org,2002:ActiveRecord"
3
3
 
4
4
  def self.yaml_new(klass, tag, val)
5
- klass.find(val['attributes']['id'])
5
+ if ActiveRecord::VERSION::MAJOR == 3
6
+ klass.unscoped.find(val['attributes'][klass.primary_key])
7
+ else # Rails 2
8
+ klass.with_exclusive_scope { klass.find(val['attributes'][klass.primary_key]) }
9
+ end
6
10
  rescue ActiveRecord::RecordNotFound
7
11
  raise Delayed::DeserializationError
8
12
  end
@@ -0,0 +1,34 @@
1
+ class Module
2
+ yaml_as "tag:ruby.yaml.org,2002:module"
3
+
4
+ def self.yaml_new(klass, tag, val)
5
+ klass
6
+ end
7
+
8
+ def to_yaml(options = {})
9
+ YAML.quick_emit(nil, options) do |out|
10
+ out.scalar(taguri, name, :plain)
11
+ end
12
+ end
13
+
14
+ def yaml_tag_read_class(name)
15
+ # Constantize the object so that ActiveSupport can attempt
16
+ # its auto loading magic. Will raise LoadError if not successful.
17
+ name.constantize
18
+ name
19
+ end
20
+ end
21
+
22
+ class Class
23
+ yaml_as "tag:ruby.yaml.org,2002:class"
24
+ remove_method :to_yaml if respond_to?(:to_yaml) && method(:to_yaml).owner == Class # use Module's to_yaml
25
+ end
26
+
27
+ class Struct
28
+ def self.yaml_tag_read_class(name)
29
+ # Constantize the object so that ActiveSupport can attempt
30
+ # its auto loading magic. Will raise LoadError if not successful.
31
+ name.constantize
32
+ "Struct::#{ name }"
33
+ end
34
+ end
data/lib/delayed/tasks.rb CHANGED
@@ -6,6 +6,6 @@ namespace :jobs do
6
6
 
7
7
  desc "Start a delayed_job worker."
8
8
  task :work => :environment do
9
- Delayed::Worker.new(:min_priority => ENV['MIN_PRIORITY'], :max_priority => ENV['MAX_PRIORITY'], :quiet => false).start
9
+ Delayed::Worker.new(:min_priority => ENV['MIN_PRIORITY'], :max_priority => ENV['MAX_PRIORITY'], :queues => (ENV['QUEUES'] || ENV['QUEUE'] || '').split(','), :quiet => false).start
10
10
  end
11
11
  end