delayed_job 2.0.8 → 2.1.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/.gitignore +2 -0
- data/README.textile +14 -58
- data/Rakefile +53 -0
- data/VERSION +1 -0
- data/benchmarks.rb +33 -0
- data/delayed_job.gemspec +125 -0
- data/init.rb +1 -0
- data/lib/delayed/backend/active_record.rb +11 -13
- data/lib/delayed/backend/base.rb +14 -58
- data/lib/delayed/backend/couch_rest.rb +109 -0
- data/lib/delayed/backend/data_mapper.rb +8 -12
- data/lib/delayed/backend/mongo_mapper.rb +8 -12
- data/lib/delayed/command.rb +3 -8
- data/lib/delayed/message_sending.rb +10 -19
- data/lib/delayed/performable_method.rb +5 -48
- data/lib/delayed/railtie.rb +4 -0
- data/lib/delayed/recipes.rb +5 -24
- data/lib/delayed/worker.rb +26 -27
- data/lib/delayed/yaml_ext.rb +40 -0
- data/lib/delayed_job.rb +1 -1
- data/lib/generators/delayed_job/delayed_job_generator.rb +34 -0
- data/lib/generators/delayed_job/templates/migration.rb +21 -0
- data/lib/generators/delayed_job/templates/script +5 -0
- data/spec/autoloaded/clazz.rb +7 -0
- data/spec/autoloaded/struct.rb +7 -0
- data/spec/backend/couch_rest_job_spec.rb +15 -0
- data/spec/backend/mongo_mapper_job_spec.rb +11 -11
- data/spec/backend/shared_backend_spec.rb +41 -109
- data/spec/message_sending_spec.rb +1 -46
- data/spec/performable_method_spec.rb +22 -45
- data/spec/sample_jobs.rb +0 -1
- data/spec/setup/couch_rest.rb +7 -0
- data/spec/spec_helper.rb +6 -3
- data/spec/worker_spec.rb +6 -29
- metadata +174 -260
- data/lib/delayed/deserialization_error.rb +0 -4
- data/spec/delayed_method_spec.rb +0 -46
- data/spec/story_spec.rb +0 -17
@@ -0,0 +1,109 @@
|
|
1
|
+
require 'couchrest'
|
2
|
+
|
3
|
+
#extent couchrest to handle delayed_job serialization.
|
4
|
+
class CouchRest::ExtendedDocument
|
5
|
+
yaml_as "tag:ruby.yaml.org,2002:CouchRest"
|
6
|
+
|
7
|
+
def reload
|
8
|
+
job = self.class.get self['_id']
|
9
|
+
job.each {|k,v| self[k] = v}
|
10
|
+
end
|
11
|
+
def self.find(id)
|
12
|
+
get id
|
13
|
+
end
|
14
|
+
def self.yaml_new(klass, tag, val)
|
15
|
+
klass.get(val['_id'])
|
16
|
+
end
|
17
|
+
def ==(other)
|
18
|
+
if other.is_a? ::CouchRest::ExtendedDocument
|
19
|
+
self['_id'] == other['_id']
|
20
|
+
else
|
21
|
+
super
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
#couchrest adapter
|
27
|
+
module Delayed
|
28
|
+
module Backend
|
29
|
+
module CouchRest
|
30
|
+
class Job < ::CouchRest::ExtendedDocument
|
31
|
+
include Delayed::Backend::Base
|
32
|
+
use_database ::CouchRest::Server.new.database('delayed_job')
|
33
|
+
|
34
|
+
property :handler
|
35
|
+
property :last_error
|
36
|
+
property :locked_by
|
37
|
+
property :priority, :default => 0
|
38
|
+
property :attempts, :default => 0
|
39
|
+
property :run_at, :cast_as => 'Time'
|
40
|
+
property :locked_at, :cast_as => 'Time'
|
41
|
+
property :failed_at, :cast_as => 'Time'
|
42
|
+
timestamps!
|
43
|
+
|
44
|
+
set_callback :save, :before, :set_default_run_at
|
45
|
+
|
46
|
+
view_by(:failed_at, :locked_by, :run_at,
|
47
|
+
:map => "function(doc){" +
|
48
|
+
" if(doc['couchrest-type'] == 'Delayed::Backend::CouchRest::Job') {" +
|
49
|
+
" emit([doc.failed_at || null, doc.locked_by || null, doc.run_at || null], null);}" +
|
50
|
+
" }")
|
51
|
+
view_by(:failed_at, :locked_at, :run_at,
|
52
|
+
:map => "function(doc){" +
|
53
|
+
" if(doc['couchrest-type'] == 'Delayed::Backend::CouchRest::Job') {" +
|
54
|
+
" emit([doc.failed_at || null, doc.locked_at || null, doc.run_at || null], null);}" +
|
55
|
+
" }")
|
56
|
+
|
57
|
+
def self.db_time_now; Time.now; end
|
58
|
+
def self.find_available(worker_name, limit = 5, max_run_time = ::Delayed::Worker.max_run_time)
|
59
|
+
ready = ready_jobs
|
60
|
+
mine = my_jobs worker_name
|
61
|
+
expire = expired_jobs max_run_time
|
62
|
+
jobs = (ready + mine + expire)[0..limit-1].sort_by { |j| j.priority }
|
63
|
+
jobs = jobs.find_all { |j| j.priority >= Worker.min_priority } if Worker.min_priority
|
64
|
+
jobs = jobs.find_all { |j| j.priority <= Worker.max_priority } if Worker.max_priority
|
65
|
+
jobs
|
66
|
+
end
|
67
|
+
def self.clear_locks!(worker_name)
|
68
|
+
jobs = my_jobs worker_name
|
69
|
+
jobs.each { |j| j.locked_by, j.locked_at = nil, nil; }
|
70
|
+
database.bulk_save jobs
|
71
|
+
end
|
72
|
+
def self.delete_all
|
73
|
+
database.bulk_save all.each { |doc| doc['_deleted'] = true }
|
74
|
+
end
|
75
|
+
|
76
|
+
def lock_exclusively!(max_run_time, worker = worker_name)
|
77
|
+
return false if locked_by_other?(worker) and not expired?(max_run_time)
|
78
|
+
case
|
79
|
+
when locked_by_me?(worker)
|
80
|
+
self.locked_at = self.class.db_time_now
|
81
|
+
when (unlocked? or (locked_by_other?(worker) and expired?(max_run_time)))
|
82
|
+
self.locked_at, self.locked_by = self.class.db_time_now, worker
|
83
|
+
end
|
84
|
+
save
|
85
|
+
rescue RestClient::Conflict
|
86
|
+
false
|
87
|
+
end
|
88
|
+
|
89
|
+
private
|
90
|
+
def self.ready_jobs
|
91
|
+
options = {:startkey => [nil, nil], :endkey => [nil, nil, db_time_now]}
|
92
|
+
by_failed_at_and_locked_by_and_run_at options
|
93
|
+
end
|
94
|
+
def self.my_jobs(worker_name)
|
95
|
+
options = {:startkey => [nil, worker_name], :endkey => [nil, worker_name, {}]}
|
96
|
+
by_failed_at_and_locked_by_and_run_at options
|
97
|
+
end
|
98
|
+
def self.expired_jobs(max_run_time)
|
99
|
+
options = {:startkey => [nil,'0'], :endkey => [nil, db_time_now - max_run_time, db_time_now]}
|
100
|
+
by_failed_at_and_locked_at_and_run_at options
|
101
|
+
end
|
102
|
+
def unlocked?; locked_by.nil?; end
|
103
|
+
def expired?(time); locked_at < self.class.db_time_now - time; end
|
104
|
+
def locked_by_me?(worker); not locked_by.nil? and locked_by == worker; end
|
105
|
+
def locked_by_other?(worker); not locked_by.nil? and locked_by != worker; end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
@@ -2,19 +2,15 @@ require 'dm-core'
|
|
2
2
|
require 'dm-observer'
|
3
3
|
require 'dm-aggregates'
|
4
4
|
|
5
|
-
|
6
|
-
|
7
|
-
module ClassMethods
|
8
|
-
def load_for_delayed_job(id)
|
9
|
-
find!(id)
|
10
|
-
end
|
11
|
-
end
|
5
|
+
DataMapper::Resource.class_eval do
|
6
|
+
yaml_as "tag:ruby.yaml.org,2002:DataMapper"
|
12
7
|
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
8
|
+
def self.yaml_new(klass, tag, val)
|
9
|
+
klass.find(val['id'])
|
10
|
+
end
|
11
|
+
|
12
|
+
def to_yaml_properties
|
13
|
+
['@id']
|
18
14
|
end
|
19
15
|
end
|
20
16
|
|
@@ -1,18 +1,14 @@
|
|
1
1
|
require 'mongo_mapper'
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
end
|
3
|
+
MongoMapper::Document.class_eval do
|
4
|
+
yaml_as "tag:ruby.yaml.org,2002:MongoMapper"
|
5
|
+
|
6
|
+
def self.yaml_new(klass, tag, val)
|
7
|
+
klass.find(val['_id'])
|
8
|
+
end
|
10
9
|
|
11
|
-
|
12
|
-
|
13
|
-
"#{self.class};#{id}"
|
14
|
-
end
|
15
|
-
end
|
10
|
+
def to_yaml_properties
|
11
|
+
['@_id']
|
16
12
|
end
|
17
13
|
end
|
18
14
|
|
data/lib/delayed/command.rb
CHANGED
@@ -44,12 +44,8 @@ module Delayed
|
|
44
44
|
opts.on('-m', '--monitor', 'Start monitor process.') do
|
45
45
|
@monitor = true
|
46
46
|
end
|
47
|
-
|
48
|
-
|
49
|
-
end
|
50
|
-
opts.on('-p', '--prefix NAME', "String to be prefixed to worker process names") do |prefix|
|
51
|
-
@options[:prefix] = prefix
|
52
|
-
end
|
47
|
+
|
48
|
+
|
53
49
|
end
|
54
50
|
@args = opts.parse!(args)
|
55
51
|
end
|
@@ -79,7 +75,6 @@ module Delayed
|
|
79
75
|
|
80
76
|
def run_process(process_name, dir)
|
81
77
|
Daemons.run_proc(process_name, :dir => dir, :dir_mode => :normal, :monitor => @monitor, :ARGV => @args) do |*args|
|
82
|
-
$0 = File.join @options[:prefix], process_name if @options[:prefix]
|
83
78
|
run process_name
|
84
79
|
end
|
85
80
|
end
|
@@ -90,7 +85,7 @@ module Delayed
|
|
90
85
|
# Re-open file handles
|
91
86
|
@files_to_reopen.each do |file|
|
92
87
|
begin
|
93
|
-
file.reopen file.path
|
88
|
+
file.reopen file.path
|
94
89
|
file.sync = true
|
95
90
|
rescue ::Exception
|
96
91
|
end
|
@@ -1,18 +1,19 @@
|
|
1
|
+
require 'active_support/basic_object'
|
2
|
+
|
1
3
|
module Delayed
|
2
4
|
class DelayProxy < ActiveSupport::BasicObject
|
3
5
|
def initialize(target, options)
|
4
6
|
@target = target
|
5
7
|
@options = options
|
6
8
|
end
|
7
|
-
|
9
|
+
|
8
10
|
def method_missing(method, *args)
|
9
|
-
Job.create(
|
10
|
-
:payload_object => PerformableMethod.new(@target, method.to_sym, args)
|
11
|
-
|
12
|
-
}.merge(@options))
|
11
|
+
Job.create @options.merge(
|
12
|
+
:payload_object => PerformableMethod.new(@target, method.to_sym, args)
|
13
|
+
)
|
13
14
|
end
|
14
15
|
end
|
15
|
-
|
16
|
+
|
16
17
|
module MessageSending
|
17
18
|
def delay(options = {})
|
18
19
|
DelayProxy.new(self, options)
|
@@ -30,24 +31,14 @@ module Delayed
|
|
30
31
|
end
|
31
32
|
|
32
33
|
module ClassMethods
|
33
|
-
def handle_asynchronously(method
|
34
|
+
def handle_asynchronously(method)
|
34
35
|
aliased_method, punctuation = method.to_s.sub(/([?!=])$/, ''), $1
|
35
36
|
with_method, without_method = "#{aliased_method}_with_delay#{punctuation}", "#{aliased_method}_without_delay#{punctuation}"
|
36
37
|
define_method(with_method) do |*args|
|
37
|
-
|
38
|
-
curr_opts.each_key do |key|
|
39
|
-
if (val = curr_opts[key]).is_a?(Proc)
|
40
|
-
curr_opts[key] = if val.arity == 1
|
41
|
-
val.call(self)
|
42
|
-
else
|
43
|
-
val.call
|
44
|
-
end
|
45
|
-
end
|
46
|
-
end
|
47
|
-
delay(curr_opts).__send__(without_method, *args)
|
38
|
+
delay.__send__(without_method, *args)
|
48
39
|
end
|
49
40
|
alias_method_chain method, :delay
|
50
41
|
end
|
51
42
|
end
|
52
43
|
end
|
53
|
-
end
|
44
|
+
end
|
@@ -1,62 +1,19 @@
|
|
1
|
-
class Class
|
2
|
-
def load_for_delayed_job(arg)
|
3
|
-
self
|
4
|
-
end
|
5
|
-
|
6
|
-
def dump_for_delayed_job
|
7
|
-
name
|
8
|
-
end
|
9
|
-
end
|
10
|
-
|
11
1
|
module Delayed
|
12
2
|
class PerformableMethod < Struct.new(:object, :method, :args)
|
13
|
-
STRING_FORMAT = /^LOAD\;([A-Z][\w\:]+)(?:\;(\w+))?$/
|
14
|
-
|
15
|
-
class LoadError < StandardError
|
16
|
-
end
|
17
|
-
|
18
3
|
def initialize(object, method, args)
|
19
|
-
raise NoMethodError, "undefined method `#{method}' for #{object.inspect}" unless object.respond_to?(method
|
4
|
+
raise NoMethodError, "undefined method `#{method}' for #{object.inspect}" unless object.respond_to?(method)
|
20
5
|
|
21
|
-
self.object =
|
22
|
-
self.args = args
|
6
|
+
self.object = object
|
7
|
+
self.args = args
|
23
8
|
self.method = method.to_sym
|
24
9
|
end
|
25
10
|
|
26
11
|
def display_name
|
27
|
-
|
28
|
-
"#{$1}#{$2 ? '#' : '.'}#{method}"
|
29
|
-
else
|
30
|
-
"#{object.class}##{method}"
|
31
|
-
end
|
12
|
+
"#{object.class}##{method}"
|
32
13
|
end
|
33
14
|
|
34
15
|
def perform
|
35
|
-
|
36
|
-
rescue PerformableMethod::LoadError
|
37
|
-
# We cannot do anything about objects that can't be loaded
|
38
|
-
true
|
39
|
-
end
|
40
|
-
|
41
|
-
private
|
42
|
-
|
43
|
-
def load(obj)
|
44
|
-
if STRING_FORMAT === obj
|
45
|
-
$1.constantize.load_for_delayed_job($2)
|
46
|
-
else
|
47
|
-
obj
|
48
|
-
end
|
49
|
-
rescue => e
|
50
|
-
Delayed::Worker.logger.warn "Could not load object for job: #{e.message}"
|
51
|
-
raise PerformableMethod::LoadError
|
52
|
-
end
|
53
|
-
|
54
|
-
def dump(obj)
|
55
|
-
if obj.respond_to?(:dump_for_delayed_job)
|
56
|
-
"LOAD;#{obj.dump_for_delayed_job}"
|
57
|
-
else
|
58
|
-
obj
|
59
|
-
end
|
16
|
+
object.send(method, *args) if object
|
60
17
|
end
|
61
18
|
end
|
62
19
|
end
|
data/lib/delayed/railtie.rb
CHANGED
data/lib/delayed/recipes.rb
CHANGED
@@ -6,17 +6,6 @@
|
|
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
|
-
#
|
20
9
|
|
21
10
|
Capistrano::Configuration.instance.load do
|
22
11
|
namespace :delayed_job do
|
@@ -24,27 +13,19 @@ Capistrano::Configuration.instance.load do
|
|
24
13
|
fetch(:rails_env, false) ? "RAILS_ENV=#{fetch(:rails_env)}" : ''
|
25
14
|
end
|
26
15
|
|
27
|
-
def args
|
28
|
-
fetch(:delayed_job_args, "")
|
29
|
-
end
|
30
|
-
|
31
|
-
def roles
|
32
|
-
fetch(:delayed_job_server_role, :app)
|
33
|
-
end
|
34
|
-
|
35
16
|
desc "Stop the delayed_job process"
|
36
|
-
task :stop, :roles =>
|
17
|
+
task :stop, :roles => :app do
|
37
18
|
run "cd #{current_path};#{rails_env} script/delayed_job stop"
|
38
19
|
end
|
39
20
|
|
40
21
|
desc "Start the delayed_job process"
|
41
|
-
task :start, :roles =>
|
42
|
-
run "cd #{current_path};#{rails_env} script/delayed_job start
|
22
|
+
task :start, :roles => :app do
|
23
|
+
run "cd #{current_path};#{rails_env} script/delayed_job start"
|
43
24
|
end
|
44
25
|
|
45
26
|
desc "Restart the delayed_job process"
|
46
|
-
task :restart, :roles =>
|
47
|
-
run "cd #{current_path};#{rails_env} script/delayed_job restart
|
27
|
+
task :restart, :roles => :app do
|
28
|
+
run "cd #{current_path};#{rails_env} script/delayed_job restart"
|
48
29
|
end
|
49
30
|
end
|
50
31
|
end
|
data/lib/delayed/worker.rb
CHANGED
@@ -1,13 +1,14 @@
|
|
1
1
|
require 'timeout'
|
2
2
|
require 'active_support/core_ext/numeric/time'
|
3
|
+
require 'active_support/core_ext/class/attribute_accessors'
|
4
|
+
require 'active_support/core_ext/kernel'
|
3
5
|
|
4
6
|
module Delayed
|
5
7
|
class Worker
|
6
|
-
cattr_accessor :min_priority, :max_priority, :max_attempts, :max_run_time, :
|
8
|
+
cattr_accessor :min_priority, :max_priority, :max_attempts, :max_run_time, :sleep_delay, :logger
|
7
9
|
self.sleep_delay = 5
|
8
10
|
self.max_attempts = 25
|
9
11
|
self.max_run_time = 4.hours
|
10
|
-
self.default_priority = 0
|
11
12
|
|
12
13
|
# By default failed jobs are destroyed after too many attempts. If you want to keep them around
|
13
14
|
# (perhaps to inspect the reason for the failure), set this to false.
|
@@ -49,7 +50,6 @@ module Delayed
|
|
49
50
|
@quiet = options[:quiet]
|
50
51
|
self.class.min_priority = options[:min_priority] if options.has_key?(:min_priority)
|
51
52
|
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)
|
53
53
|
end
|
54
54
|
|
55
55
|
# Every worker has a unique name which by default is the pid of the process. There are some
|
@@ -85,7 +85,7 @@ module Delayed
|
|
85
85
|
break if $exit
|
86
86
|
|
87
87
|
if count.zero?
|
88
|
-
sleep(
|
88
|
+
sleep(@@sleep_delay)
|
89
89
|
else
|
90
90
|
say "#{count} jobs processed at %.4f j/s, %d failed ..." % [count / realtime, result.last]
|
91
91
|
end
|
@@ -124,38 +124,29 @@ module Delayed
|
|
124
124
|
end
|
125
125
|
say "#{job.name} completed after %.4f" % runtime
|
126
126
|
return true # did work
|
127
|
-
rescue
|
128
|
-
job
|
129
|
-
failed(job)
|
130
|
-
rescue Exception => error
|
131
|
-
handle_failed_job(job, error)
|
127
|
+
rescue Exception => e
|
128
|
+
handle_failed_job(job, e)
|
132
129
|
return false # work failed
|
133
130
|
end
|
134
131
|
|
135
132
|
# Reschedule the job in the future (when a job fails).
|
136
133
|
# Uses an exponential scale depending on the number of failed attempts.
|
137
134
|
def reschedule(job, time = nil)
|
138
|
-
if (job.attempts += 1) < max_attempts
|
139
|
-
job.
|
135
|
+
if (job.attempts += 1) < self.class.max_attempts
|
136
|
+
time ||= Job.db_time_now + (job.attempts ** 4) + 5
|
137
|
+
job.run_at = time
|
140
138
|
job.unlock
|
141
139
|
job.save!
|
142
140
|
else
|
143
141
|
say "PERMANENTLY removing #{job.name} because of #{job.attempts} consecutive failures.", Logger::INFO
|
144
|
-
failed(job)
|
145
|
-
end
|
146
|
-
end
|
147
142
|
|
148
|
-
def failed(job)
|
149
|
-
begin
|
150
143
|
if job.payload_object.respond_to? :on_permanent_failure
|
151
|
-
|
152
|
-
|
144
|
+
say "Running on_permanent_failure hook"
|
145
|
+
job.payload_object.on_permanent_failure
|
153
146
|
end
|
154
|
-
|
155
|
-
|
147
|
+
|
148
|
+
self.class.destroy_failed_jobs ? job.destroy : job.update_attributes(:failed_at => Delayed::Job.db_time_now)
|
156
149
|
end
|
157
|
-
|
158
|
-
self.class.destroy_failed_jobs ? job.destroy : job.update_attributes(:failed_at => Delayed::Job.db_time_now)
|
159
150
|
end
|
160
151
|
|
161
152
|
def say(text, level = Logger::INFO)
|
@@ -164,10 +155,6 @@ module Delayed
|
|
164
155
|
logger.add level, "#{Time.now.strftime('%FT%T%z')}: #{text}" if logger
|
165
156
|
end
|
166
157
|
|
167
|
-
def max_attempts(job)
|
168
|
-
job.max_attempts || self.class.max_attempts
|
169
|
-
end
|
170
|
-
|
171
158
|
protected
|
172
159
|
|
173
160
|
def handle_failed_job(job, error)
|
@@ -179,7 +166,19 @@ module Delayed
|
|
179
166
|
# Run the next job we can get an exclusive lock on.
|
180
167
|
# If no jobs are left we return nil
|
181
168
|
def reserve_and_run_one_job
|
182
|
-
|
169
|
+
|
170
|
+
# We get up to 5 jobs from the db. In case we cannot get exclusive access to a job we try the next.
|
171
|
+
# this leads to a more even distribution of jobs across the worker processes
|
172
|
+
job = Delayed::Job.find_available(name, 5, self.class.max_run_time).detect do |job|
|
173
|
+
if job.lock_exclusively!(self.class.max_run_time, name)
|
174
|
+
say "acquired lock on #{job.name}"
|
175
|
+
true
|
176
|
+
else
|
177
|
+
say "failed to acquire exclusive lock for #{job.name}", Logger::WARN
|
178
|
+
false
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
183
182
|
run(job) if job
|
184
183
|
end
|
185
184
|
end
|