delayed_job 2.1.4 → 3.0.0.pre

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 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