emipair-delayed_job 2.0.3.1

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.
Files changed (91) hide show
  1. data/.gitignore +2 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.textile +213 -0
  4. data/Rakefile +33 -0
  5. data/VERSION +1 -0
  6. data/benchmarks.rb +33 -0
  7. data/contrib/delayed_job.monitrc +14 -0
  8. data/contrib/delayed_job_multiple.monitrc +23 -0
  9. data/emipair-delayed_job.gemspec +25 -0
  10. data/generators/delayed_job/delayed_job_generator.rb +22 -0
  11. data/generators/delayed_job/templates/migration.rb +21 -0
  12. data/generators/delayed_job/templates/script +5 -0
  13. data/init.rb +1 -0
  14. data/lib/delayed/backend/active_record.rb +90 -0
  15. data/lib/delayed/backend/base.rb +111 -0
  16. data/lib/delayed/backend/data_mapper.rb +147 -0
  17. data/lib/delayed/backend/mongo_mapper.rb +110 -0
  18. data/lib/delayed/command.rb +109 -0
  19. data/lib/delayed/message_sending.rb +22 -0
  20. data/lib/delayed/performable_method.rb +62 -0
  21. data/lib/delayed/railtie.rb +10 -0
  22. data/lib/delayed/recipes.rb +31 -0
  23. data/lib/delayed/tasks.rb +15 -0
  24. data/lib/delayed/worker.rb +214 -0
  25. data/lib/delayed_job.rb +15 -0
  26. data/lib/passive_support.rb +4 -0
  27. data/lib/passive_support/basic_object.rb +16 -0
  28. data/lib/passive_support/core_ext.rb +8 -0
  29. data/lib/passive_support/core_ext/class.rb +1 -0
  30. data/lib/passive_support/core_ext/class/attribute_accessors.rb +57 -0
  31. data/lib/passive_support/core_ext/date.rb +10 -0
  32. data/lib/passive_support/core_ext/date/behavior.rb +42 -0
  33. data/lib/passive_support/core_ext/date/calculations.rb +241 -0
  34. data/lib/passive_support/core_ext/date/conversions.rb +107 -0
  35. data/lib/passive_support/core_ext/date_time.rb +12 -0
  36. data/lib/passive_support/core_ext/date_time/calculations.rb +126 -0
  37. data/lib/passive_support/core_ext/date_time/conversions.rb +107 -0
  38. data/lib/passive_support/core_ext/enumerable.rb +120 -0
  39. data/lib/passive_support/core_ext/kernel.rb +5 -0
  40. data/lib/passive_support/core_ext/kernel/agnostics.rb +11 -0
  41. data/lib/passive_support/core_ext/kernel/daemonizing.rb +7 -0
  42. data/lib/passive_support/core_ext/kernel/debugger.rb +16 -0
  43. data/lib/passive_support/core_ext/kernel/reporting.rb +59 -0
  44. data/lib/passive_support/core_ext/kernel/requires.rb +24 -0
  45. data/lib/passive_support/core_ext/module.rb +20 -0
  46. data/lib/passive_support/core_ext/module/aliasing.rb +74 -0
  47. data/lib/passive_support/core_ext/module/attr_accessor_with_default.rb +31 -0
  48. data/lib/passive_support/core_ext/module/attr_internal.rb +32 -0
  49. data/lib/passive_support/core_ext/module/delegation.rb +135 -0
  50. data/lib/passive_support/core_ext/module/inclusion.rb +30 -0
  51. data/lib/passive_support/core_ext/module/introspection.rb +90 -0
  52. data/lib/passive_support/core_ext/module/loading.rb +23 -0
  53. data/lib/passive_support/core_ext/module/model_naming.rb +25 -0
  54. data/lib/passive_support/core_ext/module/synchronization.rb +39 -0
  55. data/lib/passive_support/core_ext/numeric.rb +9 -0
  56. data/lib/passive_support/core_ext/numeric/bytes.rb +50 -0
  57. data/lib/passive_support/core_ext/numeric/conversions.rb +19 -0
  58. data/lib/passive_support/core_ext/numeric/time.rb +81 -0
  59. data/lib/passive_support/core_ext/object.rb +6 -0
  60. data/lib/passive_support/core_ext/object/blank.rb +76 -0
  61. data/lib/passive_support/core_ext/object/conversions.rb +15 -0
  62. data/lib/passive_support/core_ext/object/extending.rb +80 -0
  63. data/lib/passive_support/core_ext/object/instance_variables.rb +74 -0
  64. data/lib/passive_support/core_ext/object/misc.rb +90 -0
  65. data/lib/passive_support/core_ext/object/singleton_class.rb +13 -0
  66. data/lib/passive_support/core_ext/string.rb +1 -0
  67. data/lib/passive_support/core_ext/string/constantize.rb +7 -0
  68. data/lib/passive_support/core_ext/time.rb +46 -0
  69. data/lib/passive_support/core_ext/time/behavior.rb +13 -0
  70. data/lib/passive_support/core_ext/time/calculations.rb +313 -0
  71. data/lib/passive_support/core_ext/time/conversions.rb +90 -0
  72. data/lib/passive_support/core_ext/time/zones.rb +86 -0
  73. data/lib/passive_support/duration.rb +100 -0
  74. data/lib/passive_support/ordered_hash.rb +158 -0
  75. data/rails/init.rb +5 -0
  76. data/recipes/delayed_job.rb +1 -0
  77. data/spec/backend/active_record_job_spec.rb +46 -0
  78. data/spec/backend/data_mapper_job_spec.rb +16 -0
  79. data/spec/backend/mongo_mapper_job_spec.rb +94 -0
  80. data/spec/backend/shared_backend_spec.rb +268 -0
  81. data/spec/delayed_method_spec.rb +58 -0
  82. data/spec/performable_method_spec.rb +42 -0
  83. data/spec/sample_jobs.rb +25 -0
  84. data/spec/setup/active_record.rb +33 -0
  85. data/spec/setup/data_mapper.rb +24 -0
  86. data/spec/setup/mongo_mapper.rb +17 -0
  87. data/spec/spec_helper.rb +19 -0
  88. data/spec/story_spec.rb +17 -0
  89. data/spec/worker_spec.rb +225 -0
  90. data/tasks/jobs.rake +1 -0
  91. metadata +323 -0
