delayed-job-ajaycb 2.0.10

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 (39) hide show
  1. data/MIT-LICENSE +20 -0
  2. data/README.textile +250 -0
  3. data/contrib/delayed_job.monitrc +14 -0
  4. data/contrib/delayed_job_multiple.monitrc +23 -0
  5. data/generators/delayed_job/delayed_job_generator.rb +28 -0
  6. data/generators/delayed_job/templates/migration.rb +22 -0
  7. data/generators/delayed_job/templates/migration_queue_name.rb +12 -0
  8. data/generators/delayed_job/templates/script +5 -0
  9. data/lib/delayed/backend/active_record.rb +97 -0
  10. data/lib/delayed/backend/base.rb +126 -0
  11. data/lib/delayed/backend/data_mapper.rb +125 -0
  12. data/lib/delayed/backend/mongo_mapper.rb +110 -0
  13. data/lib/delayed/command.rb +116 -0
  14. data/lib/delayed/deserialization_error.rb +4 -0
  15. data/lib/delayed/message_sending.rb +53 -0
  16. data/lib/delayed/performable_method.rb +62 -0
  17. data/lib/delayed/railtie.rb +10 -0
  18. data/lib/delayed/recipes.rb +50 -0
  19. data/lib/delayed/tasks.rb +15 -0
  20. data/lib/delayed/worker.rb +190 -0
  21. data/lib/delayed_job.rb +15 -0
  22. data/rails/init.rb +5 -0
  23. data/recipes/delayed_job.rb +1 -0
  24. data/spec/backend/active_record_job_spec.rb +70 -0
  25. data/spec/backend/data_mapper_job_spec.rb +16 -0
  26. data/spec/backend/mongo_mapper_job_spec.rb +94 -0
  27. data/spec/backend/shared_backend_spec.rb +342 -0
  28. data/spec/delayed_method_spec.rb +46 -0
  29. data/spec/message_sending_spec.rb +89 -0
  30. data/spec/performable_method_spec.rb +53 -0
  31. data/spec/sample_jobs.rb +26 -0
  32. data/spec/setup/active_record.rb +34 -0
  33. data/spec/setup/data_mapper.rb +8 -0
  34. data/spec/setup/mongo_mapper.rb +17 -0
  35. data/spec/spec_helper.rb +28 -0
  36. data/spec/story_spec.rb +17 -0
  37. data/spec/worker_spec.rb +237 -0
  38. data/tasks/jobs.rake +1 -0
  39. metadata +329 -0
