blaxter-delayed_job 2.0.7 → 2.1.0
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/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
|