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 +4 -4
- data/lib/delayed/monitor.rb +77 -21
- data/lib/delayed/tasks.rb +31 -11
- data/lib/delayed/version.rb +1 -1
- data/spec/delayed/__snapshots__/monitor_spec.rb.snap +681 -303
- data/spec/delayed/job_spec.rb +0 -6
- data/spec/delayed/monitor_spec.rb +292 -221
- data/spec/helper.rb +21 -11
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 984a910472fc4f1dedafcf4060cf076bcda408196bfa80f08039ed2527533a41
|
|
4
|
+
data.tar.gz: 98f7de42e2964ee1576ba6ae3fc2b39cbe76a841263054a5b9634f7e55ce1114
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: dcb018c429ff591409796e410c570f49408eb0b8363759f6c8970ac4ee1773abccc117b5398c3596ecf8491c0e70db548bd4a2ea54ef39a77fa78da43b6cbfe4
|
|
7
|
+
data.tar.gz: 020a785c7bfc81456b8cc7b8ddace55fad1f9468edb3b9cef0ef066eb6aed64b580d30974b48ce97bc7b1602294c9901c7c26fa294d3e4b4ecebd177aef677c8
|
data/lib/delayed/monitor.rb
CHANGED
|
@@ -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(
|
|
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(
|
|
118
|
+
failed_count_grouped.merge(live_count_grouped) { |_, l, f| l + f }
|
|
74
119
|
else
|
|
75
|
-
jobs
|
|
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
|
|
129
|
+
grouped_count(jobs.future)
|
|
81
130
|
end
|
|
82
131
|
|
|
83
132
|
def locked_count_grouped
|
|
84
|
-
@memo[:locked_count_grouped] ||= jobs.claimed
|
|
133
|
+
@memo[:locked_count_grouped] ||= grouped_count(jobs.claimed)
|
|
85
134
|
end
|
|
86
135
|
|
|
87
136
|
def erroring_count_grouped
|
|
88
|
-
jobs.erroring
|
|
137
|
+
grouped_count(jobs.erroring)
|
|
89
138
|
end
|
|
90
139
|
|
|
91
140
|
def failed_count_grouped
|
|
92
|
-
@memo[:failed_count_grouped] ||= jobs.failed
|
|
141
|
+
@memo[:failed_count_grouped] ||= grouped_count(jobs.failed)
|
|
93
142
|
end
|
|
94
143
|
|
|
95
144
|
def max_lock_age_grouped
|
|
96
|
-
|
|
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
|
-
|
|
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
|
-
|
|
109
|
-
max_age =
|
|
110
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
127
|
-
|
|
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
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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'
|
data/lib/delayed/version.rb
CHANGED