drewda_delayed_job 3.0.3

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