delayed 2.1.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 984a910472fc4f1dedafcf4060cf076bcda408196bfa80f08039ed2527533a41
4
- data.tar.gz: 98f7de42e2964ee1576ba6ae3fc2b39cbe76a841263054a5b9634f7e55ce1114
3
+ metadata.gz: 6d365ce7fa45762c6122da4a21a7c96e30893c15534f76bb26cbb4e3f721f7c6
4
+ data.tar.gz: ca6deb755821f865e354a4ff62375d7c918c81e0b9b4be5f665aa9a347ee90e9
5
5
  SHA512:
6
- metadata.gz: dcb018c429ff591409796e410c570f49408eb0b8363759f6c8970ac4ee1773abccc117b5398c3596ecf8491c0e70db548bd4a2ea54ef39a77fa78da43b6cbfe4
7
- data.tar.gz: 020a785c7bfc81456b8cc7b8ddace55fad1f9468edb3b9cef0ef066eb6aed64b580d30974b48ce97bc7b1602294c9901c7c26fa294d3e4b4ecebd177aef677c8
6
+ metadata.gz: 936fabfd27c2ae4ee68c5f999f8b10e778a171b7f438e191a6bdadfc4c3b16da455e5f1620ff0044fc7efa257308fe0679fc74ec879342d0c5c5a248a6d657d0
7
+ data.tar.gz: 5cb3cdb9e392ed6ae999d768fe910d0587fa1c5cb652cd5a2cd2f5ce2fc88a8598c105a199e831e0c3185af47cc310e82ada88bc862862fe4c9ebd291166a958
@@ -10,26 +10,22 @@ module Delayed
10
10
 
11
11
  # high-level queue states (live => erroring => failed)
12
12
  scope :live, -> { where(failed_at: nil) }
13
- scope :erroring, -> { where(arel_table[:attempts].gt(0)).merge(unscoped.live) }
13
+ scope :erroring, -> { where(erroring_clause).merge(unscoped.live) }
14
14
  scope :failed, -> { where.not(failed_at: nil) }
15
15
 
16
16
  # live queue states (future vs pending)
17
- scope :future, ->(as_of = db_time_now) { merge(unscoped.live).where(arel_table[:run_at].gt(as_of)) }
18
- scope :pending, ->(as_of = db_time_now) { merge(unscoped.live).where(arel_table[:run_at].lteq(as_of)) }
17
+ scope :future, ->(as_of = db_time_now) { merge(unscoped.live).where(future_clause(as_of)) }
18
+ scope :pending, ->(as_of = db_time_now) { merge(unscoped.live).where(pending_clause(as_of)) }
19
19
 
20
20
  # pending queue states (claimed vs claimable)
21
21
  scope :claimed, ->(as_of = db_time_now) {
22
- where(arel_table[:locked_at].gteq(db_time_now - lock_timeout))
23
- .merge(unscoped.pending(as_of))
22
+ where(claimed_clause(as_of)).merge(unscoped.pending(as_of))
24
23
  }
25
24
  scope :claimed_by, ->(worker, as_of = db_time_now) {
26
- where(locked_by: worker.name)
27
- .claimed(as_of)
25
+ where(locked_by: worker.name).claimed(as_of)
28
26
  }
29
27
  scope :claimable, ->(as_of = db_time_now) {
30
- where(locked_at: nil)
31
- .or(where(arel_table[:locked_at].lt(db_time_now - lock_timeout)))
32
- .merge(unscoped.pending(as_of))
28
+ where(claimable_clause(as_of)).merge(unscoped.pending(as_of))
33
29
  }
34
30
  scope :claimable_by, ->(worker, as_of = db_time_now) {
35
31
  claimable(as_of)
@@ -40,6 +36,26 @@ module Delayed
40
36
  .by_priority
41
37
  }
42
38
 
39
+ def self.erroring_clause
40
+ arel_table[:attempts].gt(0)
41
+ end
42
+
43
+ def self.future_clause(as_of = db_time_now)
44
+ arel_table[:run_at].gt(as_of)
45
+ end
46
+
47
+ def self.pending_clause(as_of = db_time_now)
48
+ arel_table[:run_at].lteq(as_of)
49
+ end
50
+
51
+ def self.claimed_clause(as_of = db_time_now)
52
+ arel_table[:locked_at].gteq(as_of - lock_timeout)
53
+ end
54
+
55
+ def self.claimable_clause(as_of = db_time_now)
56
+ arel_table[:locked_at].eq(nil).or arel_table[:locked_at].lt(as_of - lock_timeout)
57
+ end
58
+
43
59
  before_save :set_default_run_at, :set_name
44
60
 
45
61
  REENQUEUE_BUFFER = 30.seconds
