delayed 2.0.3 → 2.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4227961278ab7f1ed435e8357cbf83998ee6d1d597f981fc4d1ab92a0d1069fa
4
- data.tar.gz: fd108cb072de3ec495d2713326821b231cfe8ffa153782a80f84da06d272ecfa
3
+ metadata.gz: 984a910472fc4f1dedafcf4060cf076bcda408196bfa80f08039ed2527533a41
4
+ data.tar.gz: 98f7de42e2964ee1576ba6ae3fc2b39cbe76a841263054a5b9634f7e55ce1114
5
5
  SHA512:
6
- metadata.gz: 36fd83967d57e3ae7d58a2edfceb790d35bcf3f9a49a9bf3de9e34c15528ba2c3ae36468c7847e8e49d7d90ca8b03ef1e39a62620257fb06cb8f321a5af0909e
7
- data.tar.gz: df0443f6ca1f859e5b4155a0b38aa72310e0607f57c30ba31dcd0fda39825e0ae81903d0b019886c7405e607d1817f3733c47fbfed81b25cb897440cb2be8da8
6
+ metadata.gz: dcb018c429ff591409796e410c570f49408eb0b8363759f6c8970ac4ee1773abccc117b5398c3596ecf8491c0e70db548bd4a2ea54ef39a77fa78da43b6cbfe4
7
+ data.tar.gz: 020a785c7bfc81456b8cc7b8ddace55fad1f9468edb3b9cef0ef066eb6aed64b580d30974b48ce97bc7b1602294c9901c7c26fa294d3e4b4ecebd177aef677c8
@@ -35,6 +35,33 @@ module Delayed
35
35
  send(:"#{metric}_grouped")
36
36
  end
37
37
 
38
+ def self.sql_now_in_utc
39
+ case ActiveRecord::Base.connection.adapter_name
40
+ when 'PostgreSQL'
41
+ "TIMEZONE('UTC', NOW())"
42
+ when 'MySQL', 'Mysql2'
43
+ "UTC_TIMESTAMP()"
44
+ else
45
+ "CURRENT_TIMESTAMP"
46
+ end
47
+ end
48
+
49
+ def self.parse_utc_time(string)
50
+ # Depending on Rails version & DB adapter, this will be either a String or a DateTime.
51
+ # If it's a DateTime, and if connection is running with the `:local` time zone config,
52
+ # then by default Rails incorrectly assumes it's in local time instead of UTC.
53
+ # We use `strftime` to strip the encoded TZ info and re-parse it as UTC.
54
+ #
55
+ # Example:
56
+ # - "2026-02-05 10:01:23" -> DB-returned string
57
+ # - "2026-02-05 10:01:23 -0600" -> Rails-parsed DateTime with incorrect TZ
58
+ # - "2026-02-05 10:01:23" -> `strftime` output
59
+ # - "2026-02-05 04:01:23 -0600" -> Re-parsed as UTC and converted to local time
60
+ string = string.strftime('%Y-%m-%d %H:%M:%S') if string.respond_to?(:strftime)
61
+
62
+ ActiveSupport::TimeZone.new("UTC").parse(string)
63
+ end
64
+
38
65
  private
39
66
 
40
67
  attr_reader :jobs
@@ -75,7 +102,15 @@ module Delayed
75
102
 
76
103
  def grouped_min(scope, column)
77
104
  Delayed::Job.from(scope.select("priority, queue, MIN(#{column}) AS #{column}"))
