delayed 2.1.0 → 2.2.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.
@@ -13,6 +13,7 @@ describe Delayed::Job do
13
13
  Delayed::Worker.max_claims = 1 # disable multithreading because SimpleJob is not threadsafe
14
14
  Delayed::Worker.default_priority = 99
15
15
  Delayed::Worker.delay_jobs = true
16
+ Delayed::Worker.deny_stale_enqueues = false
16
17
  Delayed::Worker.default_queue_name = 'default_tracking'
17
18
  SimpleJob.runs = 0
18
19
  described_class.delete_all
@@ -133,6 +134,42 @@ describe Delayed::Job do
133
134
  expect(described_class.enqueue(SimpleJob.new)).to be_instance_of(described_class)
134
135
  end
135
136
  end
137
+
138
+ context 'with deny_stale_enqueues = true' do
139
+ before { Delayed::Worker.deny_stale_enqueues = true }
140
+
141
+ it 'raises StaleEnqueueError when run_at is beyond lock_timeout in the past' do
142
+ stale_time = described_class.db_time_now - described_class.lock_timeout - 1.minute
143
+ expect {
144
+ described_class.enqueue SimpleJob.new, run_at: stale_time
145
+ }.to raise_error(Delayed::StaleEnqueueError, /Cannot enqueue a job in the distant past/)
146
+ end
147
+
148
+ it 'allows run_at within lock_timeout of now' do
149
+ recent_past = described_class.db_time_now - described_class.lock_timeout + 1.minute
150
+ job = described_class.enqueue SimpleJob.new, run_at: recent_past
151
+ expect(job).to be_persisted
152
+ end
153
+
154
+ it 'allows run_at in the future' do
155
+ future = described_class.db_time_now + 5.minutes
156
+ job = described_class.enqueue SimpleJob.new, run_at: future
157
+ expect(job).to be_persisted
158
+ end
159
+
160
+ it 'allows enqueue without run_at' do
161
+ job = described_class.enqueue SimpleJob.new
162
+ expect(job).to be_persisted
163
+ end
164
+ end
165
+
166
+ context 'with deny_stale_enqueues = false (default)' do
167
+ it 'allows run_at far in the past' do
168
+ stale_time = described_class.db_time_now - 1.day
169
+ job = described_class.enqueue SimpleJob.new, run_at: stale_time
170
+ expect(job).to be_persisted
171
+ end
172
+ end
136
173
  end
137
174
 
138
175
  describe 'callbacks' do
@@ -345,7 +345,7 @@ RSpec.describe Delayed::Monitor do
345
345
  end
346
346
  end
347
347
 
348
- describe '#query_for' do
348
+ describe 'SQL' do
349
349
  let(:monitor) { described_class.new }
350
350
  let(:queries) { [] }
351
351
  let(:now) { '2025-11-10 17:20:13 UTC' }
@@ -360,30 +360,28 @@ RSpec.describe Delayed::Monitor do
360
360
  value = value.value if value.is_a?(ActiveModel::Attribute)
361
361
  sql = sql.sub(/(\?|\$\d)/, ActiveRecord::Base.connection.quote(value))
362
362
  end
363
- queries << sql
363
+ queries << QueryUnderTest.for(sql)
364
+ queries << "---"
364
365
  end
365
366
  end
366
367
 
367
- def queries_for(metric)
368
- monitor.query_for(metric)
369
- queries.map { |q| QueryUnderTest.for(q) }
368
+ def query_descriptions
369
+ described_class::METRICS.each do |metric|
370
+ queries << "-- QUERIES FOR `#{metric}`:"
371
+ queries << "---------------------------------"
372
+ monitor.query_for(metric)
373
+ queries << "-- (no new queries)" unless queries.last == '---'
374
+ end
375
+ queries.dup.map { |query| query.try(:full_description) || query }
370
376
  end
371
377
 
372
- described_class::METRICS.each do |metric|
373
- context "('#{metric}')" do
374
- it "runs the expected #{current_adapter} query for #{metric}" do
375
- expect(queries_for(metric).map(&:formatted).join("\n")).to match_snapshot
376
- end
377
-
378
- it "produces the expected #{current_adapter} query plan for #{metric}" do
379
- expect(queries_for(metric).map(&:explain).join("\n")).to match_snapshot
380
- end
378
+ it "runs the expected #{current_adapter} queries with the expected plans" do
379
+ expect(query_descriptions.join("\n")).to match_snapshot
380
+ end
381
381
 