@@ -0,0 +1,111 @@
1
+ module Delayed
2
+ module Backend
3
+ class DeserializationError < StandardError
4
+ end
5
+
6
+ module Base
7
+ def self.included(base)
8
+ base.extend ClassMethods
9
+ end
10
+
11
+ module ClassMethods
12
+ # Add a job to the queue
13
+ def enqueue(*args)
14
+ object = args.shift
15
+ unless object.respond_to?(:perform)
16
+ raise ArgumentError, 'Cannot enqueue items which do not respond to perform'
17
+ end
18
+
19
+ priority = args.first || 0
20
+ run_at = args[1]
21
+ self.create(:payload_object => object, :priority => priority.to_i, :run_at => run_at)
22
+ end
23
+
24
+ # Hook method that is called before a new worker is forked
25
+ def before_fork
26
+ end
27
+
28
+ # Hook method that is called after a new worker is forked
29
+ def after_fork
30
+ end
31
+
32
+ def work_off(num = 100)
33
+ warn "[DEPRECATION] `Delayed::Job.work_off` is deprecated. Use `Delayed::Worker.new.work_off instead."
34
+ Delayed::Worker.new.work_off(num)
35
+ end
36
+ end
37
+
38
+ ParseObjectFromYaml = /\!ruby\/\w+\:([^\s]+)/
39
+
40
+ def failed?
41
+ failed_at
42
+ end
43
+ alias_method :failed, :failed?
44
+
45
+ def payload_object
46
+ @payload_object ||= deserialize(self['handler'])
47
+ end
48
+
49
+ def name
50
+ @name ||= begin
51
+ payload = payload_object
52
+ if payload.respond_to?(:display_name)
53
+ payload.display_name
54
+ else
55
+ payload.class.name
56
+ end
57
+ end
58
+ end
59
+
60
+ def payload_object=(object)
61
+ self['handler'] = object.to_yaml
62
+ end
63
+
64
+ # Moved into its own method so that new_relic can trace it.
65
+ def invoke_job
66
+ payload_object.perform
67
+ end
68
+
69
+ # Unlock this job (note: not saved to DB)
70
+ def unlock
71
+ self.locked_at = nil
72
+ self.locked_by = nil
73
+ end
74
+
75
+ private
76
+
77
+ def deserialize(source)
78
+ handler = YAML.load(source) rescue nil
79
+
80
+ unless handler.respond_to?(:perform)
81
+ if handler.nil? && source =~ ParseObjectFromYaml
82
+ handler_class = $1
83
+ end
84
+ attempt_to_load(handler_class || handler.class)
85
+ handler = YAML.load(source)
86
+ end
87
+
88
+ return handler if handler.respond_to?(:perform)
89
+
90
+ raise DeserializationError,
91
+ 'Job failed to load: Unknown handler. Try to manually require the appropriate file.'
92
+ rescue TypeError, LoadError, NameError => e
93
+ raise DeserializationError,
94
+ "Job failed to load: #{e.message}. Try to manually require the required file."
95
+ end
96
+
97
+ # Constantize the object so that ActiveSupport can attempt
98
+ # its auto loading magic. Will raise LoadError if not successful.
99
+ def attempt_to_load(klass)
100
+ klass.constantize
101
+ end
102
+
103
+ protected
104
+
105
+ def set_default_run_at
106
+ self.run_at ||= self.class.db_time_now
107
+ end
108
+
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,147 @@
1
+ require 'dm-core'
2
+ require 'dm-observer'
3
+ require 'dm-aggregates'
4
+
5
+ module EMI
6
+ module DelayedJob
7
+ module ClassMethods
8
+ def load_for_delayed_job(id)
9
+ return self unless id
10
+
11
+ first(:id => id) || raise(DataMapper::ObjectNotFoundError, "Unable to find #{self.name} with id #{id}")
12
+ end
13
+ end
14
+ end
15
+ end
16
+
17
+ module EMI
18
+ module DelayedJob
19
+ module InstanceMethods
20
+ def dump_for_delayed_job
21
+ "#{self.class};#{self.id}"
22
+ end
23
+ end
24
+ end
25
+ end
26
+
27
+ # use append_(extensions|inclusions) here instead of broke-ass nonsense that was here
28
+ DataMapper::Model.append_extensions(EMI::DelayedJob::ClassMethods)
29
+ DataMapper::Model.append_inclusions(EMI::DelayedJob::InstanceMethods)
30
+
31
+ module Delayed
32
+ module Backend
33
+ module DataMapper
34
+ class Job
35
+ include ::DataMapper::Resource
36
+ include Delayed::Backend::Base
37
+
38
+ storage_names[:default] = 'delayed_jobs'
39
+ # def self.database_name=(database_sym)
40
+ # @database_name = database_sym
41
+ # storage_names[database_sym] = 'delayed_jobs'
42
+ # end
43
+ #
44
+ # def self.default_repository_name
45
+ # @database_name || :default
46
+ # end
47
+
48
+ property :id, Serial
49
+ property :priority, Integer, :default => 0, :index => :run_at_priority
50
+ property :attempts, Integer, :default => 0
51
+ property :handler, Text, :lazy => false
52
+ property :run_at, Time, :index => :run_at_priority
53
+ property :locked_at, Time, :index => true
54
+ property :locked_by, String, :length => 255
55
+ property :failed_at, Time
56
+ property :last_error, Text
57
+
58
+ def self.db_time_now
59
+ Time.now
60
+ end
61
+
62
+ def self.find_available(worker_name, limit = 5, max_run_time = Worker.max_run_time)
63
+
64
+ simple_conditions = { :run_at.lte => db_time_now, :limit => limit, :failed_at => nil, :order => [:priority.asc, :run_at.asc] }
65
+
66
+ # respect priorities
67
+ simple_conditions[:priority.gte] = Worker.min_priority if Worker.min_priority
68
+ simple_conditions[:priority.lte] = Worker.max_priority if Worker.max_priority
69
+
70
+ # lockable
71
+ lockable = (
72
+ # not locked or past the max time
73
+ ( all(:locked_at => nil ) | all(:locked_at.lt => db_time_now - max_run_time)) |
74
+
75
+ # OR locked by our worker
76
+ all(:locked_by => worker_name))
77
+
78
+ # plus some other boring junk
79
+ (lockable).all( simple_conditions )
80
+ end
81
+
82
+ # When a worker is exiting, make sure we don't have any locked jobs.
83
+ def self.clear_locks!(worker_name)
84
+ all(:locked_by => worker_name).update(:locked_at => nil, :locked_by => nil)
85
+ end
86
+
87
+ # Lock this job for this worker.
88
+ # Returns true if we have the lock, false otherwise.
89
+ def lock_exclusively!(max_run_time, worker = worker_name)
90
+
91
+ now = self.class.db_time_now
92
+ overtime = now - max_run_time
93
+
94
+ # FIXME - this is a bit gross
95
+ # DM doesn't give us the number of rows affected by a collection update
96
+ # so we have to circumvent some niceness in DM::Collection here
97
+ job_lock_owned_by_worker = locked_by == worker
98
+ collection =
99
+ unless job_lock_owned_by_worker
100
+ job_run_lte_now = self.class.all(:id => id, :run_at.lte => now)
101
+ all_unlocked = self.class.all(:locked_at => nil)
102
+ all_locked_before_overtime = self.class.all(:locked_at.lt => overtime)
103
+ (job_run_lte_now & (all_unlocked | all_locked_before_overtime) )
104
+ else
105
+ all_locked_by_worker = self.class.all(:id => id, :locked_by => worker)
106
+ all_locked_by_worker
107
+ end
108
+ attributes = collection.model.new(:locked_at => now, :locked_by => worker).dirty_attributes
109
+ affected_rows = self.repository.update(attributes, collection)
110
+ if affected_rows == 1
111
+ self.locked_at = now
112
+ self.locked_by = worker
113
+ return true
114
+ else
115
+ return false
116
+ end
117
+ end
118
+
119
+ # these are common to the other backends, so we provide an implementation
120
+ def self.delete_all
121
+ Delayed::Job.auto_migrate!
122
+ end
123
+
124
+ def self.find id
125
+ get id
126
+ end
127
+
128
+ def update_attributes(attributes)
129
+ attributes.each do |k,v|
130
+ self[k] = v
131
+ end
132
+ self.save
133
+ end
134
+ end
135
+
136
+ class JobObserver
137
+ include ::DataMapper::Observer
138
+
139
+ observe Job
140
+
141
+ before :save do
142
+ self.run_at ||= self.class.db_time_now
143
+ end
144
+ end
145
+ end
146
+ end
147
+ end
@@ -0,0 +1,110 @@
1
+ require 'mongo_mapper'
2
+
3
+ module ::MongoMapper
4
+ module Document
5
+ module ClassMethods
6
+ def load_for_delayed_job(id)
7
+ find!(id)
8
+ end
9
+ end
10
+
11
+ module InstanceMethods
12
+ def dump_for_delayed_job
13
+ "#{self.class};#{id}"
14
+ end
15
+ end
16
+ end
17
+ end
18
+
19
+ module Delayed
20
+ module Backend
21
+ module MongoMapper
22
+ class Job
23
+ include ::MongoMapper::Document
24
+ include Delayed::Backend::Base
25
+ set_collection_name 'delayed_jobs'
26
+
27
+ key :priority, Integer, :default => 0
28
+ key :attempts, Integer, :default => 0
29
+ key :handler, String
30
+ key :run_at, Time
31
+ key :locked_at, Time
32
+ key :locked_by, String, :index => true
33
+ key :failed_at, Time
34
+ key :last_error, String
35
+ timestamps!
36
+
37
+ before_save :set_default_run_at
38
+
39
+ ensure_index [[:priority, 1], [:run_at, 1]]
40
+
41
+ def self.before_fork
42
+ ::MongoMapper.connection.close
43
+ end
44
+
45
+ def self.after_fork
46
+ ::MongoMapper.connect(RAILS_ENV)
47
+ end
48
+
49
+ def self.db_time_now
50
+ Time.now.utc
51
+ end
52
+
53
+ def self.find_available(worker_name, limit = 5, max_run_time = Worker.max_run_time)
54
+ right_now = db_time_now
55
+
56
+ conditions = {
57
+ :run_at => {"$lte" => right_now},
58
+ :limit => -limit, # In mongo, positive limits are 'soft' and negative are 'hard'
59
+ :failed_at => nil,
60
+ :sort => [['priority', 1], ['run_at', 1]]
61
+ }
62
+
63
+ where = "this.locked_at == null || this.locked_at < #{make_date(right_now - max_run_time)}"
64
+
65
+ (conditions[:priority] ||= {})['$gte'] = Worker.min_priority.to_i if Worker.min_priority
66
+ (conditions[:priority] ||= {})['$lte'] = Worker.max_priority.to_i if Worker.max_priority
67
+
68
+ results = all(conditions.merge(:locked_by => worker_name))
69
+ results += all(conditions.merge('$where' => where)) if results.size < limit
70
+ results
71
+ end
72
+
73
+ # When a worker is exiting, make sure we don't have any locked jobs.
74
+ def self.clear_locks!(worker_name)
75
+ collection.update({:locked_by => worker_name}, {"$set" => {:locked_at => nil, :locked_by => nil}}, :multi => true)
76
+ end
77
+
78
+ # Lock this job for this worker.
79
+ # Returns true if we have the lock, false otherwise.
80
+ def lock_exclusively!(max_run_time, worker = worker_name)
81
+ right_now = self.class.db_time_now
82
+ overtime = right_now - max_run_time.to_i
83
+
84
+ query = "this.locked_at == null || this.locked_at < #{make_date(overtime)} || this.locked_by == #{worker.to_json}"
85
+ conditions = {:_id => id, :run_at => {"$lte" => right_now}, "$where" => query}
86
+
87
+ collection.update(conditions, {"$set" => {:locked_at => right_now, :locked_by => worker}})
88
+ affected_rows = collection.find({:_id => id, :locked_by => worker}).count
89
+ if affected_rows == 1
90
+ self.locked_at = right_now
91
+ self.locked_by = worker
92
+ return true
93
+ else
94
+ return false
95
+ end
96
+ end
97
+
98
+ private
99
+
100
+ def self.make_date(date_or_seconds)
101
+ "new Date(#{date_or_seconds.to_f * 1000})"
102
+ end
103
+
104
+ def make_date(date)
105
+ self.class.make_date(date)
106
+ end
107
+ end
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,109 @@
1
+ require 'rubygems'
2
+ require 'daemons'
3
+ require 'optparse'
4
+
5
+ module Delayed
6
+ class Command
7
+ attr_accessor :worker_count
8
+
9
+ def initialize(args)
10
+ @files_to_reopen = []
11
+ @options = {
12
+ :quiet => true,
13
+ :pid_dir => "#{RAILS_ROOT}/tmp/pids",
14
+ :log_file => "#{RAILS_ROOT}/log/delayed_job.log"
15
+ }
16
+
17
+ @worker_count = 1
18
+
19
+ opts = OptionParser.new do |opts|
20
+ opts.banner = "Usage: #{File.basename($0)} [options] start|stop|restart|run"
21
+
22
+ opts.on('-h', '--help', 'Show this message') do
23
+ puts opts
24
+ exit 1
25
+ end
26
+ opts.on('-e', '--environment=NAME', 'Specifies the environment to run this delayed jobs under (test/development/production).') do |e|
27
+ STDERR.puts "The -e/--environment option has been deprecated and has no effect. Use RAILS_ENV and see http://github.com/collectiveidea/delayed_job/issues/#issue/7"
28
+ end
29
+ opts.on('--min-priority N', 'Minimum priority of jobs to run.') do |n|
30
+ @options[:min_priority] = n
31
+ end
32
+ opts.on('--max-priority N', 'Maximum priority of jobs to run.') do |n|
33
+ @options[:max_priority] = n
34
+ end
35
+ opts.on('-n', '--number_of_workers=workers', "Number of unique workers to spawn") do |worker_count|
36
+ @worker_count = worker_count.to_i rescue 1
37
+ end
38
+ opts.on('--pid-dir=DIR', 'Specifies an alternate directory in which to store the process ids.') do |dir|
39
+ @options[:pid_dir] = dir
40
+ end
41
+ opts.on('--log-file=FILE', 'Specified an alternate log file.') do |file|
42
+ @options[:log_file] = file
43
+ end
44
+ opts.on('-i', '--identifier=n', 'A numeric identifier for the worker.') do |n|
45
+ @options[:identifier] = n
46
+ end
47
+ opts.on('--max-attempts=N', 'Maximum number of times a job is retried on error') do |n|
48
+ @options[:max_attempts] = n
49
+ end
50
+ end
51
+ @args = opts.parse!(args)
52
+ end
53
+
54
+ def daemonize
55
+ Delayed::Worker.backend.before_fork
56
+
57
+ ObjectSpace.each_object(File) do |file|
58
+ @files_to_reopen << file unless file.closed?
59
+ end
60
+
61
+ dir = @options[:pid_dir]
62
+ Dir.mkdir(dir) unless File.exists?(dir)
63
+
64
+ if @worker_count > 1 && @options[:identifier]
65
+ raise ArgumentError, 'Cannot specify both --number-of-workers and --identifier'
66
+ elsif @worker_count == 1 && @options[:identifier]
67
+ process_name = "delayed_job.#{@options[:identifier]}"
68
+ run_process(process_name, dir)
69
+ else
70
+ worker_count.times do |worker_index|
71
+ process_name = worker_count == 1 ? "delayed_job" : "delayed_job.#{worker_index}"
72
+ run_process(process_name, dir)
73
+ end
74
+ end
75
+ end
76
+
77
+ def run_process(process_name, dir)
78
+ Daemons.run_proc(process_name, :dir => dir, :dir_mode => :normal, :ARGV => @args) do |*args|
79
+ run process_name
80
+ end
81
+ end
82
+
83
+ def run(worker_name = nil)
84
+ Dir.chdir(RAILS_ROOT)
85
+
86
+ # Re-open file handles
87
+ @files_to_reopen.each do |file|
88
+ begin
89
+ file.reopen file.path
90
+ file.sync = true
91
+ rescue ::Exception
92
+ end
93
+ end
94
+
95
+ Delayed::Worker.logger = Logger.new(@options[:log_file])
96
+ Delayed::Worker.backend.after_fork
97
+
98
+ Delayed::Worker.max_attempts = @options[:max_attempts].to_i if @options[:max_attempts] && @options[:max_attempts].to_i > 0
99
+
100
+ worker = Delayed::Worker.new(@options)
101
+ worker.name_prefix = "#{worker_name} "
102
+ worker.start
103
+ rescue => e
104
+ Rails.logger.fatal e
105
+ STDERR.puts e.message
106
+ exit 1
107
+ end
108
+ end
109
+ end