inst-jobs 3.3.1 → 3.4.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: 3d68b34c07b15f04a5313d008bcca9c3ba29ddaadcef78f4faf4b5a9e421b0be
4
- data.tar.gz: 3e8a3404c80e4ab24dcbb42ffa6cdb2cbe50059ac783c37a2b7d58154519e23e
3
+ metadata.gz: a4ab079657570b1e9ce94abdd938367ba912afa077ccad380f38f81862fe5a43
4
+ data.tar.gz: a0366132ef9d5cced326cdecef3446217857e668760fae47a72f664060fc380a
5
5
  SHA512:
6
- metadata.gz: 06aa4b9cf01c2f71e28963b40b647594775c3ba1f671935fb90a7fa92d416caa139ef0f970fa1f30d6301c7ade05fcf6b5518ad38544c08d2c079df72a4ee7e1
7
- data.tar.gz: ea2966eb4501fad7e37339ba3ee8796ed19ff592b7d91cda9bed10a861b0d7d967b8b7ca3486abab0eab329ae860ea4ecd9c97d98c878d3fb265b7142756a6cb
6
+ metadata.gz: 04d51cfd152ee4a6e5145df09d1f8fec52d60c4eb74287e49d693a29f62498ca9a09655ac0dbb5ce6137817e309a7a47fb5918b737d41a2f78f64c6c8c43ff4e
7
+ data.tar.gz: ed4b8778c404ea2a5f57644e71b5b5bdb8be9f135056acd8eba46dcb7fec58eb39cc5bf5deb05cd4bf3e6754334d7fedb9fe5239291b276bebc1595946d97b45
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ class AddCancelledAtToDelayedJobs < ActiveRecord::Migration[7.1]
4
+ def change
5
+ add_column :delayed_jobs, :cancelled_at, :datetime
6
+ add_column :failed_jobs, :cancelled_at, :datetime
7
+ end
8
+ end
@@ -23,6 +23,7 @@ module Delayed
23
23
  # Contains the work object as a YAML field.
24
24
  class Job < AbstractJob
25
25
  include Delayed::Backend::Base
26
+
26
27
  self.table_name = :delayed_jobs
27
28
 
28
29
  attr_accessor :enqueue_result
@@ -214,6 +215,12 @@ module Delayed
214
215
  where(locked_by: worker_name).update_all(locked_by: nil, locked_at: nil)
215
216
  end
216
217
 
218
+ def self.cancel!
219
+ deleted = where("locked_by IS NULL OR locked_by=?", ON_HOLD_LOCKED_BY).delete_all
220
+ marked = update_all(cancelled_at: db_time_now)
221
+ [deleted, marked]
222
+ end
223
+
217
224
  def self.strand_size(strand)
218
225
  where(strand:).count
219
226
  end
@@ -612,6 +619,18 @@ module Delayed
612
619
  single_step_create
613
620
  end
614
621
 
622
+ # Delete unlocked jobs; if the job is locked mark it for the job worker
623
+ # to cancel within a reasonable amount of time
624
+ def cancel!
625
+ if self.class.where(id:).where("locked_by IS NULL OR locked_by=?", ON_HOLD_LOCKED_BY).delete_all == 1
626
+ @destroyed = true
627
+ return true
628
+ end
629
+
630
+ update!(cancelled_at: self.class.db_time_now)
631
+ false
632
+ end
633
+
615
634
  def fail!
616
635
  attrs = attributes
617
636
  attrs["original_job_id"] = attrs.delete("id") if Failed.columns_hash.key?("original_job_id")
@@ -633,6 +652,7 @@ module Delayed
633
652
 
634
653
  class Failed < Job
635
654
  include Delayed::Backend::Base
655
+
636
656
  self.table_name = :failed_jobs
637
657
 
638
658
  def self.cleanup_old_jobs(before_date, batch_size: 10_000)
@@ -644,6 +664,7 @@ module Delayed
644
664
  "last_error",
645
665
  "locked_at",
646
666
  "failed_at",