@@ -13,6 +13,7 @@ module Delayed
13
13
  set_queue_name
14
14
  set_priority
15
15
  handle_dst
16
+ reject_stale_run_at
16
17
  handle_deprecation
17
18
  options
18
19
  end
@@ -51,6 +52,18 @@ module Delayed
51
52
  end
52
53
  end
53
54
 
55
+ def reject_stale_run_at
56
+ return unless Delayed::Worker.deny_stale_enqueues
57
+ return unless options[:run_at]
58
+
59
+ threshold = Job.db_time_now - Job.lock_timeout
60
+ return unless options[:run_at] < threshold
61
+
62
+ raise StaleEnqueueError,
63
+ "Cannot enqueue a job in the distant past (run_at: #{options[:run_at].iso8601}," \
64
+ " threshold: #{threshold.iso8601}). This is usually a bug."
65
+ end
66
+
54
67
  def handle_deprecation
55
68
  unless options[:payload_object].respond_to?(:perform)
56
69
  raise ArgumentError,
@@ -14,4 +14,6 @@ module Delayed
14
14
  class FatalBackendError < RuntimeError; end
15
15
 
16
16
  class DeserializationError < StandardError; end
17
+
18
+ class StaleEnqueueError < StandardError; end
17
19
  end
@@ -38,7 +38,7 @@ module Delayed
38
38
  def self.sql_now_in_utc
39
39
  case ActiveRecord::Base.connection.adapter_name
40
40
  when 'PostgreSQL'
41
- "TIMEZONE('UTC', NOW())"
41
+ "TIMEZONE('UTC', STATEMENT_TIMESTAMP())"
42
42
  when 'MySQL', 'Mysql2'
43
43
  "UTC_TIMESTAMP()"
44
44
  else
@@ -95,103 +95,129 @@ module Delayed
95
95
  }
96
96
  end
97
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)
98
+ # This method generates a query that scans the specified scope, groups by
99
+ # priority and queue, and calculates the specified aggregates. An outer
100
+ # query is executed for priority bucketing and appending db_now_utc (to
101
+ # avoid running these computations for each tuple in the inner query).
102
+ def grouped_query(scope, include_db_time: false, **kwargs)
103
+ inner_selects = kwargs.map { |key, (agg, expr)| as_expression(agg, expr, key) }
104
+ outer_selects = kwargs.map { |key, (agg, _)| as_expression(agg == :count ? :sum : agg, key, key) }
105
+ outer_selects << "#{self.class.sql_now_in_utc} AS db_now_utc" if include_db_time
106
+
107
+ Delayed::Job
108
+ .from(scope.select(:priority, :queue, *inner_selects).group(:priority, :queue))
109
+ .group(priority_case_statement, :queue).select(
110
+ *outer_selects,
111
+ "#{priority_case_statement} AS priority",
112
+ 'queue AS queue',
113
+ ).group_by { |j| [j.priority.to_i, j.queue] }
114
+ .transform_values(&:first)
101
115
  end
102
116
 
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)
117
+ def as_expression(aggregate_function, aggregate_expression, column_name)
118
+ "#{aggregate_function.to_s.upcase}(#{aggregate_expression}) AS #{column_name}"
114
119
  end
115
120
 
116
121
  def count_grouped
117
- if Job.connection.supports_partial_index?
118
- failed_count_grouped.merge(live_count_grouped) { |_, l, f| l + f }
119
- else
120
- grouped_count(jobs)
121
- end
122
+ failed_count_grouped.merge(live_count_grouped) { |_, l, f| l + f }
122
123
  end
123
124
 
124
125
  def live_count_grouped
125
- grouped_count(jobs.live)
126
+ live_counts.transform_values(&:count)
126
127
  end
127
128
 
128
129
  def future_count_grouped
129
- grouped_count(jobs.future)
130
+ live_counts.transform_values(&:future_count)
130
131
  end
131
132
 
132
- def locked_count_grouped
133
- @memo[:locked_count_grouped] ||= grouped_count(jobs.claimed)
133
+ def erroring_count_grouped
134
+ live_counts.transform_values(&:erroring_count)
134
135
  end
135
136
 
136
- def erroring_count_grouped
137
- grouped_count(jobs.erroring)
137
+ def locked_count_grouped
138
+ pending_counts.transform_values(&:claimed_count)
138
139
  end
139
140
 
140
141
  def failed_count_grouped
141
- @memo[:failed_count_grouped] ||= grouped_count(jobs.failed)
142
+ failed_counts.transform_values(&:count)
142
143
  end
143
144
 
144
145
  def max_lock_age_grouped
145
- oldest_locked_at_query.transform_values { |j| db_now(j) - j.locked_at }
146
+ pending_counts.transform_values { |j| time_ago(db_now(j), j.locked_at) }
146
147
  end
