delayed 3.1.0 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +158 -0
- data/app/models/delayed/limit.rb +188 -0
- data/db/migrate/10_create_delayed_limits.rb +30 -0
- data/lib/delayed/active_job_adapter.rb +1 -1
- data/lib/delayed/backend/base.rb +9 -10
- data/lib/delayed/backend/job_preparer.rb +6 -0
- data/lib/delayed/job_wrapper.rb +46 -3
- data/lib/delayed/limitable.rb +83 -0
- data/lib/delayed/version.rb +1 -1
- data/lib/delayed.rb +3 -0
- data/spec/delayed/active_job_adapter_spec.rb +306 -2
- data/spec/delayed/job_spec.rb +62 -2
- data/spec/delayed/limit_spec.rb +182 -0
- data/spec/delayed/limitable_spec.rb +303 -0
- data/spec/helper.rb +7 -2
- metadata +24 -6
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'helper'
|
|
4
|
+
|
|
5
|
+
RSpec.describe Delayed::Limitable do
|
|
6
|
+
let(:purpose) { :limitable_spec }
|
|
7
|
+
|
|
8
|
+
around do |example|
|
|
9
|
+
original_limits = Delayed::Limit.limits
|
|
10
|
+
example.run
|
|
11
|
+
ensure
|
|
12
|
+
Delayed::Limit.instance_variable_set(:@limits, original_limits)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
before do
|
|
16
|
+
allow(Delayed::Limit).to receive(:within_limit).and_yield
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
it 'is automatically included in ActiveJob classes' do
|
|
20
|
+
expect(ActiveJob::Base).to respond_to(:with_limit)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
describe '.with_limit' do
|
|
24
|
+
context 'when on: is not specified' do
|
|
25
|
+
let(:job_class) do
|
|
26
|
+
p = purpose
|
|
27
|
+
Class.new(ActiveJob::Base) do
|
|
28
|
+
with_limit p, max: 4, per: 1.second
|
|
29
|
+
|
|
30
|
+
define_method(:perform) { :result }
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
it 'wraps perform with within_limit' do
|
|
35
|
+
job_class.new.perform
|
|
36
|
+
expect(Delayed::Limit).to have_received(:within_limit)
|
|
37
|
+
.with(purpose, wait_timeout: 5.seconds).once
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
it 'registers the limit policy with Delayed::Limit' do
|
|
41
|
+
job_class
|
|
42
|
+
expect(Delayed::Limit.limits.fetch(purpose)).to eq(max: 4, per: 1.second)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
context 'when purpose is not specified' do
|
|
47
|
+
let(:job_class) do
|
|
48
|
+
Class.new(ActiveJob::Base) do
|
|
49
|
+
define_method(:perform) { :result }
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
before do
|
|
54
|
+
stub_const('LimitableDefaultPurposeJob', job_class)
|
|
55
|
+
job_class.with_limit(max: 4, per: 1.second)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
it 'defaults the purpose to the underscored class name' do
|
|
59
|
+
job_class.new.perform
|
|
60
|
+
expect(Delayed::Limit).to have_received(:within_limit)
|
|
61
|
+
.with(:limitable_default_purpose_job, wait_timeout: 5.seconds).once
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
context 'when wait_timeout: is specified' do
|
|
66
|
+
let(:job_class) do
|
|
67
|
+
p = purpose
|
|
68
|
+
Class.new(ActiveJob::Base) do
|
|
69
|
+
with_limit p, max: 4, per: 1.second, wait_timeout: 30.seconds
|
|
70
|
+
|
|
71
|
+
define_method(:perform) { :result }
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
it 'passes the wait_timeout through to within_limit' do
|
|
76
|
+
job_class.new.perform
|
|
77
|
+
expect(Delayed::Limit).to have_received(:within_limit)
|
|
78
|
+
.with(purpose, wait_timeout: 30.seconds).once
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
context 'when on: is a single method name' do
|
|
83
|
+
let(:job_class) do
|
|
84
|
+
p = purpose
|
|
85
|
+
Class.new(ActiveJob::Base) do
|
|
86
|
+
with_limit p, max: 4, per: 1.second, on: :do_work
|
|
87
|
+
|
|
88
|
+
define_method(:perform) { do_work && dont_do_work }
|
|
89
|
+
define_method(:do_work) { :result_b }
|
|
90
|
+
define_method(:dont_do_work) { :result_a }
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
it 'wraps the specified method with within_limit' do
|
|
95
|
+
job_class.new.do_work
|
|
96
|
+
expect(Delayed::Limit).to have_received(:within_limit)
|
|
97
|
+
.with(purpose, wait_timeout: 5.seconds).once
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
it 'does not add an extra within_limit call when perform delegates to the wrapped method' do
|
|
101
|
+
job_class.new.perform
|
|
102
|
+
expect(Delayed::Limit).to have_received(:within_limit).once
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
context 'when on: is an array of method names' do
|
|
107
|
+
let(:job_class) do
|
|
108
|
+
p = purpose
|
|
109
|
+
Class.new(ActiveJob::Base) do
|
|
110
|
+
with_limit p, max: 4, per: 1.second, on: %i(do_work_a do_work_b)
|
|
111
|
+
|
|
112
|
+
def perform; end
|
|
113
|
+
define_method(:do_work_a) { :result_a }
|
|
114
|
+
define_method(:do_work_b) { :result_b }
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
it 'wraps each named method with within_limit' do
|
|
119
|
+
job_class.new.tap do |job|
|
|
120
|
+
job.perform
|
|
121
|
+
job.do_work_a
|
|
122
|
+
job.do_work_b
|
|
123
|
+
end
|
|
124
|
+
expect(Delayed::Limit).to have_received(:within_limit)
|
|
125
|
+
.with(purpose, wait_timeout: 5.seconds).twice
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
context 'when the purpose was registered in advance and no config is given' do
|
|
130
|
+
before { Delayed::Limit.register!(purpose, max: 4, per: 1.second) }
|
|
131
|
+
|
|
132
|
+
let(:job_class) do
|
|
133
|
+
p = purpose
|
|
134
|
+
Class.new(ActiveJob::Base) do
|
|
135
|
+
with_limit p
|
|
136
|
+
|
|
137
|
+
define_method(:perform) { :result }
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
it 'references the registered limit by purpose' do
|
|
142
|
+
job_class.new.perform
|
|
143
|
+
expect(Delayed::Limit).to have_received(:within_limit)
|
|
144
|
+
.with(purpose, wait_timeout: 5.seconds).once
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
context 'when the config contains an unknown key' do
|
|
149
|
+
it 'raises ArgumentError' do
|
|
150
|
+
p = purpose
|
|
151
|
+
expect {
|
|
152
|
+
Class.new(ActiveJob::Base) { with_limit p, max: 4, per: 1.second, wait_timout: 1.second }
|
|
153
|
+
}.to raise_error(ArgumentError, /wait_timout/)
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
context 'when with_limit is declared multiple times' do
|
|
158
|
+
let(:other_purpose) { :"#{purpose}_b" }
|
|
159
|
+
|
|
160
|
+
let(:job_class) do
|
|
161
|
+
p = purpose
|
|
162
|
+
other = other_purpose
|
|
163
|
+
Class.new(ActiveJob::Base) do
|
|
164
|
+
with_limit p, max: 4, per: 1.second, wait_timeout: 100.seconds, on: :do_work_a, retry_jitter: 0
|
|
165
|
+
with_limit other, max: 2, per: 1.minute, wait_timeout: 30.seconds, on: :do_work_b, retry_jitter: 0
|
|
166
|
+
|
|
167
|
+
define_method(:perform) { raise Delayed::Limit::LimitExceededError }
|
|
168
|
+
define_method(:do_work_a) { :result_a }
|
|
169
|
+
define_method(:do_work_b) { :result_b }
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
before { stub_const('LimitableMultipleLimitsJob', job_class) }
|
|
174
|
+
|
|
175
|
+
it 'registers each limit policy' do
|
|
176
|
+
job_class
|
|
177
|
+
expect(Delayed::Limit.limits.fetch(purpose)).to eq(max: 4, per: 1.second)
|
|
178
|
+
expect(Delayed::Limit.limits.fetch(other_purpose)).to eq(max: 2, per: 1.minute)
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
it 'wraps each method with its own limit' do
|
|
182
|
+
job_class.new.tap do |job|
|
|
183
|
+
job.do_work_a
|
|
184
|
+
job.do_work_b
|
|
185
|
+
end
|
|
186
|
+
expect(Delayed::Limit).to have_received(:within_limit)
|
|
187
|
+
.with(purpose, wait_timeout: 100.seconds).once
|
|
188
|
+
expect(Delayed::Limit).to have_received(:within_limit)
|
|
189
|
+
.with(other_purpose, wait_timeout: 30.seconds).once
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
it 'defines only a single retry handler' do
|
|
193
|
+
handlers = job_class.rescue_handlers.select do |class_name, _|
|
|
194
|
+
class_name == Delayed::Limit::LimitExceededError.name
|
|
195
|
+
end
|
|
196
|
+
expect(handlers.size).to eq(1)
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
it 'retries according to the first declaration (its wait_timeout floors the retry wait)' do
|
|
200
|
+
job = job_class.new
|
|
201
|
+
allow(job).to receive(:retry_job)
|
|
202
|
+
job.perform_now
|
|
203
|
+
expect(job).to have_received(:retry_job)
|
|
204
|
+
.with(a_hash_including(wait: 100.seconds))
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
context 'when included in a plain (non-ActiveJob) class' do
|
|
209
|
+
let(:klass) do
|
|
210
|
+
p = purpose
|
|
211
|
+
Class.new do
|
|
212
|
+
include Delayed::Limitable
|
|
213
|
+
|
|
214
|
+
with_limit p, max: 4, per: 1.second, on: :do_work
|
|
215
|
+
|
|
216
|
+
define_method(:do_work) { :result }
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
it 'wraps the specified method with within_limit' do
|
|
221
|
+
expect(klass.new.do_work).to eq(:result)
|
|
222
|
+
expect(Delayed::Limit).to have_received(:within_limit)
|
|
223
|
+
.with(purpose, wait_timeout: 5.seconds).once
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
it 'registers the limit policy with Delayed::Limit' do
|
|
227
|
+
klass
|
|
228
|
+
expect(Delayed::Limit.limits.fetch(purpose)).to eq(max: 4, per: 1.second)
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
it 'leaves LimitExceededError handling to the caller' do
|
|
232
|
+
allow(Delayed::Limit).to receive(:within_limit).and_raise(Delayed::Limit::LimitExceededError)
|
|
233
|
+
expect { klass.new.do_work }.to raise_error(Delayed::Limit::LimitExceededError)
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
describe 'retry behavior' do
|
|
239
|
+
let(:job_class) do
|
|
240
|
+
p = purpose
|
|
241
|
+
Class.new(ActiveJob::Base) do
|
|
242
|
+
with_limit p, max: 4, per: 1.second, retry_jitter: 0
|
|
243
|
+
|
|
244
|
+
define_method(:perform) { raise Delayed::Limit::LimitExceededError }
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
before { stub_const('LimitableRetryJob', job_class) }
|
|
249
|
+
|
|
250
|
+
it 'retries the job when the limit is exceeded' do
|
|
251
|
+
job = job_class.new
|
|
252
|
+
allow(job).to receive(:retry_job)
|
|
253
|
+
job.perform_now
|
|
254
|
+
expect(job).to have_received(:retry_job)
|
|
255
|
+
.with(a_hash_including(wait: a_value > 0, error: an_instance_of(Delayed::Limit::LimitExceededError)))
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
it 'floors the retry wait at the wait_timeout' do
|
|
259
|
+
job = job_class.new
|
|
260
|
+
allow(job).to receive(:retry_job)
|
|
261
|
+
job.perform_now
|
|
262
|
+
expect(job).to have_received(:retry_job)
|
|
263
|
+
.with(a_hash_including(wait: 5.seconds))
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
context 'when a custom wait: is specified' do
|
|
267
|
+
let(:job_class) do
|
|
268
|
+
p = purpose
|
|
269
|
+
Class.new(ActiveJob::Base) do
|
|
270
|
+
with_limit p, max: 4, per: 1.second, retry_wait: ->(_attempts) { 123 }
|
|
271
|
+
|
|
272
|
+
define_method(:perform) { raise Delayed::Limit::LimitExceededError }
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
it 'uses the custom wait' do
|
|
277
|
+
job = job_class.new
|
|
278
|
+
allow(job).to receive(:retry_job)
|
|
279
|
+
job.perform_now
|
|
280
|
+
expect(job).to have_received(:retry_job)
|
|
281
|
+
.with(a_hash_including(wait: 123))
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
context 'when retry_attempts: is specified' do
|
|
286
|
+
let(:job_class) do
|
|
287
|
+
p = purpose
|
|
288
|
+
Class.new(ActiveJob::Base) do
|
|
289
|
+
with_limit p, max: 4, per: 1.second, retry_attempts: 1
|
|
290
|
+
|
|
291
|
+
define_method(:perform) { raise Delayed::Limit::LimitExceededError }
|
|
292
|
+
end
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
it 'stops retrying once the attempts are exhausted' do
|
|
296
|
+
job = job_class.new
|
|
297
|
+
allow(job).to receive(:retry_job)
|
|
298
|
+
expect { job.perform_now }.to raise_error(Delayed::Limit::LimitExceededError)
|
|
299
|
+
expect(job).not_to have_received(:retry_job)
|
|
300
|
+
end
|
|
301
|
+
end
|
|
302
|
+
end
|
|
303
|
+
end
|
data/spec/helper.rb
CHANGED
|
@@ -73,8 +73,10 @@ Dir['db/migrate/*.rb'].each { |f| require_relative("../#{f}") }
|
|
|
73
73
|
ActiveRecord::Schema.define do
|
|
74
74
|
if ActiveRecord::VERSION::MAJOR >= 7
|
|
75
75
|
drop_table :delayed_jobs, if_exists: true
|
|
76
|
-
|
|
77
|
-
|
|
76
|
+
drop_table :delayed_limits, if_exists: true
|
|
77
|
+
else
|
|
78
|
+
drop_table :delayed_jobs if ActiveRecord::Base.connection.table_exists?(:delayed_jobs)
|
|
79
|
+
drop_table :delayed_limits if ActiveRecord::Base.connection.table_exists?(:delayed_limits)
|
|
78
80
|
end
|
|
79
81
|
|
|
80
82
|
# Let's prove reversibility when we set up our test DB:
|
|
@@ -94,6 +96,7 @@ ActiveRecord::Schema.define do
|
|
|
94
96
|
run_migration(RemoveLegacyIndex)
|
|
95
97
|
run_migration(AddRunAtAndNameNotNullCheck)
|
|
96
98
|
run_migration(ValidateRunAtAndNameNotNull)
|
|
99
|
+
run_migration(CreateDelayedLimits)
|
|
97
100
|
|
|
98
101
|
# Test that these index migrations can be re-applied idempotently.
|
|
99
102
|
# (In case identical indexes had been manually applied previously.)
|
|
@@ -296,6 +299,8 @@ QueryUnderTest = Struct.new(:sql, :connection) do
|
|
|
296
299
|
.gsub(/AS ("|`)?(\w+)("|`)?/) { "AS #{Regexp.last_match(2)[0...63]}" }
|
|
297
300
|
# newline and indent when aliased columns are listed
|
|
298
301
|
.gsub(/AS (\w+),/) { "AS #{Regexp.last_match(1)},\n " }
|
|
302
|
+
# normalize 'FROM (...) AS' subqueries (changes across Rails versions)
|
|
303
|
+
.gsub(") AS subquery") { ") subquery" }
|
|
299
304
|
# remove quotes around column names in aggregate functions
|
|
300
305
|
.gsub(/(MIN|MAX|COUNT|SUM)\(("|`)(\w+)("|`)\)/) { "#{Regexp.last_match(1)}(#{Regexp.last_match(3)})" }
|
|
301
306
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: delayed
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version:
|
|
4
|
+
version: 4.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Nathan Griffith
|
|
@@ -16,10 +16,9 @@ authors:
|
|
|
16
16
|
- Matt Griffin
|
|
17
17
|
- Steve Richert
|
|
18
18
|
- Tobias Lütke
|
|
19
|
-
autorequire:
|
|
20
19
|
bindir: bin
|
|
21
20
|
cert_chain: []
|
|
22
|
-
date:
|
|
21
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
23
22
|
dependencies:
|
|
24
23
|
- !ruby/object:Gem::Dependency
|
|
25
24
|
name: activerecord
|
|
@@ -35,6 +34,20 @@ dependencies:
|
|
|
35
34
|
- - ">="
|
|
36
35
|
- !ruby/object:Gem::Version
|
|
37
36
|
version: '6.0'
|
|
37
|
+
- !ruby/object:Gem::Dependency
|
|
38
|
+
name: benchmark
|
|
39
|
+
requirement: !ruby/object:Gem::Requirement
|
|
40
|
+
requirements:
|
|
41
|
+
- - ">="
|
|
42
|
+
- !ruby/object:Gem::Version
|
|
43
|
+
version: '0'
|
|
44
|
+
type: :runtime
|
|
45
|
+
prerelease: false
|
|
46
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
47
|
+
requirements:
|
|
48
|
+
- - ">="
|
|
49
|
+
- !ruby/object:Gem::Version
|
|
50
|
+
version: '0'
|
|
38
51
|
- !ruby/object:Gem::Dependency
|
|
39
52
|
name: concurrent-ruby
|
|
40
53
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -66,6 +79,7 @@ files:
|
|
|
66
79
|
- README.md
|
|
67
80
|
- Rakefile
|
|
68
81
|
- app/models/delayed/job.rb
|
|
82
|
+
- app/models/delayed/limit.rb
|
|
69
83
|
- db/migrate/01_create_delayed_jobs.rb
|
|
70
84
|
- db/migrate/02_add_name_to_delayed_jobs.rb
|
|
71
85
|
- db/migrate/03_add_index_to_delayed_jobs_name.rb
|
|
@@ -75,6 +89,7 @@ files:
|
|
|
75
89
|
- db/migrate/07_remove_legacy_index.rb
|
|
76
90
|
- db/migrate/08_add_run_at_and_name_not_null_check.rb
|
|
77
91
|
- db/migrate/09_validate_run_at_and_name_not_null.rb
|
|
92
|
+
- db/migrate/10_create_delayed_limits.rb
|
|
78
93
|
- lib/delayed.rb
|
|
79
94
|
- lib/delayed/active_job_adapter.rb
|
|
80
95
|
- lib/delayed/backend/base.rb
|
|
@@ -84,6 +99,7 @@ files:
|
|
|
84
99
|
- lib/delayed/helpers/migration.rb
|
|
85
100
|
- lib/delayed/job_wrapper.rb
|
|
86
101
|
- lib/delayed/lifecycle.rb
|
|
102
|
+
- lib/delayed/limitable.rb
|
|
87
103
|
- lib/delayed/message_sending.rb
|
|
88
104
|
- lib/delayed/monitor.rb
|
|
89
105
|
- lib/delayed/performable_mailer.rb
|
|
@@ -112,6 +128,8 @@ files:
|
|
|
112
128
|
- spec/delayed/active_job_adapter_spec.rb
|
|
113
129
|
- spec/delayed/helpers/migration_spec.rb
|
|
114
130
|
- spec/delayed/job_spec.rb
|
|
131
|
+
- spec/delayed/limit_spec.rb
|
|
132
|
+
- spec/delayed/limitable_spec.rb
|
|
115
133
|
- spec/delayed/monitor_spec.rb
|
|
116
134
|
- spec/delayed/plugins/instrumentation_spec.rb
|
|
117
135
|
- spec/delayed/priority_spec.rb
|
|
@@ -134,7 +152,6 @@ metadata:
|
|
|
134
152
|
bug_tracker_uri: https://github.com/betterment/delayed/issues
|
|
135
153
|
source_code_uri: https://github.com/betterment/delayed
|
|
136
154
|
rubygems_mfa_required: 'true'
|
|
137
|
-
post_install_message:
|
|
138
155
|
rdoc_options: []
|
|
139
156
|
require_paths:
|
|
140
157
|
- lib
|
|
@@ -149,8 +166,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
149
166
|
- !ruby/object:Gem::Version
|
|
150
167
|
version: '0'
|
|
151
168
|
requirements: []
|
|
152
|
-
rubygems_version:
|
|
153
|
-
signing_key:
|
|
169
|
+
rubygems_version: 4.0.16
|
|
154
170
|
specification_version: 4
|
|
155
171
|
summary: a multi-threaded, SQL-driven ActiveJob backend used at Betterment to process
|
|
156
172
|
millions of background jobs per day
|
|
@@ -165,6 +181,8 @@ test_files:
|
|
|
165
181
|
- spec/delayed/active_job_adapter_spec.rb
|
|
166
182
|
- spec/delayed/helpers/migration_spec.rb
|
|
167
183
|
- spec/delayed/job_spec.rb
|
|
184
|
+
- spec/delayed/limit_spec.rb
|
|
185
|
+
- spec/delayed/limitable_spec.rb
|
|
168
186
|
- spec/delayed/monitor_spec.rb
|
|
169
187
|
- spec/delayed/plugins/instrumentation_spec.rb
|
|
170
188
|
- spec/delayed/priority_spec.rb
|