drewda_delayed_job 3.0.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. data/MIT-LICENSE +20 -0
  2. data/README.textile +314 -0
  3. data/contrib/delayed_job.monitrc +14 -0
  4. data/contrib/delayed_job_multiple.monitrc +23 -0
  5. data/lib/delayed/backend/base.rb +184 -0
  6. data/lib/delayed/backend/shared_spec.rb +595 -0
  7. data/lib/delayed/command.rb +108 -0
  8. data/lib/delayed/deserialization_error.rb +4 -0
  9. data/lib/delayed/lifecycle.rb +84 -0
  10. data/lib/delayed/message_sending.rb +54 -0
  11. data/lib/delayed/performable_mailer.rb +21 -0
  12. data/lib/delayed/performable_method.rb +37 -0
  13. data/lib/delayed/plugin.rb +15 -0
  14. data/lib/delayed/plugins/clear_locks.rb +15 -0
  15. data/lib/delayed/psych_ext.rb +132 -0
  16. data/lib/delayed/railtie.rb +16 -0
  17. data/lib/delayed/recipes.rb +50 -0
  18. data/lib/delayed/serialization/active_record.rb +19 -0
  19. data/lib/delayed/syck_ext.rb +34 -0
  20. data/lib/delayed/tasks.rb +11 -0
  21. data/lib/delayed/worker.rb +242 -0
  22. data/lib/delayed/yaml_ext.rb +10 -0
  23. data/lib/delayed_job.rb +21 -0
  24. data/lib/generators/delayed_job/delayed_job_generator.rb +11 -0
  25. data/lib/generators/delayed_job/templates/script +5 -0
  26. data/recipes/delayed_job.rb +1 -0
  27. data/spec/autoloaded/clazz.rb +7 -0
  28. data/spec/autoloaded/instance_clazz.rb +6 -0
  29. data/spec/autoloaded/instance_struct.rb +6 -0
  30. data/spec/autoloaded/struct.rb +7 -0
  31. data/spec/delayed/backend/test.rb +113 -0
  32. data/spec/delayed/serialization/test.rb +0 -0
  33. data/spec/fixtures/bad_alias.yml +1 -0
  34. data/spec/lifecycle_spec.rb +67 -0
  35. data/spec/message_sending_spec.rb +113 -0
  36. data/spec/performable_mailer_spec.rb +44 -0
  37. data/spec/performable_method_spec.rb +89 -0
  38. data/spec/sample_jobs.rb +75 -0
  39. data/spec/spec_helper.rb +53 -0
  40. data/spec/test_backend_spec.rb +13 -0
  41. data/spec/worker_spec.rb +19 -0
  42. data/spec/yaml_ext_spec.rb +41 -0
  43. metadata +214 -0
