blaxter-delayed_job 2.0.7 → 2.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/VERSION +1 -1
- data/lib/delayed/job.rb +26 -13
- data/lib/delayed/job_handler.rb +129 -0
- data/lib/delayed/worker.rb +88 -41
- data/lib/delayed_job.rb +1 -0
- data/spec/job_spec.rb +3 -5
- metadata +5 -4
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
2.0
|
1
|
+
2.1.0
|
data/lib/delayed/job.rb
CHANGED
@@ -24,7 +24,7 @@ module Delayed
|
|
24
24
|
cattr_accessor :destroy_successful_jobs
|
25
25
|
self.destroy_successful_jobs = false
|
26
26
|
|
27
|
-
NextTaskSQL = '(run_at <= ? AND (locked_at IS NULL OR locked_at < ?)
|
27
|
+
NextTaskSQL = '(run_at <= ? AND (locked_at IS NULL OR locked_at < ?)) AND failed_at IS NULL AND finished_at IS NULL'
|
28
28
|
NextTaskOrder = 'priority DESC, run_at ASC'
|
29
29
|
|
30
30
|
ParseObjectFromYaml = /\!ruby\/\w+\:([^\s]+)/
|
@@ -35,11 +35,24 @@ module Delayed
|
|
35
35
|
|
36
36
|
class << self
|
37
37
|
# When a worker is exiting, make sure we don't have any locked jobs.
|
38
|
-
def clear_locks!(
|
38
|
+
def clear_locks!(worker_name)
|
39
39
|
update_all("locked_by = null, locked_at = null", ["locked_by = ?", worker_name])
|
40
40
|
end
|
41
41
|
|
42
|
-
#
|
42
|
+
# Unlock all jobs in the database, use careful!
|
43
|
+
def unlock_all!
|
44
|
+
unfinished.each do |job|
|
45
|
+
job.unlock
|
46
|
+
job.save!
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# Add a job to the queue. Parameters (positional):
|
51
|
+
# - job
|
52
|
+
# - priority (default is 0)
|
53
|
+
# - run_at (timestamp, default is right now)
|
54
|
+
# You could ignore the first parameter but including a block, which will
|
55
|
+
# be taken as the job to enqueue.
|
43
56
|
def enqueue(*args, &block)
|
44
57
|
object = block_given? ? EvaledJob.new(&block) : args.shift
|
45
58
|
|
@@ -53,15 +66,16 @@ module Delayed
|
|
53
66
|
Job.create(:payload_object => object, :priority => priority.to_i, :run_at => run_at)
|
54
67
|
end
|
55
68
|
|
56
|
-
# Conditions used for find_available method. This is in a separated method
|
57
|
-
# (or method chain it) more easy so you can
|
69
|
+
# Conditions used for find_available method. This is in a separated method
|
70
|
+
# to be able to override (or method chain it) more easy so you can
|
71
|
+
# customize the behaviour of your workers.
|
58
72
|
def conditions_available(options = {})
|
59
73
|
max_run_time = options[:max_run_time] || MAX_RUN_TIME
|
60
74
|
worker_name = options[:worker_name] || Worker::DEFAULT_WORKER_NAME
|
61
75
|
|
62
76
|
sql = NextTaskSQL.dup
|
63
77
|
time_now = db_time_now
|
64
|
-
conditions = [time_now, time_now - max_run_time
|
78
|
+
conditions = [time_now, time_now - max_run_time]
|
65
79
|
if options[:min_priority]
|
66
80
|
sql << ' AND (priority >= ?)'
|
67
81
|
conditions << options[:min_priority]
|
@@ -104,7 +118,7 @@ module Delayed
|
|
104
118
|
find_available( options ).each do |job|
|
105
119
|
t = job.run_with_lock( max_run_time, worker_name )
|
106
120
|
if t.nil?
|
107
|
-
Delayed::Worker.logger.info "
|
121
|
+
Delayed::Worker.logger.info "#{worker_name} No work done."
|
108
122
|
else
|
109
123
|
return t
|
110
124
|
end
|
@@ -183,9 +197,9 @@ module Delayed
|
|
183
197
|
save!
|
184
198
|
else
|
185
199
|
if self.attempts > 0
|
186
|
-
Delayed::Worker.logger.info "
|
200
|
+
Delayed::Worker.logger.info "[JOB] PERMANENTLY removing #{name} because of #{attempts} consequetive failures."
|
187
201
|
else
|
188
|
-
Delayed::Worker.logger.info "
|
202
|
+
Delayed::Worker.logger.info "[JOB] PERMANENTLY removing #{name} because no attempts for this job"
|
189
203
|
end
|
190
204
|
if destroy_failed_jobs
|
191
205
|
destroy
|
@@ -199,10 +213,9 @@ module Delayed
|
|
199
213
|
# Try to run one job.
|
200
214
|
# Returns true/false (work done/work failed) or nil if job can't be locked.
|
201
215
|
def run_with_lock(max_run_time = MAX_RUN_TIME, worker_name = Worker::DEFAULT_WORKER_NAME)
|
202
|
-
Delayed::Worker.logger.info "* [JOB] #{worker_name} aquiring lock on #{name}"
|
203
216
|
unless lock_exclusively!(max_run_time, worker_name)
|
204
217
|
# We did not get the lock, some other worker process must have
|
205
|
-
Delayed::Worker.logger.warn "
|
218
|
+
Delayed::Worker.logger.warn "[JOB] failed to aquire exclusive lock for #{name}"
|
206
219
|
return nil # no work done
|
207
220
|
end
|
208
221
|
|
@@ -212,7 +225,7 @@ module Delayed
|
|
212
225
|
end
|
213
226
|
destroy_successful_jobs ? destroy :
|
214
227
|
update_attribute(:finished_at, Time.now)
|
215
|
-
Delayed::Worker.logger.info "
|
228
|
+
Delayed::Worker.logger.info "[JOB] #{name} completed after %.4f" % runtime
|
216
229
|
return true # did work
|
217
230
|
rescue Exception => e
|
218
231
|
reschedule e.message, e.backtrace
|
@@ -250,7 +263,7 @@ module Delayed
|
|
250
263
|
|
251
264
|
# This is a good hook if you need to report job processing errors in additional or different ways
|
252
265
|
def log_exception(error)
|
253
|
-
Delayed::Worker.logger.error "
|
266
|
+
Delayed::Worker.logger.error "[JOB] #{name} failed with #{error.class.name}: #{error.message} - #{attempts} failed attempts"
|
254
267
|
if Delayed::HIDE_BACKTRACE
|
255
268
|
Delayed::Worker.logger.error error.to_s.split("\n").first
|
256
269
|
else
|
@@ -0,0 +1,129 @@
|
|
1
|
+
# Handle asynchronously launch of jobs, one per grouped value
|
2
|
+
#
|
3
|
+
# It needs the following method to be defined:
|
4
|
+
# - name: return the name of the worker
|
5
|
+
# - group_by: return the method to be called to obtain the object
|
6
|
+
# needed to group jobs
|
7
|
+
# - log: log method
|
8
|
+
#
|
9
|
+
# Implements the following interface:
|
10
|
+
# - initialize_launcher(Fixnum max_allowed) (Must be called at beginning)
|
11
|
+
# - launch(Delayed::Job job) => bool (launched or not)
|
12
|
+
# - jobs_in_execution => Fixnum
|
13
|
+
# - report_jobs_state => prints info to stdout
|
14
|
+
# - check_thread_sanity => maintenance operation
|
15
|
+
module Delayed
|
16
|
+
module JobLauncher
|
17
|
+
MAX_ACTIVE_JOBS = 50
|
18
|
+
|
19
|
+
# Initialize the launcher, you can specified the maximun number of
|
20
|
+
# jobs executing in parallel, by default MAX_ACTIVE_JOBS constant
|
21
|
+
#
|
22
|
+
# The launcher has a hash with the following structure:
|
23
|
+
# {}
|
24
|
+
# |- id
|
25
|
+
# | `{}
|
26
|
+
# | |-:thread => Thread
|
27
|
+
# | |-:job => Delayed::Job
|
28
|
+
# | `-:started_at => Time
|
29
|
+
# `-...
|
30
|
+
# If group_by specified an ActiveRecord::Base object, id will be the
|
31
|
+
# primary key of those objects.
|
32
|
+
def initialize_launcher(max_active_jobs=MAX_ACTIVE_JOBS)
|
33
|
+
@max_active_jobs = max_active_jobs
|
34
|
+
@jobs = {}
|
35
|
+
end
|
36
|
+
|
37
|
+
# Launch the job in a thread and register it. Returns whether the job
|
38
|
+
# has been launched or not.
|
39
|
+
def launch(job)
|
40
|
+
return false unless can_execute job
|
41
|
+
t = Thread.new do
|
42
|
+
begin
|
43
|
+
job.run_with_lock Job::MAX_RUN_TIME, name
|
44
|
+
ensure
|
45
|
+
unregister_job job
|
46
|
+
end
|
47
|
+
end
|
48
|
+
register_job job, t
|
49
|
+
return true
|
50
|
+
end
|
51
|
+
|
52
|
+
# Print information about the current state to stdout
|
53
|
+
def report_jobs_state
|
54
|
+
if jobs_in_execution > 0
|
55
|
+
margin = 20
|
56
|
+
title = "Jobs In Execution"
|
57
|
+
puts "\n #{'='*margin} #{title} #{'='*margin} "
|
58
|
+
puts " There are #{jobs_in_execution} jobs running."
|
59
|
+
each_job_in_execution do |job, started_at, thread|
|
60
|
+
duration = Duration.new(Time.now - started_at)
|
61
|
+
puts "\tJob #{job.id}: #{job}"
|
62
|
+
puts "\t Running on #{thread} (#{thread.status}) for #{duration}"
|
63
|
+
end
|
64
|
+
puts " #{'=' * (margin * 2 + title + 2)} "
|
65
|
+
else
|
66
|
+
puts "\n\tThere is no jobs in execution right now!"
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# Sanity check of dead threads for precaution, but probably won't be
|
71
|
+
# necessary
|
72
|
+
def check_thread_sanity
|
73
|
+
@jobs.values.each do |v|
|
74
|
+
thread = v[:thread]
|
75
|
+
unless thread.alive?
|
76
|
+
log "Dead thread? Terminate it!, This should not be happening"
|
77
|
+
thread.terminate
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
# Number of jobs executing right now
|
83
|
+
def jobs_in_execution
|
84
|
+
@jobs.size
|
85
|
+
end
|
86
|
+
|
87
|
+
# ^ public methods -------------------------------------------------------
|
88
|
+
private
|
89
|
+
# v private methods ------------------------------------------------------
|
90
|
+
|
91
|
+
# Whether we can or not execute this job
|
92
|
+
def can_execute(job)
|
93
|
+
object = get_object(job)
|
94
|
+
object && ! is_there_job_in_execution_for(object) &&
|
95
|
+
jobs_in_execution < @max_active_jobs
|
96
|
+
end
|
97
|
+
|
98
|
+
def each_job_in_execution
|
99
|
+
@jobs.each_pair do |key, value|
|
100
|
+
yield value[:job], value[:started_at], value[:thread]
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def is_there_job_in_execution_for(o)
|
105
|
+
!! @jobs[o]
|
106
|
+
end
|
107
|
+
|
108
|
+
def unregister_job(job)
|
109
|
+
@jobs.delete get_object(job)
|
110
|
+
end
|
111
|
+
|
112
|
+
def register_job(job, thread)
|
113
|
+
@jobs[get_object(job)] = {
|
114
|
+
:thread => thread,
|
115
|
+
:job => job,
|
116
|
+
:started_at => Time.now
|
117
|
+
}
|
118
|
+
end
|
119
|
+
|
120
|
+
def get_object(job)
|
121
|
+
object = job.send group_by
|
122
|
+
if object.is_a? ActiveRecord::Base
|
123
|
+
object.id
|
124
|
+
else
|
125
|
+
object
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end # module JobLauncher
|
129
|
+
end # module Delayed
|
data/lib/delayed/worker.rb
CHANGED
@@ -3,8 +3,6 @@ module Delayed
|
|
3
3
|
|
4
4
|
class Worker
|
5
5
|
SLEEP = 5
|
6
|
-
JOBS_EACH = 100 # Jobs executed in each iteration
|
7
|
-
|
8
6
|
DEFAULT_WORKER_NAME = "host:#{Socket.gethostname} pid:#{Process.pid}" rescue "pid:#{Process.pid}"
|
9
7
|
# Indicates that we have catched a signal and we have to exit asap
|
10
8
|
cattr_accessor :exit
|
@@ -17,11 +15,13 @@ module Delayed
|
|
17
15
|
RAILS_DEFAULT_LOGGER
|
18
16
|
end
|
19
17
|
|
20
|
-
|
21
|
-
|
18
|
+
include JobLauncher
|
19
|
+
|
20
|
+
# Every worker has a unique name which by default is the pid of the process (so you should
|
21
|
+
# have only one unless override this in the constructor).
|
22
22
|
#
|
23
|
-
# Thread.new { Delayed::Worker.new(
|
24
|
-
# Thread.new { Delayed::Worker.new(
|
23
|
+
# Thread.new { Delayed::Worker.new(:name => "Worker 1").start }
|
24
|
+
# Thread.new { Delayed::Worker.new(:name => "Worker 2").start }
|
25
25
|
#
|
26
26
|
# There are some advantages to overriding this with something which survives worker retarts:
|
27
27
|
# Workers can safely resume working on tasks which are locked by themselves.
|
@@ -31,70 +31,117 @@ module Delayed
|
|
31
31
|
# Constraints for this worker, what kind of jobs is gonna execute?
|
32
32
|
attr_accessor :min_priority, :max_priority, :job_types, :only_for
|
33
33
|
|
34
|
+
# The jobs will be group by this attribute. Each delayed_job is gonna be executed must
|
35
|
+
# respond to `:group_by`. The jobs will be group by that and only one job can be in
|
36
|
+
# execution.
|
37
|
+
attr_accessor :group_by
|
38
|
+
|
39
|
+
# Whether log, also, to stdout or not
|
34
40
|
attr_accessor :quiet
|
35
41
|
|
36
|
-
#
|
37
|
-
|
38
|
-
|
39
|
-
#
|
40
|
-
# - name: the name of the worker, mandatory if you are going to create several workers
|
41
|
-
# - quiet: log to stdout (besides the normal logger)
|
42
|
-
# - min_priority: constraint for selecting what jobs to execute (integer)
|
43
|
-
# - max_priority: constraint for selecting what jobs to execute (integer)
|
44
|
-
# - job_types: constraint for selecting what jobs to execute (String or Array)
|
42
|
+
# Seconds to sleep between each loop running available jobs
|
43
|
+
attr_accessor :sleep_time
|
44
|
+
|
45
|
+
# A worker will be in a loop trying to execute pending jobs looking in the database for that
|
45
46
|
def initialize(options={})
|
46
|
-
[
|
47
|
+
[:quiet, :name, :min_priority, :max_priority, :job_types, :only_for, :group_by,
|
48
|
+
:sleep_time
|
49
|
+
].each do |attr_name|
|
47
50
|
send "#{attr_name}=", options.delete(attr_name)
|
48
51
|
end
|
49
52
|
# Default values
|
50
53
|
self.name = DEFAULT_WORKER_NAME if self.name.nil?
|
51
54
|
self.quiet = true if self.quiet.nil?
|
55
|
+
self.sleep_time = SLEEP if self.sleep_time.nil?
|
56
|
+
|
52
57
|
@options = options
|
58
|
+
initialize_launcher
|
53
59
|
end
|
54
60
|
|
55
61
|
def start
|
56
|
-
say "
|
62
|
+
say "===> Starting job worker #{name}"
|
57
63
|
|
58
|
-
trap('TERM') {
|
59
|
-
trap('INT') {
|
64
|
+
trap('TERM') { signal_interrupt }
|
65
|
+
trap('INT') { signal_interrupt }
|
60
66
|
|
61
67
|
loop do
|
62
|
-
|
63
|
-
|
64
|
-
realtime = Benchmark.realtime do
|
65
|
-
result = Job.work_off constraints.merge(@options)
|
66
|
-
end
|
67
|
-
|
68
|
-
count = result.sum
|
69
|
-
|
70
|
-
if count.zero?
|
71
|
-
sleep(SLEEP) unless self.exit
|
68
|
+
if group_by
|
69
|
+
group_by_loop
|
72
70
|
else
|
73
|
-
|
71
|
+
normal_loop
|
74
72
|
end
|
75
73
|
break if self.exit
|
76
74
|
end
|
77
|
-
|
78
75
|
ensure
|
79
76
|
Job.clear_locks! name
|
77
|
+
say "<=== Finishing job worker #{name}"
|
78
|
+
end
|
79
|
+
|
80
|
+
def jobs_to_execute
|
81
|
+
Job.find_available constraints
|
80
82
|
end
|
81
83
|
|
82
84
|
def say(text)
|
83
|
-
text = "#{name}: #{text}"
|
84
85
|
puts text unless self.quiet
|
85
86
|
logger.info text if logger
|
86
87
|
end
|
88
|
+
alias :log :say
|
87
89
|
|
88
90
|
protected
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
91
|
+
|
92
|
+
def signal_interrupt
|
93
|
+
if @signal && Time.now - @signal <= 1
|
94
|
+
@signal = Time.now
|
95
|
+
report_jobs_state
|
96
|
+
return
|
97
|
+
else
|
98
|
+
now = Time.now
|
99
|
+
@signal = now
|
100
|
+
sleep 1
|
101
|
+
return if @signal != now
|
102
|
+
end
|
103
|
+
say 'Exiting...'
|
104
|
+
self.exit = true
|
105
|
+
end
|
106
|
+
|
107
|
+
def sleep_for_a_little_while
|
108
|
+
sleep(sleep_time.to_i) unless self.exit
|
109
|
+
end
|
110
|
+
|
111
|
+
def group_by_loop
|
112
|
+
check_thread_sanity
|
113
|
+
jobs_to_execute.each do |job|
|
114
|
+
if launch job
|
115
|
+
log "Launched job #{job.name}, there are #{jobs_in_execution} jobs in execution"
|
116
|
+
end
|
98
117
|
end
|
118
|
+
sleep_for_a_little_while
|
119
|
+
end
|
120
|
+
|
121
|
+
def normal_loop
|
122
|
+
result = nil
|
123
|
+
|
124
|
+
realtime = Benchmark.realtime do
|
125
|
+
result = Job.work_off constraints
|
126
|
+
end
|
127
|
+
|
128
|
+
count = result.sum
|
129
|
+
|
130
|
+
if count.zero?
|
131
|
+
sleep_for_a_little_while
|
132
|
+
else
|
133
|
+
say "#{count} jobs processed at %.4f j/s, %d failed ..." % [count / realtime, result.last]
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
def constraints
|
138
|
+
{:max_run_time => Job::MAX_RUN_TIME,
|
139
|
+
:worker_name => name,
|
140
|
+
:limit => 5,
|
141
|
+
:min_priority => min_priority,
|
142
|
+
:max_priority => max_priority,
|
143
|
+
:only_for => only_for,
|
144
|
+
:job_types => job_types }.merge @options
|
145
|
+
end
|
99
146
|
end
|
100
147
|
end
|
data/lib/delayed_job.rb
CHANGED
@@ -2,6 +2,7 @@ autoload :ActiveRecord, 'activerecord'
|
|
2
2
|
|
3
3
|
require File.dirname(__FILE__) + '/delayed/message_sending'
|
4
4
|
require File.dirname(__FILE__) + '/delayed/performable_method'
|
5
|
+
require File.dirname(__FILE__) + '/delayed/job_handler'
|
5
6
|
require File.dirname(__FILE__) + '/delayed/job'
|
6
7
|
require File.dirname(__FILE__) + '/delayed/worker'
|
7
8
|
|
data/spec/job_spec.rb
CHANGED
@@ -428,10 +428,10 @@ describe Delayed::Job do
|
|
428
428
|
SimpleJob.runs.should == 1 # runs the one open job
|
429
429
|
end
|
430
430
|
|
431
|
-
it "should find
|
431
|
+
it "should find only ur own jobs unless they are locked" do
|
432
432
|
SimpleJob.runs.should == 0
|
433
433
|
Delayed::Job.work_off :worker_name => 'worker1'
|
434
|
-
SimpleJob.runs.should ==
|
434
|
+
SimpleJob.runs.should == 1 # runs open job, no worker1 jobs that were already locked
|
435
435
|
end
|
436
436
|
end
|
437
437
|
|
@@ -453,9 +453,7 @@ describe Delayed::Job do
|
|
453
453
|
it "should ignore locks when finding our own jobs" do
|
454
454
|
SimpleJob.runs.should == 0
|
455
455
|
Delayed::Job.work_off :worker_name => 'worker1'
|
456
|
-
SimpleJob.runs.should ==
|
457
|
-
# This is useful in the case of a crash/restart on worker1,
|
458
|
-
# but make sure multiple workers on the same host have unique names!
|
456
|
+
SimpleJob.runs.should == 2 # runs open job plus worker1 jobs (unless locked)
|
459
457
|
end
|
460
458
|
|
461
459
|
end
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: blaxter-delayed_job
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 11
|
5
5
|
prerelease: false
|
6
6
|
segments:
|
7
7
|
- 2
|
8
|
+
- 1
|
8
9
|
- 0
|
9
|
-
|
10
|
-
version: 2.0.7
|
10
|
+
version: 2.1.0
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- "Tobias L\xC3\xBCtke"
|
@@ -16,7 +16,7 @@ autorequire:
|
|
16
16
|
bindir: bin
|
17
17
|
cert_chain: []
|
18
18
|
|
19
|
-
date: 2010-
|
19
|
+
date: 2010-09-16 00:00:00 +02:00
|
20
20
|
default_executable:
|
21
21
|
dependencies: []
|
22
22
|
|
@@ -35,6 +35,7 @@ files:
|
|
35
35
|
- generators/delayed_job/templates/migration.rb
|
36
36
|
- init.rb
|
37
37
|
- lib/delayed/job.rb
|
38
|
+
- lib/delayed/job_handler.rb
|
38
39
|
- lib/delayed/message_sending.rb
|
39
40
|
- lib/delayed/performable_method.rb
|
40
41
|
- lib/delayed/worker.rb
|