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