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