@@ -0,0 +1,16 @@
1
+ require 'delayed_job'
2
+ require 'rails'
3
+
4
+ module Delayed
5
+ class Railtie < Rails::Railtie
6
+ initializer :after_initialize do
7
+ ActiveSupport.on_load(:action_mailer) do
8
+ ActionMailer::Base.send(:extend, Delayed::DelayMail)
9
+ end
10
+ end
11
+
12
+ rake_tasks do
13
+ load 'delayed/tasks.rb'
14
+ end
15
+ end
16
+ 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_job_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,19 @@
1
+ if defined?(ActiveRecord)
2
+ class ActiveRecord::Base
3
+ yaml_as "tag:ruby.yaml.org,2002:ActiveRecord"
4
+
5
+ def self.yaml_new(klass, tag, val)
6
+ if ActiveRecord::VERSION::MAJOR == 3
7
+ klass.unscoped.find(val['attributes'][klass.primary_key])
8
+ else # Rails 2
9
+ klass.with_exclusive_scope { klass.find(val['attributes'][klass.primary_key]) }
10
+ end
11
+ rescue ActiveRecord::RecordNotFound
12
+ raise Delayed::DeserializationError, "ActiveRecord::RecordNotFound, class: #{klass} , primary key: #{val['attributes'][klass.primary_key]} "
13
+ end
14
+
15
+ def to_yaml_properties
16
+ ['@attributes']
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,34 @@
1
+ class Module
2
+ yaml_as "tag:ruby.yaml.org,2002:module"
3
+
4
+ def self.yaml_new(klass, tag, val)
5
+ val.constantize
6
+ end
7
+
8
+ def to_yaml(options = {})
9
+ YAML.quick_emit(nil, options) do |out|
10
+ out.scalar(taguri, name, :plain)
11
+ end
12
+ end
13
+
14
+ def yaml_tag_read_class(name)
15
+ # Constantize the object so that ActiveSupport can attempt
16
+ # its auto loading magic. Will raise LoadError if not successful.
17
+ name.constantize
18
+ name
19
+ end
20
+ end
21
+
22
+ class Class
23
+ yaml_as "tag:ruby.yaml.org,2002:class"
24
+ remove_method :to_yaml if respond_to?(:to_yaml) && method(:to_yaml).owner == Class # use Module's to_yaml
25
+ end
26
+
27
+ class Struct
28
+ def self.yaml_tag_read_class(name)
29
+ # Constantize the object so that ActiveSupport can attempt
30
+ # its auto loading magic. Will raise LoadError if not successful.
31
+ name.constantize
32
+ "Struct::#{ name }"
33
+ end
34
+ end
@@ -0,0 +1,11 @@
1
+ namespace :jobs do
2
+ desc "Clear the delayed_job queue."
3
+ task :clear => :environment do
4
+ Delayed::Job.delete_all
5
+ end
6
+
7
+ desc "Start a delayed_job worker."
8
+ task :work => :environment do
9
+ Delayed::Worker.new(:min_priority => ENV['MIN_PRIORITY'], :max_priority => ENV['MAX_PRIORITY'], :queues => (ENV['QUEUES'] || ENV['QUEUE'] || '').split(','), :quiet => false).start
10
+ end
11
+ end
@@ -0,0 +1,242 @@
1
+ require 'timeout'
2
+ require 'active_support/core_ext/numeric/time'
3
+ require 'active_support/core_ext/class/attribute_accessors'
4
+ require 'active_support/core_ext/kernel'
5
+ require 'active_support/core_ext/enumerable'
6
+ require 'logger'
7
+ require 'benchmark'
8
+
9
+ module Delayed
10
+ class Worker
11
+ DEFAULT_SLEEP_DELAY = 5
12
+ DEFAULT_MAX_ATTEMPTS = 25
13
+ DEFAULT_MAX_RUN_TIME = 4.hours
14
+ DEFAULT_DEFAULT_PRIORITY = 0
15
+ DEFAULT_DELAY_JOBS = true
16
+ DEFAULT_QUEUES = []
17
+ DEFAULT_READ_AHEAD = 5
18
+
19
+ cattr_accessor :min_priority, :max_priority, :max_attempts, :max_run_time,
20
+ :default_priority, :sleep_delay, :logger, :delay_jobs, :queues,
21
+ :read_ahead, :plugins, :destroy_failed_jobs
22
+
23
+ cattr_reader :backend
24
+
25
+ # name_prefix is ignored if name is set directly
26
+ attr_accessor :name_prefix
27
+
28
+ def self.reset
29
+ self.sleep_delay = DEFAULT_SLEEP_DELAY
30
+ self.max_attempts = DEFAULT_MAX_ATTEMPTS
31
+ self.max_run_time = DEFAULT_MAX_RUN_TIME
32
+ self.default_priority = DEFAULT_DEFAULT_PRIORITY
33
+ self.delay_jobs = DEFAULT_DELAY_JOBS
34
+ self.queues = DEFAULT_QUEUES
35
+ self.read_ahead = DEFAULT_READ_AHEAD
36
+ end
37
+
38
+ reset
39
+
40
+ # Add or remove plugins in this list before the worker is instantiated
41
+ self.plugins = [Delayed::Plugins::ClearLocks]
42
+
43
+ # By default failed jobs are destroyed after too many attempts. If you want to keep them around
44
+ # (perhaps to inspect the reason for the failure), set this to false.
45
+ self.destroy_failed_jobs = true
46
+
47
+ self.logger = if defined?(Rails)
48
+ Rails.logger
49
+ elsif defined?(RAILS_DEFAULT_LOGGER)
50
+ RAILS_DEFAULT_LOGGER
51
+ end
52
+
53
+ def self.backend=(backend)
54
+ if backend.is_a? Symbol
55
+ require "delayed/serialization/#{backend}"
56
+ require "delayed/backend/#{backend}"
57
+ backend = "Delayed::Backend::#{backend.to_s.classify}::Job".constantize
58
+ end
59
+ @@backend = backend
60
+ silence_warnings { ::Delayed.const_set(:Job, backend) }
61
+ end
62
+
63
+ def self.guess_backend
64
+ warn "[DEPRECATION] guess_backend is deprecated. Please remove it from your code."
65
+ end
66
+
67
+ def self.before_fork
68
+ unless @files_to_reopen
69
+ @files_to_reopen = []
70
+ ObjectSpace.each_object(File) do |file|
71
+ @files_to_reopen << file unless file.closed?
72
+ end
73
+ end
74
+
75
+ backend.before_fork
76
+ end
77
+
78
+ def self.after_fork
79
+ # Re-open file handles
80
+ @files_to_reopen.each do |file|
81
+ begin
82
+ file.reopen file.path, "a+"
83
+ file.sync = true
84
+ rescue ::Exception
85
+ end
86
+ end
87
+
88
+ backend.after_fork
89
+ end
90
+
91
+ def self.lifecycle
92
+ @lifecycle ||= Delayed::Lifecycle.new
93
+ end
94
+
95
+ def initialize(options={})
96
+ @quiet = options.has_key?(:quiet) ? options[:quiet] : true
97
+ self.class.min_priority = options[:min_priority] if options.has_key?(:min_priority)
98
+ self.class.max_priority = options[:max_priority] if options.has_key?(:max_priority)
99
+ self.class.sleep_delay = options[:sleep_delay] if options.has_key?(:sleep_delay)
100
+ self.class.read_ahead = options[:read_ahead] if options.has_key?(:read_ahead)
101
+ self.class.queues = options[:queues] if options.has_key?(:queues)
102
+
103
+ self.plugins.each { |klass| klass.new }
104
+ end
105
+
106
+ # Every worker has a unique name which by default is the pid of the process. There are some
107
+ # advantages to overriding this with something which survives worker retarts: Workers can#
108
+ # safely resume working on tasks which are locked by themselves. The worker will assume that
109
+ # it crashed before.
110
+ def name
111
+ return @name unless @name.nil?
112
+ "#{@name_prefix}host:#{Socket.gethostname} pid:#{Process.pid}" rescue "#{@name_prefix}pid:#{Process.pid}"
113
+ end
114
+
115
+ # Sets the name of the worker.
116
+ # Setting the name to nil will reset the default worker name
117
+ def name=(val)
118
+ @name = val
119
+ end
120
+
121
+ def start
122
+ trap('TERM') { say 'Exiting...'; stop }
123
+ trap('INT') { say 'Exiting...'; stop }
124
+
125
+ say "Starting job worker"
126
+
127
+ self.class.lifecycle.run_callbacks(:execute, self) do
128
+ loop do
129
+ self.class.lifecycle.run_callbacks(:loop, self) do
130
+ result = nil
131
+
132
+ realtime = Benchmark.realtime do
133
+ result = work_off
134
+ end
135
+
136
+ count = result.sum
137
+
138
+ break if stop?
139
+
140
+ if count.zero?
141
+ sleep(self.class.sleep_delay)
142
+ else
143
+ say "#{count} jobs processed at %.4f j/s, %d failed ..." % [count / realtime, result.last]
144
+ end
145
+ end
146
+
147
+ break if stop?
148
+ end
149
+ end
150
+ end
151
+
152
+ def stop
153
+ @exit = true
154
+ end
155
+
156
+ def stop?
157
+ !!@exit
158
+ end
159
+
160
+ # Do num jobs and return stats on success/failure.
161
+ # Exit early if interrupted.
162
+ def work_off(num = 100)
163
+ success, failure = 0, 0
164
+
165
+ num.times do
166
+ case reserve_and_run_one_job
167
+ when true
168
+ success += 1
169
+ when false
170
+ failure += 1
171
+ else
172
+ break # leave if no work could be done
173
+ end
174
+ break if stop? # leave if we're exiting
175
+ end
176
+
177
+ return [success, failure]
178
+ end
179
+
180
+ def run(job)
181
+ runtime = Benchmark.realtime do
182
+ Timeout.timeout(self.class.max_run_time.to_i) { job.invoke_job }
183
+ job.destroy
184
+ end
185
+ say "#{job.name} completed after %.4f" % runtime
186
+ return true # did work
187
+ rescue DeserializationError => error
188
+ job.last_error = "{#{error.message}\n#{error.backtrace.join("\n")}"
189
+ failed(job)
190
+ rescue Exception => error
191
+ self.class.lifecycle.run_callbacks(:error, self, job){ handle_failed_job(job, error) }
192
+ return false # work failed
193
+ end
194
+
195
+ # Reschedule the job in the future (when a job fails).
196
+ # Uses an exponential scale depending on the number of failed attempts.
197
+ def reschedule(job, time = nil)
198
+ if (job.attempts += 1) < max_attempts(job)
199
+ time ||= job.reschedule_at
200
+ job.run_at = time
201
+ job.unlock
202
+ job.save!
203
+ else
204
+ say "PERMANENTLY removing #{job.name} because of #{job.attempts} consecutive failures.", Logger::INFO
205
+ failed(job)
206
+ end
207
+ end
208
+
209
+ def failed(job)
210
+ self.class.lifecycle.run_callbacks(:failure, self, job) do
211
+ job.hook(:failure)
212
+ self.class.destroy_failed_jobs ? job.destroy : job.fail!
213
+ end
214
+ end
215
+
216
+ def say(text, level = Logger::INFO)
217
+ text = "[Worker(#{name})] #{text}"
218
+ puts text unless @quiet
219
+ logger.add level, "#{Time.now.strftime('%FT%T%z')}: #{text}" if logger
220
+ end
221
+
222
+ def max_attempts(job)
223
+ job.max_attempts || self.class.max_attempts
224
+ end
225
+
226
+ protected
227
+
228
+ def handle_failed_job(job, error)
229
+ job.last_error = "{#{error.message}\n#{error.backtrace.join("\n")}"
230
+ say "#{job.name} failed with #{error.class.name}: #{error.message} - #{job.attempts} failed attempts", Logger::ERROR
231
+ reschedule(job)
232
+ end
233
+
234
+ # Run the next job we can get an exclusive lock on.
235
+ # If no jobs are left we return nil
236
+ def reserve_and_run_one_job
237
+ job = Delayed::Job.reserve(self)
238
+ self.class.lifecycle.run_callbacks(:perform, self, job){ result = run(job) } if job
239
+ end
240
+ end
241
+
242
+ end
@@ -0,0 +1,10 @@
1
+ # These extensions allow properly serializing and autoloading of
2
+ # Classes, Modules and Structs
3
+
4
+ require 'yaml'
5
+ if YAML.parser.class.name =~ /syck|yecht/i
6
+ require File.expand_path('../syck_ext', __FILE__)
7
+ require File.expand_path('../serialization/active_record', __FILE__)
8
+ else
9
+ require File.expand_path('../psych_ext', __FILE__)
10
+ end
@@ -0,0 +1,21 @@
1
+ require 'active_support'
2
+ require 'delayed/message_sending'
3
+ require 'delayed/performable_method'
4
+
5
+ # PerformableMailer is compatible with ActionMailer 3 (and possibly 3.1)
6
+ if defined?(ActionMailer)
7
+ require 'action_mailer/version'
8
+ require 'delayed/performable_mailer' if 3 == ActionMailer::VERSION::MAJOR
9
+ end
10
+
11
+ require 'delayed/yaml_ext'
12
+ require 'delayed/lifecycle'
13
+ require 'delayed/plugin'
14
+ require 'delayed/plugins/clear_locks'
15
+ require 'delayed/backend/base'
16
+ require 'delayed/worker'
17
+ require 'delayed/deserialization_error'
18
+ require 'delayed/railtie' if defined?(Rails::Railtie)
19
+
20
+ Object.send(:include, Delayed::MessageSending)
21
+ Module.send(:include, Delayed::MessageSending::ClassMethods)
@@ -0,0 +1,11 @@
1
+ require 'rails/generators'
2
+
3
+ class DelayedJobGenerator < Rails::Generators::Base
4
+
5
+ self.source_paths << File.join(File.dirname(__FILE__), 'templates')
6
+
7
+ def create_script_file
8
+ template 'script', 'script/delayed_job'
9
+ chmod 'script/delayed_job', 0755
10
+ end
11
+ end
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require File.expand_path(File.join(File.dirname(__FILE__), '..', 'config', 'environment'))
4
+ require 'delayed/command'
5
+ Delayed::Command.new(ARGV).daemonize
@@ -0,0 +1 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib', 'delayed', 'recipes'))
@@ -0,0 +1,7 @@
1
+ # Make sure this file does not get required manually
2
+ module Autoloaded
3
+ class Clazz
4
+ def perform
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,6 @@
1
+ module Autoloaded
2
+ class InstanceClazz
3
+ def perform
4
+ end
5
+ end
6
+ end