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,10 @@
1
+ # These extensions allow properly serializing and autoloading of
2
+ # Classes, Modules and Structs
3
+
4
+ require 'yaml'
5
+ if /syck|yecht/i.match?(YAML.parser.class.name)
6
+ require File.expand_path('syck_ext', __dir__)
7
+ require File.expand_path('serialization/active_record', __dir__)
8
+ else
9
+ require File.expand_path('psych_ext', __dir__)
10
+ end
@@ -0,0 +1 @@
1
+ require 'delayed'
@@ -0,0 +1 @@
1
+ require 'delayed'
@@ -0,0 +1,7 @@
1
+ require 'rails/generators/base'
2
+
3
+ module Delayed
4
+ class Generator < Rails::Generators::Base
5
+ source_paths << File.join(File.dirname(__FILE__), 'templates')
6
+ end
7
+ end
@@ -0,0 +1,28 @@
1
+ require "generators/delayed/generator"
2
+ require "generators/delayed/next_migration_version"
3
+ require "rails/generators/migration"
4
+ require "rails/generators/active_record"
5
+
6
+ # Extend the DelayedJobGenerator so that it creates an AR migration
7
+ module Delayed
8
+ class MigrationGenerator < Generator
9
+ include Rails::Generators::Migration
10
+ extend NextMigrationVersion
11
+
12
+ source_paths << File.join(File.dirname(__FILE__), "templates")
13
+
14
+ def create_migration_file
15
+ migration_template "migration.rb", "db/migrate/create_delayed_jobs.rb", migration_version: migration_version
16
+ end
17
+
18
+ def self.next_migration_number(dirname)
19
+ ActiveRecord::Generators::Base.next_migration_number dirname
20
+ end
21
+
22
+ private
23
+
24
+ def migration_version
25
+ "[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]" if ActiveRecord::VERSION::MAJOR >= 5
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,14 @@
1
+ module Delayed
2
+ module NextMigrationVersion
3
+ # while methods have moved around this has been the implementation
4
+ # since ActiveRecord 3.0
5
+ def next_migration_number(dirname)
6
+ next_migration_number = current_migration_number(dirname) + 1
7
+ if ActiveRecord::Base.timestamped_migrations
8
+ [Time.now.utc.strftime("%Y%m%d%H%M%S"), format("%.14d", next_migration_number)].max
9
+ else
10
+ format("%.3d", next_migration_number)
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,22 @@
1
+ class CreateDelayedJobs < ActiveRecord::Migration<%= migration_version %>
2
+ def self.up
3
+ create_table :delayed_jobs do |table|
4
+ table.integer :priority, default: 0, null: false # Allows some jobs to jump to the front of the queue
5
+ table.integer :attempts, default: 0, null: false # Provides for retries, but still fail eventually.
6
+ table.text :handler, null: false # YAML-encoded string of the object that will do work
7
+ table.text :last_error # reason for last failure (See Note below)
8
+ table.datetime :run_at # When to run. Could be Time.zone.now for immediately, or sometime in the future.
9
+ table.datetime :locked_at # Set when a client is working on this object
10
+ table.datetime :failed_at # Set when all retries have failed
11
+ table.string :locked_by # Who is working on this object (if locked)
12
+ table.string :queue # The name of the queue this job is in
13
+ table.timestamps null: true
14
+ end
15
+
16
+ add_index :delayed_jobs, [:priority, :run_at], name: "delayed_jobs_priority"
17
+ end
18
+
19
+ def self.down
20
+ drop_table :delayed_jobs
21
+ end
22
+ end
@@ -0,0 +1,6 @@
1
+ # Make sure this file does not get required manually
2
+ module Autoloaded
3
+ class Clazz
4
+ def perform; end
5
+ end
6
+ end
@@ -0,0 +1,5 @@
1
+ module Autoloaded
2
+ class InstanceClazz
3
+ def perform; end
4
+ end
5
+ end
@@ -0,0 +1,6 @@
1
+ module Autoloaded
2
+ InstanceStruct = ::Struct.new(nil)
3
+ class InstanceStruct
4
+ def perform; end
5
+ end
6
+ end
@@ -0,0 +1,7 @@
1
+ # Make sure this file does not get required manually
2
+ module Autoloaded
3
+ Struct = ::Struct.new(nil)
4
+ class Struct
5
+ def perform; end
6
+ end
7
+ end
data/spec/database.yml ADDED
@@ -0,0 +1,25 @@
1
+ mysql:
2
+ adapter: mysql
3
+ host: 127.0.0.1
4
+ database: delayed_job_test
5
+ username: root
6
+ port: 3306
7
+ encoding: utf8
8
+
9
+ mysql2:
10
+ adapter: mysql2
11
+ host: 127.0.0.1
12
+ database: delayed_job_test
13
+ username: root
14
+ port: 3306
15
+ encoding: utf8
16
+
17
+ postgresql:
18
+ adapter: postgresql
19
+ host: 127.0.0.1
20
+ url: <%= ENV.fetch('DATABASE_URL', 'postgresql://127.0.0.1/delayed_job_test') %>
21
+ password: postgres
22
+
23
+ sqlite3:
24
+ adapter: sqlite3
25
+ database: "tmp/database.sqlite"
@@ -0,0 +1,267 @@
1
+ require 'helper'
2
+
3
+ RSpec.describe Delayed::ActiveJobAdapter do
4
+ let(:arbitrary_time) do
5
+ Time.parse('2021-01-05 03:34:33 UTC')
6
+ end
7
+ let(:queue_adapter) { :delayed }
8
+ let(:job_class) do
9
+ Class.new(ActiveJob::Base) do # rubocop:disable Rails/ApplicationJob
10
+ def perform; end
11
+ end
12
+ end
13
+
14
+ before do
15
+ stub_const 'JobClass', job_class
16
+ end
17
+
18
+ around do |example|
19
+ adapter_was = ActiveJob::Base.queue_adapter
20
+ ActiveJob::Base.queue_adapter = queue_adapter
21
+ example.run
22
+ ensure
23
+ ActiveJob::Base.queue_adapter = adapter_was
24
+ end
25
+
26
+ describe '.set' do
27
+ it 'supports priority as an integer' do
28
+ JobClass.set(priority: 43).perform_later
29
+
30
+ expect(Delayed::Job.last.priority).to be_reporting
31
+ expect(Delayed::Job.last.priority).to eq(43)
32
+ end
33
+
34
+ it 'supports priority as a Delayed::Priority' do
35
+ JobClass.set(priority: Delayed::Priority.eventual).perform_later
36
+
37
+ expect(Delayed::Job.last.priority).to be_eventual
38
+ expect(Delayed::Job.last.priority).to eq(20)
39
+ end
40
+
41
+ it 'supports priority as a symbol' do
42
+ JobClass.set(priority: :eventual).perform_later
43
+
44
+ expect(Delayed::Job.last.priority).to be_eventual
45
+ expect(Delayed::Job.last.priority).to eq(20)
46
+ end
47
+
48
+ it 'raises an error when run_at is used' do
49
+ expect { JobClass.set(run_at: arbitrary_time).perform_later }
50
+ .to raise_error(/`:run_at` is not supported./)
51
+ end
52
+
53
+ it 'converts wait_until to run_at' do
54
+ JobClass.set(wait_until: arbitrary_time).perform_later
55
+
56
+ expect(Delayed::Job.last.run_at).to eq('2021-01-05 03:34:33 UTC')
57
+ end
58
+
59
+ context 'when running at a specific time' do
60
+ around do |example|
61
+ Timecop.freeze(arbitrary_time) { example.run }
62
+ end
63
+
64
+ it 'adds wait input to current time' do
65
+ JobClass.set(wait: (1.day + 1.hour + 1.minute)).perform_later
66
+
67
+ expect(Delayed::Job.last.run_at).to eq('2021-01-06 04:35:33 UTC')
68
+ end
69
+ end
70
+
71
+ context 'when the Delayed::Job class supports arbitrary attributes' do
72
+ before do
73
+ Delayed::Job.class_eval do
74
+ def foo=(value)
75
+ self.queue = "foo-#{value}"
76
+ end
77
+ end
78
+ end
79
+
80
+ after do
81
+ Delayed::Job.undef_method(:foo=)
82
+ end
83
+
84
+ it 'calls the expected setter' do
85
+ JobClass.set(foo: 'bar').perform_later
86
+
87
+ expect(Delayed::Job.last.queue).to eq('foo-bar')
88
+ end
89
+ end
90
+
91
+ context 'when the ActiveJob performable defines a max_attempts' do
92
+ let(:job_class) do
93
+ Class.new(ActiveJob::Base) do # rubocop:disable Rails/ApplicationJob
94
+ def perform; end
95
+
96
+ def max_attempts
97
+ 3
98
+ end
99
+ end
100
+ end
101
+
102
+ it 'surfaces max_attempts on the JobWrapper' do
103
+ JobClass.perform_later
104
+
105
+ expect(Delayed::Job.last.max_attempts).to eq 3
106
+ end
107
+ end
108
+
109
+ context 'when the ActiveJob performable defines an arbitrary method' do
110
+ let(:job_class) do
111
+ Class.new(ActiveJob::Base) do # rubocop:disable Rails/ApplicationJob
112
+ def perform; end
113
+
114
+ def arbitrary_method
115
+ 'hello'
116
+ end
117
+ end
118
+ end
119
+
120
+ it 'surfaces arbitrary_method on the JobWrapper' do
121
+ JobClass.perform_later
122
+
123
+ expect(Delayed::Job.last.payload_object.arbitrary_method).to eq 'hello'
124
+ end
125
+ end
126
+ end
127
+
128
+ describe '.perform_later' do
129
+ it 'applies the default ActiveJob queue and priority' do
130
+ JobClass.perform_later
131
+
132
+ expect(Delayed::Job.last.queue).to eq('default')
133
+ expect(Delayed::Job.last.priority).to eq(10)
134
+ end
135
+
136
+ it 'supports overriding queue and priority' do
137
+ JobClass.set(queue: 'a', priority: 3).perform_later
138
+
139
+ expect(Delayed::Job.last.queue).to eq('a')
140
+ expect(Delayed::Job.last.priority).to eq(3)
141
+ end
142
+
143
+ context 'when all default queues and priorities are nil' do
144
+ before do
145
+ ActiveJob::Base.queue_name = nil
146
+ ActiveJob::Base.priority = nil
147
+ Delayed::Worker.default_queue_name = nil
148
+ Delayed::Worker.default_priority = nil
149
+ end
150
+
151
+ it 'applies no queue or priority' do
152
+ JobClass.perform_later
153
+
154
+ expect(Delayed::Job.last.queue).to be_nil
155
+ expect(Delayed::Job.last.priority).to eq(0)
156
+ end
157
+
158
+ it 'supports overriding queue and priority' do
159
+ JobClass.set(queue: 'a', priority: 3).perform_later
160
+
161
+ expect(Delayed::Job.last.queue).to eq('a')
162
+ expect(Delayed::Job.last.priority).to eq(3)
163
+ end
164
+ end
165
+
166
+ context 'when there is a default Delayed queue and priority, but not ActiveJob' do
167
+ before do
168
+ ActiveJob::Base.queue_name = nil
169
+ ActiveJob::Base.priority = nil
170
+ Delayed::Worker.default_queue_name = 'dj_default'
171
+ Delayed::Worker.default_priority = 99
172
+ end
173
+
174
+ it 'applies the default Delayed queue and priority' do
175
+ JobClass.perform_later
176
+
177
+ expect(Delayed::Job.last.queue).to eq('dj_default')
178
+ expect(Delayed::Job.last.priority).to eq(99)
179
+ end
180
+
181
+ it 'supports overriding queue and priority' do
182
+ JobClass.set(queue: 'a', priority: 3).perform_later
183
+
184
+ expect(Delayed::Job.last.queue).to eq('a')
185
+ expect(Delayed::Job.last.priority).to eq(3)
186
+ end
187
+ end
188
+
189
+ context 'when ActiveJob specifies a different default queue and priority' do
190
+ before do
191
+ ActiveJob::Base.queue_name = 'aj_default'
192
+ ActiveJob::Base.priority = 11
193
+ end
194
+
195
+ it 'applies the default ActiveJob queue and priority' do
196
+ JobClass.perform_later
197
+
198
+ expect(Delayed::Job.last.queue).to eq('aj_default')
199
+ expect(Delayed::Job.last.priority).to eq(11)
200
+ end
201
+
202
+ it 'supports overriding queue and priority' do
203
+ JobClass.set(queue: 'a', priority: 3).perform_later
204
+
205
+ expect(Delayed::Job.last.queue).to eq('a')
206
+ expect(Delayed::Job.last.priority).to eq(3)
207
+ end
208
+ end
209
+
210
+ context 'when ActiveJob uses queue_with_priority' do
211
+ let(:job_class) do
212
+ Class.new(ActiveJob::Base) do # rubocop:disable Rails/ApplicationJob
213
+ queue_with_priority Delayed::Priority.reporting
214
+
215
+ def perform; end
216
+ end
217
+ end
218
+
219
+ it 'applies the specified priority' do
220
+ JobClass.perform_later
221
+
222
+ expect(Delayed::Job.last.priority).to eq(30)
223
+ end
224
+ end
225
+
226
+ context 'when using the ActiveJob test adapter' do
227
+ let(:queue_adapter) { :test }
228
+
229
+ it 'applies the default ActiveJob queue and priority' do
230
+ JobClass.perform_later
231
+
232
+ if ActiveJob.gem_version < Gem::Version.new('6')
233
+ expect(JobClass.queue_adapter.enqueued_jobs.first).to include(job: JobClass, queue: 'default')
234
+ else
235
+ expect(JobClass.queue_adapter.enqueued_jobs.first).to include(job: JobClass, 'priority' => nil, queue: 'default')
236
+ end
237
+ end
238
+
239
+ context 'when ActiveJob specifies a different default queue and priority' do
240
+ before do
241
+ ActiveJob::Base.queue_name = 'aj_default'
242
+ ActiveJob::Base.priority = 11
243
+ end
244
+
245
+ it 'applies the default ActiveJob queue and priority' do
246
+ JobClass.perform_later
247
+
248
+ if ActiveJob.gem_version < Gem::Version.new('6')
249
+ expect(JobClass.queue_adapter.enqueued_jobs.first).to include(job: JobClass, queue: 'aj_default')
250
+ else
251
+ expect(JobClass.queue_adapter.enqueued_jobs.first).to include(job: JobClass, 'priority' => 11, queue: 'aj_default')
252
+ end
253
+ end
254
+ end
255
+
256
+ it 'supports overriding queue, priority, and wait_until' do
257
+ JobClass.set(queue: 'a', priority: 3, wait_until: arbitrary_time).perform_later
258
+
259
+ if ActiveJob.gem_version < Gem::Version.new('6')
260
+ expect(JobClass.queue_adapter.enqueued_jobs.first).to include(job: JobClass, queue: 'a', at: arbitrary_time.to_f)
261
+ else
262
+ expect(JobClass.queue_adapter.enqueued_jobs.first).to include(job: JobClass, 'priority' => 3, queue: 'a', at: arbitrary_time.to_f)
263
+ end
264
+ end
265
+ end
266
+ end
267
+ end
@@ -0,0 +1,953 @@
1
+ require 'helper'
2
+
3
+ describe Delayed::Job do
4
+ let(:worker) { Delayed::Worker.new }
5
+
6
+ def create_job(opts = {})
7
+ described_class.create(opts.merge(payload_object: SimpleJob.new))
8
+ end
9
+
10
+ before do
11
+ Delayed::Worker.max_priority = nil
12
+ Delayed::Worker.min_priority = nil
13
+ Delayed::Worker.max_claims = 1 # disable multithreading because SimpleJob is not threadsafe
14
+ Delayed::Worker.default_priority = 99
15
+ Delayed::Worker.delay_jobs = true
16
+ Delayed::Worker.default_queue_name = 'default_tracking'
17
+ SimpleJob.runs = 0
18
+ described_class.delete_all
19
+ end
20
+
21
+ it 'sets run_at automatically if not set' do
22
+ expect(described_class.create(payload_object: ErrorJob.new).run_at).not_to be_nil
23
+ end
24
+
25
+ it 'does not set run_at automatically if already set' do
26
+ later = described_class.db_time_now + 5.minutes
27
+ job = described_class.create(payload_object: ErrorJob.new, run_at: later)
28
+ expect(job.run_at).to be_within(1).of(later)
29
+ end
30
+
31
+ describe '#reload' do
32
+ it 'reloads the payload' do
33
+ job = described_class.enqueue payload_object: SimpleJob.new
34
+ expect(job.payload_object.object_id).not_to eq(job.reload.payload_object.object_id)
35
+ end
36
+ end
37
+
38
+ describe 'enqueue' do
39
+ it "allows enqueue hook to modify job at DB level" do
40
+ later = described_class.db_time_now + 20.minutes
41
+ job = described_class.enqueue payload_object: EnqueueJobMod.new
42
+ expect(described_class.find(job.id).run_at).to be_within(1).of(later)
43
+ end
44
+
45
+ context 'with a hash' do
46
+ it "raises ArgumentError when handler doesn't respond_to :perform" do
47
+ expect { described_class.enqueue(payload_object: Object.new) }.to raise_error(ArgumentError)
48
+ end
49
+
50
+ it 'is able to set priority' do
51
+ job = described_class.enqueue payload_object: SimpleJob.new, priority: 5
52
+ expect(job.priority).to eq(5)
53
+ end
54
+
55
+ it 'is able to set priority by symbol name' do
56
+ job = described_class.enqueue SimpleJob.new, priority: :eventual
57
+ expect(job.priority).to be_eventual
58
+ expect(job.priority).to eq(20)
59
+ end
60
+
61
+ it 'uses default priority' do
62
+ job = described_class.enqueue payload_object: SimpleJob.new
63
+ expect(job.priority).to eq(99)
64
+ end
65
+
66
+ it 'is able to set run_at' do
67
+ later = described_class.db_time_now + 5.minutes
68
+ job = described_class.enqueue payload_object: SimpleJob.new, run_at: later
69
+ expect(job.run_at).to be_within(1).of(later)
70
+ end
71
+
72
+ it 'is able to set queue' do
73
+ job = described_class.enqueue payload_object: NamedQueueJob.new, queue: 'tracking'
74
+ expect(job.queue).to eq('tracking')
75
+ end
76
+
77
+ it 'uses default queue' do
78
+ job = described_class.enqueue payload_object: SimpleJob.new
79
+ expect(job.queue).to eq(Delayed::Worker.default_queue_name)
80
+ end
81
+
82
+ it "uses the payload object's queue" do
83
+ job = described_class.enqueue payload_object: NamedQueueJob.new
84
+ expect(job.queue).to eq(NamedQueueJob.new.queue_name)
85
+ end
86
+ end
87
+
88
+ context 'with multiple arguments' do
89
+ it "raises ArgumentError when handler doesn't respond_to :perform" do
90
+ expect { described_class.enqueue(Object.new) }.to raise_error(ArgumentError)
91
+ end
92
+
93
+ it 'increases count after enqueuing items' do
94
+ described_class.enqueue SimpleJob.new
95
+ expect(described_class.count).to eq(1)
96
+ end
97
+
98
+ it 'uses default priority when it is not set' do
99
+ @job = described_class.enqueue SimpleJob.new
100
+ expect(@job.priority).to eq(99)
101
+ end
102
+
103
+ it 'works with jobs in modules' do
104
+ M::ModuleJob.runs = 0
105
+ job = described_class.enqueue M::ModuleJob.new
106
+ expect { job.invoke_job }.to change { M::ModuleJob.runs }.from(0).to(1)
107
+ end
108
+
109
+ it 'does not mutate the options hash' do
110
+ options = { priority: 1 }
111
+ described_class.enqueue SimpleJob.new, options
112
+ expect(options).to eq(priority: 1)
113
+ end
114
+ end
115
+
116
+ context 'with delay_jobs = false' do
117
+ before(:each) do
118
+ Delayed::Worker.delay_jobs = false
119
+ end
120
+
121
+ it 'does not increase count after enqueuing items' do
122
+ described_class.enqueue SimpleJob.new
123
+ expect(described_class.count).to eq(0)
124
+ end
125
+
126
+ it 'invokes the enqueued job' do
127
+ job = SimpleJob.new
128
+ expect(job).to receive(:perform)
129
+ described_class.enqueue job
130
+ end
131
+
132
+ it 'returns a job, not the result of invocation' do
133
+ expect(described_class.enqueue(SimpleJob.new)).to be_instance_of(described_class)
134
+ end
135
+ end
136
+ end
137
+
138
+ describe 'callbacks' do
139
+ before(:each) do
140
+ CallbackJob.messages = []
141
+ end
142
+
143
+ %w(before success after).each do |callback|
144
+ it "calls #{callback} with job" do
145
+ job = described_class.enqueue(CallbackJob.new)
146
+ expect(job.payload_object).to receive(callback).with(job)
147
+ job.invoke_job
148
+ end
149
+ end
150
+
151
+ it 'calls before and after callbacks' do
152
+ job = described_class.enqueue(CallbackJob.new)
153
+ expect(CallbackJob.messages).to eq(['enqueue'])
154
+ job.invoke_job
155
+ expect(CallbackJob.messages).to eq(%w(enqueue before perform success after))
156
+ end
157
+
158
+ it 'calls the after callback with an error' do
159
+ job = described_class.enqueue(CallbackJob.new)
160
+ expect(job.payload_object).to receive(:perform).and_raise(RuntimeError.new('fail'))
161
+
162
+ expect { job.invoke_job }.to raise_error(RuntimeError, 'fail')
163
+ expect(CallbackJob.messages).to eq(['enqueue', 'before', 'error: RuntimeError', 'after'])
164
+ end
165
+
166
+ it 'calls error when before raises an error' do
167
+ job = described_class.enqueue(CallbackJob.new)
168
+ expect(job.payload_object).to receive(:before).and_raise(RuntimeError.new('fail'))
169
+ expect { job.invoke_job }.to raise_error(RuntimeError, 'fail')
170
+ expect(CallbackJob.messages).to eq(['enqueue', 'error: RuntimeError', 'after'])
171
+ end
172
+ end
173
+
174
+ describe 'payload_object' do
175
+ it 'raises a DeserializationError when the job class is totally unknown' do
176
+ job = described_class.new handler: '--- !ruby/object:JobThatDoesNotExist {}'
177
+ expect { job.payload_object }.to raise_error(Delayed::DeserializationError)
178
+ end
179
+
180
+ it 'raises a DeserializationError when the job struct is totally unknown' do
181
+ job = described_class.new handler: '--- !ruby/struct:StructThatDoesNotExist {}'
182
+ expect { job.payload_object }.to raise_error(Delayed::DeserializationError)
183
+ end
184
+
185
+ it 'raises a DeserializationError when the YAML.load raises argument error' do
186
+ job = described_class.new handler: '--- !ruby/struct:GoingToRaiseArgError {}'
187
+ expect(YAML).to receive(:load_dj).and_raise(ArgumentError)
188
+ expect { job.payload_object }.to raise_error(Delayed::DeserializationError)
189
+ end
190
+
191
+ it 'raises a DeserializationError when the YAML.load raises syntax error' do
192
+ # only test with Psych since the other YAML parsers don't raise a SyntaxError
193
+ unless /syck|yecht/i.match?(YAML.parser.class.name)
194
+ job = described_class.new handler: 'message: "no ending quote'
195
+ expect { job.payload_object }.to raise_error(Delayed::DeserializationError)
196
+ end
197
+ end
198
+ end
199
+
200
+ describe 'reserve' do
201
+ before do
202
+ Delayed::Worker.max_run_time = 2.minutes
203
+ end
204
+
205
+ after do
206
+ Time.zone = nil
207
+ end
208
+
209
+ it 'does not reserve failed jobs' do
210
+ create_job attempts: 50, failed_at: described_class.db_time_now
211
+ expect(described_class.reserve(worker)).to eq []
212
+ end
213
+
214
+ it 'does not reserve jobs scheduled for the future' do
215
+ create_job run_at: described_class.db_time_now + 1.minute
216
+ expect(described_class.reserve(worker)).to eq []
217
+ end
218
+
219
+ it 'reserves jobs scheduled for the past' do
220
+ job = create_job run_at: described_class.db_time_now - 1.minute
221
+ expect(described_class.reserve(worker)).to eq([job])
222
+ end
223
+
224
+ it 'reserves jobs scheduled for the past when time zones are involved' do
225
+ Time.zone = 'US/Eastern'
226
+ job = create_job run_at: described_class.db_time_now - 1.minute
227
+ expect(described_class.reserve(worker)).to eq([job])
228
+ end
229
+
230
+ it 'does not reserve jobs locked by other workers' do
231
+ job = create_job
232
+ other_worker = Delayed::Worker.new
233
+ other_worker.name = 'other_worker'
234
+ expect(described_class.reserve(other_worker)).to eq([job])
235
+ expect(described_class.reserve(worker)).to eq []
236
+ end
237
+
238
+ it 'reserves open jobs' do
239
+ job = create_job
240
+ expect(described_class.reserve(worker)).to eq([job])
241
+ end
242
+
243
+ it 'reserves expired jobs' do
244
+ job = create_job(locked_by: 'some other worker',
245
+ locked_at: described_class.db_time_now - Delayed::Worker.max_run_time - 1.minute)
246
+ expect(described_class.reserve(worker)).to eq([job])
247
+ end
248
+
249
+ it 'reserves own jobs' do
250
+ job = create_job(locked_by: worker.name, locked_at: (described_class.db_time_now - 1.minute))
251
+ expect(described_class.reserve(worker)).to eq([job])
252
+ end
253
+ end
254
+
255
+ describe '#name' do
256
+ it 'is the class name of the job that was enqueued' do
257
+ expect(described_class.create(payload_object: ErrorJob.new).name).to eq('ErrorJob')
258
+ end
259
+
260
+ it 'is the class name of the performable job if it is an ActiveJob' do
261
+ job_wrapper = ActiveJob::QueueAdapters::DelayedJobAdapter::JobWrapper.new(ActiveJobJob.new.serialize)
262
+ expect(described_class.create(payload_object: job_wrapper).name).to eq('ActiveJobJob')
263
+ end
264
+
265
+ it 'is the method that will be called if its a performable method object' do
266
+ job = described_class.new(payload_object: NamedJob.new)
267
+ expect(job.name).to eq('named_job')
268
+ end
269
+
270
+ it 'is the instance method that will be called if its a performable method object' do
271
+ job = Story.create(text: '...').delay.save
272
+ expect(job.name).to eq('Story#save')
273
+ end
274
+
275
+ it 'parses from handler on deserialization error' do
276
+ job = Story.create(text: '...').delay.text
277
+ job.payload_object.object.destroy
278
+ expect(job.reload.name).to eq('Delayed::PerformableMethod')
279
+ end
280
+ end
281
+
282
+ context 'worker prioritization' do
283
+ after do
284
+ Delayed::Worker.max_claims = nil
285
+ Delayed::Worker.max_priority = nil
286
+ Delayed::Worker.min_priority = nil
287
+ Delayed::Worker.read_ahead = nil
288
+ end
289
+
290
+ it 'fetches jobs ordered by priority' do
291
+ 10.times { described_class.enqueue SimpleJob.new, priority: rand(10) }
292
+ Delayed::Worker.read_ahead = 10
293
+ Delayed::Worker.max_claims = 10
294
+ jobs = described_class.reserve(worker)
295
+ expect(jobs.size).to eq(10)
296
+ jobs.each_cons(2) do |a, b|
297
+ expect(a.priority).to be <= b.priority
298
+ end
299
+ end
300
+
301
+ it 'only finds jobs greater than or equal to min priority' do
302
+ min = 5
303
+ Delayed::Worker.min_priority = min
304
+ Delayed::Worker.max_claims = 2
305
+ [4, 5, 6].sort_by { |_i| rand }.each { |i| create_job priority: i }
306
+ jobs = described_class.reserve(worker)
307
+ expect(jobs.map(&:priority).min).to be >= min
308
+ jobs.map(&:destroy)
309
+ expect(described_class.reserve(worker)).to eq []
310
+ end
311
+
312
+ it 'only finds jobs less than or equal to max priority' do
313
+ max = 5
314
+ Delayed::Worker.max_priority = max
315
+ Delayed::Worker.max_claims = 2
316
+ [4, 5, 6].sort_by { |_i| rand }.each { |i| create_job priority: i }
317
+ jobs = described_class.reserve(worker)
318
+ expect(jobs.map(&:priority).max).to be <= max
319
+ jobs.map(&:destroy)
320
+ expect(described_class.reserve(worker)).to eq []
321
+ end
322
+ end
323
+
324
+ context 'clear_locks!' do
325
+ before do
326
+ @job = create_job(locked_by: 'worker1', locked_at: described_class.db_time_now)
327
+ end
328
+
329
+ it 'clears locks for the given worker' do
330
+ described_class.clear_locks!('worker1')
331
+ expect(described_class.reserve(worker)).to eq([@job])
332
+ end
333
+
334
+ it 'does not clear locks for other workers' do
335
+ described_class.clear_locks!('different_worker')
336
+ expect(described_class.reserve(worker)).not_to include(@job)
337
+ end
338
+ end
339
+
340
+ context 'unlock' do
341
+ before do
342
+ @job = create_job(locked_by: 'worker', locked_at: described_class.db_time_now)
343
+ end
344
+
345
+ it 'clears locks' do
346
+ @job.unlock
347
+ expect(@job.locked_by).to be_nil
348
+ expect(@job.locked_at).to be_nil
349
+ end
350
+ end
351
+
352
+ context 'large handler' do
353
+ before do
354
+ text = 'Lorem ipsum dolor sit amet. ' * 1000
355
+ @job = described_class.enqueue Delayed::PerformableMethod.new(text, :length, {})
356
+ end
357
+
358
+ it 'has an id' do
359
+ expect(@job.id).not_to be_nil
360
+ end
361
+ end
362
+
363
+ context 'named queues' do
364
+ context 'when worker has one queue set' do
365
+ before do
366
+ Delayed::Worker.queues = ['large']
367
+ end
368
+
369
+ it 'only works off jobs which are from its queue' do
370
+ expect(SimpleJob.runs).to eq(0)
371
+
372
+ create_job(queue: 'large')
373
+ create_job(queue: 'small')
374
+ worker.work_off
375
+
376
+ expect(SimpleJob.runs).to eq(1)
377
+ end
378
+ end
379
+
380
+ context 'when worker has two queue set' do
381
+ before do
382
+ Delayed::Worker.queues = %w(large small)
383
+ end
384
+
385
+ it 'only works off jobs which are from its queue' do
386
+ expect(SimpleJob.runs).to eq(0)
387
+
388
+ create_job(queue: 'large')
389
+ create_job(queue: 'small')
390
+ create_job(queue: 'medium')
391
+ create_job
392
+ worker.work_off
393
+
394
+ expect(SimpleJob.runs).to eq(2)
395
+ end
396
+ end
397
+
398
+ context 'when worker does not have queue set' do
399
+ before(:each) do
400
+ Delayed::Worker.queues = []
401
+ end
402
+
403
+ it 'works off all jobs' do
404
+ expect(SimpleJob.runs).to eq(0)
405
+
406
+ create_job(queue: 'one')
407
+ create_job(queue: 'two')
408
+ create_job
409
+ worker.work_off
410
+
411
+ expect(SimpleJob.runs).to eq(3)
412
+ end
413
+ end
414
+ end
415
+
416
+ context 'max_attempts' do
417
+ before(:each) do
418
+ @job = described_class.enqueue SimpleJob.new
419
+ end
420
+
421
+ it 'is not defined' do
422
+ expect(@job.max_attempts).to be_nil
423
+ end
424
+
425
+ it 'uses the max_attempts value on the payload when defined' do
426
+ expect(@job.payload_object).to receive(:max_attempts).and_return(99)
427
+ expect(@job.max_attempts).to eq(99)
428
+ end
429
+ end
430
+
431
+ describe '#max_run_time' do
432
+ before(:each) { @job = described_class.enqueue SimpleJob.new }
433
+
434
+ it 'is not defined' do
435
+ expect(@job.max_run_time).to be_nil
436
+ end
437
+
438
+ it 'results in a default run time when not defined' do
439
+ expect(worker.max_run_time(@job)).to eq(20.minutes)
440
+ end
441
+
442
+ it 'uses the max_run_time value on the payload when defined' do
443
+ expect(@job.payload_object).to receive(:max_run_time).and_return(10.minutes)
444
+ expect(@job.max_run_time).to eq(10.minutes)
445
+ end
446
+
447
+ it 'results in an overridden run time when defined' do
448
+ expect(@job.payload_object).to receive(:max_run_time).and_return(15.minutes)
449
+ expect(worker.max_run_time(@job)).to eq(15.minutes)
450
+ end
451
+
452
+ it 'job set max_run_time can not exceed default max run time' do
453
+ expect(@job.payload_object).to receive(:max_run_time).and_return(20.minutes + 60)
454
+ expect(worker.max_run_time(@job)).to eq(20.minutes)
455
+ end
456
+ end
457
+
458
+ describe 'destroy_failed_jobs' do
459
+ context 'with a SimpleJob' do
460
+ before(:each) do
461
+ @job = described_class.enqueue SimpleJob.new
462
+ end
463
+
464
+ it 'is not defined' do
465
+ expect(@job.destroy_failed_jobs?).to be false
466
+ end
467
+
468
+ it 'uses the destroy failed jobs value on the payload when defined' do
469
+ expect(@job.payload_object).to receive(:destroy_failed_jobs?).and_return(true)
470
+ expect(@job.destroy_failed_jobs?).to be true
471
+ end
472
+ end
473
+
474
+ context 'with a job that raises DserializationError' do
475
+ before(:each) do
476
+ @job = described_class.new handler: '--- !ruby/struct:GoingToRaiseArgError {}'
477
+ end
478
+
479
+ it 'falls back reasonably' do
480
+ expect(YAML).to receive(:load_dj).and_raise(ArgumentError)
481
+ expect(@job.destroy_failed_jobs?).to be false
482
+ end
483
+ end
484
+ end
485
+
486
+ describe 'yaml serialization' do
487
+ context 'when serializing jobs' do
488
+ it 'raises error ArgumentError for new records' do
489
+ story = Story.new(text: 'hello')
490
+ if story.respond_to?(:new_record?)
491
+ expect { story.delay.tell }.to raise_error(
492
+ ArgumentError,
493
+ "job cannot be created for non-persisted record: #{story.inspect}",
494
+ )
495
+ end
496
+ end
497
+
498
+ it 'raises error ArgumentError for destroyed records' do
499
+ story = Story.create(text: 'hello')
500
+ story.destroy
501
+ expect { story.delay.tell }.to raise_error(
502
+ ArgumentError,
503
+ "job cannot be created for non-persisted record: #{story.inspect}",
504
+ )
505
+ end
506
+ end
507
+
508
+ context 'when reload jobs back' do
509
+ it 'reloads changed attributes' do
510
+ story = Story.create(text: 'hello')
511
+ job = story.delay.tell
512
+ story.text = 'goodbye'
513
+ story.save!
514
+ expect(job.reload.payload_object.object.text).to eq('goodbye')
515
+ end
516
+
517
+ it 'raises deserialization error for destroyed records' do
518
+ story = Story.create(text: 'hello')
519
+ job = story.delay.tell
520
+ story.destroy
521
+ expect { job.reload.payload_object }.to raise_error(Delayed::DeserializationError)
522
+ end
523
+ end
524
+ end
525
+
526
+ describe 'worker integration' do
527
+ before do
528
+ described_class.delete_all
529
+ SimpleJob.runs = 0
530
+ end
531
+
532
+ describe 'running a job' do
533
+ it 'fails after Worker.max_run_time' do
534
+ Delayed::Worker.max_run_time = 1.second
535
+ job = described_class.create payload_object: LongRunningJob.new
536
+ worker.run(job)
537
+ expect(job.error).not_to be_nil
538
+ expect(job.reload.last_error).to match(/expired/)
539
+ expect(job.reload.last_error).to match(/Delayed::Worker\.max_run_time is only 1 second/)
540
+ expect(job.attempts).to eq(1)
541
+ end
542
+
543
+ context 'when the job raises a deserialization error' do
544
+ it 'marks the job as failed' do
545
+ job = described_class.create! handler: '--- !ruby/object:JobThatDoesNotExist {}'
546
+ expect_any_instance_of(described_class).to receive(:destroy_failed_jobs?).and_return(false)
547
+ worker.work_off
548
+ job.reload
549
+ expect(job).to be_failed
550
+ end
551
+ end
552
+ end
553
+
554
+ describe 'failed jobs' do
555
+ before do
556
+ @job = described_class.enqueue(ErrorJob.new, run_at: described_class.db_time_now - 1)
557
+ end
558
+
559
+ it 'records last_error when destroy_failed_jobs = false, max_attempts = 1' do
560
+ Delayed::Worker.max_attempts = 1
561
+ worker.run(@job)
562
+ @job.reload
563
+ expect(@job.error).not_to be_nil
564
+ expect(@job.last_error).to match(/did not work/)
565
+ expect(@job.attempts).to eq(1)
566
+ expect(@job).to be_failed
567
+ end
568
+
569
+ it 're-schedules jobs after failing' do
570
+ worker.work_off
571
+ @job.reload
572
+ expect(@job.last_error).to match(/did not work/)
573
+ expect(@job.last_error).to match(/sample_jobs.rb:\d+:in `perform'/)
574
+ expect(@job.attempts).to eq(1)
575
+ expect(@job.run_at).to be > described_class.db_time_now - 10.minutes
576
+ expect(@job.run_at).to be < described_class.db_time_now + 10.minutes
577
+ expect(@job.locked_by).to be_nil
578
+ expect(@job.locked_at).to be_nil
579
+ end
580
+
581
+ it 're-schedules jobs with handler provided time if present' do
582
+ job = described_class.enqueue(CustomRescheduleJob.new(99.minutes))
583
+ worker.run(job)
584
+ job.reload
585
+
586
+ expect((described_class.db_time_now + 99.minutes - job.run_at).abs).to be < 1
587
+ end
588
+
589
+ it "does not fail when the triggered error doesn't have a message" do
590
+ error_with_nil_message = StandardError.new
591
+ expect(error_with_nil_message).to receive(:message).twice.and_return(nil)
592
+ expect(@job).to receive(:invoke_job).and_raise error_with_nil_message
593
+ expect { worker.run(@job) }.not_to raise_error
594
+ end
595
+ end
596
+
597
+ context 'reschedule' do
598
+ before do
599
+ @job = described_class.create payload_object: SimpleJob.new
600
+ end
601
+
602
+ shared_examples_for 'any failure more than Worker.max_attempts times' do
603
+ context "when the job's payload has a #failure hook" do
604
+ before do
605
+ @job = described_class.create payload_object: OnPermanentFailureJob.new
606
+ expect(@job.payload_object).to respond_to(:failure)
607
+ end
608
+
609
+ it 'runs that hook' do
610
+ expect(@job.payload_object).to receive(:failure)
611
+ worker.reschedule(@job)
612
+ end
613
+
614
+ it 'handles error in hook' do
615
+ Delayed::Worker.destroy_failed_jobs = false
616
+ @job.payload_object.raise_error = true
617
+ expect { worker.reschedule(@job) }.not_to raise_error
618
+ expect(@job.failed_at).not_to be_nil
619
+ end
620
+ end
621
+
622
+ context "when the job's payload has no #failure hook" do
623
+ # It's a little tricky to test this in a straightforward way,
624
+ # because putting a not_to receive expectation on
625
+ # @job.payload_object.failure makes that object incorrectly return
626
+ # true to payload_object.respond_to? :failure, which is what
627
+ # reschedule uses to decide whether to call failure. So instead, we
628
+ # just make sure that the payload_object as it already stands doesn't
629
+ # respond_to? failure, then shove it through the iterated reschedule
630
+ # loop and make sure we don't get a NoMethodError (caused by calling
631
+ # that nonexistent failure method).
632
+
633
+ before do
634
+ expect(@job.payload_object).not_to respond_to(:failure)
635
+ end
636
+
637
+ it 'does not try to run that hook' do
638
+ expect {
639
+ Delayed::Worker.max_attempts.times { worker.reschedule(@job) }
640
+ }.not_to raise_exception
641
+ end
642
+ end
643
+ end
644
+
645
+ context 'and we want to destroy jobs' do
646
+ before do
647
+ Delayed::Worker.destroy_failed_jobs = true
648
+ end
649
+
650
+ it_behaves_like 'any failure more than Worker.max_attempts times'
651
+
652
+ it 'is destroyed if it failed more than Worker.max_attempts times' do
653
+ expect(@job).to receive(:destroy)
654
+ Delayed::Worker.max_attempts.times { worker.reschedule(@job) }
655
+ end
656
+
657
+ it 'is destroyed if the job has destroy failed jobs set' do
658
+ Delayed::Worker.destroy_failed_jobs = false
659
+ expect(@job).to receive(:destroy_failed_jobs?).and_return(true)
660
+ expect(@job).to receive(:destroy)
661
+ Delayed::Worker.max_attempts.times { worker.reschedule(@job) }
662
+ end
663
+
664
+ it 'is not destroyed if failed fewer than Worker.max_attempts times' do
665
+ expect(@job).not_to receive(:destroy)
666
+ (Delayed::Worker.max_attempts - 1).times { worker.reschedule(@job) }
667
+ end
668
+ end
669
+
670
+ context "and we don't want to destroy jobs" do
671
+ it_behaves_like 'any failure more than Worker.max_attempts times'
672
+
673
+ context 'and destroy failed jobs is false' do
674
+ it 'is failed if it failed more than Worker.max_attempts times' do
675
+ expect(@job.reload).not_to be_failed
676
+ Delayed::Worker.max_attempts.times { worker.reschedule(@job) }
677
+ expect(@job.reload).to be_failed
678
+ end
679
+
680
+ it 'is not failed if it failed fewer than Worker.max_attempts times' do
681
+ (Delayed::Worker.max_attempts - 1).times { worker.reschedule(@job) }
682
+ expect(@job.reload).not_to be_failed
683
+ end
684
+ end
685
+
686
+ context 'and destroy failed jobs for job is false' do
687
+ before do
688
+ Delayed::Worker.destroy_failed_jobs = true
689
+ end
690
+
691
+ it 'is failed if it failed more than Worker.max_attempts times' do
692
+ expect(@job).to receive(:destroy_failed_jobs?).and_return(false)
693
+ expect(@job.reload).not_to be_failed
694
+ Delayed::Worker.max_attempts.times { worker.reschedule(@job) }
695
+ expect(@job.reload).to be_failed
696
+ end
697
+
698
+ it 'is not failed if it failed fewer than Worker.max_attempts times' do
699
+ (Delayed::Worker.max_attempts - 1).times { worker.reschedule(@job) }
700
+ expect(@job.reload).not_to be_failed
701
+ end
702
+ end
703
+ end
704
+ end
705
+ end
706
+
707
+ describe "reserve_with_scope" do
708
+ let(:relation_class) { described_class.limit(1).class }
709
+ let(:worker) { instance_double(Delayed::Worker, name: "worker01", read_ahead: 1, max_claims: 1) }
710
+ let(:scope) do
711
+ instance_double(relation_class, update_all: nil, limit: [job]).tap do |s|
712
+ allow(s).to receive(:where).and_return(s)
713
+ end
714
+ end
715
+ let(:job) { instance_double(described_class, id: 1, assign_attributes: true, changes_applied: true) }
716
+
717
+ before do
718
+ allow(described_class.connection).to receive(:adapter_name).at_least(:once).and_return(dbms)
719
+ end
720
+
721
+ context "for mysql adapters" do
722
+ let(:dbms) { "MySQL" }
723
+
724
+ it "uses the optimized sql version" do
725
+ allow(described_class).to receive(:reserve_with_scope_using_default_sql)
726
+ described_class.reserve_with_scope(scope, worker, Time.current)
727
+ expect(described_class).not_to have_received(:reserve_with_scope_using_default_sql)
728
+ end
729
+ end
730
+
731
+ context "for a dbms without a specific implementation" do
732
+ let(:dbms) { "OtherDB" }
733
+
734
+ it "uses the plain sql version" do
735
+ allow(described_class).to receive(:reserve_with_scope_using_default_sql)
736
+ described_class.reserve_with_scope(scope, worker, Time.current)
737
+ expect(described_class).to have_received(:reserve_with_scope_using_default_sql).once
738
+ end
739
+ end
740
+ end
741
+
742
+ context "db_time_now" do
743
+ after do
744
+ Time.zone = nil
745
+ ActiveRecord::Base.default_timezone = :local
746
+ end
747
+
748
+ it "returns time in current time zone if set" do
749
+ Time.zone = "Arizona"
750
+ expect(described_class.db_time_now.zone).to eq("MST")
751
+ end
752
+
753
+ it "returns UTC time if that is the AR default" do
754
+ Time.zone = nil
755
+ ActiveRecord::Base.default_timezone = :utc
756
+ expect(described_class.db_time_now.zone).to eq "UTC"
757
+ end
758
+
759
+ it "returns local time if that is the AR default" do
760
+ Time.zone = "Arizona"
761
+ ActiveRecord::Base.default_timezone = :local
762
+ expect(described_class.db_time_now.zone).to eq("MST")
763
+ end
764
+ end
765
+
766
+ context "ActiveRecord::Base.table_name_prefix" do
767
+ it "when prefix is not set, use 'delayed_jobs' as table name" do
768
+ ::ActiveRecord::Base.table_name_prefix = nil
769
+ described_class.set_delayed_job_table_name
770
+
771
+ expect(described_class.table_name).to eq "delayed_jobs"
772
+ end
773
+
774
+ it "when prefix is set, prepend it before default table name" do
775
+ ::ActiveRecord::Base.table_name_prefix = "custom_"
776
+ described_class.set_delayed_job_table_name
777
+
778
+ expect(described_class.table_name).to eq "custom_delayed_jobs"
779
+
780
+ ::ActiveRecord::Base.table_name_prefix = nil
781
+ described_class.set_delayed_job_table_name
782
+ end
783
+ end
784
+
785
+ describe '#age_alert?' do
786
+ let(:now) { described_class.db_time_now }
787
+ let(:run_at) { now - 1.minute }
788
+ let(:locked_at) { nil }
789
+
790
+ around do |example|
791
+ Delayed::Priority.names = { high: 0 }
792
+ Delayed::Priority.alerts = { high: { age: 5.minutes } }
793
+ Timecop.freeze(now) { example.run }
794
+ ensure
795
+ Delayed::Priority.names = nil
796
+ end
797
+
798
+ subject { described_class.enqueue(SimpleJob.new, run_at: run_at, locked_at: locked_at) }
799
+
800
+ it 'returns false' do
801
+ expect(subject.alert_age).to eq(5.minutes)
802
+ expect(subject.age).to be_within(1).of(1.minute)
803
+ expect(subject.age_alert?).to eq(false)
804
+ end
805
+
806
+ context 'when the job is older than specified alert age' do
807
+ let(:run_at) { now - 6.minutes }
808
+
809
+ it 'returns true' do
810
+ expect(subject.alert_age).to eq(5.minutes)
811
+ expect(subject.age).to be_within(1).of(6.minutes)
812
+ expect(subject.age_alert?).to eq(true)
813
+ end
814
+ end
815
+
816
+ context 'when the job has been running for a long time but was picked up quickly' do
817
+ let(:run_at) { now - 1.hour - 1.minute }
818
+ let(:locked_at) { now - 1.hour }
819
+
820
+ it 'returns false' do
821
+ expect(subject.alert_age).to eq 5.minutes
822
+ expect(subject.age).to be_within(1).of(1.minute)
823
+ expect(subject.age_alert?).to eq(false)
824
+ end
825
+ end
826
+
827
+ context 'when the job class defines an alert_age override' do
828
+ before do
829
+ stub_const('JobWithAlertAge', Struct.new(:perform) do
830
+ def alert_age
831
+ 30.seconds
832
+ end
833
+ end)
834
+ end
835
+
836
+ subject { described_class.enqueue(JobWithAlertAge.new, run_at: run_at, locked_at: locked_at) }
837
+
838
+ it 'obeys the override' do
839
+ expect(subject.alert_age).to eq(30.seconds)
840
+ expect(subject.age).to be_within(1).of(1.minute)
841
+ expect(subject.age_alert?).to eq(true)
842
+ end
843
+ end
844
+ end
845
+
846
+ describe '#run_time_alert?' do
847
+ let(:now) { described_class.db_time_now }
848
+ let(:locked_at) { now - 1.minute }
849
+
850
+ around do |example|
851
+ Delayed::Priority.names = { high: 0 }
852
+ Delayed::Priority.alerts = { high: { run_time: 5.minutes } }
853
+ Timecop.freeze(now) { example.run }
854
+ ensure
855
+ Delayed::Priority.names = nil
856
+ end
857
+
858
+ subject { described_class.enqueue(SimpleJob.new, locked_at: locked_at) }
859
+
860
+ it 'returns false' do
861
+ expect(subject.alert_run_time).to eq(5.minutes)
862
+ expect(subject.run_time).to be_within(1).of(1.minute)
863
+ expect(subject.run_time_alert?).to eq(false)
864
+ end
865
+
866
+ context 'when the job is not locked (e.g. delay_jobs is false)' do
867
+ let(:locked_at) { nil }
868
+
869
+ it 'returns nil' do
870
+ expect(subject.alert_run_time).to eq(5.minutes)
871
+ expect(subject.run_time).to be_nil
872
+ expect(subject.run_time_alert?).to be_nil
873
+ end
874
+ end
875
+
876
+ context 'when the job has been running longer than specified alert run_time' do
877
+ let(:locked_at) { now - 6.minutes }
878
+
879
+ it 'returns true' do
880
+ expect(subject.alert_run_time).to eq(5.minutes)
881
+ expect(subject.run_time).to be_within(1).of(6.minutes)
882
+ expect(subject.run_time_alert?).to eq(true)
883
+ end
884
+ end
885
+
886
+ context 'when the job class defines an alert_run_time override' do
887
+ before do
888
+ stub_const('JobWithAlertRunTime', Struct.new(:perform) do
889
+ def alert_run_time
890
+ 30.seconds
891
+ end
892
+ end)
893
+ end
894
+
895
+ subject { described_class.enqueue(JobWithAlertRunTime.new, locked_at: locked_at) }
896
+
897
+ it 'obeys the override' do
898
+ expect(subject.alert_run_time).to eq(30.seconds)
899
+ expect(subject.run_time).to be_within(1).of(1.minute)
900
+ expect(subject.run_time_alert?).to eq(true)
901
+ end
902
+ end
903
+ end
904
+
905
+ describe '#attempts_alert?' do
906
+ let(:now) { described_class.db_time_now }
907
+ let(:attempts) { 1 }
908
+
909
+ around do |example|
910
+ Delayed::Priority.names = { high: 0 }
911
+ Delayed::Priority.alerts = { high: { attempts: 5 } }
912
+ Timecop.freeze(now) { example.run }
913
+ ensure
914
+ Delayed::Priority.names = nil
915
+ end
916
+
917
+ subject { described_class.enqueue(SimpleJob.new, attempts: attempts) }
918
+
919
+ it 'returns false' do
920
+ expect(subject.alert_attempts).to eq 5
921
+ expect(subject.attempts).to eq 1
922
+ expect(subject.attempts_alert?).to eq false
923
+ end
924
+
925
+ context 'when the job reaches the specified alert attempts' do
926
+ let(:attempts) { 6 }
927
+
928
+ it 'returns true' do
929
+ expect(subject.alert_attempts).to eq 5
930
+ expect(subject.attempts).to eq 6
931
+ expect(subject.attempts_alert?).to eq true
932
+ end
933
+ end
934
+
935
+ context 'when the job class defines an alert_attempts override' do
936
+ before do
937
+ stub_const('JobWithAlertAttempts', Struct.new(:perform) do
938
+ def alert_attempts
939
+ 1
940
+ end
941
+ end)
942
+ end
943
+
944
+ subject { described_class.enqueue(JobWithAlertAttempts.new, attempts: attempts) }
945
+
946
+ it 'obeys the override' do
947
+ expect(subject.alert_attempts).to eq 1
948
+ expect(subject.attempts).to eq 1
949
+ expect(subject.attempts_alert?).to eq true
950
+ end
951
+ end
952
+ end
953
+ end