delayed 2.0.2 → 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: 16d1a1a8e3f41bee282a4d725afdf3be6419fceba8c5cef58a3eb35a7a406823
4
- data.tar.gz: 9ad241b6880c13997ea7b58ba2bcfba02a1beb6692b050698fac30301cc9c90c
3
+ metadata.gz: 984a910472fc4f1dedafcf4060cf076bcda408196bfa80f08039ed2527533a41
4
+ data.tar.gz: 98f7de42e2964ee1576ba6ae3fc2b39cbe76a841263054a5b9634f7e55ce1114
5
5
  SHA512:
6
- metadata.gz: a63a4f0a56f26c9b5bb3a987b6c2877068016823b21efb059744effc0eeb68255b4b193e7f0e0292f6d790e12138ad8c57bab032a7f1f3c1a89e2c533db45df6
7
- data.tar.gz: 7912ee6d5d633293a2f1707d890c2fd5a60cbdb1c44ec215a8d2414db35902a272e4caad4ece1947c6475f4e90a7b5c1fe54ef320549681c04c948d7786cd4a8
6
+ metadata.gz: dcb018c429ff591409796e410c570f49408eb0b8363759f6c8970ac4ee1773abccc117b5398c3596ecf8491c0e70db548bd4a2ea54ef39a77fa78da43b6cbfe4
7
+ data.tar.gz: 020a785c7bfc81456b8cc7b8ddace55fad1f9468edb3b9cef0ef066eb6aed64b580d30974b48ce97bc7b1602294c9901c7c26fa294d3e4b4ecebd177aef677c8
@@ -18,7 +18,7 @@ module Delayed
18
18
  cattr_accessor :sleep_delay, instance_writer: false, default: 60
19
19
 
20
20
  def initialize
21
- @jobs = Job.group(priority_case_statement).group(:queue)
21
+ @jobs = Job.group(:priority, :queue)
22
22
  @jobs = @jobs.where(queue: Worker.queues) if Worker.queues.any?
23
23
  @memo = {}
24
24
  end
@@ -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
@@ -68,63 +95,92 @@ module Delayed
68
95
  }
69
96
  end
70
97
 
98
+ def grouped_count(scope)
99
+ Delayed::Job.from(scope.select('priority, queue, COUNT(*) AS count'))
100
+ .group(priority_case_statement, :queue).sum(:count)
101
+ end
102
+
103
+ def grouped_min(scope, column)
104
+ Delayed::Job.from(scope.select("priority, queue, MIN(#{column}) AS #{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)
114
+ end
115
+
71
116
  def count_grouped
72
117
  if Job.connection.supports_partial_index?
73
- failed_count_grouped.merge(jobs.live.count) { |_, l, f| l + f }
118
+ failed_count_grouped.merge(live_count_grouped) { |_, l, f| l + f }
74
119
  else
75
- jobs.count
120
+ grouped_count(jobs)
76
121
  end
77
122
  end
78
123
 
124
+ def live_count_grouped
125
+ grouped_count(jobs.live)
126
+ end
127
+
79
128
  def future_count_grouped
80
- jobs.future.count
129
+ grouped_count(jobs.future)
81
130
  end
82
131
 
83
132
  def locked_count_grouped
84
- @memo[:locked_count_grouped] ||= jobs.claimed.count
133
+ @memo[:locked_count_grouped] ||= grouped_count(jobs.claimed)
85
134
  end
86
135
 
87
136
  def erroring_count_grouped
88
- jobs.erroring.count
137
+ grouped_count(jobs.erroring)
89
138
  end
90
139
 
91
140
  def failed_count_grouped
92
- @memo[:failed_count_grouped] ||= jobs.failed.count
141
+ @memo[:failed_count_grouped] ||= grouped_count(jobs.failed)
93
142
  end
94
143
 
95
144
  def max_lock_age_grouped
96
- oldest_locked_job_grouped.each_with_object({}) do |job, metrics|
97
- metrics[[job.priority.to_i, job.queue]] = Job.db_time_now - job.locked_at
98
- end
145
+ oldest_locked_at_query.transform_values { |j| db_now(j) - j.locked_at }
99
146
  end
100
147
 
101
148
  def max_age_grouped
102
- oldest_workable_job_grouped.each_with_object({}) do |job, metrics|
103
- metrics[[job.priority.to_i, job.queue]] = Job.db_time_now - job.run_at
104
- end
149
+ oldest_run_at_query.transform_values { |j| db_now(j) - j.run_at }
105
150
  end
106
151
 
107
152
  def alert_age_percent_grouped
108
- oldest_workable_job_grouped.each_with_object({}) do |job, metrics|
109
- max_age = Job.db_time_now - job.run_at
110
- metrics[[job.priority.to_i, job.queue]] = [max_age / job.priority.alert_age * 100, 100].min if job.priority.alert_age
153
+ oldest_run_at_query.each_with_object({}) do |((priority, queue), j), metrics|
154
+ max_age = db_now(j) - j.run_at
155
+ alert_age = Priority.new(priority).alert_age
156
+ metrics[[priority, queue]] = [max_age / alert_age * 100, 100].min if alert_age
111
157
  end
112
158
  end
113
159
 
114
160
  def workable_count_grouped
115
- jobs.claimable.count
161
+ grouped_count(jobs.claimable)
116
162
  end
117
163
 
118
164
  alias working_count_grouped locked_count_grouped
119
165
 
120
166
  def oldest_locked_job_grouped
121
- jobs.claimed
122
- .select("#{priority_case_statement} AS priority, queue, MIN(locked_at) AS locked_at")
167
+ oldest_locked_at_query.transform_values(&:locked_at)
123
168
  end
124
169
 
125
170
  def oldest_workable_job_grouped
126
- @memo[:oldest_workable_job_grouped] ||= jobs.claimable
127
- .select("(#{priority_case_statement}) AS priority, queue, MIN(run_at) AS 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)
128
184
  end
129
185
 
130
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.2'
4
+ VERSION = '2.1.0'
5
5
  end