xspond-delayed_job 1.8.5

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,22 @@
1
+ module Delayed
2
+ module MessageSending
3
+ def send_later(method, *args)
4
+ Delayed::Job.enqueue Delayed::PerformableMethod.new(self, method.to_sym, args)
5
+ end
6
+
7
+ def send_at(time, method, *args)
8
+ Delayed::Job.enqueue(Delayed::PerformableMethod.new(self, method.to_sym, args), 0, time)
9
+ end
10
+
11
+ module ClassMethods
12
+ def handle_asynchronously(method)
13
+ aliased_method, punctuation = method.to_s.sub(/([?!=])$/, ''), $1
14
+ with_method, without_method = "#{aliased_method}_with_send_later#{punctuation}", "#{aliased_method}_without_send_later#{punctuation}"
15
+ define_method(with_method) do |*args|
16
+ send_later(without_method, *args)
17
+ end
18
+ alias_method_chain method, :send_later
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,55 @@
1
+ module Delayed
2
+ class PerformableMethod < Struct.new(:object, :method, :args)
3
+ CLASS_STRING_FORMAT = /^CLASS\:([A-Z][\w\:]+)$/
4
+ AR_STRING_FORMAT = /^AR\:([A-Z][\w\:]+)\:(\d+)$/
5
+
6
+ def initialize(object, method, args)
7
+ raise NoMethodError, "undefined method `#{method}' for #{self.inspect}" unless object.respond_to?(method)
8
+
9
+ self.object = dump(object)
10
+ self.args = args.map { |a| dump(a) }
11
+ self.method = method.to_sym
12
+ end
13
+
14
+ def display_name
15
+ case self.object
16
+ when CLASS_STRING_FORMAT then "#{$1}.#{method}"
17
+ when AR_STRING_FORMAT then "#{$1}##{method}"
18
+ else "Unknown##{method}"
19
+ end
20
+ end
21
+
22
+ def perform
23
+ load(object).send(method, *args.map{|a| load(a)})
24
+ rescue ActiveRecord::RecordNotFound
25
+ # We cannot do anything about objects which were deleted in the meantime
26
+ true
27
+ end
28
+
29
+ private
30
+
31
+ def load(arg)
32
+ case arg
33
+ when CLASS_STRING_FORMAT then $1.constantize
34
+ when AR_STRING_FORMAT then $1.constantize.find($2)
35
+ else arg
36
+ end
37
+ end
38
+
39
+ def dump(arg)
40
+ case arg
41
+ when Class then class_to_string(arg)
42
+ when ActiveRecord::Base then ar_to_string(arg)
43
+ else arg
44
+ end
45
+ end
46
+
47
+ def ar_to_string(obj)
48
+ "AR:#{obj.class}:#{obj.id}"
49
+ end
50
+
51
+ def class_to_string(obj)
52
+ "CLASS:#{obj.name}"
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,27 @@
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
+ Capistrano::Configuration.instance.load do
11
+ namespace :delayed_job do
12
+ desc "Stop the delayed_job process"
13
+ task :stop, :roles => :app do
14
+ run "cd #{current_path}; RAILS_ENV=#{rails_env} script/delayed_job stop"
15
+ end
16
+
17
+ desc "Start the delayed_job process"
18
+ task :start, :roles => :app do
19
+ run "cd #{current_path}; RAILS_ENV=#{rails_env} script/delayed_job start"
20
+ end
21
+
22
+ desc "Restart the delayed_job process"
23
+ task :restart, :roles => :app do
24
+ run "cd #{current_path}; RAILS_ENV=#{rails_env} script/delayed_job restart"
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,17 @@
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
+ worker_count = ENV['WORKER_COUNT'].to_i rescue 1
14
+ worker_count = 1 if worker_count < 1
15
+ Delayed::Worker.new(:min_priority => ENV['MIN_PRIORITY'], :max_priority => ENV['MAX_PRIORITY'], :worker_count => worker_count).start
16
+ end
17
+ end
@@ -0,0 +1,256 @@
1
+ module Delayed
2
+ class Worker
3
+ @@sleep_delay = 5
4
+
5
+ cattr_accessor :sleep_delay
6
+
7
+ cattr_accessor :logger
8
+ self.logger = if defined?(Merb::Logger)
9
+ Merb.logger
10
+ elsif defined?(RAILS_DEFAULT_LOGGER)
11
+ RAILS_DEFAULT_LOGGER
12
+ end
13
+
14
+ # name_prefix is ignored if name is set directly
15
+ attr_accessor :name_prefix
16
+ attr_accessor :worker_count
17
+
18
+ def job_max_run_time
19
+ Delayed::Job.max_run_time
20
+ end
21
+
22
+ # Every worker has a unique name which by default is the pid of the process.
23
+ # There are some advantages to overriding this with something which survives worker retarts:
24
+ # Workers can safely resume working on tasks which are locked by themselves. The worker will assume that it crashed before.
25
+ def name
26
+ return @name unless @name.nil?
27
+ "#{@name_prefix}host:#{Socket.gethostname} pid:#{Process.pid}" rescue "#{@name_prefix}pid:#{Process.pid}"
28
+ end
29
+
30
+ # Sets the name of the worker.
31
+ # Setting the name to nil will reset the default worker name
32
+ def name=(val)
33
+ @name = val
34
+ end
35
+
36
+ def initialize(options={})
37
+ @quiet = options[:quiet]
38
+ Delayed::Job.min_priority = options[:min_priority] if options.has_key?(:min_priority)
39
+ Delayed::Job.max_priority = options[:max_priority] if options.has_key?(:max_priority)
40
+ self.worker_count = options[:worker_count] || 1
41
+
42
+ @ready_queue = SizedQueue.new(1)
43
+ @work_queue = Queue.new
44
+ @results_queue = Queue.new
45
+ end
46
+
47
+ def start
48
+ say "*** Starting job worker #{name}"
49
+
50
+ trap('TERM') { say 'Exiting...'; $exit = true }
51
+ trap('INT') { say 'Exiting...'; $exit = true }
52
+
53
+ consumers = []
54
+ 1.upto(worker_count) {|i| consumers << start_consumer(i) }
55
+
56
+ monitor = Thread.new { monitor_consumers(consumers) }
57
+
58
+ loop do
59
+ result = nil
60
+
61
+ realtime = Benchmark.realtime do
62
+ result = work_off
63
+ end
64
+
65
+ count = result.sum
66
+
67
+ break if $exit
68
+
69
+ if count.zero?
70
+ sleep(@@sleep_delay)
71
+ else
72
+ say "#{count} jobs processed at %.4f j/s, %d failed ..." % [count / realtime, result.last]
73
+ end
74
+
75
+ break if $exit
76
+ end
77
+
78
+ worker_count.times {|i| @work_queue << nil }
79
+ @ready_queue.pop while !@ready_queue.empty?
80
+ monitor.run if monitor.status
81
+ monitor.value
82
+ consumers.each do |c|
83
+ begin
84
+ c.value
85
+ rescue Exception => e
86
+ say e, Logger::ERROR
87
+ end
88
+ end
89
+
90
+ ensure
91
+ 1.upto(worker_count) do |i|
92
+ Delayed::Job.clear_locks!("#{name} #{i}")
93
+ end
94
+ end
95
+
96
+ def say(text, level = Logger::INFO)
97
+ puts text unless @quiet
98
+ logger.add level, text if logger
99
+ end
100
+
101
+ protected
102
+
103
+ # After the worker has returned, this is used to
104
+ # determine if the job succeeded.
105
+ def check_result(job)
106
+ job.reload
107
+
108
+ if !job.locked_by.nil?
109
+ say "* Worker process died without processing job (#{job.name}).", Logger::ERROR
110
+ job.unlock
111
+ job.save
112
+ end
113
+
114
+ false
115
+ rescue ActiveRecord::RecordNotFound
116
+ true
117
+ end
118
+
119
+ # A small helper method which resets the connections pools
120
+ # so that database connections are not reused. Called after
121
+ # fork in the child process
122
+ def clear_connection_handler
123
+ pools = Delayed::Job.connection_handler.connection_pools
124
+ pools.each_pair {|k,v| pools[k] = v.class.new(v.spec) }
125
+ true
126
+ end
127
+
128
+ # A method which checks each of the consumer threads
129
+ # restarting them if they fail.
130
+ def monitor_consumers(consumers)
131
+ while !$exit
132
+ consumers.each_with_index do |c,i|
133
+ if c.status.nil?
134
+ begin
135
+ c.value
136
+ rescue Exception => e
137
+ say e, Logger::ERROR
138
+ end
139
+ consumers[i] = start_consumer(i)
140
+ elsif c.status == false
141
+ say "Thread exitted but should not have", Logger::ERROR
142
+ consumers[i] = start_consumer(i)
143
+ end
144
+ end
145
+ sleep(5)
146
+ end
147
+ end
148
+
149
+ # Queue up the next job we can get an exclusive lock on.
150
+ # If no jobs are left we return nil
151
+ def reserve_and_queue_one_job(num, max_run_time = job_max_run_time)
152
+ lock_name = "#{name} #{num}"
153
+
154
+ # We get up to 5 jobs from the db. In case we cannot get exclusive access to a job we try the next.
155
+ # this leads to a more even distribution of jobs across the worker processes
156
+ job = Delayed::Job.find_available(lock_name, 5, max_run_time).detect do |job|
157
+ if job.lock_exclusively!(max_run_time, lock_name)
158
+ say "* [Worker(#{name})] acquired lock on #{job.name}"
159
+ true
160
+ else
161
+ say "* [Worker(#{name})] failed to acquire exclusive lock for #{job.name}", Logger::WARN
162
+ false
163
+ end
164
+ end
165
+
166
+ if job.nil?
167
+ @work_queue << true
168
+ nil # we didn't do any work, all 5 were not lockable
169
+ else
170
+ @work_queue << job
171
+ true
172
+ end
173
+ end
174
+
175
+ # Forks the process retrying on failure and reconnecting
176
+ # the database within the child process.
177
+ def safe_fork
178
+ begin
179
+ if block_given?
180
+ fork do
181
+ clear_connection_handler
182
+ yield
183
+ end
184
+ else
185
+ pid = fork
186
+ clear_connection_handler unless pid
187
+ pid
188
+ end
189
+ rescue Errno::EWOULDBLOCK
190
+ sleep 5
191
+ retry
192
+ end
193
+ end
194
+
195
+ # Generates a consumer thread that queues itself up
196
+ # to receive work.
197
+ def start_consumer(num)
198
+ Thread.new do
199
+ while !$exit
200
+ @ready_queue << num
201
+ job = @work_queue.pop
202
+ break if job.nil?
203
+ next if job == true
204
+
205
+ if job.locked_by != "#{name} #{num}"
206
+ say "* #{job.name} acquired for incorrect worker thread.", Logger::ERROR
207
+ next
208
+ end
209
+
210
+ pid = safe_fork do
211
+ $0 = "#{$0} #{job.name}"
212
+ job.run(job_max_run_time)
213
+ exit! # Prevents rails from closing parent database connections
214
+ end
215
+
216
+ Process.wait(pid)
217
+ @results_queue << job
218
+ end
219
+ end
220
+ end
221
+
222
+ # Generates a thread to collect and process the results from the workers.
223
+ def start_status_collector
224
+ Thread.new do
225
+ while job = @results_queue.pop
226
+ check_result(job) ? (@success += 1) : (@failure += 1)
227
+ end
228
+ Delayed::Job.connection_pool.release_connection
229
+ end
230
+ end
231
+
232
+ # Do num jobs and return stats on success/failure.
233
+ # Exit early if interrupted.
234
+ def work_off(num = 100)
235
+ @success, @failure = 0, 0
236
+
237
+ status_thread = start_status_collector
238
+
239
+ num.times do
240
+ sleep(0.5) while @ready_queue.empty? && !$exit
241
+ break if $exit
242
+
243
+ num = @ready_queue.pop
244
+ break if reserve_and_queue_one_job(num).nil? # leave if no work could be done
245
+
246
+ break if $exit # leave if we're exiting
247
+ end
248
+
249
+ # Stop status collector
250
+ @results_queue << nil
251
+ status_thread.value
252
+
253
+ return [@success, @failure]
254
+ end
255
+ end
256
+ end
@@ -0,0 +1,13 @@
1
+ autoload :ActiveRecord, 'activerecord'
2
+
3
+ require File.dirname(__FILE__) + '/delayed/message_sending'
4
+ require File.dirname(__FILE__) + '/delayed/performable_method'
5
+ require File.dirname(__FILE__) + '/delayed/job'
6
+ require File.dirname(__FILE__) + '/delayed/worker'
7
+
8
+ Object.send(:include, Delayed::MessageSending)
9
+ Module.send(:include, Delayed::MessageSending::ClassMethods)
10
+
11
+ if defined?(Merb::Plugins)
12
+ Merb::Plugins.add_rakefiles File.dirname(__FILE__) / 'delayed' / 'tasks'
13
+ end
@@ -0,0 +1 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib', 'delayed', 'recipes'))
data/spec/database.rb ADDED
@@ -0,0 +1,43 @@
1
+ $:.unshift(File.dirname(__FILE__) + '/../lib')
2
+ $:.unshift(File.dirname(__FILE__) + '/../../rspec/lib')
3
+
4
+ require 'rubygems'
5
+ require 'active_record'
6
+ gem 'sqlite3-ruby'
7
+
8
+ require File.dirname(__FILE__) + '/../init'
9
+ require 'spec'
10
+
11
+ ActiveRecord::Base.logger = Logger.new('/tmp/dj.log')
12
+ ActiveRecord::Base.establish_connection(:adapter => 'sqlite3', :database => '/tmp/jobs.sqlite')
13
+ ActiveRecord::Migration.verbose = false
14
+ ActiveRecord::Base.default_timezone = :utc if Time.zone.nil?
15
+
16
+ ActiveRecord::Schema.define do
17
+
18
+ create_table :delayed_jobs, :force => true do |table|
19
+ table.integer :priority, :default => 0
20
+ table.integer :attempts, :default => 0
21
+ table.text :handler
22
+ table.string :last_error
23
+ table.datetime :run_at
24
+ table.datetime :locked_at
25
+ table.string :locked_by
26
+ table.datetime :failed_at
27
+ table.timestamps
28
+ end
29
+
30
+ create_table :stories, :force => true do |table|
31
+ table.string :text
32
+ end
33
+
34
+ end
35
+
36
+
37
+ # Purely useful for test cases...
38
+ class Story < ActiveRecord::Base
39
+ def tell; text; end
40
+ def whatever(n, _); tell*n; end
41
+
42
+ handle_asynchronously :whatever
43
+ end
@@ -0,0 +1,150 @@
1
+ require File.dirname(__FILE__) + '/database'
2
+
3
+ class SimpleJob
4
+ cattr_accessor :runs; self.runs = 0
5
+ def perform; @@runs += 1; end
6
+ end
7
+
8
+ class RandomRubyObject
9
+ def say_hello
10
+ 'hello'
11
+ end
12
+ end
13
+
14
+ class ErrorObject
15
+
16
+ def throw
17
+ raise ActiveRecord::RecordNotFound, '...'
18
+ false
19
+ end
20
+
21
+ end
22
+
23
+ class StoryReader
24
+
25
+ def read(story)
26
+ "Epilog: #{story.tell}"
27
+ end
28
+
29
+ end
30
+
31
+ class StoryReader
32
+
33
+ def read(story)
34
+ "Epilog: #{story.tell}"
35
+ end
36
+
37
+ end
38
+
39
+ describe 'random ruby objects' do
40
+ before { Delayed::Job.delete_all }
41
+
42
+ it "should respond_to :send_later method" do
43
+
44
+ RandomRubyObject.new.respond_to?(:send_later)
45
+
46
+ end
47
+
48
+ it "should raise a ArgumentError if send_later is called but the target method doesn't exist" do
49
+ lambda { RandomRubyObject.new.send_later(:method_that_deos_not_exist) }.should raise_error(NoMethodError)
50
+ end
51
+
52
+ it "should add a new entry to the job table when send_later is called on it" do
53
+ Delayed::Job.count.should == 0
54
+
55
+ RandomRubyObject.new.send_later(:to_s)
56
+
57
+ Delayed::Job.count.should == 1
58
+ end
59
+
60
+ it "should add a new entry to the job table when send_later is called on the class" do
61
+ Delayed::Job.count.should == 0
62
+
63
+ RandomRubyObject.send_later(:to_s)
64
+
65
+ Delayed::Job.count.should == 1
66
+ end
67
+
68
+ it "should run get the original method executed when the job is performed" do
69
+
70
+ RandomRubyObject.new.send_later(:say_hello)
71
+
72
+ Delayed::Job.count.should == 1
73
+ end
74
+
75
+ it "should ignore ActiveRecord::RecordNotFound errors because they are permanent" do
76
+
77
+ job = ErrorObject.new.send_later(:throw)
78
+
79
+ Delayed::Job.count.should == 1
80
+
81
+ job.run_with_lock(Delayed::Job.max_run_time, 'worker')
82
+
83
+ Delayed::Job.count.should == 0
84
+
85
+ end
86
+
87
+ it "should store the object as string if its an active record" do
88
+ story = Story.create :text => 'Once upon...'
89
+ story.send_later(:tell)
90
+
91
+ job = Delayed::Job.find(:first)
92
+ job.payload_object.class.should == Delayed::PerformableMethod
93
+ job.payload_object.object.should == "AR:Story:#{story.id}"
94
+ job.payload_object.method.should == :tell
95
+ job.payload_object.args.should == []
96
+ job.payload_object.perform.should == 'Once upon...'
97
+ end
98
+
99
+ it "should store arguments as string if they an active record" do
100
+
101
+ story = Story.create :text => 'Once upon...'
102
+
103
+ reader = StoryReader.new
104
+ reader.send_later(:read, story)
105
+
106
+ job = Delayed::Job.find(:first)
107
+ job.payload_object.class.should == Delayed::PerformableMethod
108
+ job.payload_object.method.should == :read
109
+ job.payload_object.args.should == ["AR:Story:#{story.id}"]
110
+ job.payload_object.perform.should == 'Epilog: Once upon...'
111
+ end
112
+
113
+ it "should call send later on methods which are wrapped with handle_asynchronously" do
114
+ story = Story.create :text => 'Once upon...'
115
+
116
+ Delayed::Job.count.should == 0
117
+
118
+ story.whatever(1, 5)
119
+
120
+ Delayed::Job.count.should == 1
121
+ job = Delayed::Job.find(:first)
122
+ job.payload_object.class.should == Delayed::PerformableMethod
123
+ job.payload_object.method.should == :whatever_without_send_later
124
+ job.payload_object.args.should == [1, 5]
125
+ job.payload_object.perform.should == 'Once upon...'
126
+ end
127
+
128
+ context "send_at" do
129
+ it "should queue a new job" do
130
+ lambda do
131
+ "string".send_at(1.hour.from_now, :length)
132
+ end.should change { Delayed::Job.count }.by(1)
133
+ end
134
+
135
+ it "should schedule the job in the future" do
136
+ time = 1.hour.from_now
137
+ job = "string".send_at(time, :length)
138
+ job.run_at.should == time
139
+ end
140
+
141
+ it "should store payload as PerformableMethod" do
142
+ job = "string".send_at(1.hour.from_now, :count, 'r')
143
+ job.payload_object.class.should == Delayed::PerformableMethod
144
+ job.payload_object.method.should == :count
145
+ job.payload_object.args.should == ['r']
146
+ job.payload_object.perform.should == 1
147
+ end
148
+ end
149
+
150
+ end