jmcnevin-delayed_job 2.0.4
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.
- data/.gitignore +3 -0
- data/MIT-LICENSE +20 -0
- data/README.textile +213 -0
- data/Rakefile +45 -0
- data/VERSION +1 -0
- data/benchmarks.rb +33 -0
- data/contrib/delayed_job.monitrc +14 -0
- data/contrib/delayed_job_multiple.monitrc +23 -0
- data/delayed_job.gemspec +107 -0
- data/generators/delayed_job/delayed_job_generator.rb +21 -0
- data/generators/delayed_job/templates/migration.rb +21 -0
- data/init.rb +1 -0
- data/lib/delayed/backend/active_record.rb +90 -0
- data/lib/delayed/backend/base.rb +111 -0
- data/lib/delayed/backend/data_mapper.rb +125 -0
- data/lib/delayed/backend/mongo_mapper.rb +110 -0
- data/lib/delayed/message_sending.rb +22 -0
- data/lib/delayed/performable_method.rb +62 -0
- data/lib/delayed/railtie.rb +10 -0
- data/lib/delayed/recipes.rb +31 -0
- data/lib/delayed/tasks.rb +15 -0
- data/lib/delayed/worker.rb +199 -0
- data/lib/delayed_job.rb +14 -0
- data/rails/init.rb +5 -0
- data/recipes/delayed_job.rb +1 -0
- data/spec/backend/active_record_job_spec.rb +46 -0
- data/spec/backend/data_mapper_job_spec.rb +16 -0
- data/spec/backend/mongo_mapper_job_spec.rb +94 -0
- data/spec/backend/shared_backend_spec.rb +265 -0
- data/spec/delayed_method_spec.rb +59 -0
- data/spec/performable_method_spec.rb +42 -0
- data/spec/sample_jobs.rb +25 -0
- data/spec/setup/active_record.rb +33 -0
- data/spec/setup/data_mapper.rb +8 -0
- data/spec/setup/mongo_mapper.rb +17 -0
- data/spec/spec_helper.rb +26 -0
- data/spec/story_spec.rb +17 -0
- data/spec/worker_spec.rb +216 -0
- data/tasks/jobs.rake +1 -0
- metadata +241 -0
|
@@ -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,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)
|
|
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,31 @@
|
|
|
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
|
+
def rails_env
|
|
13
|
+
fetch(:rails_env, false) ? "RAILS_ENV=#{fetch(:rails_env)}" : ''
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
desc "Stop the delayed_job process"
|
|
17
|
+
task :stop, :roles => :app do
|
|
18
|
+
run "cd #{current_path};#{rails_env} script/delayed_job stop"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
desc "Start the delayed_job process"
|
|
22
|
+
task :start, :roles => :app do
|
|
23
|
+
run "cd #{current_path};#{rails_env} script/delayed_job start"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
desc "Restart the delayed_job process"
|
|
27
|
+
task :restart, :roles => :app do
|
|
28
|
+
run "cd #{current_path};#{rails_env} script/delayed_job restart"
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
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,199 @@
|
|
|
1
|
+
require 'timeout'
|
|
2
|
+
require 'active_support/core_ext/numeric/time'
|
|
3
|
+
|
|
4
|
+
module Delayed
|
|
5
|
+
class Worker
|
|
6
|
+
cattr_accessor :min_priority,
|
|
7
|
+
:max_priority,
|
|
8
|
+
:max_attempts,
|
|
9
|
+
:max_run_time,
|
|
10
|
+
:default_priority,
|
|
11
|
+
:sleep_delay,
|
|
12
|
+
:logger,
|
|
13
|
+
:trap_signals,
|
|
14
|
+
:destroy_failed_jobs
|
|
15
|
+
|
|
16
|
+
cattr_reader :backend
|
|
17
|
+
|
|
18
|
+
# name_prefix is ignored if name is set directly
|
|
19
|
+
attr_accessor :name_prefix
|
|
20
|
+
|
|
21
|
+
# default worker settings
|
|
22
|
+
self.sleep_delay = 5
|
|
23
|
+
self.max_attempts = 25
|
|
24
|
+
self.max_run_time = 4.hours
|
|
25
|
+
self.default_priority = 0
|
|
26
|
+
self.trap_signals = true
|
|
27
|
+
self.destroy_failed_jobs = true
|
|
28
|
+
|
|
29
|
+
self.logger = if defined?(Merb::Logger)
|
|
30
|
+
Merb.logger
|
|
31
|
+
elsif defined?(RAILS_DEFAULT_LOGGER)
|
|
32
|
+
RAILS_DEFAULT_LOGGER
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def self.backend=(backend)
|
|
36
|
+
if backend.is_a? Symbol
|
|
37
|
+
require "delayed/backend/#{backend}"
|
|
38
|
+
backend = "Delayed::Backend::#{backend.to_s.classify}::Job".constantize
|
|
39
|
+
end
|
|
40
|
+
@@backend = backend
|
|
41
|
+
silence_warnings { ::Delayed.const_set(:Job, backend) }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def self.guess_backend
|
|
45
|
+
self.backend ||= if defined?(ActiveRecord)
|
|
46
|
+
:active_record
|
|
47
|
+
elsif defined?(MongoMapper)
|
|
48
|
+
:mongo_mapper
|
|
49
|
+
else
|
|
50
|
+
logger.warn "Could not decide on a backend, defaulting to active_record"
|
|
51
|
+
:active_record
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def initialize(options={})
|
|
56
|
+
@quiet = !!options.delete[:quiet]
|
|
57
|
+
|
|
58
|
+
options.each do |k,v|
|
|
59
|
+
method_name = :"#{k}="
|
|
60
|
+
if self.class.respond_to?(method_name)
|
|
61
|
+
self.class.send(method_name, v)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Every worker has a unique name which by default is the pid of the process. There are some
|
|
67
|
+
# advantages to overriding this with something which survives worker retarts: Workers can#
|
|
68
|
+
# safely resume working on tasks which are locked by themselves. The worker will assume that
|
|
69
|
+
# it crashed before.
|
|
70
|
+
def name
|
|
71
|
+
return @name unless @name.nil?
|
|
72
|
+
"#{@name_prefix}host:#{Socket.gethostname} pid:#{Process.pid}" rescue "#{@name_prefix}pid:#{Process.pid}"
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Sets the name of the worker.
|
|
76
|
+
# Setting the name to nil will reset the default worker name
|
|
77
|
+
def name=(val)
|
|
78
|
+
@name = val
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def start
|
|
82
|
+
say "*** Starting job worker #{name}"
|
|
83
|
+
|
|
84
|
+
if self.trap_signals
|
|
85
|
+
trap('TERM') { say 'Exiting...'; $exit = true }
|
|
86
|
+
trap('INT') { say 'Exiting...'; $exit = true }
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
loop do
|
|
90
|
+
result = nil
|
|
91
|
+
|
|
92
|
+
realtime = Benchmark.realtime do
|
|
93
|
+
result = work_off
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
count = result.sum
|
|
97
|
+
|
|
98
|
+
break if $exit
|
|
99
|
+
|
|
100
|
+
if count.zero?
|
|
101
|
+
sleep(@@sleep_delay)
|
|
102
|
+
else
|
|
103
|
+
say "#{count} jobs processed at %.4f j/s, %d failed ..." % [count / realtime, result.last]
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
break if $exit
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
ensure
|
|
110
|
+
Delayed::Job.clear_locks!(name)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Do num jobs and return stats on success/failure.
|
|
114
|
+
# Exit early if interrupted.
|
|
115
|
+
def work_off(num = 100)
|
|
116
|
+
success, failure = 0, 0
|
|
117
|
+
|
|
118
|
+
num.times do
|
|
119
|
+
case reserve_and_run_one_job
|
|
120
|
+
when true
|
|
121
|
+
success += 1
|
|
122
|
+
when false
|
|
123
|
+
failure += 1
|
|
124
|
+
else
|
|
125
|
+
break # leave if no work could be done
|
|
126
|
+
end
|
|
127
|
+
break if $exit # leave if we're exiting
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
return [success, failure]
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def run(job)
|
|
134
|
+
runtime = Benchmark.realtime do
|
|
135
|
+
Timeout.timeout(self.class.max_run_time.to_i) { job.invoke_job }
|
|
136
|
+
job.destroy
|
|
137
|
+
end
|
|
138
|
+
# TODO: warn if runtime > max_run_time ?
|
|
139
|
+
say "* [JOB] #{name} completed after %.4f" % runtime
|
|
140
|
+
return true # did work
|
|
141
|
+
# rescue Exception => e # rescuing Exception can have some strange downsides in my experience
|
|
142
|
+
rescue => e
|
|
143
|
+
handle_failed_job(job, e)
|
|
144
|
+
return false # work failed
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Reschedule the job in the future (when a job fails).
|
|
148
|
+
# Uses an exponential scale depending on the number of failed attempts.
|
|
149
|
+
def reschedule(job, time = nil)
|
|
150
|
+
if (job.attempts += 1) < self.class.max_attempts
|
|
151
|
+
time ||= Job.db_time_now + (job.attempts ** 4) + 5
|
|
152
|
+
job.run_at = time
|
|
153
|
+
job.unlock
|
|
154
|
+
job.save!
|
|
155
|
+
else
|
|
156
|
+
say "* [JOB] PERMANENTLY removing #{job.name} because of #{job.attempts} consecutive failures.", Logger::INFO
|
|
157
|
+
|
|
158
|
+
if job.payload_object.respond_to? :on_permanent_failure
|
|
159
|
+
say "* [JOB] Running on_permanent_failure hook"
|
|
160
|
+
job.payload_object.on_permanent_failure
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
self.class.destroy_failed_jobs ? job.destroy : job.update_attributes(:failed_at => Delayed::Job.db_time_now)
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def say(text, level = Logger::INFO)
|
|
168
|
+
puts text unless @quiet
|
|
169
|
+
logger.add level, "#{Time.now.strftime('%FT%T%z')}: #{text}" if logger
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
protected
|
|
173
|
+
|
|
174
|
+
def handle_failed_job(job, error)
|
|
175
|
+
job.last_error = error.message + "\n" + error.backtrace.join("\n")
|
|
176
|
+
say "* [JOB] #{name} failed with #{error.class.name}: #{error.message} - #{job.attempts} failed attempts", Logger::ERROR
|
|
177
|
+
reschedule(job)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Run the next job we can get an exclusive lock on.
|
|
181
|
+
# If no jobs are left we return nil
|
|
182
|
+
def reserve_and_run_one_job
|
|
183
|
+
|
|
184
|
+
# We get up to 5 jobs from the db. In case we cannot get exclusive access to a job we try the next.
|
|
185
|
+
# this leads to a more even distribution of jobs across the worker processes
|
|
186
|
+
job = Delayed::Job.find_available(name, 5, self.class.max_run_time).detect do |job|
|
|
187
|
+
if job.lock_exclusively!(self.class.max_run_time, name)
|
|
188
|
+
say "* [Worker(#{name})] acquired lock on #{job.name}"
|
|
189
|
+
true
|
|
190
|
+
else
|
|
191
|
+
say "* [Worker(#{name})] failed to acquire exclusive lock for #{job.name}", Logger::WARN
|
|
192
|
+
false
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
run(job) if job
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
end
|
data/lib/delayed_job.rb
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
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/railtie' if defined?(::Rails::Railtie)
|
|
8
|
+
|
|
9
|
+
Object.send(:include, Delayed::MessageSending)
|
|
10
|
+
Module.send(:include, Delayed::MessageSending::ClassMethods)
|
|
11
|
+
|
|
12
|
+
if defined?(Merb::Plugins)
|
|
13
|
+
Merb::Plugins.add_rakefiles File.dirname(__FILE__) / 'delayed' / 'tasks'
|
|
14
|
+
end
|
data/rails/init.rb
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
require File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib', 'delayed', 'recipes'))
|
|
@@ -0,0 +1,46 @@
|
|
|
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
|
+
context "db_time_now" do
|
|
22
|
+
it "should return time in current time zone if set" do
|
|
23
|
+
Time.zone = 'Eastern Time (US & Canada)'
|
|
24
|
+
%w(EST EDT).should include(Delayed::Job.db_time_now.zone)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
it "should return UTC time if that is the AR default" do
|
|
28
|
+
Time.zone = nil
|
|
29
|
+
ActiveRecord::Base.default_timezone = :utc
|
|
30
|
+
Delayed::Backend::ActiveRecord::Job.db_time_now.zone.should == 'UTC'
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
it "should return local time if that is the AR default" do
|
|
34
|
+
Time.zone = 'Central Time (US & Canada)'
|
|
35
|
+
ActiveRecord::Base.default_timezone = :local
|
|
36
|
+
%w(CST CDT).should include(Delayed::Backend::ActiveRecord::Job.db_time_now.zone)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
describe "after_fork" do
|
|
41
|
+
it "should call reconnect on the connection" do
|
|
42
|
+
ActiveRecord::Base.connection.should_receive(:reconnect!)
|
|
43
|
+
Delayed::Backend::ActiveRecord::Job.after_fork
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
require 'backend/shared_backend_spec'
|
|
3
|
+
require 'delayed/backend/data_mapper'
|
|
4
|
+
|
|
5
|
+
describe Delayed::Backend::DataMapper::Job do
|
|
6
|
+
before(:all) do
|
|
7
|
+
@backend = Delayed::Backend::DataMapper::Job
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
before(:each) do
|
|
11
|
+
# reset database before each example is run
|
|
12
|
+
DataMapper.auto_migrate!
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
it_should_behave_like 'a backend'
|
|
16
|
+
end
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
require 'backend/shared_backend_spec'
|
|
3
|
+
require 'delayed/backend/mongo_mapper'
|
|
4
|
+
|
|
5
|
+
describe Delayed::Backend::MongoMapper::Job do
|
|
6
|
+
before(:all) do
|
|
7
|
+
@backend = Delayed::Backend::MongoMapper::Job
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
before(:each) do
|
|
11
|
+
MongoMapper.database.collections.each(&:remove)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
it_should_behave_like 'a backend'
|
|
15
|
+
|
|
16
|
+
describe "indexes" do
|
|
17
|
+
it "should have combo index on priority and run_at" do
|
|
18
|
+
@backend.collection.index_information.detect { |index| index[0] == 'priority_1_run_at_1' }.should_not be_nil
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
it "should have index on locked_by" do
|
|
22
|
+
@backend.collection.index_information.detect { |index| index[0] == 'locked_by_1' }.should_not be_nil
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
describe "delayed method" do
|
|
27
|
+
class MongoStoryReader
|
|
28
|
+
def read(story)
|
|
29
|
+
"Epilog: #{story.tell}"
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
class MongoStory
|
|
34
|
+
include ::MongoMapper::Document
|
|
35
|
+
key :text, String
|
|
36
|
+
|
|
37
|
+
def tell
|
|
38
|
+
text
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
it "should ignore not found errors because they are permanent" do
|
|
43
|
+
story = MongoStory.create :text => 'Once upon a time...'
|
|
44
|
+
job = story.send_later(:tell)
|
|
45
|
+
story.destroy
|
|
46
|
+
lambda { job.invoke_job }.should_not raise_error
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
it "should store the object as string" do
|
|
50
|
+
story = MongoStory.create :text => 'Once upon a time...'
|
|
51
|
+
job = story.send_later(:tell)
|
|
52
|
+
|
|
53
|
+
job.payload_object.class.should == Delayed::PerformableMethod
|
|
54
|
+
job.payload_object.object.should == "LOAD;MongoStory;#{story.id}"
|
|
55
|
+
job.payload_object.method.should == :tell
|
|
56
|
+
job.payload_object.args.should == []
|
|
57
|
+
job.payload_object.perform.should == 'Once upon a time...'
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
it "should store arguments as string" do
|
|
61
|
+
story = MongoStory.create :text => 'Once upon a time...'
|
|
62
|
+
job = MongoStoryReader.new.send_later(:read, story)
|
|
63
|
+
job.payload_object.class.should == Delayed::PerformableMethod
|
|
64
|
+
job.payload_object.method.should == :read
|
|
65
|
+
job.payload_object.args.should == ["LOAD;MongoStory;#{story.id}"]
|
|
66
|
+
job.payload_object.perform.should == 'Epilog: Once upon a time...'
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
describe "before_fork" do
|
|
71
|
+
after do
|
|
72
|
+
MongoMapper.connection.connect_to_master
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
it "should disconnect" do
|
|
76
|
+
lambda do
|
|
77
|
+
Delayed::Backend::MongoMapper::Job.before_fork
|
|
78
|
+
end.should change { !!MongoMapper.connection.connected? }.from(true).to(false)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
describe "after_fork" do
|
|
83
|
+
before do
|
|
84
|
+
MongoMapper.connection.close
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
it "should call reconnect" do
|
|
88
|
+
lambda do
|
|
89
|
+
Delayed::Backend::MongoMapper::Job.after_fork
|
|
90
|
+
end.should change { !!MongoMapper.connection.connected? }.from(false).to(true)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
end
|