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 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