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