delayed 0.1.0
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.
- checksums.yaml +7 -0
- data/LICENSE +20 -0
- data/README.md +560 -0
- data/Rakefile +35 -0
- data/lib/delayed.rb +72 -0
- data/lib/delayed/active_job_adapter.rb +65 -0
- data/lib/delayed/backend/base.rb +166 -0
- data/lib/delayed/backend/job_preparer.rb +43 -0
- data/lib/delayed/exceptions.rb +14 -0
- data/lib/delayed/job.rb +250 -0
- data/lib/delayed/lifecycle.rb +85 -0
- data/lib/delayed/message_sending.rb +65 -0
- data/lib/delayed/monitor.rb +134 -0
- data/lib/delayed/performable_mailer.rb +22 -0
- data/lib/delayed/performable_method.rb +47 -0
- data/lib/delayed/plugin.rb +15 -0
- data/lib/delayed/plugins/connection.rb +13 -0
- data/lib/delayed/plugins/instrumentation.rb +39 -0
- data/lib/delayed/priority.rb +164 -0
- data/lib/delayed/psych_ext.rb +135 -0
- data/lib/delayed/railtie.rb +7 -0
- data/lib/delayed/runnable.rb +46 -0
- data/lib/delayed/serialization/active_record.rb +18 -0
- data/lib/delayed/syck_ext.rb +42 -0
- data/lib/delayed/tasks.rb +40 -0
- data/lib/delayed/worker.rb +233 -0
- data/lib/delayed/yaml_ext.rb +10 -0
- data/lib/delayed_job.rb +1 -0
- data/lib/delayed_job_active_record.rb +1 -0
- data/lib/generators/delayed/generator.rb +7 -0
- data/lib/generators/delayed/migration_generator.rb +28 -0
- data/lib/generators/delayed/next_migration_version.rb +14 -0
- data/lib/generators/delayed/templates/migration.rb +22 -0
- data/spec/autoloaded/clazz.rb +6 -0
- data/spec/autoloaded/instance_clazz.rb +5 -0
- data/spec/autoloaded/instance_struct.rb +6 -0
- data/spec/autoloaded/struct.rb +7 -0
- data/spec/database.yml +25 -0
- data/spec/delayed/active_job_adapter_spec.rb +267 -0
- data/spec/delayed/job_spec.rb +953 -0
- data/spec/delayed/monitor_spec.rb +276 -0
- data/spec/delayed/plugins/instrumentation_spec.rb +49 -0
- data/spec/delayed/priority_spec.rb +154 -0
- data/spec/delayed/serialization/active_record_spec.rb +15 -0
- data/spec/delayed/tasks_spec.rb +116 -0
- data/spec/helper.rb +196 -0
- data/spec/lifecycle_spec.rb +77 -0
- data/spec/message_sending_spec.rb +149 -0
- data/spec/performable_mailer_spec.rb +68 -0
- data/spec/performable_method_spec.rb +123 -0
- data/spec/psych_ext_spec.rb +94 -0
- data/spec/sample_jobs.rb +117 -0
- data/spec/worker_spec.rb +235 -0
- data/spec/yaml_ext_spec.rb +48 -0
- metadata +326 -0
@@ -0,0 +1,68 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
class MyMailer < ActionMailer::Base
|
4
|
+
def signup(email)
|
5
|
+
mail to: email, subject: 'Delaying Emails', from: 'delayedjob@example.com', body: 'Delaying Emails Body'
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
describe ActionMailer::Base do
|
10
|
+
describe 'delay' do
|
11
|
+
it 'enqueues a PerformableEmail job' do
|
12
|
+
expect {
|
13
|
+
job = MyMailer.delay.signup('john@example.com')
|
14
|
+
expect(job.payload_object.class).to eq(Delayed::PerformableMailer)
|
15
|
+
expect(job.payload_object.method_name).to eq(:signup)
|
16
|
+
expect(job.payload_object.args).to eq(['john@example.com'])
|
17
|
+
}.to change { Delayed::Job.count }.by(1)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
describe 'delay on a mail object' do
|
22
|
+
it 'raises an exception' do
|
23
|
+
expect {
|
24
|
+
MyMailer.signup('john@example.com').delay
|
25
|
+
}.to raise_error(RuntimeError)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
describe Delayed::PerformableMailer do
|
30
|
+
describe 'perform' do
|
31
|
+
it 'calls the method and #deliver on the mailer' do
|
32
|
+
email = double('email', deliver: true)
|
33
|
+
mailer_class = double('MailerClass', signup: email)
|
34
|
+
mailer = described_class.new(mailer_class, :signup, ['john@example.com'])
|
35
|
+
|
36
|
+
expect(mailer_class).to receive(:signup).with('john@example.com')
|
37
|
+
expect(email).to receive(:deliver)
|
38
|
+
mailer.perform
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
if defined?(ActionMailer::Parameterized::Mailer)
|
45
|
+
describe ActionMailer::Parameterized::Mailer do
|
46
|
+
describe 'delay' do
|
47
|
+
it 'enqueues a PerformableEmail job' do
|
48
|
+
expect {
|
49
|
+
job = MyMailer.with(foo: 1, bar: 2).delay.signup('john@example.com')
|
50
|
+
expect(job.payload_object.class).to eq(Delayed::PerformableMailer)
|
51
|
+
expect(job.payload_object.object.class).to eq(described_class)
|
52
|
+
expect(job.payload_object.object.instance_variable_get('@mailer')).to eq(MyMailer)
|
53
|
+
expect(job.payload_object.object.instance_variable_get('@params')).to eq(foo: 1, bar: 2)
|
54
|
+
expect(job.payload_object.method_name).to eq(:signup)
|
55
|
+
expect(job.payload_object.args).to eq(['john@example.com'])
|
56
|
+
}.to change { Delayed::Job.count }.by(1)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
describe 'delay on a mail object' do
|
61
|
+
it 'raises an exception' do
|
62
|
+
expect {
|
63
|
+
MyMailer.with(foo: 1, bar: 2).signup('john@example.com').delay
|
64
|
+
}.to raise_error(RuntimeError)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,123 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
describe Delayed::PerformableMethod do
|
4
|
+
describe 'perform' do
|
5
|
+
before do
|
6
|
+
@method = described_class.new('foo', :count, ['o'])
|
7
|
+
end
|
8
|
+
|
9
|
+
context 'with the persisted record cannot be found' do
|
10
|
+
before do
|
11
|
+
@method.object = nil
|
12
|
+
end
|
13
|
+
|
14
|
+
it 'does nothing if object is nil' do
|
15
|
+
expect { @method.perform }.not_to raise_error
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
it 'calls the method on the object' do
|
20
|
+
expect(@method.object).to receive(:count).with('o')
|
21
|
+
@method.perform
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
it "raises a NoMethodError if target method doesn't exist" do
|
26
|
+
expect {
|
27
|
+
described_class.new(Object, :method_that_does_not_exist, [])
|
28
|
+
}.to raise_error(NoMethodError)
|
29
|
+
end
|
30
|
+
|
31
|
+
it 'does not raise NoMethodError if target method is private' do
|
32
|
+
clazz = Class.new do
|
33
|
+
def private_method; end
|
34
|
+
private :private_method
|
35
|
+
end
|
36
|
+
expect { described_class.new(clazz.new, :private_method, []) }.not_to raise_error
|
37
|
+
end
|
38
|
+
|
39
|
+
context 'when it receives an object that is not persisted' do
|
40
|
+
let(:object) { double(persisted?: false, expensive_operation: true) }
|
41
|
+
|
42
|
+
it 'raises an ArgumentError' do
|
43
|
+
expect { described_class.new(object, :expensive_operation, []) }.to raise_error ArgumentError
|
44
|
+
end
|
45
|
+
|
46
|
+
it 'does not raise ArgumentError if the object acts like a Her model' do
|
47
|
+
allow(object.class).to receive(:save_existing).and_return(true)
|
48
|
+
expect { described_class.new(object, :expensive_operation, []) }.not_to raise_error
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
describe 'display_name' do
|
53
|
+
it 'returns class_name#method_name for instance methods' do
|
54
|
+
expect(described_class.new('foo', :count, ['o']).display_name).to eq('String#count')
|
55
|
+
end
|
56
|
+
|
57
|
+
it 'returns class_name.method_name for class methods' do
|
58
|
+
expect(described_class.new(Class, :inspect, []).display_name).to eq('Class.inspect')
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
describe 'hooks' do
|
63
|
+
%w(before after success).each do |hook|
|
64
|
+
it "delegates #{hook} hook to object" do
|
65
|
+
story = Story.create
|
66
|
+
job = story.delay.tell
|
67
|
+
|
68
|
+
expect(story).to receive(hook).with(job)
|
69
|
+
job.invoke_job
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
it 'delegates enqueue hook to object' do
|
74
|
+
story = Story.create
|
75
|
+
expect(story).to receive(:enqueue).with(an_instance_of(Delayed::Job))
|
76
|
+
story.delay.tell
|
77
|
+
end
|
78
|
+
|
79
|
+
it 'delegates error hook to object' do
|
80
|
+
story = Story.create
|
81
|
+
expect(story).to receive(:error).with(an_instance_of(Delayed::Job), an_instance_of(RuntimeError))
|
82
|
+
expect(story).to receive(:tell).and_raise(RuntimeError)
|
83
|
+
expect { story.delay.tell.invoke_job }.to raise_error(RuntimeError)
|
84
|
+
end
|
85
|
+
|
86
|
+
it 'delegates failure hook to object' do
|
87
|
+
method = described_class.new('object', :size, [])
|
88
|
+
expect(method.object).to receive(:failure)
|
89
|
+
method.failure
|
90
|
+
end
|
91
|
+
|
92
|
+
context 'with delay_job == false' do
|
93
|
+
before do
|
94
|
+
Delayed::Worker.delay_jobs = false
|
95
|
+
end
|
96
|
+
|
97
|
+
after do
|
98
|
+
Delayed::Worker.delay_jobs = true
|
99
|
+
end
|
100
|
+
|
101
|
+
%w(before after success).each do |hook|
|
102
|
+
it "delegates #{hook} hook to object" do
|
103
|
+
story = Story.create
|
104
|
+
expect(story).to receive(hook).with(an_instance_of(Delayed::Job))
|
105
|
+
story.delay.tell
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
it 'delegates error hook to object' do
|
110
|
+
story = Story.create
|
111
|
+
expect(story).to receive(:error).with(an_instance_of(Delayed::Job), an_instance_of(RuntimeError))
|
112
|
+
expect(story).to receive(:tell).and_raise(RuntimeError)
|
113
|
+
expect { story.delay.tell }.to raise_error(RuntimeError)
|
114
|
+
end
|
115
|
+
|
116
|
+
it 'delegates failure hook to object' do
|
117
|
+
method = described_class.new('object', :size, [])
|
118
|
+
expect(method.object).to receive(:failure)
|
119
|
+
method.failure
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
require 'helper'
|
2
|
+
require 'active_support/core_ext/string/strip'
|
3
|
+
|
4
|
+
describe 'Psych::Visitors::ToRuby', if: defined?(Psych::Visitors::ToRuby) do
|
5
|
+
context BigDecimal do
|
6
|
+
it 'deserializes correctly' do
|
7
|
+
deserialized = YAML.load_dj("--- !ruby/object:BigDecimal 18:0.1337E2\n...\n")
|
8
|
+
|
9
|
+
expect(deserialized).to be_an_instance_of(BigDecimal)
|
10
|
+
expect(deserialized).to eq(BigDecimal('13.37'))
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
context ActiveRecord::Base do
|
15
|
+
it 'serializes and deserializes in a version-independent way' do
|
16
|
+
Story.create.tap do |story|
|
17
|
+
serialized = YAML.dump_dj(story)
|
18
|
+
expect(serialized).to eq <<-YAML.strip_heredoc
|
19
|
+
--- !ruby/ActiveRecord:Story
|
20
|
+
attributes:
|
21
|
+
story_id: #{story.id}
|
22
|
+
YAML
|
23
|
+
|
24
|
+
deserialized = YAML.load_dj(serialized)
|
25
|
+
expect(deserialized).to be_an_instance_of(Story)
|
26
|
+
expect(deserialized).to eq Story.find(story.id)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
it 'ignores garbage when deserializing' do
|
31
|
+
Story.create.tap do |story|
|
32
|
+
serialized = <<-YML.strip_heredoc
|
33
|
+
--- !ruby/ActiveRecord:Story
|
34
|
+
attributes:
|
35
|
+
story_id: #{story.id}
|
36
|
+
other_stuff: 'boo'
|
37
|
+
asdf: { fish: true }
|
38
|
+
YML
|
39
|
+
|
40
|
+
deserialized = YAML.load_dj(serialized)
|
41
|
+
expect(deserialized).to be_an_instance_of(Story)
|
42
|
+
expect(deserialized).to eq Story.find(story.id)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
context Singleton do
|
48
|
+
it 'serializes and deserializes generic singleton classes' do
|
49
|
+
serialized = <<-YML.strip_heredoc
|
50
|
+
- !ruby/object:SingletonClass {}
|
51
|
+
- !ruby/object:SingletonClass {}
|
52
|
+
YML
|
53
|
+
deserialized = YAML.load_dj(
|
54
|
+
YAML.load_dj(serialized).to_yaml,
|
55
|
+
)
|
56
|
+
|
57
|
+
expect(deserialized).to contain_exactly(SingletonClass.instance, SingletonClass.instance)
|
58
|
+
end
|
59
|
+
|
60
|
+
it 'deserializes ActiveModel::NullMutationTracker' do
|
61
|
+
serialized = <<-YML.strip_heredoc
|
62
|
+
- !ruby/object:ActiveModel::NullMutationTracker {}
|
63
|
+
- !ruby/object:ActiveModel::NullMutationTracker {}
|
64
|
+
YML
|
65
|
+
deserialized = YAML.load_dj(
|
66
|
+
YAML.load_dj(serialized).to_yaml,
|
67
|
+
)
|
68
|
+
|
69
|
+
expect(deserialized).to contain_exactly(ActiveModel::NullMutationTracker.instance, ActiveModel::NullMutationTracker.instance)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
context 'load_tag handling' do
|
74
|
+
# This only broadly works in ruby 2.0 but will cleanly work through load_dj
|
75
|
+
# here because this class is so simple it only touches our extention
|
76
|
+
YAML.load_tags['!ruby/object:RenamedClass'] = SimpleJob
|
77
|
+
# This is how ruby 2.1 and newer works throughout the yaml handling
|
78
|
+
YAML.load_tags['!ruby/object:RenamedString'] = 'SimpleJob'
|
79
|
+
|
80
|
+
it 'deserializes class tag' do
|
81
|
+
deserialized = YAML.load_dj("--- !ruby/object:RenamedClass\ncheck: 12\n")
|
82
|
+
|
83
|
+
expect(deserialized).to be_an_instance_of(SimpleJob)
|
84
|
+
expect(deserialized.instance_variable_get(:@check)).to eq(12)
|
85
|
+
end
|
86
|
+
|
87
|
+
it 'deserializes string tag' do
|
88
|
+
deserialized = YAML.load_dj("--- !ruby/object:RenamedString\ncheck: 12\n")
|
89
|
+
|
90
|
+
expect(deserialized).to be_an_instance_of(SimpleJob)
|
91
|
+
expect(deserialized.instance_variable_get(:@check)).to eq(12)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
data/spec/sample_jobs.rb
ADDED
@@ -0,0 +1,117 @@
|
|
1
|
+
NamedJob = Struct.new(:perform)
|
2
|
+
class NamedJob
|
3
|
+
def display_name
|
4
|
+
'named_job'
|
5
|
+
end
|
6
|
+
end
|
7
|
+
|
8
|
+
class SimpleJob
|
9
|
+
cattr_accessor(:runs) { 0 }
|
10
|
+
def perform
|
11
|
+
self.class.runs += 1
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
class NamedQueueJob < SimpleJob
|
16
|
+
def queue_name
|
17
|
+
'job_tracking'
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
class ErrorJob
|
22
|
+
cattr_accessor(:runs) { 0 }
|
23
|
+
def perform
|
24
|
+
raise StandardError, 'did not work'
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
class FailureJob < ErrorJob
|
29
|
+
def max_attempts
|
30
|
+
1
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
CustomRescheduleJob = Struct.new(:offset)
|
35
|
+
class CustomRescheduleJob
|
36
|
+
cattr_accessor(:runs) { 0 }
|
37
|
+
def perform
|
38
|
+
raise 'did not work'
|
39
|
+
end
|
40
|
+
|
41
|
+
def reschedule_at(time, _attempts)
|
42
|
+
time + offset
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
class LongRunningJob
|
47
|
+
def perform
|
48
|
+
sleep 250
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
class OnPermanentFailureJob < SimpleJob
|
53
|
+
attr_writer :raise_error
|
54
|
+
|
55
|
+
def initialize
|
56
|
+
@raise_error = false
|
57
|
+
end
|
58
|
+
|
59
|
+
def failure
|
60
|
+
raise 'did not work' if @raise_error
|
61
|
+
end
|
62
|
+
|
63
|
+
def max_attempts
|
64
|
+
1
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
module M
|
69
|
+
class ModuleJob
|
70
|
+
cattr_accessor(:runs) { 0 }
|
71
|
+
def perform
|
72
|
+
self.class.runs += 1
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
class CallbackJob
|
78
|
+
cattr_accessor :messages
|
79
|
+
|
80
|
+
def enqueue(_job)
|
81
|
+
self.class.messages << 'enqueue'
|
82
|
+
end
|
83
|
+
|
84
|
+
def before(_job)
|
85
|
+
self.class.messages << 'before'
|
86
|
+
end
|
87
|
+
|
88
|
+
def perform
|
89
|
+
self.class.messages << 'perform'
|
90
|
+
end
|
91
|
+
|
92
|
+
def after(_job)
|
93
|
+
self.class.messages << 'after'
|
94
|
+
end
|
95
|
+
|
96
|
+
def success(_job)
|
97
|
+
self.class.messages << 'success'
|
98
|
+
end
|
99
|
+
|
100
|
+
def error(_job, error)
|
101
|
+
self.class.messages << "error: #{error.class}"
|
102
|
+
end
|
103
|
+
|
104
|
+
def failure(_job)
|
105
|
+
self.class.messages << 'failure'
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
class EnqueueJobMod < SimpleJob
|
110
|
+
def enqueue(job)
|
111
|
+
job.run_at = 20.minutes.from_now
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
class ActiveJobJob < ActiveJob::Base # rubocop:disable Rails/ApplicationJob
|
116
|
+
def perform; end
|
117
|
+
end
|
data/spec/worker_spec.rb
ADDED
@@ -0,0 +1,235 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
describe Delayed::Worker do
|
4
|
+
before do
|
5
|
+
described_class.sleep_delay = 0
|
6
|
+
end
|
7
|
+
|
8
|
+
describe 'start' do
|
9
|
+
it 'runs the :execute lifecycle hook' do
|
10
|
+
performances = []
|
11
|
+
plugin = Class.new(Delayed::Plugin) do
|
12
|
+
callbacks do |lifecycle|
|
13
|
+
lifecycle.before(:execute) { performances << true }
|
14
|
+
lifecycle.after(:execute) { |arg| performances << arg }
|
15
|
+
lifecycle.around(:execute) { |arg, &block| performances << block.call(arg) }
|
16
|
+
end
|
17
|
+
end
|
18
|
+
Delayed.plugins << plugin
|
19
|
+
|
20
|
+
subject.send(:stop) # prevent start from running more than one loop
|
21
|
+
allow(Delayed::Job).to receive(:reserve).and_return([])
|
22
|
+
subject.start
|
23
|
+
expect(performances).to eq [true, nil, nil]
|
24
|
+
expect(Delayed::Job).to have_received(:reserve)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# rubocop:disable RSpec/SubjectStub
|
29
|
+
describe '#run!' do
|
30
|
+
before do
|
31
|
+
allow(Delayed.logger).to receive(:info).and_call_original
|
32
|
+
allow(subject).to receive(:interruptable_sleep).and_call_original
|
33
|
+
end
|
34
|
+
|
35
|
+
context 'when there are no jobs' do
|
36
|
+
before do
|
37
|
+
allow(Delayed::Job).to receive(:reserve).and_return([])
|
38
|
+
end
|
39
|
+
|
40
|
+
it 'does not log and then sleeps' do
|
41
|
+
subject.run!
|
42
|
+
expect(Delayed.logger).not_to have_received(:info)
|
43
|
+
expect(subject).to have_received(:interruptable_sleep)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
context 'when there is a job worked off' do
|
48
|
+
around do |example|
|
49
|
+
max_claims_was = described_class.max_claims
|
50
|
+
described_class.max_claims = max_claims
|
51
|
+
example.run
|
52
|
+
ensure
|
53
|
+
described_class.max_claims = max_claims_was
|
54
|
+
end
|
55
|
+
|
56
|
+
before do
|
57
|
+
allow(Delayed::Job).to receive(:reserve).and_return([job], [])
|
58
|
+
end
|
59
|
+
|
60
|
+
let(:max_claims) { 1 }
|
61
|
+
let(:job) do
|
62
|
+
instance_double(
|
63
|
+
Delayed::Job,
|
64
|
+
id: 123,
|
65
|
+
max_run_time: 10,
|
66
|
+
name: 'MyJob',
|
67
|
+
run_at: Delayed::Job.db_time_now,
|
68
|
+
created_at: Delayed::Job.db_time_now,
|
69
|
+
priority: Delayed::Priority.interactive,
|
70
|
+
queue: 'testqueue',
|
71
|
+
attempts: 0,
|
72
|
+
invoke_job: true,
|
73
|
+
destroy: true,
|
74
|
+
)
|
75
|
+
end
|
76
|
+
|
77
|
+
it 'logs the count and does not sleep' do
|
78
|
+
subject.run!
|
79
|
+
expect(Delayed.logger).to have_received(:info).with(/1 jobs processed/)
|
80
|
+
expect(subject).not_to have_received(:interruptable_sleep)
|
81
|
+
end
|
82
|
+
|
83
|
+
context 'when max_claims is 2' do
|
84
|
+
let(:max_claims) { 2 }
|
85
|
+
|
86
|
+
it 'logs the count and sleeps' do
|
87
|
+
subject.run!
|
88
|
+
expect(Delayed.logger).to have_received(:info).with(/1 jobs processed/)
|
89
|
+
expect(subject).to have_received(:interruptable_sleep)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
# rubocop:enable RSpec/SubjectStub
|
95
|
+
|
96
|
+
describe 'job_say' do
|
97
|
+
before do
|
98
|
+
@worker = described_class.new
|
99
|
+
@job = double('job', id: 123, name: 'ExampleJob', queue: nil)
|
100
|
+
end
|
101
|
+
|
102
|
+
it 'logs with job name and id' do
|
103
|
+
expect(@job).to receive(:queue)
|
104
|
+
expect(@worker).to receive(:say)
|
105
|
+
.with('Job ExampleJob (id=123) message', 'info')
|
106
|
+
@worker.job_say(@job, 'message')
|
107
|
+
end
|
108
|
+
|
109
|
+
it 'logs with job name, queue and id' do
|
110
|
+
expect(@job).to receive(:queue).and_return('test')
|
111
|
+
expect(@worker).to receive(:say)
|
112
|
+
.with('Job ExampleJob (id=123) (queue=test) message', 'info')
|
113
|
+
@worker.job_say(@job, 'message')
|
114
|
+
end
|
115
|
+
|
116
|
+
it 'has a configurable default log level' do
|
117
|
+
described_class.default_log_level = 'error'
|
118
|
+
|
119
|
+
expect(@worker).to receive(:say)
|
120
|
+
.with('Job ExampleJob (id=123) message', 'error')
|
121
|
+
@worker.job_say(@job, 'message')
|
122
|
+
ensure
|
123
|
+
described_class.default_log_level = 'info'
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
context 'worker read-ahead' do
|
128
|
+
before do
|
129
|
+
@read_ahead = described_class.read_ahead
|
130
|
+
end
|
131
|
+
|
132
|
+
after do
|
133
|
+
described_class.read_ahead = @read_ahead
|
134
|
+
end
|
135
|
+
|
136
|
+
it 'reads five jobs' do
|
137
|
+
expect(described_class.new.read_ahead).to eq(5)
|
138
|
+
end
|
139
|
+
|
140
|
+
it 'reads a configurable number of jobs' do
|
141
|
+
described_class.read_ahead = 15
|
142
|
+
expect(described_class.new.read_ahead).to eq(15)
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
context 'worker job reservation' do
|
147
|
+
it 'handles error during job reservation' do
|
148
|
+
expect(Delayed::Job).to receive(:reserve).and_raise(Exception)
|
149
|
+
described_class.new.work_off
|
150
|
+
end
|
151
|
+
|
152
|
+
it 'gives up after 10 backend failures' do
|
153
|
+
expect(Delayed::Job).to receive(:reserve).exactly(10).times.and_raise(Exception)
|
154
|
+
worker = described_class.new
|
155
|
+
9.times { worker.work_off }
|
156
|
+
expect(lambda { worker.work_off }).to raise_exception Delayed::FatalBackendError
|
157
|
+
end
|
158
|
+
|
159
|
+
it 'allows the backend to attempt recovery from reservation errors' do
|
160
|
+
expect(Delayed::Job).to receive(:reserve).and_raise(Exception)
|
161
|
+
expect(Delayed::Job).to receive(:recover_from).with(instance_of(Exception))
|
162
|
+
described_class.new.work_off
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
describe '#say' do
|
167
|
+
before(:each) do
|
168
|
+
@worker = described_class.new
|
169
|
+
@worker.name = 'ExampleJob'
|
170
|
+
time = Time.now
|
171
|
+
allow(Time).to receive(:now).and_return(time)
|
172
|
+
@text = 'Job executed'
|
173
|
+
@worker_name = '[Worker(ExampleJob)]'
|
174
|
+
@expected_time = time.strftime('%FT%T%z')
|
175
|
+
end
|
176
|
+
|
177
|
+
around do |example|
|
178
|
+
logger = Delayed.logger
|
179
|
+
Delayed.logger = double('job')
|
180
|
+
example.run
|
181
|
+
ensure
|
182
|
+
Delayed.logger = logger
|
183
|
+
end
|
184
|
+
|
185
|
+
it 'logs a message on the default log level' do
|
186
|
+
expect(Delayed.logger).to receive(:send)
|
187
|
+
.with('info', "#{@expected_time}: #{@worker_name} #{@text}")
|
188
|
+
@worker.say(@text)
|
189
|
+
end
|
190
|
+
|
191
|
+
it 'logs a message on a custom log level' do
|
192
|
+
expect(Delayed.logger).to receive(:send)
|
193
|
+
.with('error', "#{@expected_time}: #{@worker_name} #{@text}")
|
194
|
+
@worker.say(@text, 'error')
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
describe 'plugin registration' do
|
199
|
+
it 'does not double-register plugins on worker instantiation' do
|
200
|
+
performances = 0
|
201
|
+
plugin = Class.new(Delayed::Plugin) do
|
202
|
+
callbacks do |lifecycle|
|
203
|
+
lifecycle.before(:enqueue) { performances += 1 }
|
204
|
+
end
|
205
|
+
end
|
206
|
+
Delayed.plugins << plugin
|
207
|
+
|
208
|
+
described_class.new
|
209
|
+
described_class.new
|
210
|
+
Delayed::Job.enqueue SimpleJob.new
|
211
|
+
|
212
|
+
expect(performances).to eq(1)
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
describe 'thread callback' do
|
217
|
+
it 'wraps code after thread is checked out' do
|
218
|
+
performances = Concurrent::AtomicFixnum.new(0)
|
219
|
+
plugin = Class.new(Delayed::Plugin) do
|
220
|
+
callbacks do |lifecycle|
|
221
|
+
lifecycle.before(:thread) { performances.increment }
|
222
|
+
end
|
223
|
+
end
|
224
|
+
Delayed.plugins << plugin
|
225
|
+
|
226
|
+
Delayed::Job.delete_all
|
227
|
+
Delayed::Job.enqueue SimpleJob.new
|
228
|
+
worker = described_class.new
|
229
|
+
|
230
|
+
worker.work_off
|
231
|
+
|
232
|
+
expect(performances.value).to eq(1)
|
233
|
+
end
|
234
|
+
end
|
235
|
+
end
|