78
- .group(priority_case_statement, :queue).minimum(column)
105
+ .group(priority_case_statement, :queue)
106
+ .select(<<~SQL.squish)
107
+ (#{priority_case_statement}) AS priority,
108
+ queue,
109
+ MIN(#{column}) AS #{column},
110
+ #{self.class.sql_now_in_utc} AS db_now_utc
111
+ SQL
112
+ .group_by { |j| [j.priority.to_i, j.queue] }
113
+ .transform_values(&:first)
79
114
  end
80
115
 
81
116
  def count_grouped
@@ -107,16 +142,16 @@ module Delayed
107
142
  end
108
143
 
109
144
  def max_lock_age_grouped
110
- oldest_locked_job_grouped.transform_values { |locked_at| Job.db_time_now - locked_at }
145
+ oldest_locked_at_query.transform_values { |j| db_now(j) - j.locked_at }
111
146
  end
112
147
 
113
148
  def max_age_grouped
114
- oldest_workable_job_grouped.transform_values { |run_at| Job.db_time_now - run_at }
149
+ oldest_run_at_query.transform_values { |j| db_now(j) - j.run_at }
115
150
  end
116
151
 
117
152
  def alert_age_percent_grouped
118
- oldest_workable_job_grouped.each_with_object({}) do |((priority, queue), run_at), metrics|
119
- max_age = Job.db_time_now - run_at
153
+ oldest_run_at_query.each_with_object({}) do |((priority, queue), j), metrics|
154
+ max_age = db_now(j) - j.run_at
120
155
  alert_age = Priority.new(priority).alert_age
121
156
  metrics[[priority, queue]] = [max_age / alert_age * 100, 100].min if alert_age
122
157
  end
@@ -129,11 +164,23 @@ module Delayed
129
164
  alias working_count_grouped locked_count_grouped
130
165
 
131
166
  def oldest_locked_job_grouped
132
- grouped_min(jobs.claimed, :locked_at)
167
+ oldest_locked_at_query.transform_values(&:locked_at)
133
168
  end
134
169
 
135
170
  def oldest_workable_job_grouped
136
- @memo[:oldest_workable_job_grouped] ||= grouped_min(jobs.claimable, :run_at)
171
+ oldest_run_at_query.transform_values(&:run_at)
172
+ end
173
+
174
+ def oldest_locked_at_query
175
+ @memo[:oldest_locked_at_query] ||= grouped_min(jobs.claimed, :locked_at)
176
+ end
177
+
178
+ def oldest_run_at_query
179
+ @memo[:oldest_run_at_query] ||= grouped_min(jobs.claimable, :run_at)
180
+ end
181
+
182
+ def db_now(record)
183
+ self.class.parse_utc_time(record.db_now_utc)
137
184
  end
138
185
 
139
186
  def priority_case_statement
data/lib/delayed/tasks.rb CHANGED
@@ -10,17 +10,37 @@ namespace :delayed do
10
10
 
11
11
  next unless defined?(Rails.application.config)
12
12
 
13
- # By default, Rails < 6.1 overrides eager_load to 'false' inside of rake tasks, which is not ideal in production environments.
14
- # Additionally, the classic Rails autoloader is not threadsafe, so we do not want any autoloading after we start the worker.
15
- # While the zeitwork autoloader technically does not need this workaround, we will still eager load for consistency's sake.
16
- # We will use the cache_classes config as a proxy for determining if we should eager load before booting workers.
17
- if !Rails.application.config.respond_to?(:rake_eager_load) && Rails.application.config.cache_classes
18
- Rails.application.config.eager_load = true
19
- Rails::Application::Finisher.initializers
20
- .find { |i| i.name == :eager_load! }
21
- .bind(Rails.application)
22
- .run
23
- end
13
+ # By default, Rails wants to disable eager loading inside of `rake`
14
+ # commands, even if `eager_load` is set to true. This is done to speed up
15
+ # the boot time of rake tasks that don't need the entire application loaded.
16
+ #
17
+ # The problem is that long-lived processes like `delayed` **do** want to
18
+ # eager load the application before spawning any threads or forks.
19
+ # (Especially if in a production environment where we want full load-order
20
+ # parity with the `rails server` processes!)
21
+ #
22
+ # When a Rails app boots, it chooses whether to eager load based on its
23
+ # `eager_load` config and whether or not it was initiated by a `rake`
24
+ # command. If it did eager load, we don't want to eager load again, but if
25
+ # it was initiated by a `rake` command, it sets `eager_load` to false before
26
+ # the point at which `delayed` starts setting up _its_ rake environment.
27
+ #
28
+ # So we cannot rely on that config to know whether or not to eager load --
29
+ # instead we must make an inference:
30
+ # - Newer rails versions (~7.0+) have a `config.rake_eager_load` option,
31
+ # which tells us whether the app has already eager loaded in a `rake`
32
+ # context.
33
+ # - If `rake_eager_loading` is not defined or `false`, we will then check
34
+ # `cache_classes` & explicitly eager load if true.
35
+
36
+ eager_loaded = Rails.application.config.respond_to?(:rake_eager_load) && Rails.application.config.rake_eager_load
37
+ next if eager_loaded || !Rails.application.config.cache_classes
38
+
39
+ Rails.application.config.eager_load = true
40
+ Rails::Application::Finisher.initializers
41
+ .find { |i| i.name == :eager_load! }
42
+ .bind(Rails.application)
43
+ .run
24
44
  end
25
45
 
26
46
  desc 'start a delayed worker'
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Delayed
4
- VERSION = '2.0.3'
4
+ VERSION = '2.1.0'
5
5
  end
@@ -318,10 +318,10 @@ GroupAggregate (cost=...)
318
318
  SNAP
319
319
 
320
320
  snapshots["runs the expected postgresql query for max_lock_age 1"] = <<-SNAP
321
- SELECT MIN(locked_at) AS minimum_locked_at, CASE WHEN priority >= 0
321
+ SELECT (CASE WHEN priority >= 0
322
322
  AND priority < 10 THEN 0 WHEN priority >= 10
323
323
  AND priority < 20 THEN 10 WHEN priority >= 20
324
- AND priority < 30 THEN 20 WHEN priority >= 30 THEN 30 END AS case_when_priority_0_and_priority_10_then_0_when_priority_10_an, \"queue\" AS queue
324
+ AND priority < 30 THEN 20 WHEN priority >= 30 THEN 30 END) AS priority, queue, MIN(locked_at) AS locked_at, TIMEZONE('UTC', NOW()) AS db_now_utc
325
325
  FROM (SELECT priority, queue, MIN(locked_at) AS locked_at
326
326
  FROM \"delayed_jobs\"
327
327
  WHERE \"delayed_jobs\".\"locked_at\" >= '2025-11-10 16:59:43'
@@ -336,7 +336,7 @@ SNAP
336
336
 
337
337
  snapshots["produces the expected postgresql query plan for max_lock_age 1"] = <<-SNAP
338
338
  GroupAggregate (cost=...)
339
- Output: min(subquery.locked_at), (CASE WHEN ((subquery.priority >= 0) AND (subquery.priority < 10)) THEN 0 WHEN ((subquery.priority >= 10) AND (subquery.priority < 20)) THEN 10 WHEN ((subquery.priority >= 20) AND (subquery.priority < 30)) THEN 20 WHEN (subquery.priority >= 30) THEN 30 ELSE NULL::integer END), subquery.queue
339
+ Output: (CASE WHEN ((subquery.priority >= 0) AND (subquery.priority < 10)) THEN 0 WHEN ((subquery.priority >= 10) AND (subquery.priority < 20)) THEN 10 WHEN ((subquery.priority >= 20) AND (subquery.priority < 30)) THEN 20 WHEN (subquery.priority >= 30) THEN 30 ELSE NULL::integer END), subquery.queue, min(subquery.locked_at), timezone('UTC'::text, now())
340
340
  Group Key: (CASE WHEN ((subquery.priority >= 0) AND (subquery.priority < 10)) THEN 0 WHEN ((subquery.priority >= 10) AND (subquery.priority < 20)) THEN 10 WHEN ((subquery.priority >= 20) AND (subquery.priority < 30)) THEN 20 WHEN (subquery.priority >= 30) THEN 30 ELSE NULL::integer END), subquery.queue
341
341
  -> Sort (cost=...)
342
342
  Output: (CASE WHEN ((subquery.priority >= 0) AND (subquery.priority < 10)) THEN 0 WHEN ((subquery.priority >= 10) AND (subquery.priority < 20)) THEN 10 WHEN ((subquery.priority >= 20) AND (subquery.priority < 30)) THEN 20 WHEN (subquery.priority >= 30) THEN 30 ELSE NULL::integer END), subquery.queue, subquery.locked_at
@@ -357,7 +357,7 @@ SNAP
357
357
 
358
358
  snapshots["[legacy index] produces the expected postgresql query plan for max_lock_age 1"] = <<-SNAP
359
359
  GroupAggregate (cost=...)
360
- Output: min(subquery.locked_at), (CASE WHEN ((subquery.priority >= 0) AND (subquery.priority < 10)) THEN 0 WHEN ((subquery.priority >= 10) AND (subquery.priority < 20)) THEN 10 WHEN ((subquery.priority >= 20) AND (subquery.priority < 30)) THEN 20 WHEN (subquery.priority >= 30) THEN 30 ELSE NULL::integer END), subquery.queue
360
+ Output: (CASE WHEN ((subquery.priority >= 0) AND (subquery.priority < 10)) THEN 0 WHEN ((subquery.priority >= 10) AND (subquery.priority < 20)) THEN 10 WHEN ((subquery.priority >= 20) AND (subquery.priority < 30)) THEN 20 WHEN (subquery.priority >= 30) THEN 30 ELSE NULL::integer END), subquery.queue, min(subquery.locked_at), timezone('UTC'::text, now())
361
361
  Group Key: (CASE WHEN ((subquery.priority >= 0) AND (subquery.priority < 10)) THEN 0 WHEN ((subquery.priority >= 10) AND (subquery.priority < 20)) THEN 10 WHEN ((subquery.priority >= 20) AND (subquery.priority < 30)) THEN 20 WHEN (subquery.priority >= 30) THEN 30 ELSE NULL::integer END), subquery.queue
362
362
  -> Sort (cost=...)
363
363
  Output: (CASE WHEN ((subquery.priority >= 0) AND (subquery.priority < 10)) THEN 0 WHEN ((subquery.priority >= 10) AND (subquery.priority < 20)) THEN 10 WHEN ((subquery.priority >= 20) AND (subquery.priority < 30)) THEN 20 WHEN (subquery.priority >= 30) THEN 30 ELSE NULL::integer END), subquery.queue, subquery.locked_at
@@ -377,10 +377,10 @@ GroupAggregate (cost=...)
377
377
  SNAP
378
378
 
379
379
  snapshots["runs the expected postgresql query for max_age 1"] = <<-SNAP
380
- SELECT MIN(run_at) AS minimum_run_at, CASE WHEN priority >= 0
380
+ SELECT (CASE WHEN priority >= 0
381
381
  AND priority < 10 THEN 0 WHEN priority >= 10
382
382
  AND priority < 20 THEN 10 WHEN priority >= 20
383
- AND priority < 30 THEN 20 WHEN priority >= 30 THEN 30 END AS case_when_priority_0_and_priority_10_then_0_when_priority_10_an, \"queue\" AS queue
383
+ AND priority < 30 THEN 20 WHEN priority >= 30 THEN 30 END) AS priority, queue, MIN(run_at) AS run_at, TIMEZONE('UTC', NOW()) AS db_now_utc
384
384
  FROM (SELECT priority, queue, MIN(run_at) AS run_at
385
385
  FROM \"delayed_jobs\"
386
386
  WHERE (\"delayed_jobs\".\"locked_at\" IS NULL
@@ -396,7 +396,7 @@ SNAP
396
396
 
397
397
  snapshots["produces the expected postgresql query plan for max_age 1"] = <<-SNAP
398
398
  GroupAggregate (cost=...)
399
- Output: min(subquery.run_at), (CASE WHEN ((subquery.priority >= 0) AND (subquery.priority < 10)) THEN 0 WHEN ((subquery.priority >= 10) AND (subquery.priority < 20)) THEN 10 WHEN ((subquery.priority >= 20) AND (subquery.priority < 30)) THEN 20 WHEN (subquery.priority >= 30) THEN 30 ELSE NULL::integer END), subquery.queue
399
+ Output: (CASE WHEN ((subquery.priority >= 0) AND (subquery.priority < 10)) THEN 0 WHEN ((subquery.priority >= 10) AND (subquery.priority < 20)) THEN 10 WHEN ((subquery.priority >= 20) AND (subquery.priority < 30)) THEN 20 WHEN (subquery.priority >= 30) THEN 30 ELSE NULL::integer END), subquery.queue, min(subquery.run_at), timezone('UTC'::text, now())
400
400
  Group Key: (CASE WHEN ((subquery.priority >= 0) AND (subquery.priority < 10)) THEN 0 WHEN ((subquery.priority >= 10) AND (subquery.priority < 20)) THEN 10 WHEN ((subquery.priority >= 20) AND (subquery.priority < 30)) THEN 20 WHEN (subquery.priority >= 30) THEN 30 ELSE NULL::integer END), subquery.queue
401
401
  -> Sort (cost=...)
402
402
  Output: (CASE WHEN ((subquery.priority >= 0) AND (subquery.priority < 10)) THEN 0 WHEN ((subquery.priority >= 10) AND (subquery.priority < 20)) THEN 10 WHEN ((subquery.priority >= 20) AND (subquery.priority < 30)) THEN 20 WHEN (subquery.priority >= 30) THEN 30 ELSE NULL::integer END), subquery.queue, subquery.run_at
@@ -417,7 +417,7 @@ SNAP
417
417
 
418
418
  snapshots["[legacy index] produces the expected postgresql query plan for max_age 1"] = <<-SNAP
419
419
  GroupAggregate (cost=...)
420
- Output: min(subquery.run_at), (CASE WHEN ((subquery.priority >= 0) AND (subquery.priority < 10)) THEN 0 WHEN ((subquery.priority >= 10) AND (subquery.priority < 20)) THEN 10 WHEN ((subquery.priority >= 20) AND (subquery.priority < 30)) THEN 20 WHEN (subquery.priority >= 30) THEN 30 ELSE NULL::integer END), subquery.queue
420
+ Output: (CASE WHEN ((subquery.priority >= 0) AND (subquery.priority < 10)) THEN 0 WHEN ((subquery.priority >= 10) AND (subquery.priority < 20)) THEN 10 WHEN ((subquery.priority >= 20) AND (subquery.priority < 30)) THEN 20 WHEN (subquery.priority >= 30) THEN 30 ELSE NULL::integer END), subquery.queue, min(subquery.run_at), timezone('UTC'::text, now())
421
421
  Group Key: (CASE WHEN ((subquery.priority >= 0) AND (subquery.priority < 10)) THEN 0 WHEN ((subquery.priority >= 10) AND (subquery.priority < 20)) THEN 10 WHEN ((subquery.priority >= 20) AND (subquery.priority < 30)) THEN 20 WHEN (subquery.priority >= 30) THEN 30 ELSE NULL::integer END), subquery.queue
422
422
  -> Sort (cost=...)
423
423
  Output: (CASE WHEN ((subquery.priority >= 0) AND (subquery.priority < 10)) THEN 0 WHEN ((subquery.priority >= 10) AND (subquery.priority < 20)) THEN 10 WHEN ((subquery.priority >= 20) AND (subquery.priority < 30)) THEN 20 WHEN (subquery.priority >= 30) THEN 30 ELSE NULL::integer END), subquery.queue, subquery.run_at
@@ -556,10 +556,10 @@ GroupAggregate (cost=...)
556
556
  SNAP
557
557
 
558
558
  snapshots["runs the expected postgresql query for alert_age_percent 1"] = <<-SNAP
559
- SELECT MIN(run_at) AS minimum_run_at, CASE WHEN priority >= 0
559
+ SELECT (CASE WHEN priority >= 0
560
560
  AND priority < 10 THEN 0 WHEN priority >= 10
561
561
  AND priority < 20 THEN 10 WHEN priority >= 20
562
- AND priority < 30 THEN 20 WHEN priority >= 30 THEN 30 END AS case_when_priority_0_and_priority_10_then_0_when_priority_10_an, \"queue\" AS queue
562
+ AND priority < 30 THEN 20 WHEN priority >= 30 THEN 30 END) AS priority, queue, MIN(run_at) AS run_at, TIMEZONE('UTC', NOW()) AS db_now_utc
563
563
  FROM (SELECT priority, queue, MIN(run_at) AS run_at
564
564
  FROM \"delayed_jobs\"
565
565
  WHERE (\"delayed_jobs\".\"locked_at\" IS NULL
@@ -575,7 +575,7 @@ SNAP
575
575
 
576
576
  snapshots["produces the expected postgresql query plan for alert_age_percent 1"] = <<-SNAP
577
577
  GroupAggregate (cost=...)
578
- Output: min(subquery.run_at), (CASE WHEN ((subquery.priority >= 0) AND (subquery.priority < 10)) THEN 0 WHEN ((subquery.priority >= 10) AND (subquery.priority < 20)) THEN 10 WHEN ((subquery.priority >= 20) AND (subquery.priority < 30)) THEN 20 WHEN (subquery.priority >= 30) THEN 30 ELSE NULL::integer END), subquery.queue
578
+ Output: (CASE WHEN ((subquery.priority >= 0) AND (subquery.priority < 10)) THEN 0 WHEN ((subquery.priority >= 10) AND (subquery.priority < 20)) THEN 10 WHEN ((subquery.priority >= 20) AND (subquery.priority < 30)) THEN 20 WHEN (subquery.priority >= 30) THEN 30 ELSE NULL::integer END), subquery.queue, min(subquery.run_at), timezone('UTC'::text, now())
579
579
  Group Key: (CASE WHEN ((subquery.priority >= 0) AND (subquery.priority < 10)) THEN 0 WHEN ((subquery.priority >= 10) AND (subquery.priority < 20)) THEN 10 WHEN ((subquery.priority >= 20) AND (subquery.priority < 30)) THEN 20 WHEN (subquery.priority >= 30) THEN 30 ELSE NULL::integer END), subquery.queue
580
580
  -> Sort (cost=...)
581
581
  Output: (CASE WHEN ((subquery.priority >= 0) AND (subquery.priority < 10)) THEN 0 WHEN ((subquery.priority >= 10) AND (subquery.priority < 20)) THEN 10 WHEN ((subquery.priority >= 20) AND (subquery.priority < 30)) THEN 20 WHEN (subquery.priority >= 30) THEN 30 ELSE NULL::integer END), subquery.queue, subquery.run_at
@@ -596,7 +596,7 @@ SNAP
596
596
 
597
597
  snapshots["[legacy index] produces the expected postgresql query plan for alert_age_percent 1"] = <<-SNAP
598
598
  GroupAggregate (cost=...)
599
- Output: min(subquery.run_at), (CASE WHEN ((subquery.priority >= 0) AND (subquery.priority < 10)) THEN 0 WHEN ((subquery.priority >= 10) AND (subquery.priority < 20)) THEN 10 WHEN ((subquery.priority >= 20) AND (subquery.priority < 30)) THEN 20 WHEN (subquery.priority >= 30) THEN 30 ELSE NULL::integer END), subquery.queue
599
+ Output: (CASE WHEN ((subquery.priority >= 0) AND (subquery.priority < 10)) THEN 0 WHEN ((subquery.priority >= 10) AND (subquery.priority < 20)) THEN 10 WHEN ((subquery.priority >= 20) AND (subquery.priority < 30)) THEN 20 WHEN (subquery.priority >= 30) THEN 30 ELSE NULL::integer END), subquery.queue, min(subquery.run_at), timezone('UTC'::text, now())
600
600
  Group Key: (CASE WHEN ((subquery.priority >= 0) AND (subquery.priority < 10)) THEN 0 WHEN ((subquery.priority >= 10) AND (subquery.priority < 20)) THEN 10 WHEN ((subquery.priority >= 20) AND (subquery.priority < 30)) THEN 20 WHEN (subquery.priority >= 30) THEN 30 ELSE NULL::integer END), subquery.queue
601
601
  -> Sort (cost=...)
602
602
  Output: (CASE WHEN ((subquery.priority >= 0) AND (subquery.priority < 10)) THEN 0 WHEN ((subquery.priority >= 10) AND (subquery.priority < 20)) THEN 10 WHEN ((subquery.priority >= 20) AND (subquery.priority < 30)) THEN 20 WHEN (subquery.priority >= 30) THEN 30 ELSE NULL::integer END), subquery.queue, subquery.run_at
@@ -795,10 +795,10 @@ USE TEMP B-TREE FOR GROUP BY
795
795
  SNAP
796
796
 
797
797
  snapshots["runs the expected sqlite3 query for max_lock_age 1"] = <<-SNAP
798
- SELECT MIN(locked_at) AS minimum_locked_at, CASE WHEN priority >= 0
798
+ SELECT (CASE WHEN priority >= 0
799
799
  AND priority < 10 THEN 0 WHEN priority >= 10
800
800
  AND priority < 20 THEN 10 WHEN priority >= 20
801
- AND priority < 30 THEN 20 WHEN priority >= 30 THEN 30 END AS case_when_priority_0_and_priority_10_then_0_when_priority_10_an, \"queue\" AS queue
801
+ AND priority < 30 THEN 20 WHEN priority >= 30 THEN 30 END) AS priority, queue, MIN(locked_at) AS locked_at, CURRENT_TIMESTAMP AS db_now_utc
802
802
  FROM (SELECT priority, queue, MIN(locked_at) AS locked_at
803
803
  FROM \"delayed_jobs\"
804
804
  WHERE \"delayed_jobs\".\"locked_at\" >= '2025-11-10 16:59:43'
@@ -828,10 +828,10 @@ USE TEMP B-TREE FOR GROUP BY
828
828
  SNAP
829
829
 
830
830
  snapshots["runs the expected sqlite3 query for max_age 1"] = <<-SNAP
831
- SELECT MIN(run_at) AS minimum_run_at, CASE WHEN priority >= 0
831
+ SELECT (CASE WHEN priority >= 0
832
832
  AND priority < 10 THEN 0 WHEN priority >= 10
833
833
  AND priority < 20 THEN 10 WHEN priority >= 20
834
- AND priority < 30 THEN 20 WHEN priority >= 30 THEN 30 END AS case_when_priority_0_and_priority_10_then_0_when_priority_10_an, \"queue\" AS queue
834
+ AND priority < 30 THEN 20 WHEN priority >= 30 THEN 30 END) AS priority, queue, MIN(run_at) AS run_at, CURRENT_TIMESTAMP AS db_now_utc
835
835
  FROM (SELECT priority, queue, MIN(run_at) AS run_at
836
836
  FROM \"delayed_jobs\"
837
837
  WHERE (\"delayed_jobs\".\"locked_at\" IS NULL
@@ -929,10 +929,10 @@ USE TEMP B-TREE FOR GROUP BY
929
929
  SNAP
930
930
 
931
931
  snapshots["runs the expected sqlite3 query for alert_age_percent 1"] = <<-SNAP
932
- SELECT MIN(run_at) AS minimum_run_at, CASE WHEN priority >= 0
932
+ SELECT (CASE WHEN priority >= 0
933
933
  AND priority < 10 THEN 0 WHEN priority >= 10
934
934
  AND priority < 20 THEN 10 WHEN priority >= 20
935
- AND priority < 30 THEN 20 WHEN priority >= 30 THEN 30 END AS case_when_priority_0_and_priority_10_then_0_when_priority_10_an, \"queue\" AS queue
935
+ AND priority < 30 THEN 20 WHEN priority >= 30 THEN 30 END) AS priority, queue, MIN(run_at) AS run_at, CURRENT_TIMESTAMP AS db_now_utc
936
936
  FROM (SELECT priority, queue, MIN(run_at) AS run_at
937
937
  FROM \"delayed_jobs\"
938
938
  WHERE (\"delayed_jobs\".\"locked_at\" IS NULL
@@ -1149,10 +1149,10 @@ snapshots["[legacy index] produces the expected mysql2 query plan for failed_cou
1149
1149
  SNAP
1150
1150
 
1151
1151
  snapshots["runs the expected mysql2 query for max_lock_age 1"] = <<-SNAP
1152
- SELECT MIN(locked_at) AS minimum_locked_at, CASE WHEN priority >= 0
1152
+ SELECT (CASE WHEN priority >= 0
1153
1153
  AND priority < 10 THEN 0 WHEN priority >= 10
1154
1154
  AND priority < 20 THEN 10 WHEN priority >= 20
1155
- AND priority < 30 THEN 20 WHEN priority >= 30 THEN 30 END AS case_when_priority_0_and_priority_10_then_0_when_priority_10_an, `queue` AS queue
1155
+ AND priority < 30 THEN 20 WHEN priority >= 30 THEN 30 END) AS priority, queue, MIN(locked_at) AS locked_at, UTC_TIMESTAMP() AS db_now_utc
1156
1156
  FROM (SELECT priority, queue, MIN(locked_at) AS locked_at
1157
1157
  FROM `delayed_jobs`
1158
1158
  WHERE `delayed_jobs`.`locked_at` >= '2025-11-10 16:59:43'
@@ -1188,10 +1188,10 @@ snapshots["[legacy index] produces the expected mysql2 query plan for max_lock_a
1188
1188
  SNAP
1189
1189
 
1190
1190
  snapshots["runs the expected mysql2 query for max_age 1"] = <<-SNAP
1191
- SELECT MIN(run_at) AS minimum_run_at, CASE WHEN priority >= 0
1191
+ SELECT (CASE WHEN priority >= 0
1192
1192
  AND priority < 10 THEN 0 WHEN priority >= 10
1193
1193
  AND priority < 20 THEN 10 WHEN priority >= 20
1194
- AND priority < 30 THEN 20 WHEN priority >= 30 THEN 30 END AS case_when_priority_0_and_priority_10_then_0_when_priority_10_an, `queue` AS queue
1194
+ AND priority < 30 THEN 20 WHEN priority >= 30 THEN 30 END) AS priority, queue, MIN(run_at) AS run_at, UTC_TIMESTAMP() AS db_now_utc
1195
1195
  FROM (SELECT priority, queue, MIN(run_at) AS run_at
1196
1196
  FROM `delayed_jobs`
1197
1197
  WHERE (`delayed_jobs`.`locked_at` IS NULL
@@ -1307,10 +1307,10 @@ snapshots["[legacy index] produces the expected mysql2 query plan for workable_c
1307
1307
  SNAP
1308
1308
 
1309
1309
  snapshots["runs the expected mysql2 query for alert_age_percent 1"] = <<-SNAP
1310
- SELECT MIN(run_at) AS minimum_run_at, CASE WHEN priority >= 0
1310
+ SELECT (CASE WHEN priority >= 0
1311
1311
  AND priority < 10 THEN 0 WHEN priority >= 10
1312
1312
  AND priority < 20 THEN 10 WHEN priority >= 20
1313
- AND priority < 30 THEN 20 WHEN priority >= 30 THEN 30 END AS case_when_priority_0_and_priority_10_then_0_when_priority_10_an, `queue` AS queue
1313
+ AND priority < 30 THEN 20 WHEN priority >= 30 THEN 30 END) AS priority, queue, MIN(run_at) AS run_at, UTC_TIMESTAMP() AS db_now_utc
1314
1314
  FROM (SELECT priority, queue, MIN(run_at) AS run_at
1315
1315
  FROM `delayed_jobs`
1316
1316
  WHERE (`delayed_jobs`.`locked_at` IS NULL
@@ -1006,12 +1006,6 @@ describe Delayed::Job do
1006
1006
  end
1007
1007
  end
1008
1008
 
1009
- if ActiveRecord::VERSION::MAJOR >= 7
1010
- delegate :default_timezone=, to: ActiveRecord
1011
- else
1012
- delegate :default_timezone=, to: ActiveRecord::Base
1013
- end
1014
-
1015
1009
  context "db_time_now" do
1016
1010
  after do
1017
1011
  Time.zone = nil
@@ -14,262 +14,333 @@ RSpec.describe Delayed::Monitor do
14
14
  }
15
15
  end
16
16
 
17
- it 'emits empty metrics for all default priorities' do
18
- expect { subject.run! }
19
- .to emit_notification("delayed.monitor.run").with_payload(default_payload.except(:queue))
20
- .and emit_notification("delayed.job.count").with_payload(default_payload.merge(priority: 'interactive')).with_value(0)
21
- .and emit_notification("delayed.job.count").with_payload(default_payload.merge(priority: 'user_visible')).with_value(0)
22
- .and emit_notification("delayed.job.count").with_payload(default_payload.merge(priority: 'eventual')).with_value(0)
23
- .and emit_notification("delayed.job.count").with_payload(default_payload.merge(priority: 'reporting')).with_value(0)
24
- .and emit_notification("delayed.job.future_count").with_payload(default_payload.merge(priority: 'interactive')).with_value(0)
25
- .and emit_notification("delayed.job.future_count").with_payload(default_payload.merge(priority: 'user_visible')).with_value(0)
26
- .and emit_notification("delayed.job.future_count").with_payload(default_payload.merge(priority: 'eventual')).with_value(0)
27
- .and emit_notification("delayed.job.future_count").with_payload(default_payload.merge(priority: 'reporting')).with_value(0)
28
- .and emit_notification("delayed.job.locked_count").with_payload(default_payload.merge(priority: 'interactive')).with_value(0)
29
- .and emit_notification("delayed.job.locked_count").with_payload(default_payload.merge(priority: 'user_visible')).with_value(0)
30
- .and emit_notification("delayed.job.locked_count").with_payload(default_payload.merge(priority: 'eventual')).with_value(0)
31
- .and emit_notification("delayed.job.locked_count").with_payload(default_payload.merge(priority: 'reporting')).with_value(0)
32
- .and emit_notification("delayed.job.erroring_count").with_payload(default_payload.merge(priority: 'interactive')).with_value(0)
33
- .and emit_notification("delayed.job.erroring_count").with_payload(default_payload.merge(priority: 'user_visible')).with_value(0)
34
- .and emit_notification("delayed.job.erroring_count").with_payload(default_payload.merge(priority: 'eventual')).with_value(0)
35
- .and emit_notification("delayed.job.erroring_count").with_payload(default_payload.merge(priority: 'reporting')).with_value(0)
36
- .and emit_notification("delayed.job.failed_count").with_payload(default_payload.merge(priority: 'interactive')).with_value(0)
37
- .and emit_notification("delayed.job.failed_count").with_payload(default_payload.merge(priority: 'user_visible')).with_value(0)
38
- .and emit_notification("delayed.job.failed_count").with_payload(default_payload.merge(priority: 'eventual')).with_value(0)
39
- .and emit_notification("delayed.job.failed_count").with_payload(default_payload.merge(priority: 'reporting')).with_value(0)
40
- .and emit_notification("delayed.job.working_count").with_payload(default_payload.merge(priority: 'interactive')).with_value(0)
41
- .and emit_notification("delayed.job.working_count").with_payload(default_payload.merge(priority: 'user_visible')).with_value(0)
42
- .and emit_notification("delayed.job.working_count").with_payload(default_payload.merge(priority: 'eventual')).with_value(0)
43
- .and emit_notification("delayed.job.working_count").with_payload(default_payload.merge(priority: 'reporting')).with_value(0)
44
- .and emit_notification("delayed.job.workable_count").with_payload(default_payload.merge(priority: 'interactive')).with_value(0)
45
- .and emit_notification("delayed.job.workable_count").with_payload(default_payload.merge(priority: 'user_visible')).with_value(0)
46
- .and emit_notification("delayed.job.workable_count").with_payload(default_payload.merge(priority: 'eventual')).with_value(0)
47
- .and emit_notification("delayed.job.workable_count").with_payload(default_payload.merge(priority: 'reporting')).with_value(0)
48
- .and emit_notification("delayed.job.max_age").with_payload(default_payload.merge(priority: 'interactive')).with_value(0)
49
- .and emit_notification("delayed.job.max_age").with_payload(default_payload.merge(priority: 'user_visible')).with_value(0)
50
- .and emit_notification("delayed.job.max_age").with_payload(default_payload.merge(priority: 'eventual')).with_value(0)
51
- .and emit_notification("delayed.job.max_age").with_payload(default_payload.merge(priority: 'reporting')).with_value(0)
52
- .and emit_notification("delayed.job.max_lock_age").with_payload(default_payload.merge(priority: 'interactive')).with_value(0)
53
- .and emit_notification("delayed.job.max_lock_age").with_payload(default_payload.merge(priority: 'user_visible')).with_value(0)
54
- .and emit_notification("delayed.job.max_lock_age").with_payload(default_payload.merge(priority: 'eventual')).with_value(0)
55
- .and emit_notification("delayed.job.max_lock_age").with_payload(default_payload.merge(priority: 'reporting')).with_value(0)
56
- .and emit_notification("delayed.job.alert_age_percent").with_payload(default_payload.merge(priority: 'interactive')).with_value(0)
57
- .and emit_notification("delayed.job.alert_age_percent").with_payload(default_payload.merge(priority: 'user_visible')).with_value(0)
58
- .and emit_notification("delayed.job.alert_age_percent").with_payload(default_payload.merge(priority: 'eventual')).with_value(0)
59
- .and emit_notification("delayed.job.alert_age_percent").with_payload(default_payload.merge(priority: 'reporting')).with_value(0)
60
- end
17
+ describe '#run!' do
18
+ let(:app_local_db_time) { false }
61
19
 
62
- context 'when named priorities are customized' do
63
20
  around do |example|
64
- Delayed::Priority.names = { high: 0, low: 7 }
65
- example.run
66
- ensure
67
- Delayed::Priority.names = nil
68
- end
21
+ if app_local_db_time
22
+ Time.zone = 'US/Central'
23
+ self.default_timezone = :local
24
+ end
69
25
 
70
- it 'emits empty metrics for all custom priorities' do
71
- expect { subject.run! }
72
- .to emit_notification("delayed.monitor.run").with_payload(default_payload.except(:queue))
73
- .and emit_notification("delayed.job.count").with_payload(default_payload.merge(priority: 'high')).with_value(0)
74
- .and emit_notification("delayed.job.count").with_payload(default_payload.merge(priority: 'low')).with_value(0)
75
- .and emit_notification("delayed.job.future_count").with_payload(default_payload.merge(priority: 'high')).with_value(0)
76
- .and emit_notification("delayed.job.future_count").with_payload(default_payload.merge(priority: 'low')).with_value(0)
77
- .and emit_notification("delayed.job.locked_count").with_payload(default_payload.merge(priority: 'high')).with_value(0)
78
- .and emit_notification("delayed.job.locked_count").with_payload(default_payload.merge(priority: 'low')).with_value(0)
79
- .and emit_notification("delayed.job.erroring_count").with_payload(default_payload.merge(priority: 'high')).with_value(0)
80
- .and emit_notification("delayed.job.erroring_count").with_payload(default_payload.merge(priority: 'low')).with_value(0)
81
- .and emit_notification("delayed.job.failed_count").with_payload(default_payload.merge(priority: 'high')).with_value(0)
82
- .and emit_notification("delayed.job.failed_count").with_payload(default_payload.merge(priority: 'low')).with_value(0)
83
- .and emit_notification("delayed.job.working_count").with_payload(default_payload.merge(priority: 'high')).with_value(0)
84
- .and emit_notification("delayed.job.working_count").with_payload(default_payload.merge(priority: 'low')).with_value(0)
85
- .and emit_notification("delayed.job.workable_count").with_payload(default_payload.merge(priority: 'high')).with_value(0)
86
- .and emit_notification("delayed.job.workable_count").with_payload(default_payload.merge(priority: 'low')).with_value(0)
87
- .and emit_notification("delayed.job.max_age").with_payload(default_payload.merge(priority: 'high')).with_value(0)
88
- .and emit_notification("delayed.job.max_age").with_payload(default_payload.merge(priority: 'low')).with_value(0)
89
- .and emit_notification("delayed.job.max_lock_age").with_payload(default_payload.merge(priority: 'high')).with_value(0)
90
- .and emit_notification("delayed.job.max_lock_age").with_payload(default_payload.merge(priority: 'low')).with_value(0)
91
- .and emit_notification("delayed.job.alert_age_percent").with_payload(default_payload.merge(priority: 'high')).with_value(0)
92
- .and emit_notification("delayed.job.alert_age_percent").with_payload(default_payload.merge(priority: 'low')).with_value(0)
26
+ # On PostgreSQL, running examples in a transaction allows CURRENT_TIMESTAMP to remain stable.
27
+ # We can in turn use this to set Timecop to the same time as the DB for deterministic time math.
28
+ Delayed::Job.transaction do
29
+ now = described_class.parse_utc_time(
30
+ Delayed::Job.connection.select_value("SELECT #{described_class.sql_now_in_utc}"),
31
+ )
32
+ Timecop.freeze(now) { example.run }
33
+ end
34
+ ensure
35
+ Time.zone = nil
36
+ self.default_timezone = :utc
93
37
  end
94
- end
95
38
 
96
- context 'when there are jobs in the queue' do
97
- let(:now) { Delayed::Job.db_time_now.change(nsec: 0) }
98
- let(:job_attributes) do
99
- {
100
- run_at: now,
101
- queue: 'default',
102
- handler: "--- !ruby/object:SimpleJob\n",
103
- attempts: 0,
104
- }
105
- end
106
- let(:failed_attributes) { { run_at: now - 1.week, attempts: 1, failed_at: now - 1.day, locked_at: now - 1.day } }
107
- let(:p0_attributes) { job_attributes.merge(priority: 1, attempts: 1) }
108
- let(:p10_attributes) { job_attributes.merge(priority: 13, locked_at: now - 1.day) }
109
- let(:p20_attributes) { job_attributes.merge(priority: 23, attempts: 1) }
110
- let(:p30_attributes) { job_attributes.merge(priority: 999, locked_at: now - 1.day) }
111
- let(:p0_payload) { default_payload.merge(priority: 'interactive') }
112
- let(:p10_payload) { default_payload.merge(priority: 'user_visible') }
113
- let(:p20_payload) { default_payload.merge(priority: 'eventual') }
114
- let(:p30_payload) { default_payload.merge(priority: 'reporting') }
115
- let!(:p0_workable_job) { Delayed::Job.create! p0_attributes.merge(run_at: now - 30.seconds) }
116
- let!(:p0_failed_job) { Delayed::Job.create! p0_attributes.merge(failed_attributes) }
117
- let!(:p0_future_job) { Delayed::Job.create! p0_attributes.merge(run_at: now + 1.hour) }
118
- let!(:p0_working_job) { Delayed::Job.create! p0_attributes.merge(locked_at: now - 3.minutes) }
119
- let!(:p10_workable_job) { Delayed::Job.create! p10_attributes.merge(run_at: now - 2.minutes) }
120
- let!(:p10_failed_job) { Delayed::Job.create! p10_attributes.merge(failed_attributes) }
121
- let!(:p10_future_job) { Delayed::Job.create! p10_attributes.merge(run_at: now + 1.hour) }
122
- let!(:p10_working_job) { Delayed::Job.create! p10_attributes.merge(locked_at: now - 7.minutes) }
123
- let!(:p20_workable_job) { Delayed::Job.create! p20_attributes.merge(run_at: now - 1.hour) }
124
- let!(:p20_failed_job) { Delayed::Job.create! p20_attributes.merge(failed_attributes) }
125
- let!(:p20_future_job) { Delayed::Job.create! p20_attributes.merge(run_at: now + 1.hour) }
126
- let!(:p20_working_job) { Delayed::Job.create! p20_attributes.merge(locked_at: now - 9.minutes) }
127
- let!(:p30_workable_job) { Delayed::Job.create! p30_attributes.merge(run_at: now - 6.hours) }
128
- let!(:p30_failed_job) { Delayed::Job.create! p30_attributes.merge(failed_attributes) }
129
- let!(:p30_future_job) { Delayed::Job.create! p30_attributes.merge(run_at: now + 1.hour) }
130
- let!(:p30_working_job) { Delayed::Job.create! p30_attributes.merge(locked_at: now - 11.minutes) }
131
- let!(:p30_workable_job_in_other_queue) { Delayed::Job.create! p30_attributes.merge(run_at: now - 4.hours, queue: 'banana') }
39
+ let(:now) { Delayed::Job.db_time_now }
132
40
 
133
- around do |example|
134
- Timecop.freeze(now) { example.run }
135
- end
136
-
137
- it 'emits the expected results for each metric' do
41
+ it 'emits empty metrics for all default priorities' do
138
42
  expect { subject.run! }
139
43
  .to emit_notification("delayed.monitor.run").with_payload(default_payload.except(:queue))
140
- .and emit_notification("delayed.job.count").with_payload(p0_payload).with_value(4)
141
- .and emit_notification("delayed.job.future_count").with_payload(p0_payload).with_value(1)
142
- .and emit_notification("delayed.job.locked_count").with_payload(p0_payload).with_value(1)
143
- .and emit_notification("delayed.job.erroring_count").with_payload(p0_payload).with_value(3)
144
- .and emit_notification("delayed.job.failed_count").with_payload(p0_payload).with_value(1)
145
- .and emit_notification("delayed.job.working_count").with_payload(p0_payload).with_value(1)
146
- .and emit_notification("delayed.job.workable_count").with_payload(p0_payload).with_value(1)
147
- .and emit_notification("delayed.job.max_age").with_payload(p0_payload).with_value(30.seconds)
148
- .and emit_notification("delayed.job.max_lock_age").with_payload(p0_payload).with_value(3.minutes)
149
- .and emit_notification("delayed.job.alert_age_percent").with_payload(p0_payload).with_value(30.0.seconds / 1.minute * 100)
150
- .and emit_notification("delayed.job.count").with_payload(p10_payload).with_value(4)
151
- .and emit_notification("delayed.job.future_count").with_payload(p10_payload).with_value(1)
152
- .and emit_notification("delayed.job.locked_count").with_payload(p10_payload).with_value(1)
153
- .and emit_notification("delayed.job.erroring_count").with_payload(p10_payload).with_value(0)
154
- .and emit_notification("delayed.job.failed_count").with_payload(p10_payload).with_value(1)
155
- .and emit_notification("delayed.job.working_count").with_payload(p10_payload).with_value(1)
156
- .and emit_notification("delayed.job.workable_count").with_payload(p10_payload).with_value(1)
157
- .and emit_notification("delayed.job.max_age").with_payload(p10_payload).with_value(2.minutes)
158
- .and emit_notification("delayed.job.max_lock_age").with_payload(p10_payload).with_value(7.minutes)
159
- .and emit_notification("delayed.job.alert_age_percent").with_payload(p10_payload).with_value(2.0.minutes / 3.minutes * 100)
160
- .and emit_notification("delayed.job.count").with_payload(p20_payload).with_value(4)
161
- .and emit_notification("delayed.job.future_count").with_payload(p20_payload).with_value(1)
162
- .and emit_notification("delayed.job.locked_count").with_payload(p20_payload).with_value(1)
163
- .and emit_notification("delayed.job.erroring_count").with_payload(p20_payload).with_value(3)
164
- .and emit_notification("delayed.job.failed_count").with_payload(p20_payload).with_value(1)
165
- .and emit_notification("delayed.job.working_count").with_payload(p20_payload).with_value(1)
166
- .and emit_notification("delayed.job.workable_count").with_payload(p20_payload).with_value(1)
167
- .and emit_notification("delayed.job.max_age").with_payload(p20_payload).with_value(1.hour)
168
- .and emit_notification("delayed.job.max_lock_age").with_payload(p20_payload).with_value(9.minutes)
169
- .and emit_notification("delayed.job.alert_age_percent").with_payload(p20_payload).with_value(1.hour / 1.5.hours * 100)
170
- .and emit_notification("delayed.job.count").with_payload(p30_payload).with_value(4)
171
- .and emit_notification("delayed.job.future_count").with_payload(p30_payload).with_value(1)
172
- .and emit_notification("delayed.job.locked_count").with_payload(p30_payload).with_value(1)
173
- .and emit_notification("delayed.job.erroring_count").with_payload(p30_payload).with_value(0)
174
- .and emit_notification("delayed.job.failed_count").with_payload(p30_payload).with_value(1)
175
- .and emit_notification("delayed.job.working_count").with_payload(p30_payload).with_value(1)
176
- .and emit_notification("delayed.job.workable_count").with_payload(p30_payload).with_value(1)
177
- .and emit_notification("delayed.job.max_age").with_payload(p30_payload).with_value(6.hours)
178
- .and emit_notification("delayed.job.max_lock_age").with_payload(p30_payload).with_value(11.minutes)
179
- .and emit_notification("delayed.job.alert_age_percent").with_payload(p30_payload).with_value(100) # 6 hours / 4 hours (overflow)
180
- .and emit_notification("delayed.job.workable_count").with_payload(p30_payload.merge(queue: 'banana')).with_value(1)
181
- .and emit_notification("delayed.job.max_age").with_payload(p30_payload.merge(queue: 'banana')).with_value(4.hours)
44
+ .and emit_notification("delayed.job.count").with_payload(default_payload.merge(priority: 'interactive')).with_value(0)
45
+ .and emit_notification("delayed.job.count").with_payload(default_payload.merge(priority: 'user_visible')).with_value(0)
46
+ .and emit_notification("delayed.job.count").with_payload(default_payload.merge(priority: 'eventual')).with_value(0)
47
+ .and emit_notification("delayed.job.count").with_payload(default_payload.merge(priority: 'reporting')).with_value(0)
48
+ .and emit_notification("delayed.job.future_count").with_payload(default_payload.merge(priority: 'interactive')).with_value(0)
49
+ .and emit_notification("delayed.job.future_count").with_payload(default_payload.merge(priority: 'user_visible')).with_value(0)
50
+ .and emit_notification("delayed.job.future_count").with_payload(default_payload.merge(priority: 'eventual')).with_value(0)
51
+ .and emit_notification("delayed.job.future_count").with_payload(default_payload.merge(priority: 'reporting')).with_value(0)
52
+ .and emit_notification("delayed.job.locked_count").with_payload(default_payload.merge(priority: 'interactive')).with_value(0)
53
+ .and emit_notification("delayed.job.locked_count").with_payload(default_payload.merge(priority: 'user_visible')).with_value(0)
54
+ .and emit_notification("delayed.job.locked_count").with_payload(default_payload.merge(priority: 'eventual')).with_value(0)
55
+ .and emit_notification("delayed.job.locked_count").with_payload(default_payload.merge(priority: 'reporting')).with_value(0)
56
+ .and emit_notification("delayed.job.erroring_count").with_payload(default_payload.merge(priority: 'interactive')).with_value(0)
57
+ .and emit_notification("delayed.job.erroring_count").with_payload(default_payload.merge(priority: 'user_visible')).with_value(0)
58
+ .and emit_notification("delayed.job.erroring_count").with_payload(default_payload.merge(priority: 'eventual')).with_value(0)
59
+ .and emit_notification("delayed.job.erroring_count").with_payload(default_payload.merge(priority: 'reporting')).with_value(0)
60
+ .and emit_notification("delayed.job.failed_count").with_payload(default_payload.merge(priority: 'interactive')).with_value(0)
61
+ .and emit_notification("delayed.job.failed_count").with_payload(default_payload.merge(priority: 'user_visible')).with_value(0)
62
+ .and emit_notification("delayed.job.failed_count").with_payload(default_payload.merge(priority: 'eventual')).with_value(0)
63
+ .and emit_notification("delayed.job.failed_count").with_payload(default_payload.merge(priority: 'reporting')).with_value(0)
64
+ .and emit_notification("delayed.job.working_count").with_payload(default_payload.merge(priority: 'interactive')).with_value(0)
65
+ .and emit_notification("delayed.job.working_count").with_payload(default_payload.merge(priority: 'user_visible')).with_value(0)
66
+ .and emit_notification("delayed.job.working_count").with_payload(default_payload.merge(priority: 'eventual')).with_value(0)
67
+ .and emit_notification("delayed.job.working_count").with_payload(default_payload.merge(priority: 'reporting')).with_value(0)
68
+ .and emit_notification("delayed.job.workable_count").with_payload(default_payload.merge(priority: 'interactive')).with_value(0)
69
+ .and emit_notification("delayed.job.workable_count").with_payload(default_payload.merge(priority: 'user_visible')).with_value(0)
70
+ .and emit_notification("delayed.job.workable_count").with_payload(default_payload.merge(priority: 'eventual')).with_value(0)
71
+ .and emit_notification("delayed.job.workable_count").with_payload(default_payload.merge(priority: 'reporting')).with_value(0)
72
+ .and emit_notification("delayed.job.max_age").with_payload(default_payload.merge(priority: 'interactive')).approximately.with_value(0)
73
+ .and emit_notification("delayed.job.max_age").with_payload(default_payload.merge(priority: 'user_visible')).approximately.with_value(0)
74
+ .and emit_notification("delayed.job.max_age").with_payload(default_payload.merge(priority: 'eventual')).approximately.with_value(0)
75
+ .and emit_notification("delayed.job.max_age").with_payload(default_payload.merge(priority: 'reporting')).approximately.with_value(0)
76
+ .and emit_notification("delayed.job.max_lock_age").with_payload(default_payload.merge(priority: 'interactive')).approximately.with_value(0)
77
+ .and emit_notification("delayed.job.max_lock_age").with_payload(default_payload.merge(priority: 'user_visible')).approximately.with_value(0)
78
+ .and emit_notification("delayed.job.max_lock_age").with_payload(default_payload.merge(priority: 'eventual')).approximately.with_value(0)
79
+ .and emit_notification("delayed.job.max_lock_age").with_payload(default_payload.merge(priority: 'reporting')).approximately.with_value(0)
80
+ .and emit_notification("delayed.job.alert_age_percent").with_payload(default_payload.merge(priority: 'interactive')).approximately.with_value(0)
81
+ .and emit_notification("delayed.job.alert_age_percent").with_payload(default_payload.merge(priority: 'user_visible')).approximately.with_value(0)
82
+ .and emit_notification("delayed.job.alert_age_percent").with_payload(default_payload.merge(priority: 'eventual')).approximately.with_value(0)
83
+ .and emit_notification("delayed.job.alert_age_percent").with_payload(default_payload.merge(priority: 'reporting')).approximately.with_value(0)
182
84
  end
183
85
 
184
86
  context 'when named priorities are customized' do
185
87
  around do |example|
186
- Delayed::Priority.names = { high: 0, low: 20 }
88
+ Delayed::Priority.names = { high: 0, low: 7 }
187
89
  example.run
188
90
  ensure
189
91
  Delayed::Priority.names = nil
190
92
  end
191
- let(:p0_payload) { default_payload.merge(priority: 'high') }
192
- let(:p20_payload) { default_payload.merge(priority: 'low') }
93
+
94
+ it 'emits empty metrics for all custom priorities' do
95
+ expect { subject.run! }
96
+ .to emit_notification("delayed.monitor.run").with_payload(default_payload.except(:queue))
97
+ .and emit_notification("delayed.job.count").with_payload(default_payload.merge(priority: 'high')).with_value(0)
98
+ .and emit_notification("delayed.job.count").with_payload(default_payload.merge(priority: 'low')).with_value(0)
99
+ .and emit_notification("delayed.job.future_count").with_payload(default_payload.merge(priority: 'high')).with_value(0)
100
+ .and emit_notification("delayed.job.future_count").with_payload(default_payload.merge(priority: 'low')).with_value(0)
101
+ .and emit_notification("delayed.job.locked_count").with_payload(default_payload.merge(priority: 'high')).with_value(0)
102
+ .and emit_notification("delayed.job.locked_count").with_payload(default_payload.merge(priority: 'low')).with_value(0)
103
+ .and emit_notification("delayed.job.erroring_count").with_payload(default_payload.merge(priority: 'high')).with_value(0)
104
+ .and emit_notification("delayed.job.erroring_count").with_payload(default_payload.merge(priority: 'low')).with_value(0)
105
+ .and emit_notification("delayed.job.failed_count").with_payload(default_payload.merge(priority: 'high')).with_value(0)
106
+ .and emit_notification("delayed.job.failed_count").with_payload(default_payload.merge(priority: 'low')).with_value(0)
107
+ .and emit_notification("delayed.job.working_count").with_payload(default_payload.merge(priority: 'high')).with_value(0)
108
+ .and emit_notification("delayed.job.working_count").with_payload(default_payload.merge(priority: 'low')).with_value(0)
109
+ .and emit_notification("delayed.job.workable_count").with_payload(default_payload.merge(priority: 'high')).with_value(0)
110
+ .and emit_notification("delayed.job.workable_count").with_payload(default_payload.merge(priority: 'low')).with_value(0)
111
+ .and emit_notification("delayed.job.max_age").with_payload(default_payload.merge(priority: 'high')).approximately.with_value(0)
112
+ .and emit_notification("delayed.job.max_age").with_payload(default_payload.merge(priority: 'low')).approximately.with_value(0)
113
+ .and emit_notification("delayed.job.max_lock_age").with_payload(default_payload.merge(priority: 'high')).approximately.with_value(0)
114
+ .and emit_notification("delayed.job.max_lock_age").with_payload(default_payload.merge(priority: 'low')).approximately.with_value(0)
115
+ .and emit_notification("delayed.job.alert_age_percent").with_payload(default_payload.merge(priority: 'high')).with_value(0)
116
+ .and emit_notification("delayed.job.alert_age_percent").with_payload(default_payload.merge(priority: 'low')).with_value(0)
117
+ end
118
+ end
119
+
120
+ context 'when there are jobs in the queue' do
121
+ let(:job_attributes) do
122
+ {
123
+ run_at: now,
124
+ queue: 'default',
125
+ handler: "--- !ruby/object:SimpleJob\n",
126
+ attempts: 0,
127
+ }
128
+ end
129
+ let(:failed_attributes) { { run_at: now - 1.week, attempts: 1, failed_at: now - 1.day, locked_at: now - 1.day } }
130
+ let(:p0_attributes) { job_attributes.merge(priority: 1, attempts: 1) }
131
+ let(:p10_attributes) { job_attributes.merge(priority: 13, locked_at: now - 1.day) }
132
+ let(:p20_attributes) { job_attributes.merge(priority: 23, attempts: 1) }
133
+ let(:p30_attributes) { job_attributes.merge(priority: 999, locked_at: now - 1.day) }
134
+ let(:p0_payload) { default_payload.merge(priority: 'interactive') }
135
+ let(:p10_payload) { default_payload.merge(priority: 'user_visible') }
136
+ let(:p20_payload) { default_payload.merge(priority: 'eventual') }
137
+ let(:p30_payload) { default_payload.merge(priority: 'reporting') }
138
+ let!(:p0_workable_job) { Delayed::Job.create! p0_attributes.merge(run_at: now - 30.seconds) }
139
+ let!(:p0_failed_job) { Delayed::Job.create! p0_attributes.merge(failed_attributes) }
140
+ let!(:p0_future_job) { Delayed::Job.create! p0_attributes.merge(run_at: now + 1.hour) }
141
+ let!(:p0_working_job) { Delayed::Job.create! p0_attributes.merge(locked_at: now - 3.minutes) }
142
+ let!(:p10_workable_job) { Delayed::Job.create! p10_attributes.merge(run_at: now - 2.minutes) }
143
+ let!(:p10_failed_job) { Delayed::Job.create! p10_attributes.merge(failed_attributes) }
144
+ let!(:p10_future_job) { Delayed::Job.create! p10_attributes.merge(run_at: now + 1.hour) }
145
+ let!(:p10_working_job) { Delayed::Job.create! p10_attributes.merge(locked_at: now - 7.minutes) }
146
+ let!(:p20_workable_job) { Delayed::Job.create! p20_attributes.merge(run_at: now - 1.hour) }
147
+ let!(:p20_failed_job) { Delayed::Job.create! p20_attributes.merge(failed_attributes) }
148
+ let!(:p20_future_job) { Delayed::Job.create! p20_attributes.merge(run_at: now + 1.hour) }
149
+ let!(:p20_working_job) { Delayed::Job.create! p20_attributes.merge(locked_at: now - 9.minutes) }
150
+ let!(:p30_workable_job) { Delayed::Job.create! p30_attributes.merge(run_at: now - 6.hours) }
151
+ let!(:p30_failed_job) { Delayed::Job.create! p30_attributes.merge(failed_attributes) }
152
+ let!(:p30_future_job) { Delayed::Job.create! p30_attributes.merge(run_at: now + 1.hour) }
153
+ let!(:p30_working_job) { Delayed::Job.create! p30_attributes.merge(locked_at: now - 11.minutes) }
154
+ let!(:p30_workable_job_in_other_queue) { Delayed::Job.create! p30_attributes.merge(run_at: now - 4.hours, queue: 'banana') }
193
155
 
194
156
  it 'emits the expected results for each metric' do
195
157
  expect { subject.run! }
196
158
  .to emit_notification("delayed.monitor.run").with_payload(default_payload.except(:queue))
197
- .and emit_notification("delayed.job.count").with_payload(p0_payload).with_value(8)
198
- .and emit_notification("delayed.job.future_count").with_payload(p0_payload).with_value(2)
199
- .and emit_notification("delayed.job.locked_count").with_payload(p0_payload).with_value(2)
159
+ .and emit_notification("delayed.job.count").with_payload(p0_payload).with_value(4)
160
+ .and emit_notification("delayed.job.future_count").with_payload(p0_payload).with_value(1)
161
+ .and emit_notification("delayed.job.locked_count").with_payload(p0_payload).with_value(1)
200
162
  .and emit_notification("delayed.job.erroring_count").with_payload(p0_payload).with_value(3)
201
- .and emit_notification("delayed.job.failed_count").with_payload(p0_payload).with_value(2)
202
- .and emit_notification("delayed.job.working_count").with_payload(p0_payload).with_value(2)
203
- .and emit_notification("delayed.job.workable_count").with_payload(p0_payload).with_value(2)
204
- .and emit_notification("delayed.job.max_age").with_payload(p0_payload).with_value(2.minutes)
205
- .and emit_notification("delayed.job.max_lock_age").with_payload(p0_payload).with_value(7.minutes)
206
- .and emit_notification("delayed.job.alert_age_percent").with_payload(p0_payload).with_value(0)
207
- .and emit_notification("delayed.job.count").with_payload(p20_payload).with_value(8)
208
- .and emit_notification("delayed.job.future_count").with_payload(p20_payload).with_value(2)
209
- .and emit_notification("delayed.job.locked_count").with_payload(p20_payload).with_value(2)
163
+ .and emit_notification("delayed.job.failed_count").with_payload(p0_payload).with_value(1)
164
+ .and emit_notification("delayed.job.working_count").with_payload(p0_payload).with_value(1)
165
+ .and emit_notification("delayed.job.workable_count").with_payload(p0_payload).with_value(1)
166
+ .and emit_notification("delayed.job.max_age").with_payload(p0_payload).approximately.with_value(30.seconds)
167
+ .and emit_notification("delayed.job.max_lock_age").with_payload(p0_payload).approximately.with_value(3.minutes)
168
+ .and emit_notification("delayed.job.alert_age_percent").with_payload(p0_payload).approximately.with_value(30.0.seconds / 1.minute * 100)
169
+ .and emit_notification("delayed.job.count").with_payload(p10_payload).with_value(4)
170
+ .and emit_notification("delayed.job.future_count").with_payload(p10_payload).with_value(1)
171
+ .and emit_notification("delayed.job.locked_count").with_payload(p10_payload).with_value(1)
172
+ .and emit_notification("delayed.job.erroring_count").with_payload(p10_payload).with_value(0)
173
+ .and emit_notification("delayed.job.failed_count").with_payload(p10_payload).with_value(1)
174
+ .and emit_notification("delayed.job.working_count").with_payload(p10_payload).with_value(1)
175
+ .and emit_notification("delayed.job.workable_count").with_payload(p10_payload).with_value(1)
176
+ .and emit_notification("delayed.job.max_age").with_payload(p10_payload).approximately.with_value(2.minutes)
177
+ .and emit_notification("delayed.job.max_lock_age").with_payload(p10_payload).approximately.with_value(7.minutes)
178
+ .and emit_notification("delayed.job.alert_age_percent").with_payload(p10_payload).approximately.with_value(2.0.minutes / 3.minutes * 100)
179
+ .and emit_notification("delayed.job.count").with_payload(p20_payload).with_value(4)
180
+ .and emit_notification("delayed.job.future_count").with_payload(p20_payload).with_value(1)
181
+ .and emit_notification("delayed.job.locked_count").with_payload(p20_payload).with_value(1)
210
182
  .and emit_notification("delayed.job.erroring_count").with_payload(p20_payload).with_value(3)
211
- .and emit_notification("delayed.job.failed_count").with_payload(p20_payload).with_value(2)
212
- .and emit_notification("delayed.job.working_count").with_payload(p20_payload).with_value(2)
213
- .and emit_notification("delayed.job.workable_count").with_payload(p20_payload).with_value(2)
214
- .and emit_notification("delayed.job.max_age").with_payload(p20_payload).with_value(6.hours)
215
- .and emit_notification("delayed.job.max_lock_age").with_payload(p20_payload).with_value(11.minutes)
216
- .and emit_notification("delayed.job.alert_age_percent").with_payload(p20_payload).with_value(0)
217
- .and emit_notification("delayed.job.workable_count").with_payload(p20_payload.merge(queue: 'banana')).with_value(1)
218
- .and emit_notification("delayed.job.max_age").with_payload(p20_payload.merge(queue: 'banana')).with_value(4.hours)
183
+ .and emit_notification("delayed.job.failed_count").with_payload(p20_payload).with_value(1)
184
+ .and emit_notification("delayed.job.working_count").with_payload(p20_payload).with_value(1)
185
+ .and emit_notification("delayed.job.workable_count").with_payload(p20_payload).with_value(1)
186
+ .and emit_notification("delayed.job.max_age").with_payload(p20_payload).approximately.with_value(1.hour)
187
+ .and emit_notification("delayed.job.max_lock_age").with_payload(p20_payload).approximately.with_value(9.minutes)
188
+ .and emit_notification("delayed.job.alert_age_percent").with_payload(p20_payload).approximately.with_value(1.hour / 1.5.hours * 100)
189
+ .and emit_notification("delayed.job.count").with_payload(p30_payload).with_value(4)
190
+ .and emit_notification("delayed.job.future_count").with_payload(p30_payload).with_value(1)
191
+ .and emit_notification("delayed.job.locked_count").with_payload(p30_payload).with_value(1)
192
+ .and emit_notification("delayed.job.erroring_count").with_payload(p30_payload).with_value(0)
193
+ .and emit_notification("delayed.job.failed_count").with_payload(p30_payload).with_value(1)
194
+ .and emit_notification("delayed.job.working_count").with_payload(p30_payload).with_value(1)
195
+ .and emit_notification("delayed.job.workable_count").with_payload(p30_payload).with_value(1)
196
+ .and emit_notification("delayed.job.max_age").with_payload(p30_payload).approximately.with_value(6.hours)
197
+ .and emit_notification("delayed.job.max_lock_age").with_payload(p30_payload).approximately.with_value(11.minutes)
198
+ .and emit_notification("delayed.job.alert_age_percent").with_payload(p30_payload).approximately.with_value(100) # 6 hours / 4 hours (overflow)
199
+ .and emit_notification("delayed.job.workable_count").with_payload(p30_payload.merge(queue: 'banana')).with_value(1)
200
+ .and emit_notification("delayed.job.max_age").with_payload(p30_payload.merge(queue: 'banana')).approximately.with_value(4.hours)
219
201
  end
220
202
 
221
- context 'when alert thresholds are specified' do
203
+ context 'when named priorities are customized' do
222
204
  around do |example|
223
- Delayed::Priority.alerts = { high: { age: 3.hours }, low: { age: 1.year } }
205
+ Delayed::Priority.names = { high: 0, low: 20 }
224
206
  example.run
225
207
  ensure
226
- Delayed::Priority.alerts = nil
208
+ Delayed::Priority.names = nil
227
209
  end
210
+ let(:p0_payload) { default_payload.merge(priority: 'high') }
211
+ let(:p20_payload) { default_payload.merge(priority: 'low') }
228
212
 
229
- it 'emits the expected alert_age_percent results' do
213
+ it 'emits the expected results for each metric' do
230
214
  expect { subject.run! }
231
- .to emit_notification("delayed.job.alert_age_percent").with_payload(p0_payload).with_value(2.0.minutes / 3.hours * 100)
232
- .and emit_notification("delayed.job.alert_age_percent").with_payload(p20_payload).with_value(6.0.hours / 1.year * 100)
215
+ .to emit_notification("delayed.monitor.run").with_payload(default_payload.except(:queue))
216
+ .and emit_notification("delayed.job.count").with_payload(p0_payload).with_value(8)
217
+ .and emit_notification("delayed.job.future_count").with_payload(p0_payload).with_value(2)
218
+ .and emit_notification("delayed.job.locked_count").with_payload(p0_payload).with_value(2)
219
+ .and emit_notification("delayed.job.erroring_count").with_payload(p0_payload).with_value(3)
220
+ .and emit_notification("delayed.job.failed_count").with_payload(p0_payload).with_value(2)
221
+ .and emit_notification("delayed.job.working_count").with_payload(p0_payload).with_value(2)
222
+ .and emit_notification("delayed.job.workable_count").with_payload(p0_payload).with_value(2)
223
+ .and emit_notification("delayed.job.max_age").with_payload(p0_payload).approximately.with_value(2.minutes)
224
+ .and emit_notification("delayed.job.max_lock_age").with_payload(p0_payload).approximately.with_value(7.minutes)
225
+ .and emit_notification("delayed.job.alert_age_percent").with_payload(p0_payload).approximately.with_value(0)
226
+ .and emit_notification("delayed.job.count").with_payload(p20_payload).with_value(8)
227
+ .and emit_notification("delayed.job.future_count").with_payload(p20_payload).with_value(2)
228
+ .and emit_notification("delayed.job.locked_count").with_payload(p20_payload).with_value(2)
229
+ .and emit_notification("delayed.job.erroring_count").with_payload(p20_payload).with_value(3)
230
+ .and emit_notification("delayed.job.failed_count").with_payload(p20_payload).with_value(2)
231
+ .and emit_notification("delayed.job.working_count").with_payload(p20_payload).with_value(2)
232
+ .and emit_notification("delayed.job.workable_count").with_payload(p20_payload).with_value(2)
233
+ .and emit_notification("delayed.job.max_age").with_payload(p20_payload).approximately.with_value(6.hours)
234
+ .and emit_notification("delayed.job.max_lock_age").with_payload(p20_payload).approximately.with_value(11.minutes)
235
+ .and emit_notification("delayed.job.alert_age_percent").with_payload(p20_payload).approximately.with_value(0)
236
+ .and emit_notification("delayed.job.workable_count").with_payload(p20_payload.merge(queue: 'banana')).with_value(1)
237
+ .and emit_notification("delayed.job.max_age").with_payload(p20_payload.merge(queue: 'banana')).approximately.with_value(4.hours)
238
+ end
239
+
240
+ context 'when alert thresholds are specified' do
241
+ around do |example|
242
+ Delayed::Priority.alerts = { high: { age: 3.hours }, low: { age: 1.year } }
243
+ example.run
244
+ ensure
245
+ Delayed::Priority.alerts = nil
246
+ end
247
+
248
+ it 'emits the expected alert_age_percent results' do
249
+ expect { subject.run! }
250
+ .to emit_notification("delayed.job.alert_age_percent").with_payload(p0_payload).approximately.with_value(2.0.minutes / 3.hours * 100)
251
+ .and emit_notification("delayed.job.alert_age_percent").with_payload(p20_payload).approximately.with_value(6.0.hours / 1.year * 100)
252
+ end
233
253
  end
234
254
  end
235
- end
236
255
 
237
- context 'when worker queues are specified' do
238
- around do |example|
239
- Delayed::Worker.queues = %w(banana gram)
240
- Delayed::Priority.names = { interactive: 0 } # avoid splitting by priority for simplicity
241
- Delayed::Priority.alerts = { interactive: { age: 8.hours } }
242
- example.run
243
- ensure
244
- Delayed::Priority.names = nil
245
- Delayed::Worker.queues = []
256
+ context 'when worker queues are specified' do
257
+ around do |example|
258
+ Delayed::Worker.queues = %w(banana gram)
259
+ Delayed::Priority.names = { interactive: 0 } # avoid splitting by priority for simplicity
260
+ Delayed::Priority.alerts = { interactive: { age: 8.hours } }
261
+ example.run
262
+ ensure
263
+ Delayed::Priority.names = nil
264
+ Delayed::Worker.queues = []
265
+ end
266
+ let(:banana_payload) { default_payload.merge(queue: 'banana', priority: 'interactive') }
267
+ let(:gram_payload) { default_payload.merge(queue: 'gram', priority: 'interactive') }
268
+
269
+ it 'emits the expected results for each queue' do
270
+ expect { subject.run! }
271
+ .to emit_notification("delayed.monitor.run").with_payload(default_payload.except(:queue))
272
+ .and emit_notification("delayed.job.count").with_payload(banana_payload).with_value(1)
273
+ .and emit_notification("delayed.job.future_count").with_payload(banana_payload).with_value(0)
274
+ .and emit_notification("delayed.job.locked_count").with_payload(banana_payload).with_value(0)
275
+ .and emit_notification("delayed.job.erroring_count").with_payload(banana_payload).with_value(0)
276
+ .and emit_notification("delayed.job.failed_count").with_payload(banana_payload).with_value(0)
277
+ .and emit_notification("delayed.job.working_count").with_payload(banana_payload).with_value(0)
278
+ .and emit_notification("delayed.job.workable_count").with_payload(banana_payload).with_value(1)
279
+ .and emit_notification("delayed.job.max_age").with_payload(banana_payload).approximately.with_value(4.hours)
280
+ .and emit_notification("delayed.job.max_lock_age").with_payload(banana_payload).approximately.with_value(0)
281
+ .and emit_notification("delayed.job.alert_age_percent").with_payload(banana_payload).approximately.with_value(4.0.hours / 8.hours * 100)
282
+ .and emit_notification("delayed.job.count").with_payload(gram_payload).with_value(0)
283
+ .and emit_notification("delayed.job.future_count").with_payload(gram_payload).with_value(0)
284
+ .and emit_notification("delayed.job.locked_count").with_payload(gram_payload).with_value(0)
285
+ .and emit_notification("delayed.job.erroring_count").with_payload(gram_payload).with_value(0)
286
+ .and emit_notification("delayed.job.failed_count").with_payload(gram_payload).with_value(0)
287
+ .and emit_notification("delayed.job.working_count").with_payload(gram_payload).with_value(0)
288
+ .and emit_notification("delayed.job.workable_count").with_payload(gram_payload).with_value(0)
289
+ .and emit_notification("delayed.job.max_age").with_payload(gram_payload).approximately.with_value(0)
290
+ .and emit_notification("delayed.job.max_lock_age").with_payload(gram_payload).approximately.with_value(0)
291
+ .and emit_notification("delayed.job.alert_age_percent").with_payload(gram_payload).approximately.with_value(0)
292
+ end
246
293
  end
247
- let(:banana_payload) { default_payload.merge(queue: 'banana', priority: 'interactive') }
248
- let(:gram_payload) { default_payload.merge(queue: 'gram', priority: 'interactive') }
249
294
 
250
- it 'emits the expected results for each queue' do
251
- expect { subject.run! }
252
- .to emit_notification("delayed.monitor.run").with_payload(default_payload.except(:queue))
253
- .and emit_notification("delayed.job.count").with_payload(banana_payload).with_value(1)
254
- .and emit_notification("delayed.job.future_count").with_payload(banana_payload).with_value(0)
255
- .and emit_notification("delayed.job.locked_count").with_payload(banana_payload).with_value(0)
256
- .and emit_notification("delayed.job.erroring_count").with_payload(banana_payload).with_value(0)
257
- .and emit_notification("delayed.job.failed_count").with_payload(banana_payload).with_value(0)
258
- .and emit_notification("delayed.job.working_count").with_payload(banana_payload).with_value(0)
259
- .and emit_notification("delayed.job.workable_count").with_payload(banana_payload).with_value(1)
260
- .and emit_notification("delayed.job.max_age").with_payload(banana_payload).with_value(4.hours)
261
- .and emit_notification("delayed.job.max_lock_age").with_payload(banana_payload).with_value(0)
262
- .and emit_notification("delayed.job.alert_age_percent").with_payload(banana_payload).with_value(4.0.hours / 8.hours * 100)
263
- .and emit_notification("delayed.job.count").with_payload(gram_payload).with_value(0)
264
- .and emit_notification("delayed.job.future_count").with_payload(gram_payload).with_value(0)
265
- .and emit_notification("delayed.job.locked_count").with_payload(gram_payload).with_value(0)
266
- .and emit_notification("delayed.job.erroring_count").with_payload(gram_payload).with_value(0)
267
- .and emit_notification("delayed.job.failed_count").with_payload(gram_payload).with_value(0)
268
- .and emit_notification("delayed.job.working_count").with_payload(gram_payload).with_value(0)
269
- .and emit_notification("delayed.job.workable_count").with_payload(gram_payload).with_value(0)
270
- .and emit_notification("delayed.job.max_age").with_payload(gram_payload).with_value(0)
271
- .and emit_notification("delayed.job.max_lock_age").with_payload(gram_payload).with_value(0)
272
- .and emit_notification("delayed.job.alert_age_percent").with_payload(gram_payload).with_value(0)
295
+ context 'when using app-local timezone for DB timestamps' do
296
+ let(:app_local_db_time) { true }
297
+
298
+ it 'emits the expected results for each metric' do
299
+ expect { subject.run! }
300
+ .to emit_notification("delayed.monitor.run").with_payload(default_payload.except(:queue))
301
+ .and emit_notification("delayed.job.count").with_payload(p0_payload).with_value(4)
302
+ .and emit_notification("delayed.job.future_count").with_payload(p0_payload).with_value(1)
303
+ .and emit_notification("delayed.job.locked_count").with_payload(p0_payload).with_value(1)
304
+ .and emit_notification("delayed.job.erroring_count").with_payload(p0_payload).with_value(3)
305
+ .and emit_notification("delayed.job.failed_count").with_payload(p0_payload).with_value(1)
306
+ .and emit_notification("delayed.job.working_count").with_payload(p0_payload).with_value(1)
307
+ .and emit_notification("delayed.job.workable_count").with_payload(p0_payload).with_value(1)
308
+ .and emit_notification("delayed.job.max_age").with_payload(p0_payload).approximately.with_value(30.seconds)
309
+ .and emit_notification("delayed.job.max_lock_age").with_payload(p0_payload).approximately.with_value(3.minutes)
310
+ .and emit_notification("delayed.job.alert_age_percent").with_payload(p0_payload).approximately.with_value(30.0.seconds / 1.minute * 100)
311
+ .and emit_notification("delayed.job.count").with_payload(p10_payload).with_value(4)
312
+ .and emit_notification("delayed.job.future_count").with_payload(p10_payload).with_value(1)
313
+ .and emit_notification("delayed.job.locked_count").with_payload(p10_payload).with_value(1)
314
+ .and emit_notification("delayed.job.erroring_count").with_payload(p10_payload).with_value(0)
315
+ .and emit_notification("delayed.job.failed_count").with_payload(p10_payload).with_value(1)
316
+ .and emit_notification("delayed.job.working_count").with_payload(p10_payload).with_value(1)
317
+ .and emit_notification("delayed.job.workable_count").with_payload(p10_payload).with_value(1)
318
+ .and emit_notification("delayed.job.max_age").with_payload(p10_payload).approximately.with_value(2.minutes)
319
+ .and emit_notification("delayed.job.max_lock_age").with_payload(p10_payload).approximately.with_value(7.minutes)
320
+ .and emit_notification("delayed.job.alert_age_percent").with_payload(p10_payload).approximately.with_value(2.0.minutes / 3.minutes * 100)
321
+ .and emit_notification("delayed.job.count").with_payload(p20_payload).with_value(4)
322
+ .and emit_notification("delayed.job.future_count").with_payload(p20_payload).with_value(1)
323
+ .and emit_notification("delayed.job.locked_count").with_payload(p20_payload).with_value(1)
324
+ .and emit_notification("delayed.job.erroring_count").with_payload(p20_payload).with_value(3)
325
+ .and emit_notification("delayed.job.failed_count").with_payload(p20_payload).with_value(1)
326
+ .and emit_notification("delayed.job.working_count").with_payload(p20_payload).with_value(1)
327
+ .and emit_notification("delayed.job.workable_count").with_payload(p20_payload).with_value(1)
328
+ .and emit_notification("delayed.job.max_age").with_payload(p20_payload).approximately.with_value(1.hour)
329
+ .and emit_notification("delayed.job.max_lock_age").with_payload(p20_payload).approximately.with_value(9.minutes)
330
+ .and emit_notification("delayed.job.alert_age_percent").with_payload(p20_payload).approximately.with_value(1.hour / 1.5.hours * 100)
331
+ .and emit_notification("delayed.job.count").with_payload(p30_payload).with_value(4)
332
+ .and emit_notification("delayed.job.future_count").with_payload(p30_payload).with_value(1)
333
+ .and emit_notification("delayed.job.locked_count").with_payload(p30_payload).with_value(1)
334
+ .and emit_notification("delayed.job.erroring_count").with_payload(p30_payload).with_value(0)
335
+ .and emit_notification("delayed.job.failed_count").with_payload(p30_payload).with_value(1)
336
+ .and emit_notification("delayed.job.working_count").with_payload(p30_payload).with_value(1)
337
+ .and emit_notification("delayed.job.workable_count").with_payload(p30_payload).with_value(1)
338
+ .and emit_notification("delayed.job.max_age").with_payload(p30_payload).approximately.with_value(6.hours)
339
+ .and emit_notification("delayed.job.max_lock_age").with_payload(p30_payload).approximately.with_value(11.minutes)
340
+ .and emit_notification("delayed.job.alert_age_percent").with_payload(p30_payload).approximately.with_value(100) # 6 hours / 4 hours (overflow)
341
+ .and emit_notification("delayed.job.workable_count").with_payload(p30_payload.merge(queue: 'banana')).with_value(1)
342
+ .and emit_notification("delayed.job.max_age").with_payload(p30_payload.merge(queue: 'banana')).approximately.with_value(4.hours)
343
+ end
273
344
  end
274
345
  end
275
346
  end
data/spec/helper.rb CHANGED
@@ -38,13 +38,12 @@ end
38
38
 
39
39
  ENV['RAILS_ENV'] = 'test'
40
40
 
41
- db_adapter = ENV["ADAPTER"]
42
- gemfile = ENV["BUNDLE_GEMFILE"]
43
- db_adapter ||= gemfile && gemfile[%r{gemfiles/(.*?)/}] && $1 # rubocop:disable Style/PerlBackrefs
44
- db_adapter ||= "sqlite3"
41
+ def current_adapter
42
+ ENV.fetch('ADAPTER', 'sqlite3')
43
+ end
45
44
 
46
45
  config = YAML.load(ERB.new(File.read("spec/database.yml")).result)
47
- ActiveRecord::Base.establish_connection config[db_adapter]
46
+ ActiveRecord::Base.establish_connection config[current_adapter]
48
47
  ActiveRecord::Base.logger = Delayed.logger
49
48
  ActiveJob::Base.logger = Delayed.logger
50
49
  ActiveRecord::Migration.verbose = false
@@ -58,7 +57,7 @@ end
58
57
  # MySQL 5.7 no longer supports null default values for the primary key
59
58
  # Override the default primary key type in Rails <= 4.0
60
59
  # https://stackoverflow.com/a/34555109
61
- if db_adapter == "mysql2"
60
+ if current_adapter == "mysql2"
62
61
  types = if defined?(ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter)
63
62
  # ActiveRecord 3.2+
64
63
  ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter::NATIVE_DATABASE_TYPES
@@ -208,6 +207,14 @@ else
208
207
  ActiveSupport::Dependencies.autoload_paths << File.dirname(__FILE__)
209
208
  end
210
209
 
210
+ def default_timezone=(zone)
211
+ if ActiveRecord::VERSION::MAJOR >= 7
212
+ ActiveRecord.default_timezone = zone
213
+ else
214
+ ActiveRecord::Base.default_timezone = zone
215
+ end
216
+ end
217
+
211
218
  RSpec::Matchers.define :emit_notification do |expected_event_name|
212
219
  attr_reader :actual, :expected
213
220
 
@@ -217,10 +224,15 @@ RSpec::Matchers.define :emit_notification do |expected_event_name|
217
224
 
218
225
  chain :with_payload, :expected_payload
219
226
  chain :with_value, :expected_value
227
+ chain(:approximately) { @approximately = true }
220
228
  diffable
221
229
 
222
230
  match do |block|
223
- @expected = { event_name: expected_event_name, payload: expected_payload, value: expected_value }
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
+
235
+ @expected = { event_name: expected_event_name, payload: expected_payload, value: @expected_value }
224
236
  @actuals = []
225
237
  callback = ->(name, _started, _finished, _unique_id, payload) do
226
238
  @actuals << { event_name: name, payload: payload.except(:value), value: payload[:value] }
@@ -249,10 +261,6 @@ RSpec::Matchers.define :emit_notification do |expected_event_name|
249
261
  end
250
262
  end
251
263
 
252
- def current_adapter
253
- ENV.fetch('ADAPTER', 'sqlite3')
254
- end
255
-
256
264
  def current_database
257
265
  if current_adapter == 'sqlite3'
258
266
  a_string_ending_with('tmp/database.sqlite')
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.0.3
4
+ version: 2.1.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-02 00:00:00.000000000 Z
22
+ date: 2026-02-05 00:00:00.000000000 Z
23
23
  dependencies:
24
24
  - !ruby/object:Gem::Dependency
25
25
  name: activerecord