382
- context 'when using the legacy index', :with_legacy_table_index do
383
- it "[legacy index] produces the expected #{current_adapter} query plan for #{metric}" do
384
- expect(queries_for(metric).map(&:explain).join("\n")).to match_snapshot
385
- end
386
- end
382
+ context 'when using the legacy index', :with_legacy_table_index do
383
+ it "[legacy index] runs the expected #{current_adapter} queries with the expected plans" do
384
+ expect(query_descriptions.join("\n")).to match_snapshot
387
385
  end
388
386
  end
389
387
  end
data/spec/helper.rb CHANGED
@@ -228,10 +228,7 @@ RSpec::Matchers.define :emit_notification do |expected_event_name|
228
228
  diffable
229
229
 
230
230
  match do |block|
231
- if @approximately && current_adapter != 'postgresql'
232
- @expected_value = a_value_within([2, @expected_value.abs * 0.05].max).of(@expected_value)
233
- end
234
-
231
+ @expected_value = a_value_within([2, @expected_value.abs * 0.05].max).of(@expected_value) if @approximately
235
232
  @expected = { event_name: expected_event_name, payload: expected_payload, value: @expected_value }
236
233
  @actuals = []
237
234
  callback = ->(name, _started, _finished, _unique_id, payload) do
@@ -274,6 +271,10 @@ QueryUnderTest = Struct.new(:sql, :connection) do
274
271
  new(query.respond_to?(:to_sql) ? query.to_sql : query.to_s, connection)
275
272
  end
276
273
 
274
+ def full_description
275
+ [formatted, explain].join("\n\n")
276
+ end
277
+
277
278
  def formatted
278
279
  fmt = sql.squish
279
280
 
@@ -287,6 +288,8 @@ QueryUnderTest = Struct.new(:sql, :connection) do
287
288
  .gsub(/ (AND|OR) /) { "\n #{Regexp.last_match(1).strip} " }
288
289
  # normalize and truncate 'AS' names/aliases (changes across Rails versions)
289
290
  .gsub(/AS ("|`)?(\w+)("|`)?/) { "AS #{Regexp.last_match(2)[0...63]}" }
291
+ # newline and indent when aliased columns are listed
292
+ .gsub(/AS (\w+),/) { "AS #{Regexp.last_match(1)},\n " }
290
293
  # remove quotes around column names in aggregate functions
291
294
  .gsub(/(MIN|MAX|COUNT|SUM)\(("|`)(\w+)("|`)\)/) { "#{Regexp.last_match(1)}(#{Regexp.last_match(3)})" }
292
295
  end
@@ -311,7 +314,7 @@ QueryUnderTest = Struct.new(:sql, :connection) do
311
314
  end
312
315
 
313
316
  def mysql2_explain
314
- seed_rows! # MySQL needs a bit of data to reach for indexes
317
+ seed_rows! if Delayed::Job.none? # MySQL needs a bit of data to reach for indexes
315
318
  connection.execute("ANALYZE TABLE #{Delayed::Job.table_name}")
316
319
  connection.execute("SET SESSION max_seeks_for_key = 1")
317
320
  connection.execute("EXPLAIN FORMAT=TREE #{sql}").to_a.map(&:first).join("\n")
@@ -325,7 +328,7 @@ QueryUnderTest = Struct.new(:sql, :connection) do
325
328
 
326
329
  def seed_rows!
327
330
  now = Delayed::Job.db_time_now
328
- 10.times do
331
+ 100.times do
329
332
  [true, false].repeated_combination(5).each_with_index do |(erroring, failed, locked, future), i|
330
333
  Delayed::Job.create!(
331
334
  run_at: now + (future ? i.minutes : -i.minutes),
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: 2.1.0
4
+ version: 2.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nathan Griffith
@@ -19,7 +19,7 @@ authors:
19
19
  autorequire:
20
20
  bindir: bin
21
21
  cert_chain: []
22
- date: 2026-02-05 00:00:00.000000000 Z
22
+ date: 2026-02-12 00:00:00.000000000 Z
23
23
  dependencies:
24
24
  - !ruby/object:Gem::Dependency
25
25
  name: activerecord