@@ -0,0 +1,4 @@
1
+ module Delayed
2
+ class DeserializationError < StandardError
3
+ end
4
+ end
@@ -0,0 +1,53 @@
1
+ module Delayed
2
+ class DelayProxy < ActiveSupport::BasicObject
3
+ def initialize(target, options)
4
+ @target = target
5
+ @options = options
6
+ end
7
+
8
+ def method_missing(method, *args)
9
+ Job.create({
10
+ :payload_object => PerformableMethod.new(@target, method.to_sym, args),
11
+ :priority => ::Delayed::Worker.default_priority
12
+ }.merge(@options))
13
+ end
14
+ end
15
+
16
+ module MessageSending
17
+ def delay(options = {})
18
+ DelayProxy.new(self, options)
19
+ end
20
+ alias __delay__ delay
21
+
22
+ def send_later(method, *args)
23
+ warn "[DEPRECATION] `object.send_later(:method)` is deprecated. Use `object.delay.method"
24
+ __delay__.__send__(method, *args)
25
+ end
26
+
27
+ def send_at(time, method, *args)
28
+ warn "[DEPRECATION] `object.send_at(time, :method)` is deprecated. Use `object.delay(:run_at => time).method"
29
+ __delay__(:run_at => time).__send__(method, *args)
30
+ end
31
+
32
+ module ClassMethods
33
+ def handle_asynchronously(method, opts = {})
34
+ aliased_method, punctuation = method.to_s.sub(/([?!=])$/, ''), $1
35
+ with_method, without_method = "#{aliased_method}_with_delay#{punctuation}", "#{aliased_method}_without_delay#{punctuation}"
36
+ define_method(with_method) do |*args|
37
+ curr_opts = opts.clone
38
+ curr_opts.each_key do |key|
39
+ if (val = curr_opts[key]).is_a?(Proc)
40
+ curr_opts[key] = if val.arity == 1
41
+ val.call(self)
42
+ else
43
+ val.call
44
+ end
45
+ end
46
+ end
47
+ delay(curr_opts).__send__(without_method, *args)
48
+ end
49
+ alias_method_chain method, :delay
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,62 @@
1
+ class Class
2
+ def load_for_delayed_job(arg)
3
+ self
4
+ end
5
+
6
+ def dump_for_delayed_job
7
+ name
8
+ end
9
+ end
10
+
11
+ module Delayed
12
+ class PerformableMethod < Struct.new(:object, :method, :args)
13
+ STRING_FORMAT = /^LOAD\;([A-Z][\w\:]+)(?:\;(\w+))?$/
14
+
15
+ class LoadError < StandardError
16
+ end
17
+
18
+ def initialize(object, method, args)
19
+ raise NoMethodError, "undefined method `#{method}' for #{object.inspect}" unless object.respond_to?(method, true)
20
+
21
+ self.object = dump(object)
22
+ self.args = args.map { |a| dump(a) }
23
+ self.method = method.to_sym
24
+ end
25
+
26
+ def display_name
27
+ if STRING_FORMAT === object
28
+ "#{$1}#{$2 ? '#' : '.'}#{method}"
29
+ else
30
+ "#{object.class}##{method}"
31
+ end
32
+ end
33
+
34
+ def perform
35
+ load(object).send(method, *args.map{|a| load(a)})
36
+ rescue PerformableMethod::LoadError
37
+ # We cannot do anything about objects that can't be loaded
38
+ true
39
+ end
40
+
41
+ private
42
+
43
+ def load(obj)
44
+ if STRING_FORMAT === obj
45
+ $1.constantize.load_for_delayed_job($2)
46
+ else
47
+ obj
48
+ end
49
+ rescue => e
50
+ Delayed::Worker.logger.warn "Could not load object for job: #{e.message}"
51
+ raise PerformableMethod::LoadError
52
+ end
53
+
54
+ def dump(obj)
55
+ if obj.respond_to?(:dump_for_delayed_job)
56
+ "LOAD;#{obj.dump_for_delayed_job}"
57
+ else
58
+ obj
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,10 @@
1
+ require 'delayed_job'
2
+ require 'rails'
3
+
4
+ module Delayed
5
+ class Railtie < Rails::Railtie
6
+ initializer :after_initialize do
7
+ Delayed::Worker.guess_backend
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,50 @@
1
+ # Capistrano Recipes for managing delayed_job
2
+ #
3
+ # Add these callbacks to have the delayed_job process restart when the server
4
+ # is restarted:
5
+ #
6
+ # after "deploy:stop", "delayed_job:stop"
7
+ # after "deploy:start", "delayed_job:start"
8
+ # after "deploy:restart", "delayed_job:restart"
9
+ #
10
+ # If you want to use command line options, for example to start multiple workers,
11
+ # define a Capistrano variable delayed_job_args:
12
+ #
13
+ # set :delayed_jobs_args, "-n 2"
14
+ #
15
+ # If you've got delayed_job workers running on a servers, you can also specify
16
+ # which servers have delayed_job running and should be restarted after deploy.
17
+ #
18
+ # set :delayed_job_server_role, :worker
19
+ #
20
+
21
+ Capistrano::Configuration.instance.load do
22
+ namespace :delayed_job do
23
+ def rails_env
24
+ fetch(:rails_env, false) ? "RAILS_ENV=#{fetch(:rails_env)}" : ''
25
+ end
26
+
27
+ def args
28
+ fetch(:delayed_job_args, "")
29
+ end
30
+
31
+ def roles
32
+ fetch(:delayed_job_server_role, :app)
33
+ end
34
+
35
+ desc "Stop the delayed_job process"
36
+ task :stop, :roles => lambda { roles } do
37
+ run "cd #{current_path};#{rails_env} script/delayed_job stop"
38
+ end
39
+
40
+ desc "Start the delayed_job process"
41
+ task :start, :roles => lambda { roles } do
42
+ run "cd #{current_path};#{rails_env} script/delayed_job start #{args}"
43
+ end
44
+
45
+ desc "Restart the delayed_job process"
46
+ task :restart, :roles => lambda { roles } do
47
+ run "cd #{current_path};#{rails_env} script/delayed_job restart #{args}"
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,15 @@
1
+ # Re-definitions are appended to existing tasks
2
+ task :environment
3
+ task :merb_env
4
+
5
+ namespace :jobs do
6
+ desc "Clear the delayed_job queue."
7
+ task :clear => [:merb_env, :environment] do
8
+ Delayed::Job.delete_all
9
+ end
10
+
11
+ desc "Start a delayed_job worker."
12
+ task :work => [:merb_env, :environment] do
13
+ Delayed::Worker.new(:min_priority => ENV['MIN_PRIORITY'], :max_priority => ENV['MAX_PRIORITY']).start
14
+ end
15
+ end
@@ -0,0 +1,190 @@
1
+ require 'timeout'
2
+ require 'active_support/core_ext/numeric/time'
3
+
4
+ module Delayed
5
+ class Worker
6
+ cattr_accessor :min_priority, :max_priority, :max_attempts, :max_run_time, :default_priority, :sleep_delay, :logger
7
+
8
+ attr_accessor :queues
9
+
10
+ self.sleep_delay = 5
11
+ self.max_attempts = 25
12
+ self.max_run_time = 4.hours
13
+ self.default_priority = 0
14
+
15
+ # By default failed jobs are destroyed after too many attempts. If you want to keep them around
16
+ # (perhaps to inspect the reason for the failure), set this to false.
17
+ cattr_accessor :destroy_failed_jobs
18
+ self.destroy_failed_jobs = true
19
+
20
+ self.logger = if defined?(Merb::Logger)
21
+ Merb.logger
22
+ elsif defined?(RAILS_DEFAULT_LOGGER)
23
+ RAILS_DEFAULT_LOGGER
24
+ end
25
+
26
+ # name_prefix is ignored if name is set directly
27
+ attr_accessor :name_prefix
28
+
29
+ cattr_reader :backend
30
+
31
+ def self.backend=(backend)
32
+ if backend.is_a? Symbol
33
+ require "delayed/backend/#{backend}"
34
+ backend = "Delayed::Backend::#{backend.to_s.classify}::Job".constantize
35
+ end
36
+ @@backend = backend
37
+ silence_warnings { ::Delayed.const_set(:Job, backend) }
38
+ end
39
+
40
+ def self.guess_backend
41
+ self.backend ||= if defined?(ActiveRecord)
42
+ :active_record
43
+ elsif defined?(MongoMapper)
44
+ :mongo_mapper
45
+ else
46
+ logger.warn "Could not decide on a backend, defaulting to active_record"
47
+ :active_record
48
+ end
49
+ end
50
+
51
+ def initialize(options={})
52
+ @quiet = options[:quiet]
53
+ self.class.min_priority = options[:min_priority] if options.has_key?(:min_priority)
54
+ self.class.max_priority = options[:max_priority] if options.has_key?(:max_priority)
55
+ self.class.sleep_delay = options[:sleep_delay] if options.has_key?(:sleep_delay)
56
+ self.queues = options[:queues] || ["default"]
57
+ end
58
+
59
+ # Every worker has a unique name which by default is the pid of the process. There are some
60
+ # advantages to overriding this with something which survives worker retarts: Workers can#
61
+ # safely resume working on tasks which are locked by themselves. The worker will assume that
62
+ # it crashed before.
63
+ def name
64
+ return @name unless @name.nil?
65
+ "#{@name_prefix}host:#{Socket.gethostname} pid:#{Process.pid}" rescue "#{@name_prefix}pid:#{Process.pid}"
66
+ end
67
+
68
+ # Sets the name of the worker.
69
+ # Setting the name to nil will reset the default worker name
70
+ def name=(val)
71
+ @name = val
72
+ end
73
+
74
+ def start
75
+ say "Starting job worker"
76
+
77
+ trap('TERM') { say 'Exiting...'; $exit = true }
78
+ trap('INT') { say 'Exiting...'; $exit = true }
79
+
80
+ loop do
81
+ result = nil
82
+
83
+ realtime = Benchmark.realtime do
84
+ result = work_off
85
+ end
86
+
87
+ count = result.sum
88
+
89
+ break if $exit
90
+
91
+ if count.zero?
92
+ sleep(self.class.sleep_delay)
93
+ else
94
+ say "#{count} jobs processed at %.4f j/s, %d failed ..." % [count / realtime, result.last]
95
+ end
96
+
97
+ break if $exit
98
+ end
99
+
100
+ ensure
101
+ Delayed::Job.clear_locks!(name)
102
+ end
103
+
104
+ # Do num jobs and return stats on success/failure.
105
+ # Exit early if interrupted.
106
+ def work_off(num = 100)
107
+ success, failure = 0, 0
108
+
109
+ num.times do
110
+ case reserve_and_run_one_job
111
+ when true
112
+ success += 1
113
+ when false
114
+ failure += 1
115
+ else
116
+ break # leave if no work could be done
117
+ end
118
+ break if $exit # leave if we're exiting
119
+ end
120
+
121
+ return [success, failure]
122
+ end
123
+
124
+ def run(job)
125
+ runtime = Benchmark.realtime do
126
+ Timeout.timeout(self.class.max_run_time.to_i) { job.invoke_job }
127
+ job.destroy
128
+ end
129
+ say "#{job.name} completed after %.4f" % runtime
130
+ return true # did work
131
+ rescue DeserializationError => error
132
+ job.last_error = "{#{error.message}\n#{error.backtrace.join('\n')}"
133
+ failed(job)
134
+ rescue Exception => error
135
+ handle_failed_job(job, error)
136
+ return false # work failed
137
+ end
138
+
139
+ # Reschedule the job in the future (when a job fails).
140
+ # Uses an exponential scale depending on the number of failed attempts.
141
+ def reschedule(job, time = nil)
142
+ if (job.attempts += 1) < max_attempts(job)
143
+ job.run_at = time || job.reschedule_at
144
+ job.unlock
145
+ job.save!
146
+ else
147
+ say "PERMANENTLY removing #{job.name} because of #{job.attempts} consecutive failures.", Logger::INFO
148
+ failed(job)
149
+ end
150
+ end
151
+
152
+ def failed(job)
153
+ begin
154
+ if job.payload_object.respond_to? :on_permanent_failure
155
+ say "Running on_permanent_failure hook"
156
+ job.payload_object.on_permanent_failure
157
+ end
158
+ rescue DeserializationError
159
+ # do nothing
160
+ end
161
+
162
+ self.class.destroy_failed_jobs ? job.destroy : job.update_attributes(:failed_at => Delayed::Job.db_time_now)
163
+ end
164
+
165
+ def say(text, level = Logger::INFO)
166
+ text = "[Worker(#{name})] #{text}"
167
+ puts text unless @quiet
168
+ logger.add level, "#{Time.now.strftime('%FT%T%z')}: #{text}" if logger
169
+ end
170
+
171
+ def max_attempts(job)
172
+ job.max_attempts || self.class.max_attempts
173
+ end
174
+
175
+ protected
176
+
177
+ def handle_failed_job(job, error)
178
+ job.last_error = error.message + "\n" + error.backtrace.join("\n")
179
+ say "#{job.name} failed with #{error.class.name}: #{error.message} - #{job.attempts} failed attempts", Logger::ERROR
180
+ reschedule(job)
181
+ end
182
+
183
+ # Run the next job we can get an exclusive lock on.
184
+ # If no jobs are left we return nil
185
+ def reserve_and_run_one_job
186
+ job = Delayed::Job.reserve(self)
187
+ run(job) if job
188
+ end
189
+ end
190
+ end
@@ -0,0 +1,15 @@
1
+ require 'active_support'
2
+
3
+ require File.dirname(__FILE__) + '/delayed/message_sending'
4
+ require File.dirname(__FILE__) + '/delayed/performable_method'
5
+ require File.dirname(__FILE__) + '/delayed/backend/base'
6
+ require File.dirname(__FILE__) + '/delayed/worker'
7
+ require File.dirname(__FILE__) + '/delayed/deserialization_error'
8
+ require File.dirname(__FILE__) + '/delayed/railtie' if defined?(::Rails::Railtie)
9
+
10
+ Object.send(:include, Delayed::MessageSending)
11
+ Module.send(:include, Delayed::MessageSending::ClassMethods)
12
+
13
+ if defined?(Merb::Plugins)
14
+ Merb::Plugins.add_rakefiles File.dirname(__FILE__) / 'delayed' / 'tasks'
15
+ end
@@ -0,0 +1,5 @@
1
+ require 'delayed_job'
2
+
3
+ config.after_initialize do
4
+ Delayed::Worker.guess_backend
5
+ end
@@ -0,0 +1 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib', 'delayed', 'recipes'))
@@ -0,0 +1,70 @@
1
+ require 'spec_helper'
2
+ require 'backend/shared_backend_spec'
3
+ require 'delayed/backend/active_record'
4
+
5
+ describe Delayed::Backend::ActiveRecord::Job do
6
+ before(:all) do
7
+ @backend = Delayed::Backend::ActiveRecord::Job
8
+ end
9
+
10
+ before(:each) do
11
+ Delayed::Backend::ActiveRecord::Job.delete_all
12
+ SimpleJob.runs = 0
13
+ end
14
+
15
+ after do
16
+ Time.zone = nil
17
+ end
18
+
19
+ it_should_behave_like 'a backend'
20
+
21
+ describe "reserve with queue names" do
22
+ before do
23
+ Delayed::Worker.max_run_time = 2.minutes
24
+ @worker_q1 = Delayed::Worker.new(:quiet => true, :queues => ["q1"])
25
+ @worker_q2 = Delayed::Worker.new(:quiet => true, :queues => ["q2"])
26
+ end
27
+
28
+ it "q1 should not reserve default queue" do
29
+ create_job
30
+ described_class.reserve(@worker_q1).should be_nil
31
+ described_class.reserve(@worker_q2).should be_nil
32
+ end
33
+
34
+ it "q1 should be able to reserve on q1 queue" do
35
+ create_job(:queue_name=>"q1")
36
+ described_class.reserve(@worker_q1).should_not be_nil
37
+ end
38
+
39
+ it "q1 should not be able to reserve on q2 queue" do
40
+ create_job(:queue_name=>"q2")
41
+ described_class.reserve(@worker_q1).should be_nil
42
+ end
43
+ end
44
+
45
+ context "db_time_now" do
46
+ it "should return time in current time zone if set" do
47
+ Time.zone = 'Eastern Time (US & Canada)'
48
+ %w(EST EDT).should include(Delayed::Job.db_time_now.zone)
49
+ end
50
+
51
+ it "should return UTC time if that is the AR default" do
52
+ Time.zone = nil
53
+ ActiveRecord::Base.default_timezone = :utc
54
+ Delayed::Backend::ActiveRecord::Job.db_time_now.zone.should == 'UTC'
55
+ end
56
+
57
+ it "should return local time if that is the AR default" do
58
+ Time.zone = 'Central Time (US & Canada)'
59
+ ActiveRecord::Base.default_timezone = :local
60
+ %w(CST CDT).should include(Delayed::Backend::ActiveRecord::Job.db_time_now.zone)
61
+ end
62
+ end
63
+
64
+ describe "after_fork" do
65
+ it "should call reconnect on the connection" do
66
+ ActiveRecord::Base.connection.should_receive(:reconnect!)
67
+ Delayed::Backend::ActiveRecord::Job.after_fork
68
+ end
69
+ end
70
+ end