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 CHANGED
@@ -1 +1 @@
1
- 2.0.7
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 < ?) OR (locked_by = ?)) AND failed_at IS NULL AND finished_at IS NULL'
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!( worker_name )
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
- # Add a job to the queue
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 to be able to override
57
- # (or method chain it) more easy so you can customize the behaviour of your workers.
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, worker_name]
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 "* #{worker_name} No work done."
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 "* [JOB] PERMANENTLY removing #{self.name} because of #{attempts} consequetive failures."
200
+ Delayed::Worker.logger.info "[JOB] PERMANENTLY removing #{name} because of #{attempts} consequetive failures."
187
201
  else
188
- Delayed::Worker.logger.info "* [JOB] PERMANENTLY removing #{self.name} because no attempts for this job"
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 "* [JOB] #{worker_name} - failed to aquire exclusive lock for #{name}"
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 "* [JOB] #{worker_name} - #{name} completed after %.4f" % runtime
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 "* [JOB] #{name} failed with #{error.class.name}: #{error.message} - #{attempts} failed attempts"
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
@@ -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
- # Every worker has a unique name which by default is the pid of the process (so you only are
21
- # be able to have one unless override this in the constructor).
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( :name => "Worker 1" ).start }
24
- # Thread.new { Delayed::Worker.new( :name => "Worker 2" ).start }
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
- # A worker will be in a loop trying to execute pending jobs, you can also set
37
- # a few constraints to customize the worker's behaviour.
38
- #
39
- # Named parameters:
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
- [ :quiet, :name, :min_priority, :max_priority, :job_types, :only_for ].each do |attr_name|
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 "*** Starting job worker #{name}"
62
+ say "===> Starting job worker #{name}"
57
63
 
58
- trap('TERM') { say 'Exiting...'; self.exit = true }
59
- trap('INT') { say 'Exiting...'; self.exit = true }
64
+ trap('TERM') { signal_interrupt }
65
+ trap('INT') { signal_interrupt }
60
66
 
61
67
  loop do
62
- result = nil
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
- say "#{count} jobs processed at %.4f j/s, %d failed ..." % [count / realtime, result.last]
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
- def constraints
90
- { :max_run_time => Job::MAX_RUN_TIME,
91
- :worker_name => name,
92
- :n => JOBS_EACH,
93
- :limit => 5,
94
- :min_priority => min_priority,
95
- :max_priority => max_priority,
96
- :only_for => only_for,
97
- :job_types => job_types }
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 our own jobs regardless of locks" do
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 == 3 # runs open job plus worker1 jobs that were already locked
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 == 3 # runs open job plus worker1 jobs
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: 1
4
+ hash: 11
5
5
  prerelease: false
6
6
  segments:
7
7
  - 2
8
+ - 1
8
9
  - 0
9
- - 7
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-07-20 00:00:00 +02:00
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