delayed 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (55) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +20 -0
  3. data/README.md +560 -0
  4. data/Rakefile +35 -0
  5. data/lib/delayed.rb +72 -0
  6. data/lib/delayed/active_job_adapter.rb +65 -0
  7. data/lib/delayed/backend/base.rb +166 -0
  8. data/lib/delayed/backend/job_preparer.rb +43 -0
  9. data/lib/delayed/exceptions.rb +14 -0
  10. data/lib/delayed/job.rb +250 -0
  11. data/lib/delayed/lifecycle.rb +85 -0
  12. data/lib/delayed/message_sending.rb +65 -0
  13. data/lib/delayed/monitor.rb +134 -0
  14. data/lib/delayed/performable_mailer.rb +22 -0
  15. data/lib/delayed/performable_method.rb +47 -0
  16. data/lib/delayed/plugin.rb +15 -0
  17. data/lib/delayed/plugins/connection.rb +13 -0
  18. data/lib/delayed/plugins/instrumentation.rb +39 -0
  19. data/lib/delayed/priority.rb +164 -0
  20. data/lib/delayed/psych_ext.rb +135 -0
  21. data/lib/delayed/railtie.rb +7 -0
  22. data/lib/delayed/runnable.rb +46 -0
  23. data/lib/delayed/serialization/active_record.rb +18 -0
  24. data/lib/delayed/syck_ext.rb +42 -0
  25. data/lib/delayed/tasks.rb +40 -0
  26. data/lib/delayed/worker.rb +233 -0
  27. data/lib/delayed/yaml_ext.rb +10 -0
  28. data/lib/delayed_job.rb +1 -0
  29. data/lib/delayed_job_active_record.rb +1 -0
  30. data/lib/generators/delayed/generator.rb +7 -0
  31. data/lib/generators/delayed/migration_generator.rb +28 -0
  32. data/lib/generators/delayed/next_migration_version.rb +14 -0
  33. data/lib/generators/delayed/templates/migration.rb +22 -0
  34. data/spec/autoloaded/clazz.rb +6 -0
  35. data/spec/autoloaded/instance_clazz.rb +5 -0
  36. data/spec/autoloaded/instance_struct.rb +6 -0
  37. data/spec/autoloaded/struct.rb +7 -0
  38. data/spec/database.yml +25 -0
  39. data/spec/delayed/active_job_adapter_spec.rb +267 -0
  40. data/spec/delayed/job_spec.rb +953 -0
  41. data/spec/delayed/monitor_spec.rb +276 -0
  42. data/spec/delayed/plugins/instrumentation_spec.rb +49 -0
  43. data/spec/delayed/priority_spec.rb +154 -0
  44. data/spec/delayed/serialization/active_record_spec.rb +15 -0
  45. data/spec/delayed/tasks_spec.rb +116 -0
  46. data/spec/helper.rb +196 -0
  47. data/spec/lifecycle_spec.rb +77 -0
  48. data/spec/message_sending_spec.rb +149 -0
  49. data/spec/performable_mailer_spec.rb +68 -0
  50. data/spec/performable_method_spec.rb +123 -0
  51. data/spec/psych_ext_spec.rb +94 -0
  52. data/spec/sample_jobs.rb +117 -0
  53. data/spec/worker_spec.rb +235 -0
  54. data/spec/yaml_ext_spec.rb +48 -0
  55. 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
@@ -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
@@ -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