inst-jobs 3.3.2 → 3.5.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/db/migrate/20261012154653_add_cancelled_at_to_delayed_jobs.rb +8 -0
- data/lib/delayed/backend/active_record.rb +19 -0
- data/lib/delayed/backend/base.rb +4 -0
- data/lib/delayed/settings.rb +2 -0
- data/lib/delayed/testing.rb +3 -1
- data/lib/delayed/version.rb +1 -1
- data/lib/delayed/work_queue/parent_process/server.rb +41 -2
- data/lib/delayed/worker/consul_health_check.rb +57 -10
- data/lib/delayed/worker.rb +92 -11
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b27bc2f28f4906ac69cc6982188b528bdad5897c0bed2a6a5c6445f294eb9973
|
|
4
|
+
data.tar.gz: cddeff5c83f6ed73d63fee9ae7ffe0df2a4bc18636b11c0fd4cc80e04c8bf835
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 612d7681460b68f02c11b059b09504544969123ac1f70fbb89203b69f8002a768dae8c281cd498cb610cd316ee962f58d1d8bba3b13680aff94c807931e1c68c
|
|
7
|
+
data.tar.gz: fc8126210f011236d8ca57e6a938e6e6e33c0e88f79f27cf9e612cb2bae3f743066b6527c728da3b668cf22f27c13dcc0a04764ddd02fe3c3dbc4b1ad1c2c61e
|
|
@@ -215,6 +215,12 @@ module Delayed
|
|
|
215
215
|
where(locked_by: worker_name).update_all(locked_by: nil, locked_at: nil)
|
|
216
216
|
end
|
|
217
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
|
+
|
|
218
224
|
def self.strand_size(strand)
|
|
219
225
|
where(strand:).count
|
|
220
226
|
end
|
|
@@ -613,6 +619,18 @@ module Delayed
|
|
|
613
619
|
single_step_create
|
|
614
620
|
end
|
|
615
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
|
+
|
|
616
634
|
def fail!
|
|
617
635
|
attrs = attributes
|
|
618
636
|
attrs["original_job_id"] = attrs.delete("id") if Failed.columns_hash.key?("original_job_id")
|
|
@@ -646,6 +664,7 @@ module Delayed
|
|
|
646
664
|
"last_error",
|
|
647
665
|
"locked_at",
|
|
648
666
|
"failed_at",
|
|
667
|
+
"cancelled_at",
|
|
649
668
|
"locked_by",
|
|
650
669
|
"original_job_id",
|
|
651
670
|
"requeued_job_id")
|
data/lib/delayed/backend/base.rb
CHANGED
|
@@ -234,6 +234,10 @@ module Delayed
|
|
|
234
234
|
expires_at && (self.class.db_time_now >= expires_at)
|
|
235
235
|
end
|
|
236
236
|
|
|
237
|
+
def cancelled?
|
|
238
|
+
cancelled_at && (self.class.db_time_now >= cancelled_at)
|
|
239
|
+
end
|
|
240
|
+
|
|
237
241
|
def inferred_max_attempts
|
|
238
242
|
max_attempts || Delayed::Settings.max_attempts
|
|
239
243
|
end
|
data/lib/delayed/settings.rb
CHANGED
|
@@ -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
|
data/lib/delayed/testing.rb
CHANGED
data/lib/delayed/version.rb
CHANGED
|
@@ -24,6 +24,7 @@ module Delayed
|
|
|
24
24
|
|
|
25
25
|
@exit = false
|
|
26
26
|
@self_pipe = IO.pipe
|
|
27
|
+
@last_cancellation_check = Delayed::Job.db_time_now
|
|
27
28
|
end
|
|
28
29
|
|
|
29
30
|
def connected_clients
|
|
@@ -79,6 +80,11 @@ module Delayed
|
|
|
79
80
|
check_for_work(forced_latency:)
|
|
80
81
|
end
|
|
81
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
|
|
82
88
|
end
|
|
83
89
|
|
|
84
90
|
def handle_read(socket)
|
|
@@ -97,7 +103,7 @@ module Delayed
|
|
|
97
103
|
# and terminate the work queue process, to be restarted by the parent daemon.
|
|
98
104
|
def handle_accept
|
|
99
105
|
socket, _addr = @listen_socket.accept_nonblock
|
|
100
|
-
@clients[socket] = ClientState.new(false, socket) if socket
|
|
106
|
+
@clients[socket] = ClientState.new(false, nil, socket) if socket
|
|
101
107
|
rescue IO::WaitReadable
|
|
102
108
|
logger.error("Server attempted to read listen_socket but failed with IO::WaitReadable")
|
|
103
109
|
# ignore and just try accepting again next time through the loop
|
|
@@ -116,6 +122,7 @@ module Delayed
|
|
|
116
122
|
worker_name, worker_config = Marshal.load(socket)
|
|
117
123
|
client.name = worker_name
|
|
118
124
|
client.working = false
|
|
125
|
+
client.active_job_id = nil
|
|
119
126
|
(@waiting_clients[worker_config] ||= []) << client
|
|
120
127
|
rescue SystemCallError, IOError => e
|
|
121
128
|
logger.error("Receiving message from client (#{socket}) failed: #{e.inspect}")
|
|
@@ -136,6 +143,7 @@ module Delayed
|
|
|
136
143
|
next
|
|
137
144
|
end
|
|
138
145
|
client.working = true
|
|
146
|
+
client.active_job_id = job.id
|
|
139
147
|
begin
|
|
140
148
|
logger.debug("Sending prefetched job #{job.id} to #{client.name}")
|
|
141
149
|
client_timeout { Marshal.dump(job, client.socket) }
|
|
@@ -177,6 +185,7 @@ module Delayed
|
|
|
177
185
|
end
|
|
178
186
|
client = workers.find { |worker| worker.name == worker_name }
|
|
179
187
|
client.working = true
|
|
188
|
+
client.active_job_id = locked_jobs.id
|
|
180
189
|
jobs_to_send << [client, locked_jobs]
|
|
181
190
|
end
|
|
182
191
|
end
|
|
@@ -245,6 +254,36 @@ module Delayed
|
|
|
245
254
|
end
|
|
246
255
|
end
|
|
247
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
|
+
|
|
248
287
|
def drop_socket(socket)
|
|
249
288
|
# this socket went away
|
|
250
289
|
begin
|
|
@@ -275,7 +314,7 @@ module Delayed
|
|
|
275
314
|
Timeout.timeout(@client_timeout, &)
|
|
276
315
|
end
|
|
277
316
|
|
|
278
|
-
ClientState = Struct.new(:working, :socket, :name)
|
|
317
|
+
ClientState = Struct.new(:working, :active_job_id, :socket, :name)
|
|
279
318
|
end
|
|
280
319
|
end
|
|
281
320
|
end
|
|
@@ -7,11 +7,13 @@ require "socket"
|
|
|
7
7
|
module Delayed
|
|
8
8
|
class Worker
|
|
9
9
|
class ConsulHealthCheck < HealthCheck
|
|
10
|
+
include Delayed::Logging
|
|
11
|
+
|
|
10
12
|
self.type_name = :consul
|
|
11
13
|
|
|
12
14
|
CONSUL_CONFIG_KEYS = %w[url acl_token].map(&:freeze).freeze
|
|
13
15
|
DEFAULT_SERVICE_NAME = "inst-jobs_worker"
|
|
14
|
-
attr_reader :service_client, :health_client
|
|
16
|
+
attr_reader :service_client, :health_client, :check_client
|
|
15
17
|
|
|
16
18
|
def initialize(*, **)
|
|
17
19
|
super
|
|
@@ -27,21 +29,23 @@ module Delayed
|
|
|
27
29
|
end
|
|
28
30
|
@service_client = Diplomat::Service.new(configuration: consul_config)
|
|
29
31
|
@health_client = Diplomat::Health.new(configuration: consul_config)
|
|
32
|
+
@check_client = Diplomat::Check.new(configuration: consul_config)
|
|
30
33
|
else
|
|
31
34
|
@service_client = Diplomat::Service.new
|
|
32
35
|
@health_client = Diplomat::Health.new
|
|
36
|
+
@check_client = Diplomat::Check.new
|
|
33
37
|
end
|
|
34
38
|
end
|
|
35
39
|
|
|
36
40
|
def start
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
check: check_attributes
|
|
41
|
-
})
|
|
41
|
+
registered = register
|
|
42
|
+
start_heartbeat if registered && ttl_check?
|
|
43
|
+
registered
|
|
42
44
|
end
|
|
43
45
|
|
|
44
46
|
def stop
|
|
47
|
+
@heartbeat_thread&.kill
|
|
48
|
+
@heartbeat_thread = nil
|
|
45
49
|
@service_client.deregister(worker_name)
|
|
46
50
|
end
|
|
47
51
|
|
|
@@ -56,13 +60,56 @@ module Delayed
|
|
|
56
60
|
|
|
57
61
|
private
|
|
58
62
|
|
|
63
|
+
def register
|
|
64
|
+
@service_client.register({
|
|
65
|
+
id: worker_name,
|
|
66
|
+
name: service_name,
|
|
67
|
+
check: check_attributes
|
|
68
|
+
})
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def ttl_check?
|
|
72
|
+
@config.fetch(:check_type, "script").to_s == "ttl"
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def check_id
|
|
76
|
+
"service:#{worker_name}"
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# in seconds
|
|
80
|
+
def ttl
|
|
81
|
+
Integer(@config.fetch(:ttl, 60))
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def start_heartbeat
|
|
85
|
+
@heartbeat_thread = Thread.new do
|
|
86
|
+
loop do
|
|
87
|
+
sleep(ttl / 3.0)
|
|
88
|
+
heartbeat
|
|
89
|
+
rescue => e
|
|
90
|
+
logger.warn("Consul TTL update failed for #{worker_name}: #{e.class}: #{e.message}")
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def heartbeat
|
|
96
|
+
@check_client.pass(check_id)
|
|
97
|
+
rescue Faraday::ResourceNotFound
|
|
98
|
+
logger.warn("Consul forgot #{worker_name}, re-registering")
|
|
99
|
+
register
|
|
100
|
+
end
|
|
101
|
+
|
|
59
102
|
def check_attributes
|
|
60
|
-
{
|
|
61
|
-
args: ["bash", "-c", check_script],
|
|
103
|
+
base = {
|
|
62
104
|
status: "passing",
|
|
63
|
-
interval: @config.fetch(:check_interval, "5m"),
|
|
64
105
|
deregister_critical_service_after: @config.fetch(:deregister_service_delay, "10m")
|
|
65
|
-
}
|
|
106
|
+
}
|
|
107
|
+
return base.merge(ttl: "#{ttl}s") if ttl_check?
|
|
108
|
+
|
|
109
|
+
base.merge(
|
|
110
|
+
args: ["bash", "-c", check_script],
|
|
111
|
+
interval: @config.fetch(:check_interval, "5m")
|
|
112
|
+
).tap do |h|
|
|
66
113
|
h[:docker_container_id] = docker_container_id if @config["docker"]
|
|
67
114
|
end
|
|
68
115
|
end
|
data/lib/delayed/worker.rb
CHANGED
|
@@ -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,
|
|
@@ -21,7 +26,7 @@ module Delayed
|
|
|
21
26
|
class Worker
|
|
22
27
|
include Delayed::Logging
|
|
23
28
|
|
|
24
|
-
SIGNALS = %i[INT TERM QUIT].freeze
|
|
29
|
+
SIGNALS = %i[INT TERM QUIT USR1].freeze
|
|
25
30
|
|
|
26
31
|
attr_reader :config, :queue_name, :min_priority, :max_priority, :work_queue
|
|
27
32
|
|
|
@@ -69,6 +74,7 @@ module Delayed
|
|
|
69
74
|
@health_check_config = Settings.worker_health_check_config
|
|
70
75
|
@config = options
|
|
71
76
|
@job_count = 0
|
|
77
|
+
@active_job_mutex = Mutex.new
|
|
72
78
|
|
|
73
79
|
@signal_queue = []
|
|
74
80
|
|
|
@@ -129,14 +135,29 @@ module Delayed
|
|
|
129
135
|
break
|
|
130
136
|
when :QUIT
|
|
131
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) }
|
|
132
142
|
else
|
|
133
143
|
logger.error "Unknown signal '#{sig}' received"
|
|
134
144
|
end
|
|
135
145
|
end
|
|
136
146
|
end
|
|
137
147
|
|
|
138
|
-
|
|
139
|
-
|
|
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
|
|
140
161
|
end
|
|
141
162
|
|
|
142
163
|
logger.info "Stopping worker"
|
|
@@ -151,6 +172,11 @@ module Delayed
|
|
|
151
172
|
signal_processor.join
|
|
152
173
|
end
|
|
153
174
|
|
|
175
|
+
if cancellation_checker
|
|
176
|
+
cancellation_checker.kill
|
|
177
|
+
cancellation_checker.join
|
|
178
|
+
end
|
|
179
|
+
|
|
154
180
|
@self_pipe&.each(&:close)
|
|
155
181
|
@self_pipe = nil
|
|
156
182
|
end
|
|
@@ -180,8 +206,11 @@ module Delayed
|
|
|
180
206
|
end
|
|
181
207
|
|
|
182
208
|
if job
|
|
209
|
+
@active_job_mutex.synchronize { @active_root_job = job }
|
|
183
210
|
configure_for_job(job) do
|
|
184
211
|
@job_count += perform(job)
|
|
212
|
+
@active_job_mutex.synchronize { @active_root_job = nil }
|
|
213
|
+
discard_pending_cancellation
|
|
185
214
|
|
|
186
215
|
if @max_job_count.positive? && @job_count >= @max_job_count
|
|
187
216
|
logger.debug "Max job count of #{@max_job_count} exceeded, dying"
|
|
@@ -206,21 +235,26 @@ module Delayed
|
|
|
206
235
|
end
|
|
207
236
|
end
|
|
208
237
|
|
|
209
|
-
def perform(job)
|
|
238
|
+
def perform(job, in_batch: false)
|
|
210
239
|
begin
|
|
211
240
|
count = 1
|
|
212
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?
|
|
213
243
|
|
|
214
244
|
self.class.lifecycle.run_callbacks(:perform, self, job) do
|
|
215
245
|
self.process_name = "run:#{Settings.worker_procname_prefix}#{job.id}:#{job.name}"
|
|
216
246
|
logger.info("Processing #{log_job(job, :long)}")
|
|
217
247
|
runtime = Benchmark.realtime do
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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
|
|
224
258
|
end
|
|
225
259
|
job.destroy
|
|
226
260
|
end
|
|
@@ -236,6 +270,16 @@ module Delayed
|
|
|
236
270
|
# There wasn't really a failure here so no callbacks and whatnot needed,
|
|
237
271
|
# still reschedule the job though.
|
|
238
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
|
|
239
283
|
rescue Exception => e # rubocop:disable Lint/RescueException
|
|
240
284
|
if e.is_a?(NoMemoryError)
|
|
241
285
|
GC.start # try and free up some memory before reporting the error
|
|
@@ -257,12 +301,49 @@ module Delayed
|
|
|
257
301
|
job.source = parent_job.source
|
|
258
302
|
job.create_and_lock!(name)
|
|
259
303
|
configure_for_job(job) do
|
|
260
|
-
|
|
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
|
|
261
310
|
end
|
|
262
311
|
end
|
|
263
312
|
batch.items.size
|
|
264
313
|
end
|
|
265
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
|
+
|
|
266
347
|
def handle_failed_job(job, error)
|
|
267
348
|
job.last_error = "#{error.message}\n#{error.backtrace.join("\n")}"
|
|
268
349
|
logger.error("Failed with #{error.class} [#{error.message}] (#{job.attempts} attempts)")
|
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.
|
|
4
|
+
version: 3.5.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:
|
|
13
|
+
date: 2026-10-22 00:00:00.000000000 Z
|
|
14
14
|
dependencies:
|
|
15
15
|
- !ruby/object:Gem::Dependency
|
|
16
16
|
name: activerecord
|
|
@@ -167,6 +167,7 @@ files:
|
|
|
167
167
|
- db/migrate/20220203063200_remove_old_singleton_index.rb
|
|
168
168
|
- db/migrate/20220328152900_add_failed_jobs_indicies.rb
|
|
169
169
|
- db/migrate/20220519204546_add_requeued_job_id_to_failed_jobs.rb
|
|
170
|
+
- db/migrate/20261012154653_add_cancelled_at_to_delayed_jobs.rb
|
|
170
171
|
- exe/inst_jobs
|
|
171
172
|
- lib/delayed/backend/active_record.rb
|
|
172
173
|
- lib/delayed/backend/base.rb
|