147
148
 
148
149
  def max_age_grouped
149
- oldest_run_at_query.transform_values { |j| db_now(j) - j.run_at }
150
+ live_counts.transform_values { |j| time_ago(db_now(j), j.run_at) }
150
151
  end
151
152
 
152
153
  def alert_age_percent_grouped
153
- oldest_run_at_query.each_with_object({}) do |((priority, queue), j), metrics|
154
- max_age = db_now(j) - j.run_at
154
+ live_counts.each_with_object({}) do |((priority, queue), j), metrics|
155
+ max_age = time_ago(db_now(j), j.run_at)
155
156
  alert_age = Priority.new(priority).alert_age
156
157
  metrics[[priority, queue]] = [max_age / alert_age * 100, 100].min if alert_age
157
158
  end
158
159
  end
159
160
 
160
161
  def workable_count_grouped
161
- grouped_count(jobs.claimable)
162
+ pending_counts.transform_values(&:claimable_count)
162
163
  end
163
164
 
164
165
  alias working_count_grouped locked_count_grouped
165
166
 
166
167
  def oldest_locked_job_grouped
167
- oldest_locked_at_query.transform_values(&:locked_at)
168
+ pending_counts.transform_values(&:locked_at).compact
168
169
  end
169
170
 
170
171
  def oldest_workable_job_grouped
171
- oldest_run_at_query.transform_values(&:run_at)
172
+ live_counts.transform_values(&:run_at).compact
173
+ end
174
+
175
+ def live_counts
176
+ @memo[:live_counts] ||= grouped_query(
177
+ jobs.live,
178
+ include_db_time: true,
179
+ count: [:count, '*'],
180
+ future_count: [:sum, case_when(Job.future_clause.to_sql)],
181
+ erroring_count: [:sum, case_when(Job.erroring_clause.to_sql)],
182
+ run_at: [:min, case_when(Job.pending_clause.to_sql, 'run_at')],
183
+ )
172
184
  end
173
185
 
174
- def oldest_locked_at_query
175
- @memo[:oldest_locked_at_query] ||= grouped_min(jobs.claimed, :locked_at)
186
+ def pending_counts
187
+ @memo[:pending_counts] ||= grouped_query(
188
+ jobs.pending,
189
+ include_db_time: true,
190
+ claimed_count: [:sum, case_when(Job.claimed_clause.to_sql)],
191
+ claimable_count: [:sum, case_when(Job.claimable_clause.to_sql)],
192
+ locked_at: [:min, case_when(Job.claimed_clause.to_sql, 'locked_at')],
193
+ )
176
194
  end
177
195
 
178
- def oldest_run_at_query
179
- @memo[:oldest_run_at_query] ||= grouped_min(jobs.claimable, :run_at)
196
+ def failed_counts
197
+ @memo[:failed_counts] ||= grouped_query(jobs.failed, count: [:count, '*'])
180
198
  end
181
199
 
182
200
  def db_now(record)
183
201
  self.class.parse_utc_time(record.db_now_utc)
184
202
  end
185
203
 
204
+ def time_ago(now, value)
205
+ [now - (value || now), 0].max
206
+ end
207
+
208
+ def case_when(condition, true_val = 1)
209
+ "CASE WHEN #{condition} THEN #{true_val} ELSE #{true_val == 1 ? 0 : 'NULL'} END"
210
+ end
211
+
186
212
  def priority_case_statement
187
213
  [
188
214
  'CASE',
189
215
  Priority.ranges.values.map do |range|
190
- [
191
- "WHEN priority >= #{range.first.to_i}",
192
- ("AND priority < #{range.last.to_i}" unless range.last.infinite?),
193
- "THEN #{range.first.to_i}",
194
- ].compact
216
+ if range.last.infinite?
217
+ "WHEN priority >= #{range.first.to_i} THEN #{range.first.to_i}"
218
+ else
219
+ "WHEN priority < #{range.last.to_i} THEN #{range.first.to_i}"
220
+ end
195
221
  end,
196
222
  'END',
197
223
  ].flatten.join(' ')
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Delayed
4
- VERSION = '2.1.0'
4
+ VERSION = '2.2.0'
5
5
  end
@@ -22,6 +22,8 @@ module Delayed
22
22
  cattr_accessor :read_ahead, instance_writer: false, default: 5
23
23
  cattr_accessor :destroy_failed_jobs, instance_writer: false, default: false
24
24
 
25
+ cattr_accessor :deny_stale_enqueues, instance_writer: false, default: false
26
+
25
27
  cattr_accessor :min_priority, :max_priority, instance_writer: false
26
28
 
27
29
  # TODO: Remove this and rely on ActiveJob.queue_name when no queue is specified