667
+ "cancelled_at",
647
668
  "locked_by",
648
669
  "original_job_id",
649
670
  "requeued_job_id")
@@ -31,7 +31,7 @@ module Delayed
31
31
  # :priority, :run_at, :queue, :strand, :singleton
32
32
  # Example: Delayed::Job.enqueue(object, priority: 0, run_at: time, queue: queue)
33
33
  def enqueue(object,
34
- priority: default_priority,
34
+ priority: nil,
35
35
  run_at: nil,
36
36
  expires_at: nil,
37
37
  queue: Delayed::Settings.queue,
@@ -44,6 +44,7 @@ module Delayed
44
44
  raise ArgumentError, "Cannot enqueue items which do not respond to perform"
45
45
  end
46
46
 
47
+ priority ||= default_priority
47
48
  strand ||= singleton if Settings.infer_strand_from_singleton
48
49
 
49
50
  kwargs = Settings.default_job_options.merge(kwargs)
@@ -209,7 +210,7 @@ module Delayed
209
210
 
210
211
  unless pid
211
212
  job_pid = $1.to_i
212
- running = Process.kill(0, job_pid) rescue false
213
+ running = Delayed::Util.process_running?(job_pid)
213
214
  end
214
215
 
215
216
  if running
@@ -233,6 +234,10 @@ module Delayed
233
234
  expires_at && (self.class.db_time_now >= expires_at)
234
235
  end
235
236
 
237
+ def cancelled?
238
+ cancelled_at && (self.class.db_time_now >= cancelled_at)
239
+ end
240
+
236
241
  def inferred_max_attempts
237
242
  max_attempts || Delayed::Settings.max_attempts
238
243
  end
@@ -12,8 +12,8 @@ module Delayed
12
12
  end
13
13
 
14
14
  def status(print: true, pid: self.pid)
15
- alive = pid && (Process.kill(0, pid) rescue false) && :running
16
- alive ||= :draining if pid && Process.kill(0, -pid) rescue false
15
+ alive = pid && Delayed::Util.process_running?(pid) && :running
16
+ alive ||= :draining if pid && Delayed::Util.process_running?(-pid)
17
17
  if alive
18
18
  puts "Delayed jobs #{alive}, pool PID: #{pid}" if print
19
19
  elsif print && print != :alive
@@ -25,6 +25,7 @@ module Delayed
25
25
  :silence_periodic_log,
26
26
  :sleep_delay,
27
27
  :sleep_delay_stagger,
28
+ :cancellation_check_interval,
28
29
  :slow_exit_timeout,
29
30
  :stranded_run_at_grace_period,
30
31
  :worker_health_check_type,
@@ -130,6 +131,7 @@ module Delayed
130
131
  self.max_attempts = 1
131
132
  self.sleep_delay = 2.0
132
133
  self.sleep_delay_stagger = 2.0
134
+ self.cancellation_check_interval = 10.0
133
135
  self.fetch_batch_size = 5
134
136
  self.select_random_from_batch = false
135
137
  self.silence_periodic_log = false
@@ -3,7 +3,9 @@
3
3
  module Delayed
4
4
  module Testing
5
5
  def self.run_job(job)
6
- Delayed::Worker.new.perform(job)
6
+ Delayed::Worker.running_job(job) do
7
+ Delayed::Worker.new.perform(job)
8
+ end
7
9
  end
8
10
 
9
11
  def self.drain
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Delayed
4
+ module Util
5
+ def self.process_running?(pid)
6
+ Process.kill(0, pid)
7
+ true
8
+ rescue Errno::ESRCH
9
+ false
10
+ end
11
+ end
12
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Delayed
4
- VERSION = "3.3.1"
4
+ VERSION = "3.4.0"
5
5
  end
@@ -9,6 +9,7 @@ module Delayed
9
9
  attr_reader :clients, :listen_socket
10
10
 
11
11
  include Delayed::Logging
12
+
12
13
  SIGNALS = %i[INT TERM QUIT].freeze
13
14
 
14
15
  def initialize(listen_socket, parent_pid: nil, config: Settings.parent_process)
@@ -23,6 +24,7 @@ module Delayed
23
24
 
24
25
  @exit = false
25
26
  @self_pipe = IO.pipe
27
+ @last_cancellation_check = Delayed::Job.db_time_now
26
28
  end
27
29
 
28
30
  def connected_clients
@@ -78,6 +80,11 @@ module Delayed
78
80
  check_for_work(forced_latency:)
79
81
  end
80
82
  unlock_timed_out_prefetched_jobs
83
+
84
+ return if @last_cancellation_check + Settings.cancellation_check_interval > Delayed::Job.db_time_now
85
+
86
+ @last_cancellation_check = Delayed::Job.db_time_now
87
+ check_for_cancellations
81
88
  end
82
89
 
83
90
  def handle_read(socket)
@@ -96,7 +103,7 @@ module Delayed
96
103
  # and terminate the work queue process, to be restarted by the parent daemon.
97
104
  def handle_accept
98
105
  socket, _addr = @listen_socket.accept_nonblock
99
- @clients[socket] = ClientState.new(false, socket) if socket
106
+ @clients[socket] = ClientState.new(false, nil, socket) if socket
100
107
  rescue IO::WaitReadable
101
108
  logger.error("Server attempted to read listen_socket but failed with IO::WaitReadable")
102
109
  # ignore and just try accepting again next time through the loop
@@ -115,6 +122,7 @@ module Delayed
115
122
  worker_name, worker_config = Marshal.load(socket)
116
123
  client.name = worker_name
117
124
  client.working = false
125
+ client.active_job_id = nil
118
126
  (@waiting_clients[worker_config] ||= []) << client
119
127
  rescue SystemCallError, IOError => e
120
128
  logger.error("Receiving message from client (#{socket}) failed: #{e.inspect}")
@@ -135,6 +143,7 @@ module Delayed
135
143
  next
136
144
  end
137
145
  client.working = true
146
+ client.active_job_id = job.id
138
147
  begin
139
148
  logger.debug("Sending prefetched job #{job.id} to #{client.name}")
140
149
  client_timeout { Marshal.dump(job, client.socket) }
@@ -176,6 +185,7 @@ module Delayed
176
185
  end
177
186
  client = workers.find { |worker| worker.name == worker_name }
178
187
  client.working = true
188
+ client.active_job_id = locked_jobs.id
179
189
  jobs_to_send << [client, locked_jobs]
180
190
  end
181
191
  end
@@ -244,6 +254,36 @@ module Delayed
244
254
  end
245
255
  end
246
256
 
257
+ def check_for_cancellations
258
+ job_map = @clients.values.select(&:active_job_id).index_by(&:active_job_id)
259
+ prefetched_ids = @prefetched_jobs.values.flatten.map(&:id)
260
+ return if job_map.empty? && prefetched_ids.empty?
261
+
262
+ canceled_jobs = Delayed::Job.where(id: job_map.keys + prefetched_ids)
263
+ .where(cancelled_at: ..Delayed::Job.db_time_now)
264
+ .pluck(:id)
265
+ return if canceled_jobs.empty?
266
+
267
+ canceled_prefetched = canceled_jobs & prefetched_ids
268
+ unless canceled_prefetched.empty?
269
+ logger.debug("Deleting cancelled prefetched jobs #{canceled_prefetched.join(", ")}")
270
+ Delayed::Job.where(id: canceled_prefetched, locked_by: prefetch_owner).delete_all
271
+ @prefetched_jobs.each_value { |jobs| jobs.reject! { |job| canceled_prefetched.include?(job.id) } }
272
+ end
273
+
274
+ (canceled_jobs & job_map.keys).each do |job_id|
275
+ client = job_map[job_id]
276
+ pid = client.name.split(":").last.to_i
277
+ next unless pid.positive?
278
+
279
+ begin
280
+ Process.kill("USR1", pid)
281
+ rescue Errno::ESRCH
282
+ # ignore exited process
283
+ end
284
+ end
285
+ end
286
+
247
287
  def drop_socket(socket)
248
288
  # this socket went away
249
289
  begin
@@ -274,7 +314,7 @@ module Delayed
274
314
  Timeout.timeout(@client_timeout, &)
275
315
  end
276
316
 
277
- ClientState = Struct.new(:working, :socket, :name)
317
+ ClientState = Struct.new(:working, :active_job_id, :socket, :name)
278
318
  end
279
319
  end
280
320
  end
@@ -5,11 +5,11 @@ module Delayed
5
5
  class NullHealthCheck < HealthCheck
6
6
  self.type_name = :none
7
7
 
8
- def start
8
+ def start # rubocop:disable Naming/PredicateMethod
9
9
  true
10
10
  end
11
11
 
12
- def stop
12
+ def stop # rubocop:disable Naming/PredicateMethod
13
13
  true
14
14
  end
15
15
 
@@ -3,6 +3,11 @@
3
3
  module Delayed
4
4
  class TimeoutError < RuntimeError; end
5
5
 
6
+ # Use Exception so that user code that rescues regular errors won't accidentally catch this
7
+ # rubocop:disable Lint/InheritException
8
+ class CancelledError < Exception; end
9
+ # rubocop:enable Lint/InheritException
10
+
6
11
  class RetriableError < RuntimeError
7
12
  # this error is a special case. You _should_ raise
8
13
  # it from inside the rescue block for another error,
@@ -20,7 +25,8 @@ module Delayed
20
25
 
21
26
  class Worker
22
27
  include Delayed::Logging
23
- SIGNALS = %i[INT TERM QUIT].freeze
28
+
29
+ SIGNALS = %i[INT TERM QUIT USR1].freeze
24
30
 
25
31
  attr_reader :config, :queue_name, :min_priority, :max_priority, :work_queue
26
32
 
@@ -68,6 +74,7 @@ module Delayed
68
74
  @health_check_config = Settings.worker_health_check_config
69
75
  @config = options
70
76
  @job_count = 0
77
+ @active_job_mutex = Mutex.new
71
78
 
72
79
  @signal_queue = []
73
80
 
@@ -128,14 +135,29 @@ module Delayed
128
135
  break
129
136
  when :QUIT
130
137
  @exit = true
138
+ when :USR1
139
+ # Use the explicit check in case the job changed since the signal was sent
140
+ # Use dedicated thread to avoid blocking the signal handler while making DB queries
141
+ Thread.new { cancel_active_job_if_cancelled(work_thread) }
131
142
  else
132
143
  logger.error "Unknown signal '#{sig}' received"
133
144
  end
134
145
  end
135
146
  end
136
147
 
137
- self.class.lifecycle.run_callbacks(:execute, self) do
138
- run until exit?
148
+ if work_queue.is_a?(WorkQueue::InProcess)
149
+ cancellation_checker = Thread.new do
150
+ loop do
151
+ sleep(Settings.cancellation_check_interval)
152
+ cancel_active_job_if_cancelled(work_thread)
153
+ end
154
+ end
155
+ end
156
+
157
+ Thread.handle_interrupt(CancelledError => :never) do
158
+ self.class.lifecycle.run_callbacks(:execute, self) do
159
+ run until exit?
160
+ end
139
161
  end
140
162
 
141
163
  logger.info "Stopping worker"
@@ -150,6 +172,11 @@ module Delayed
150
172
  signal_processor.join
151
173
  end
152
174
 
175
+ if cancellation_checker
176
+ cancellation_checker.kill
177
+ cancellation_checker.join
178
+ end
179
+
153
180
  @self_pipe&.each(&:close)
154
181
  @self_pipe = nil
155
182
  end
@@ -179,8 +206,11 @@ module Delayed
179
206
  end
180
207
 
181
208
  if job
209
+ @active_job_mutex.synchronize { @active_root_job = job }
182
210
  configure_for_job(job) do
183
211
  @job_count += perform(job)
212
+ @active_job_mutex.synchronize { @active_root_job = nil }
213
+ discard_pending_cancellation
184
214
 
185
215
  if @max_job_count.positive? && @job_count >= @max_job_count
186
216
  logger.debug "Max job count of #{@max_job_count} exceeded, dying"
@@ -205,25 +235,30 @@ module Delayed
205
235
  end
206
236
  end
207
237
 
208
- def perform(job)
238
+ def perform(job, in_batch: false)
209
239
  begin
210
240
  count = 1
211
241
  raise Delayed::Backend::JobExpired, "job expired at #{job.expires_at}" if job.expired?
242
+ raise CancelledError, "job #{job.id} cancelled at #{job.cancelled_at}" if job.cancelled?
212
243
 
213
244
  self.class.lifecycle.run_callbacks(:perform, self, job) do
214
245
  self.process_name = "run:#{Settings.worker_procname_prefix}#{job.id}:#{job.name}"
215
246
  logger.info("Processing #{log_job(job, :long)}")
216
247
  runtime = Benchmark.realtime do
217
- if job.batch?
218
- # each job in the batch will have perform called on it, so we don't
219
- # need a timeout around this
220
- count = perform_batch(job)
221
- else
222
- job.invoke_job
248
+ # cancellation is only delivered while the job's own code is running, so it can't
249
+ # interrupt our bookkeeping (destroying the job, or failing it in a rescue below)
250
+ Thread.handle_interrupt(CancelledError => :immediate) do
251
+ if job.batch?
252
+ # each job in the batch will have perform called on it, so we don't
253
+ # need a timeout around this
254
+ count = perform_batch(job)
255
+ else
256
+ job.invoke_job
257
+ end
223
258
  end
224
259
  job.destroy
225
260
  end
226
- logger.info("Completed #{log_job(job, :short)} #{format("%.0fms", (runtime * 1000))}")
261
+ logger.info("Completed #{log_job(job, :short)} #{format("%.0fms", runtime * 1000)}")
227
262
  end
228
263
  rescue ::Delayed::RetriableError => e
229
264
  can_retry = job.attempts + 1 < job.inferred_max_attempts
@@ -235,6 +270,16 @@ module Delayed
235
270
  # There wasn't really a failure here so no callbacks and whatnot needed,
236
271
  # still reschedule the job though.
237
272
  job.reschedule(e)
273
+ rescue CancelledError => e
274
+ logger.debug "Job #{job.id} was cancelled"
275
+ self.class.lifecycle.run_callbacks(:error, self, job, e) do
276
+ job.last_error = e.message
277
+ # Do not call on_failure, as a cancelled job must not be simply unlocked
278
+ # and thus the behavior switching that method supports must be ignored
279
+ job.permanent_failure(e)
280
+ end
281
+ # cancellation applies to the whole batch, so don't move on to the next job in it
282
+ raise if in_batch
238
283
  rescue Exception => e # rubocop:disable Lint/RescueException
239
284
  if e.is_a?(NoMemoryError)
240
285
  GC.start # try and free up some memory before reporting the error
@@ -256,12 +301,49 @@ module Delayed
256
301
  job.source = parent_job.source
257
302
  job.create_and_lock!(name)
258
303
  configure_for_job(job) do
259
- perform(job)
304
+ # we're inside the batch's :immediate block; mask again so the sub-job's own bookkeeping
305
+ # is protected. a cancellation that arrives during it is raised once this block exits,
306
+ # which cancels the rest of the batch
307
+ Thread.handle_interrupt(CancelledError => :never) do
308
+ perform(job, in_batch: true)
309
+ end
260
310
  end
261
311
  end
262
312
  batch.items.size
263
313
  end
264
314
 
315
+ def cancel_active_job_if_cancelled(work_thread)
316
+ job = @active_root_job
317
+ return unless job
318
+
319
+ # this runs on a background thread; don't hold a connection from the pool between checks
320
+ cancelled_at = Delayed::Job.connection_pool.with_connection do
321
+ Delayed::Job.where(id: job.id).pick(:cancelled_at)
322
+ end
323
+ return unless cancelled_at && cancelled_at <= Delayed::Job.db_time_now
324
+
325
+ @active_job_mutex.synchronize do
326
+ return unless @active_root_job.equal?(job)
327
+
328
+ logger.debug "Active job #{job.id} has been cancelled"
329
+ work_thread.raise(CancelledError, "job #{job.id} cancelled at #{cancelled_at}")
330
+ end
331
+ rescue => e
332
+ logger.error("Error while checking for job cancellation: #{e.inspect}")
333
+ end
334
+
335
+ # A cancellation that arrives after a job finishes is deferred by handle_interrupt,
336
+ # and would otherwise be raised as soon as the next job starts.
337
+ def discard_pending_cancellation
338
+ while Thread.pending_interrupt?(CancelledError)
339
+ begin
340
+ Thread.handle_interrupt(CancelledError => :immediate) { nil }
341
+ rescue CancelledError => e
342
+ logger.debug "Discarding cancellation for a job that already finished: #{e.message}"
343
+ end
344
+ end
345
+ end
346
+
265
347
  def handle_failed_job(job, error)
266
348
  job.last_error = "#{error.message}\n#{error.backtrace.join("\n")}"
267
349
  logger.error("Failed with #{error.class} [#{error.message}] (#{job.attempts} attempts)")
data/lib/delayed_job.rb CHANGED
@@ -40,6 +40,7 @@ require "delayed/periodic"
40
40
  require "delayed/plugin"
41
41
  require "delayed/pool"
42
42
  require "delayed/worker"
43
+ require "delayed/util"
43
44
 
44
45
  require "delayed/worker/health_check"
45
46
  require "delayed/worker/consul_health_check"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: inst-jobs
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.3.1
4
+ version: 3.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Cody Cutrer
@@ -10,7 +10,7 @@ authors:
10
10
  autorequire:
11
11
  bindir: exe
12
12
  cert_chain: []
13
- date: 2025-09-19 00:00:00.000000000 Z
13
+ date: 2026-10-19 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: activerecord
@@ -116,258 +116,6 @@ dependencies:
116
116
  - - ">="
117
117
  - !ruby/object:Gem::Version
118
118
  version: '6.0'
119
- - !ruby/object:Gem::Dependency
120
- name: bump
121
- requirement: !ruby/object:Gem::Requirement
122
- requirements:
123
- - - ">="
124
- - !ruby/object:Gem::Version
125
- version: '0'
126
- type: :development
127
- prerelease: false
128
- version_requirements: !ruby/object:Gem::Requirement
129
- requirements:
130
- - - ">="
131
- - !ruby/object:Gem::Version
132
- version: '0'
133
- - !ruby/object:Gem::Dependency
134
- name: database_cleaner
135
- requirement: !ruby/object:Gem::Requirement
136
- requirements:
137
- - - "~>"
138
- - !ruby/object:Gem::Version
139
- version: '2.0'
140
- type: :development
141
- prerelease: false
142
- version_requirements: !ruby/object:Gem::Requirement
143
- requirements:
144
- - - "~>"
145
- - !ruby/object:Gem::Version
146
- version: '2.0'
147
- - !ruby/object:Gem::Dependency
148
- name: database_cleaner-active_record
149
- requirement: !ruby/object:Gem::Requirement
150
- requirements:
151
- - - "~>"
152
- - !ruby/object:Gem::Version
153
- version: '2.0'
154
- type: :development
155
- prerelease: false
156
- version_requirements: !ruby/object:Gem::Requirement
157
- requirements:
158
- - - "~>"
159
- - !ruby/object:Gem::Version
160
- version: '2.0'
161
- - !ruby/object:Gem::Dependency
162
- name: debug
163
- requirement: !ruby/object:Gem::Requirement
164
- requirements:
165
- - - ">="
166
- - !ruby/object:Gem::Version
167
- version: '0'
168
- type: :development
169
- prerelease: false
170
- version_requirements: !ruby/object:Gem::Requirement
171
- requirements:
172
- - - ">="
173
- - !ruby/object:Gem::Version
174
- version: '0'
175
- - !ruby/object:Gem::Dependency
176
- name: diplomat
177
- requirement: !ruby/object:Gem::Requirement
178
- requirements:
179
- - - "~>"
180
- - !ruby/object:Gem::Version
181
- version: 2.6.3
182
- type: :development
183
- prerelease: false
184
- version_requirements: !ruby/object:Gem::Requirement
185
- requirements:
186
- - - "~>"
187
- - !ruby/object:Gem::Version
188
- version: 2.6.3
189
- - !ruby/object:Gem::Dependency
190
- name: mutex_m
191
- requirement: !ruby/object:Gem::Requirement
192
- requirements:
193
- - - ">="
194
- - !ruby/object:Gem::Version
195
- version: '0'
196
- type: :development
197
- prerelease: false
198
- version_requirements: !ruby/object:Gem::Requirement
199
- requirements:
200
- - - ">="
201
- - !ruby/object:Gem::Version
202
- version: '0'
203
- - !ruby/object:Gem::Dependency
204
- name: pg
205
- requirement: !ruby/object:Gem::Requirement
206
- requirements:
207
- - - ">="
208
- - !ruby/object:Gem::Version
209
- version: '0'
210
- type: :development
211
- prerelease: false
212
- version_requirements: !ruby/object:Gem::Requirement
213
- requirements:
214
- - - ">="
215
- - !ruby/object:Gem::Version
216
- version: '0'
217
- - !ruby/object:Gem::Dependency
218
- name: rack-test
219
- requirement: !ruby/object:Gem::Requirement
220
- requirements:
221
- - - ">="
222
- - !ruby/object:Gem::Version
223
- version: '0'
224
- type: :development
225
- prerelease: false
226
- version_requirements: !ruby/object:Gem::Requirement
227
- requirements:
228
- - - ">="
229
- - !ruby/object:Gem::Version
230
- version: '0'
231
- - !ruby/object:Gem::Dependency
232
- name: rake
233
- requirement: !ruby/object:Gem::Requirement
234
- requirements:
235
- - - ">="
236
- - !ruby/object:Gem::Version
237
- version: '0'
238
- type: :development
239
- prerelease: false
240
- version_requirements: !ruby/object:Gem::Requirement
241
- requirements:
242
- - - ">="
243
- - !ruby/object:Gem::Version
244
- version: '0'
245
- - !ruby/object:Gem::Dependency
246
- name: rspec
247
- requirement: !ruby/object:Gem::Requirement
248
- requirements:
249
- - - "~>"
250
- - !ruby/object:Gem::Version
251
- version: '3.10'
252
- type: :development
253
- prerelease: false
254
- version_requirements: !ruby/object:Gem::Requirement
255
- requirements:
256
- - - "~>"
257
- - !ruby/object:Gem::Version
258
- version: '3.10'
259
- - !ruby/object:Gem::Dependency
260
- name: rubocop-inst
261
- requirement: !ruby/object:Gem::Requirement
262
- requirements:
263
- - - "~>"
264
- - !ruby/object:Gem::Version
265
- version: '1'
266
- type: :development
267
- prerelease: false
268
- version_requirements: !ruby/object:Gem::Requirement
269
- requirements:
270
- - - "~>"
271
- - !ruby/object:Gem::Version
272
- version: '1'
273
- - !ruby/object:Gem::Dependency
274
- name: rubocop-rails
275
- requirement: !ruby/object:Gem::Requirement
276
- requirements:
277
- - - "~>"
278
- - !ruby/object:Gem::Version
279
- version: '2.11'
280
- type: :development
281
- prerelease: false
282
- version_requirements: !ruby/object:Gem::Requirement
283
- requirements:
284
- - - "~>"
285
- - !ruby/object:Gem::Version
286
- version: '2.11'
287
- - !ruby/object:Gem::Dependency
288
- name: rubocop-rake
289
- requirement: !ruby/object:Gem::Requirement
290
- requirements:
291
- - - "~>"
292
- - !ruby/object:Gem::Version
293
- version: '0.6'
294
- type: :development
295
- prerelease: false
296
- version_requirements: !ruby/object:Gem::Requirement
297
- requirements:
298
- - - "~>"
299
- - !ruby/object:Gem::Version
300
- version: '0.6'
301
- - !ruby/object:Gem::Dependency
302
- name: rubocop-rspec
303
- requirement: !ruby/object:Gem::Requirement
304
- requirements:
305
- - - "~>"
306
- - !ruby/object:Gem::Version
307
- version: '3.0'
308
- type: :development
309
- prerelease: false
310
- version_requirements: !ruby/object:Gem::Requirement
311
- requirements:
312
- - - "~>"
313
- - !ruby/object:Gem::Version
314
- version: '3.0'
315
- - !ruby/object:Gem::Dependency
316
- name: sinatra
317
- requirement: !ruby/object:Gem::Requirement
318
- requirements:
319
- - - ">="
320
- - !ruby/object:Gem::Version
321
- version: '0'
322
- type: :development
323
- prerelease: false
324
- version_requirements: !ruby/object:Gem::Requirement
325
- requirements:
326
- - - ">="
327
- - !ruby/object:Gem::Version
328
- version: '0'
329
- - !ruby/object:Gem::Dependency
330
- name: sinatra-contrib
331
- requirement: !ruby/object:Gem::Requirement
332
- requirements:
333
- - - "~>"
334
- - !ruby/object:Gem::Version
335
- version: '3.1'
336
- type: :development
337
- prerelease: false
338
- version_requirements: !ruby/object:Gem::Requirement
339
- requirements:
340
- - - "~>"
341
- - !ruby/object:Gem::Version
342
- version: '3.1'
343
- - !ruby/object:Gem::Dependency
344
- name: timecop
345
- requirement: !ruby/object:Gem::Requirement
346
- requirements:
347
- - - "~>"
348
- - !ruby/object:Gem::Version
349
- version: '0.9'
350
- type: :development
351
- prerelease: false
352
- version_requirements: !ruby/object:Gem::Requirement
353
- requirements:
354
- - - "~>"
355
- - !ruby/object:Gem::Version
356
- version: '0.9'
357
- - !ruby/object:Gem::Dependency
358
- name: webmock
359
- requirement: !ruby/object:Gem::Requirement
360
- requirements:
361
- - - ">="
362
- - !ruby/object:Gem::Version
363
- version: '0'
364
- type: :development
365
- prerelease: false
366
- version_requirements: !ruby/object:Gem::Requirement
367
- requirements:
368
- - - ">="
369
- - !ruby/object:Gem::Version
370
- version: '0'
371
119
  description:
372
120
  email:
373
121
  - cody@instructure.com
@@ -419,6 +167,7 @@ files:
419
167
  - db/migrate/20220203063200_remove_old_singleton_index.rb
420
168
  - db/migrate/20220328152900_add_failed_jobs_indicies.rb
421
169
  - db/migrate/20220519204546_add_requeued_job_id_to_failed_jobs.rb
170
+ - db/migrate/20261012154653_add_cancelled_at_to_delayed_jobs.rb
422
171
  - exe/inst_jobs
423
172
  - lib/delayed/backend/active_record.rb
424
173
  - lib/delayed/backend/base.rb
@@ -444,6 +193,7 @@ files:
444
193
  - lib/delayed/server/views/layout.erb
445
194
  - lib/delayed/settings.rb
446
195
  - lib/delayed/testing.rb
196
+ - lib/delayed/util.rb
447
197
  - lib/delayed/version.rb
448
198
  - lib/delayed/work_queue/in_process.rb
449
199
  - lib/delayed/work_queue/parent_process.rb