good_job 3.18.2 → 3.19.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8210d74d5adf0f1c6f9a7d080fe855c9bbb4042d8c4e1c8349da9b26452ec629
4
- data.tar.gz: 04c5daddea00fbb9a8eb6b068af973541790c7fec49bbd678a03e8e2eafc853b
3
+ metadata.gz: 6c66d226179c2cd55bfc539c943df468fc683f761b89cdc10606f03a6bb26f87
4
+ data.tar.gz: f72404539fb5bf7ef122cf65ed616276dd0457538b1319e253d6078e223ddbbb
5
5
  SHA512:
6
- metadata.gz: 9f2d6b3da26ea4fe01bff9e4a2ffa0830b34da0299af27e880a277703716fde024ca166bfa2203cf18cf53171523be9bca0754b21bba942e734506336a1020a5
7
- data.tar.gz: 789e7565b789970c679beffef4a8f8c482469e1d027f97274bae0bf40709d94da08ded1e408011773a107880cd704402db6c6e964a363258b8d26ede29598def
6
+ metadata.gz: 98d03d3abdbcc20956e37e8c498c8fa57c1b2b091c5f50c0654aa75875d9d23ca02aeb78d67f5f2a8c961a9aabfc0ad3899fcf1867c2b4350dfe56584568a4eb
7
+ data.tar.gz: f0044b4c761371391c5aec83500efece2e000a6f9741a504f244208ae403b42fd39e1324fe1e753e3ae4d67c6ae92706f9a960ab5ec2745df547acd4e0927ac7
data/CHANGELOG.md CHANGED
@@ -1,5 +1,44 @@
1
1
  # Changelog
2
2
 
3
+ ## [v3.19.0](https://github.com/bensheldon/good_job/tree/v3.19.0) (2023-09-19)
4
+
5
+ [Full Changelog](https://github.com/bensheldon/good_job/compare/v3.18.3...v3.19.0)
6
+
7
+ **Implemented enhancements:**
8
+
9
+ - "Force" discard jobs that are already running/runaway to prevent retry [\#1073](https://github.com/bensheldon/good_job/pull/1073) ([jgrau](https://github.com/jgrau))
10
+
11
+ **Closed issues:**
12
+
13
+ - Possible Memory Leak [\#1074](https://github.com/bensheldon/good_job/issues/1074)
14
+ - What's the best way to stop and discard a running job? [\#625](https://github.com/bensheldon/good_job/issues/625)
15
+
16
+ **Merged pull requests:**
17
+
18
+ - AdvisoryLockable: Abort record create if with\_advisory\_lock fails to acquire advisory lock [\#1078](https://github.com/bensheldon/good_job/pull/1078) ([bensheldon](https://github.com/bensheldon))
19
+ - Wrap all test background threads in Rails executors; better test logging/debugging [\#1077](https://github.com/bensheldon/good_job/pull/1077) ([bensheldon](https://github.com/bensheldon))
20
+
21
+ ## [v3.18.3](https://github.com/bensheldon/good_job/tree/v3.18.3) (2023-09-16)
22
+
23
+ [Full Changelog](https://github.com/bensheldon/good_job/compare/v3.18.2...v3.18.3)
24
+
25
+ **Fixed bugs:**
26
+
27
+ - Allow Probe Server's `/connect` to handle a certain number of reconnects before statusing [\#1075](https://github.com/bensheldon/good_job/pull/1075) ([bensheldon](https://github.com/bensheldon))
28
+
29
+ **Closed issues:**
30
+
31
+ - ActiveRecord::RecordNotUnique good\_jobs.id error in rspec test suite [\#1072](https://github.com/bensheldon/good_job/issues/1072)
32
+ - Probe failures on heavy usage of dashboard\(?\) [\#1068](https://github.com/bensheldon/good_job/issues/1068)
33
+
34
+ **Merged pull requests:**
35
+
36
+ - Bump actions/checkout from 3 to 4 [\#1070](https://github.com/bensheldon/good_job/pull/1070) ([dependabot[bot]](https://github.com/apps/dependabot))
37
+ - Add Skylight for demo site; create distinct development, lint, demo, production Gemfile groups; a little bit of Rubocop [\#1069](https://github.com/bensheldon/good_job/pull/1069) ([bensheldon](https://github.com/bensheldon))
38
+ - Add JRuby 9.4 to testing matrix; nerf ActiveJob::TestQueueAdapter overrides [\#1067](https://github.com/bensheldon/good_job/pull/1067) ([bensheldon](https://github.com/bensheldon))
39
+ - Reorganize dependencies to make booting JRuby easier [\#1066](https://github.com/bensheldon/good_job/pull/1066) ([bensheldon](https://github.com/bensheldon))
40
+ - Slight refactoring to CronEntry [\#1063](https://github.com/bensheldon/good_job/pull/1063) ([bensheldon](https://github.com/bensheldon))
41
+
3
42
  ## [v3.18.2](https://github.com/bensheldon/good_job/tree/v3.18.2) (2023-09-02)
4
43
 
5
44
  [Full Changelog](https://github.com/bensheldon/good_job/compare/v3.18.1...v3.18.2)
@@ -3,6 +3,7 @@
3
3
  module GoodJob
4
4
  class JobsController < GoodJob::ApplicationController
5
5
  DISCARD_MESSAGE = "Discarded through dashboard"
6
+ FORCE_DISCARD_MESSAGE = "Force discarded through dashboard"
6
7
 
7
8
  ACTIONS = {
8
9
  discard: "discarded",
@@ -66,6 +67,12 @@ module GoodJob
66
67
  redirect_back(fallback_location: jobs_path, notice: t(".notice"))
67
68
  end
68
69
 
70
+ def force_discard
71
+ @job = Job.find(params[:id])
72
+ @job.force_discard_job(FORCE_DISCARD_MESSAGE)
73
+ redirect_back(fallback_location: jobs_path, notice: t(".notice"))
74
+ end
75
+
69
76
  def reschedule
70
77
  @job = Job.find(params[:id])
71
78
  @job.reschedule_job
@@ -16,7 +16,7 @@ module GoodJob
16
16
  end
17
17
 
18
18
  def default_base_query
19
- GoodJob::BatchRecord.all.includes(:jobs)
19
+ GoodJob::BatchRecord.includes(:jobs)
20
20
  end
21
21
  end
22
22
  end
@@ -133,7 +133,12 @@ module GoodJob
133
133
  # @return [Boolean]
134
134
  attr_accessor :create_with_advisory_lock
135
135
 
136
- after_create -> { advisory_lock }, if: :create_with_advisory_lock
136
+ after_create lambda {
137
+ advisory_lock || begin
138
+ errors.add(self.class.advisory_lockable_column, "Failed to acquire advisory lock: #{lockable_key}")
139
+ raise ActiveRecord::RecordInvalid # do not reference the record because it can cause I18n missing translation error
140
+ end
141
+ }, if: :create_with_advisory_lock
137
142
  end
138
143
 
139
144
  class_methods do
@@ -222,6 +227,47 @@ module GoodJob
222
227
  connection.exec_query(pg_or_jdbc_query(query), 'GoodJob::Lockable Advisory Unlock', binds).first['unlocked']
223
228
  end
224
229
 
230
+ # Tests whether the provided key has an advisory lock on it.
231
+ # @param key [String, Symbol] Key to test lock against
232
+ # @return [Boolean]
233
+ def advisory_locked_key?(key)
234
+ query = <<~SQL.squish
235
+ SELECT 1 AS one
236
+ FROM pg_locks
237
+ WHERE pg_locks.locktype = 'advisory'
238
+ AND pg_locks.objsubid = 1
239
+ AND pg_locks.classid = ('x' || substr(md5($1::text), 1, 16))::bit(32)::int
240
+ AND pg_locks.objid = (('x' || substr(md5($2::text), 1, 16))::bit(64) << 32)::bit(32)::int
241
+ LIMIT 1
242
+ SQL
243
+ binds = [
244
+ ActiveRecord::Relation::QueryAttribute.new('key', key, ActiveRecord::Type::String.new),
245
+ ActiveRecord::Relation::QueryAttribute.new('key', key, ActiveRecord::Type::String.new),
246
+ ]
247
+ connection.exec_query(pg_or_jdbc_query(query), 'GoodJob::Lockable Advisory Locked?', binds).any?
248
+ end
249
+
250
+ # Tests whether this record is locked by the current database session.
251
+ # @param key [String, Symbol] Key to test lock against
252
+ # @return [Boolean]
253
+ def owns_advisory_lock_key?(key)
254
+ query = <<~SQL.squish
255
+ SELECT 1 AS one
256
+ FROM pg_locks
257
+ WHERE pg_locks.locktype = 'advisory'
258
+ AND pg_locks.objsubid = 1
259
+ AND pg_locks.classid = ('x' || substr(md5($1::text), 1, 16))::bit(32)::int
260
+ AND pg_locks.objid = (('x' || substr(md5($2::text), 1, 16))::bit(64) << 32)::bit(32)::int
261
+ AND pg_locks.pid = pg_backend_pid()
262
+ LIMIT 1
263
+ SQL
264
+ binds = [
265
+ ActiveRecord::Relation::QueryAttribute.new('key', key, ActiveRecord::Type::String.new),
266
+ ActiveRecord::Relation::QueryAttribute.new('key', key, ActiveRecord::Type::String.new),
267
+ ]
268
+ connection.exec_query(pg_or_jdbc_query(query), 'GoodJob::Lockable Owns Advisory Lock?', binds).any?
269
+ end
270
+
225
271
  def _advisory_lockable_column
226
272
  advisory_lockable_column || primary_key
227
273
  end
@@ -318,20 +364,7 @@ module GoodJob
318
364
  # @param key [String, Symbol] Key to test lock against
319
365
  # @return [Boolean]
320
366
  def advisory_locked?(key: lockable_key)
321
- query = <<~SQL.squish
322
- SELECT 1 AS one
323
- FROM pg_locks
324
- WHERE pg_locks.locktype = 'advisory'
325
- AND pg_locks.objsubid = 1
326
- AND pg_locks.classid = ('x' || substr(md5($1::text), 1, 16))::bit(32)::int
327
- AND pg_locks.objid = (('x' || substr(md5($2::text), 1, 16))::bit(64) << 32)::bit(32)::int
328
- LIMIT 1
329
- SQL
330
- binds = [
331
- ActiveRecord::Relation::QueryAttribute.new('key', key, ActiveRecord::Type::String.new),
332
- ActiveRecord::Relation::QueryAttribute.new('key', key, ActiveRecord::Type::String.new),
333
- ]
334
- self.class.connection.exec_query(pg_or_jdbc_query(query), 'GoodJob::Lockable Advisory Locked?', binds).any?
367
+ self.class.advisory_locked_key?(key)
335
368
  end
336
369
 
337
370
  # Tests whether this record does not have an advisory lock on it.
@@ -345,6 +378,7 @@ module GoodJob
345
378
  # @param key [String, Symbol] Key to test lock against
346
379
  # @return [Boolean]
347
380
  def owns_advisory_lock?(key: lockable_key)
381
+ self.class.owns_advisory_lock_key?(key)
348
382
  query = <<~SQL.squish
349
383
  SELECT 1 AS one
350
384
  FROM pg_locks
@@ -30,10 +30,10 @@ module GoodJob
30
30
 
31
31
  # Runs the block with self.logger silenced.
32
32
  # If self.logger is nil, simply runs the block.
33
- def self.with_logger_silenced(&block)
33
+ def self.with_logger_silenced(silent: true, &block)
34
34
  # Assign to a local variable, just in case it's modified in another thread concurrently
35
35
  logger = self.logger
36
- if logger.respond_to? :silence
36
+ if silent && logger.respond_to?(:silence)
37
37
  logger.silence(&block)
38
38
  else
39
39
  yield
@@ -60,29 +60,15 @@ module GoodJob # :nodoc:
60
60
 
61
61
  def next_at(previously_at: nil)
62
62
  if cron_proc?
63
- result = Rails.application.executor.wrap { cron.call(previously_at || last_at) }
64
- return Fugit.parse(result).next_time.to_t if result.is_a?(String)
65
-
66
- return result
67
-
63
+ result = Rails.application.executor.wrap { cron.call(previously_at || last_job_at) }
64
+ if result.is_a?(String)
65
+ Fugit.parse(result).next_time.to_t
66
+ else
67
+ result
68
+ end
69
+ else
70
+ fugit.next_time.to_t
68
71
  end
69
- fugit.next_time.to_t
70
- end
71
-
72
- def schedule
73
- return "Custom schedule" if cron_proc?
74
-
75
- fugit.original
76
- end
77
-
78
- def jobs
79
- GoodJob::Job.where(cron_key: key)
80
- end
81
-
82
- def last_at
83
- return if last_job.blank?
84
-
85
- (last_job.cron_at || last_job.created_at).localtime
86
72
  end
87
73
 
88
74
  def enabled?
@@ -113,15 +99,11 @@ module GoodJob # :nodoc:
113
99
  false
114
100
  end
115
101
 
116
- def last_job
117
- jobs.order("cron_at DESC NULLS LAST").first
118
- end
119
-
120
102
  def display_properties
121
103
  {
122
104
  key: key,
123
105
  class: job_class,
124
- cron: schedule,
106
+ cron: display_schedule,
125
107
  set: display_property(set),
126
108
  description: display_property(description),
127
109
  }.tap do |properties|
@@ -130,6 +112,24 @@ module GoodJob # :nodoc:
130
112
  end
131
113
  end
132
114
 
115
+ def display_schedule
116
+ cron_proc? ? display_property(cron) : fugit.original
117
+ end
118
+
119
+ def jobs
120
+ GoodJob::Job.where(cron_key: key)
121
+ end
122
+
123
+ def last_job
124
+ jobs.order("cron_at DESC NULLS LAST").first
125
+ end
126
+
127
+ def last_job_at
128
+ return if last_job.blank?
129
+
130
+ (last_job.cron_at || last_job.created_at).localtime
131
+ end
132
+
133
133
  private
134
134
 
135
135
  def cron
@@ -163,7 +163,7 @@ module GoodJob # :nodoc:
163
163
  case value
164
164
  when NilClass
165
165
  "None"
166
- when Proc
166
+ when Callable
167
167
  "Lambda/Callable"
168
168
  else
169
169
  value
@@ -215,32 +215,17 @@ module GoodJob
215
215
  # @return [void]
216
216
  def discard_job(message)
217
217
  with_advisory_lock do
218
- execution = head_execution(reload: true)
219
- active_job = execution.active_job(ignore_deserialization_errors: true)
220
-
221
- raise ActionForStateMismatchError if execution.finished_at.present?
222
-
223
- job_error = GoodJob::Job::DiscardJobError.new(message)
224
-
225
- update_execution = proc do
226
- execution.update(
227
- {
228
- finished_at: Time.current,
229
- error: GoodJob::Execution.format_error(job_error),
230
- }.tap do |attrs|
231
- attrs[:error_event] = ERROR_EVENT_DISCARDED if self.class.error_event_migrated?
232
- end
233
- )
234
- end
235
-
236
- if active_job.respond_to?(:instrument)
237
- active_job.send :instrument, :discard, error: job_error, &update_execution
238
- else
239
- update_execution.call
240
- end
218
+ _discard_job(message)
241
219
  end
242
220
  end
243
221
 
222
+ # Force discard a job so that it will not be executed further. Force discard allows discarding
223
+ # a running job.
224
+ # This action will add a {DiscardJobError} to the job's {Execution} and mark it as finished.
225
+ def force_discard_job(message)
226
+ _discard_job(message)
227
+ end
228
+
244
229
  # Reschedule a scheduled job so that it executes immediately (or later) by the next available execution thread.
245
230
  # @param scheduled_at [DateTime, Time] When to reschedule the job
246
231
  # @return [void]
@@ -277,5 +262,33 @@ module GoodJob
277
262
  def _head?
278
263
  _execution_id == head_execution(reload: true).id
279
264
  end
265
+
266
+ private
267
+
268
+ def _discard_job(message)
269
+ execution = head_execution(reload: true)
270
+ active_job = execution.active_job(ignore_deserialization_errors: true)
271
+
272
+ raise ActionForStateMismatchError if execution.finished_at.present?
273
+
274
+ job_error = GoodJob::Job::DiscardJobError.new(message)
275
+
276
+ update_execution = proc do
277
+ execution.update(
278
+ {
279
+ finished_at: Time.current,
280
+ error: GoodJob::Execution.format_error(job_error),
281
+ }.tap do |attrs|
282
+ attrs[:error_event] = ERROR_EVENT_DISCARDED if self.class.error_event_migrated?
283
+ end
284
+ )
285
+ end
286
+
287
+ if active_job.respond_to?(:instrument)
288
+ active_job.send :instrument, :discard, error: job_error, &update_execution
289
+ else
290
+ update_execution.call
291
+ end
292
+ end
280
293
  end
281
294
  end
@@ -31,7 +31,7 @@
31
31
  <div class="col-12 col-lg-2 text-wrap"><%= tag.span tag.code(cron_entry.job_class), class: "fs-5 mb-0" %></div>
32
32
  <div class="col-6 col-lg-2 text-wrap">
33
33
  <div class="d-lg-none small text-muted mt-1"><%= t "good_job.models.cron.schedule" %></div>
34
- <span class="font-monospace fw-bold"><%= cron_entry.schedule %></span>
34
+ <span class="font-monospace fw-bold"><%= cron_entry.display_schedule %></span>
35
35
  </div>
36
36
  <div class="col-6 col-lg-2 text-wrap small">
37
37
  <div class="d-lg-none small text-muted mt-1"><%= t "good_job.models.cron.next_scheduled" %></div>
@@ -40,7 +40,7 @@
40
40
  <div class="col-6 col-lg-2 text-wrap small">
41
41
  <% if cron_entry.last_job.present? %>
42
42
  <div class="d-lg-none small text-muted mt-1"><%= t "good_job.models.cron.last_run" %></div>
43
- <%= link_to relative_time(cron_entry.last_at), cron_entry_path(cron_entry), title: "Job #{cron_entry.last_job.id}" %>
43
+ <%= link_to relative_time(cron_entry.last_job_at), cron_entry_path(cron_entry), title: "Job #{cron_entry.last_job.id}" %>
44
44
  <% end %>
45
45
  </div>
46
46
  <div class="col d-flex gap-3 justify-content-end">
@@ -127,6 +127,13 @@
127
127
  <%=t "good_job.actions.discard" %>
128
128
  <% end %>
129
129
  </li>
130
+ <li>
131
+ <% job_force_discardable = job.status.in? [:running] %>
132
+ <%= link_to force_discard_job_path(job.id), method: :put, class: "dropdown-item #{'disabled' unless job_force_discardable}", title: t("good_job.jobs.actions.force_discard"), data: { confirm: t("good_job.jobs.actions.confirm_force_discard"), disable: true } do %>
133
+ <%= render "good_job/shared/icons/eject" %>
134
+ <%=t "good_job.actions.force_discard" %>
135
+ <% end %>
136
+ </li>
130
137
  <li>
131
138
  <%= link_to retry_job_path(job.id), method: :put, class: "dropdown-item #{'disabled' unless job.status == :discarded}", title: t("good_job.jobs.actions.retry"), data: { confirm: t("good_job.jobs.actions.confirm_retry"), disable: true } do %>
132
139
  <%= render "good_job/shared/icons/arrow_clockwise" %>
@@ -0,0 +1,4 @@
1
+ <!-- https://icons.getbootstrap.com/icons/eject/ -->
2
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-eject" viewBox="0 0 16 16">
3
+ <path d="M7.27 1.047a1 1 0 0 1 1.46 0l6.345 6.77c.6.638.146 1.683-.73 1.683H1.656C.78 9.5.326 8.455.926 7.816L7.27 1.047zM14.346 8.5 8 1.731 1.654 8.5h12.692zM.5 11.5a1 1 0 0 1 1-1h13a1 1 0 0 1 1 1v1a1 1 0 0 1-1 1h-13a1 1 0 0 1-1-1v-1zm14 0h-13v1h13v-1z" />
4
+ </svg>
@@ -4,6 +4,7 @@ de:
4
4
  actions:
5
5
  destroy: Zerstören
6
6
  discard: Verwerfen
7
+ force_discard: Verwerfen erzwingen
7
8
  inspect: Prüfen
8
9
  reschedule: Umplanen
9
10
  retry: Wiederholen
@@ -110,10 +111,14 @@ de:
110
111
  actions:
111
112
  confirm_destroy: Sind Sie sicher, dass Sie den Job zerstören wollen?
112
113
  confirm_discard: Sind Sie sicher, dass Sie den Job verwerfen wollen?
114
+ confirm_force_discard: 'Sind Sie sicher, dass Sie das Verwerfen dieses Jobs erzwingen möchten? Der Job wird als verworfen markiert, aber der laufende Job wird nicht gestoppt – er wird jedoch bei Fehlern nicht erneut versucht.
115
+
116
+ '
113
117
  confirm_reschedule: Möchten Sie den Auftrag wirklich verschieben?
114
118
  confirm_retry: Möchten Sie den Job wirklich wiederholen?
115
119
  destroy: Arbeit vernichten
116
120
  discard: Auftrag verwerfen
121
+ force_discard: Job verwerfen erzwingen
117
122
  reschedule: Auftrag neu planen
118
123
  retry: Job wiederholen
119
124
  destroy:
@@ -124,6 +129,8 @@ de:
124
129
  in_queue: in der Warteschlange
125
130
  runtime: Laufzeit
126
131
  title: Hinrichtungen
132
+ force_discard:
133
+ notice: Der Job wurde zwangsweise verworfen. Die Ausführung wird fortgesetzt, bei Fehlern wird der Vorgang jedoch nicht wiederholt
127
134
  index:
128
135
  job_pagination: Job-Paginierung
129
136
  older_jobs: Ältere Berufe
@@ -4,6 +4,7 @@ en:
4
4
  actions:
5
5
  destroy: Destroy
6
6
  discard: Discard
7
+ force_discard: Force discard
7
8
  inspect: Inspect
8
9
  reschedule: Reschedule
9
10
  retry: Retry
@@ -110,10 +111,14 @@ en:
110
111
  actions:
111
112
  confirm_destroy: Are you sure you want to destroy the job?
112
113
  confirm_discard: Are you usure you want to discard the job?
114
+ confirm_force_discard: 'Are you sure you want to force discard this job? The job will be marked as discarded but the running job will not be stopped - it will, however, not be retried on failures.
115
+
116
+ '
113
117
  confirm_reschedule: Are you sure you want to reschedule the job?
114
118
  confirm_retry: Are you sure you want to retry the job?
115
119
  destroy: Destroy job
116
120
  discard: Discard job
121
+ force_discard: Force discard job
117
122
  reschedule: Reschedule job
118
123
  retry: Retry job
119
124
  destroy:
@@ -124,6 +129,8 @@ en:
124
129
  in_queue: in queue
125
130
  runtime: runtime
126
131
  title: Executions
132
+ force_discard:
133
+ notice: Job has been force discarded. It will continue to run but it will not be retried on failures
127
134
  index:
128
135
  job_pagination: Job pagination
129
136
  older_jobs: Older jobs
@@ -4,6 +4,7 @@ es:
4
4
  actions:
5
5
  destroy: Eliminar
6
6
  discard: Descartar
7
+ force_discard: Forzar descarte
7
8
  inspect: Inspeccionar
8
9
  reschedule: Reprogramar
9
10
  retry: Reintentar
@@ -110,10 +111,12 @@ es:
110
111
  actions:
111
112
  confirm_destroy: "¿Estás seguro que querés eliminar esta tarea?"
112
113
  confirm_discard: "¿Estás seguro que querés descartar esta tarea?"
114
+ confirm_force_discard: "¿Está seguro de que desea forzar el descarte de este trabajo? El trabajo se marcará como descartado, pero el trabajo en ejecución no se detendrá; sin embargo, no se volverá a intentar en caso de falla.\n"
113
115
  confirm_reschedule: "¿Estás seguro que querés reprogramar esta tarea?"
114
116
  confirm_retry: "¿Estás seguro que querés reintentar esta tarea?"
115
117
  destroy: Eliminar tarea
116
118
  discard: Descartar tarea
119
+ force_discard: Forzar el descarte del trabajo
117
120
  reschedule: Reprogramar tarea
118
121
  retry: Reiuntentar tarea
119
122
  destroy:
@@ -124,6 +127,8 @@ es:
124
127
  in_queue: en cola
125
128
  runtime: en ejecución
126
129
  title: Ejecuciones
130
+ force_discard:
131
+ notice: Job ha sido descartado a la fuerza. Continuará ejecutándose pero no se volverá a intentar en caso de fallas.
127
132
  index:
128
133
  job_pagination: Paginación de tareas
129
134
  older_jobs: Tareas anteriores
@@ -4,6 +4,7 @@ fr:
4
4
  actions:
5
5
  destroy: Détruire
6
6
  discard: Mettre au rebut
7
+ force_discard: Forcer le rejet
7
8
  inspect: Inspecter
8
9
  reschedule: Reprogrammer
9
10
  retry: Recommencez
@@ -110,10 +111,14 @@ fr:
110
111
  actions:
111
112
  confirm_destroy: Voulez-vous vraiment détruire le job ?
112
113
  confirm_discard: Voulez-vous vraiment mettre au rebut le job ?
114
+ confirm_force_discard: 'Êtes-vous sûr de vouloir forcer l''abandon de cette tâche ? Le travail sera marqué comme abandonné mais le travail en cours d''exécution ne sera pas arrêté - il ne sera cependant pas réessayé en cas d''échec.
115
+
116
+ '
113
117
  confirm_reschedule: Voulez-vous vraiment replanifier le job ?
114
118
  confirm_retry: Voulez-vous vraiment réessayer le job ?
115
119
  destroy: Détruire le job
116
120
  discard: Mettre au rebut le job
121
+ force_discard: Forcer l'abandon du travail
117
122
  reschedule: Replanifier le job
118
123
  retry: Réessayer le job
119
124
  destroy:
@@ -124,6 +129,8 @@ fr:
124
129
  in_queue: Dans la file d'attente
125
130
  runtime: Durée
126
131
  title: Exécutions
132
+ force_discard:
133
+ notice: Le travail a été abandonné de force. Il continuera à fonctionner mais il ne sera pas réessayé en cas d'échec
127
134
  index:
128
135
  job_pagination: Pagination du job
129
136
  older_jobs: Jobs plus anciens
@@ -4,6 +4,7 @@ ja:
4
4
  actions:
5
5
  destroy: 削除する
6
6
  discard: 破棄する
7
+ force_discard: 強制破棄
7
8
  inspect: 調査する
8
9
  reschedule: 再スケジュールする
9
10
  retry: 再試行する
@@ -110,10 +111,14 @@ ja:
110
111
  actions:
111
112
  confirm_destroy: ジョブを削除してもよろしいですか?
112
113
  confirm_discard: ジョブを破棄してもよろしいですか?
114
+ confirm_force_discard: 'このジョブを強制的に破棄してもよろしいですか?ジョブは破棄済みとしてマークされますが、実行中のジョブは停止されません。ただし、失敗した場合は再試行されません。
115
+
116
+ '
113
117
  confirm_reschedule: ジョブを再スケジュールしてもよろしいですか?
114
118
  confirm_retry: ジョブを再試行してもよろしいですか?
115
119
  destroy: ジョブを削除
116
120
  discard: ジョブを破棄
121
+ force_discard: ジョブを強制的に破棄する
117
122
  reschedule: ジョブを再スケジュール
118
123
  retry: ジョブを再試行
119
124
  destroy:
@@ -124,6 +129,8 @@ ja:
124
129
  in_queue: 待機中
125
130
  runtime: 実行時間
126
131
  title: 実行
132
+ force_discard:
133
+ notice: ジョブは強制的に破棄されました。実行は継続されますが、失敗した場合は再試行されません
127
134
  index:
128
135
  job_pagination: ジョブのページネーション
129
136
  older_jobs: 古いジョブ
@@ -4,6 +4,7 @@ nl:
4
4
  actions:
5
5
  destroy: Vernietigen
6
6
  discard: Weggooien
7
+ force_discard: Forceer weggooien
7
8
  inspect: Inspecteren
8
9
  reschedule: Opnieuw plannen
9
10
  retry: Opnieuw proberen
@@ -110,10 +111,14 @@ nl:
110
111
  actions:
111
112
  confirm_destroy: Weet je zeker dat je de baan wilt vernietigen?
112
113
  confirm_discard: Wilt u de baan afwijzen?
114
+ confirm_force_discard: 'Weet u zeker dat u deze taak geforceerd wilt weggooien? De taak wordt gemarkeerd als verwijderd, maar de lopende taak wordt niet gestopt. Bij fouten wordt deze echter niet opnieuw geprobeerd.
115
+
116
+ '
113
117
  confirm_reschedule: Weet u zeker dat u de taak opnieuw wilt inplannen?
114
118
  confirm_retry: Weet u zeker dat u de taak opnieuw wilt proberen?
115
119
  destroy: Baan vernietigen
116
120
  discard: Gooi de baan weg
121
+ force_discard: Forceer taak weggooien
117
122
  reschedule: Taak opnieuw plannen
118
123
  retry: Taak opnieuw proberen
119
124
  destroy:
@@ -124,6 +129,8 @@ nl:
124
129
  in_queue: in de wachtrij
125
130
  runtime: looptijd
126
131
  title: Executies
132
+ force_discard:
133
+ notice: Baan is gedwongen verwijderd. Het blijft actief, maar wordt niet opnieuw geprobeerd als er fouten optreden
127
134
  index:
128
135
  job_pagination: Taak paginering
129
136
  older_jobs: Oudere banen
@@ -4,6 +4,7 @@ ru:
4
4
  actions:
5
5
  destroy: Разрушать
6
6
  discard: Отказаться
7
+ force_discard: Принудительно отменить
7
8
  inspect: Осмотреть
8
9
  reschedule: Перенести
9
10
  retry: Повторить попытку
@@ -134,10 +135,14 @@ ru:
134
135
  actions:
135
136
  confirm_destroy: Вы уверены, что хотите уничтожить задание?
136
137
  confirm_discard: Вы уверены, что хотите отказаться от задания?
138
+ confirm_force_discard: 'Вы уверены, что хотите принудительно отменить это задание? Задание будет помечено как отброшенное, но выполняемое задание не будет остановлено, однако в случае сбоя оно не будет повторено.
139
+
140
+ '
137
141
  confirm_reschedule: Вы уверены, что хотите перенести задание?
138
142
  confirm_retry: Вы уверены, что хотите повторить задание?
139
143
  destroy: Уничтожить работу
140
144
  discard: Отменить задание
145
+ force_discard: Принудительно отменить задание
141
146
  reschedule: Перенести задание
142
147
  retry: Повторить задание
143
148
  destroy:
@@ -148,6 +153,8 @@ ru:
148
153
  in_queue: в очереди
149
154
  runtime: время выполнения
150
155
  title: Казни
156
+ force_discard:
157
+ notice: Иов был принудительно отброшен. Он продолжит работу, но не будет повторяться в случае сбоя.
151
158
  index:
152
159
  job_pagination: Пагинация вакансий
153
160
  older_jobs: Старые рабочие места
@@ -4,6 +4,7 @@ tr:
4
4
  actions:
5
5
  destroy: Sil
6
6
  discard: İptal Et
7
+ force_discard: Atmaya zorla
7
8
  inspect: İncele
8
9
  reschedule: Yeniden planla
9
10
  retry: Tekrar dene
@@ -110,10 +111,14 @@ tr:
110
111
  actions:
111
112
  confirm_destroy: Bu işi silmek istediğinizden emin misiniz?
112
113
  confirm_discard: Bu işi iptal etmek istediğinizden emin misiniz?
114
+ confirm_force_discard: 'Bu işi zorla iptal etmek istediğinizden emin misiniz? İş atıldı olarak işaretlenecek ancak devam eden iş durdurulmayacak; ancak başarısızlık durumunda yeniden denenmeyecek.
115
+
116
+ '
113
117
  confirm_reschedule: Bu işi yeniden planlamak istediğinizden emin misiniz?
114
118
  confirm_retry: Bu işi tekrar denemek istediğinizden emin misiniz?
115
119
  destroy: İşi Sil
116
120
  discard: İşi İptal Et
121
+ force_discard: İşi zorla atmaya zorla
117
122
  reschedule: İşi Yeniden Planla
118
123
  retry: İşi Tekrar Dene
119
124
  destroy:
@@ -124,6 +129,8 @@ tr:
124
129
  in_queue: sırada
125
130
  runtime: çalışma süresi
126
131
  title: İşlemler
132
+ force_discard:
133
+ notice: İş zorla atıldı. Çalışmaya devam edecek ancak arıza durumunda yeniden denenmeyecek
127
134
  index:
128
135
  job_pagination: İş sayfalandırması
129
136
  older_jobs: Daha eski işler
@@ -4,6 +4,7 @@ uk:
4
4
  actions:
5
5
  destroy: Видалити
6
6
  discard: Відхилити
7
+ force_discard: Примусово скинути
7
8
  inspect: Оглянути
8
9
  reschedule: Перепланувати
9
10
  retry: Повторити
@@ -134,10 +135,14 @@ uk:
134
135
  actions:
135
136
  confirm_destroy: Ви впевнені, що хочете видалити завдання?
136
137
  confirm_discard: Ви впевнені, що хочете відхилити завдання?
138
+ confirm_force_discard: 'Ви впевнені, що хочете примусово скасувати цю роботу? Завдання буде позначено як відхилене, але виконуване завдання не буде зупинено – однак його не буде повторено у випадку помилок.
139
+
140
+ '
137
141
  confirm_reschedule: Ви впевнені, що хочете перепланувати завдання?
138
142
  confirm_retry: Ви впевнені, що хочете повторити завдання?
139
143
  destroy: Видалити завдання
140
144
  discard: Відхилити завдання
145
+ force_discard: Примусово скасувати завдання
141
146
  reschedule: Перепланувати завдання
142
147
  retry: Повторити завдання
143
148
  destroy:
@@ -148,6 +153,8 @@ uk:
148
153
  in_queue: у черзі
149
154
  runtime: час виконання
150
155
  title: Виконання
156
+ force_discard:
157
+ notice: Роботу примусово скасовано. Він продовжуватиме працювати, але не буде повторюватися в разі помилок
151
158
  index:
152
159
  job_pagination: Пагінація робіт
153
160
  older_jobs: Старі роботи
data/config/routes.rb CHANGED
@@ -11,6 +11,7 @@ GoodJob::Engine.routes.draw do
11
11
 
12
12
  member do
13
13
  put :discard
14
+ put :force_discard
14
15
  put :reschedule
15
16
  put :retry
16
17
  end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GoodJob
4
+ # An object that has case-equality to a Proc or Lambda by responding to #call.
5
+ # This can be used to duck-type match in a case statement.
6
+ module Callable
7
+ # Whether the object responds to #call
8
+ def self.===(other)
9
+ other.respond_to?(:call)
10
+ end
11
+ end
12
+ end
@@ -24,6 +24,9 @@ module GoodJob # :nodoc:
24
24
  WAIT_INTERVAL = 1
25
25
  # Seconds to wait if database cannot be connected to
26
26
  RECONNECT_INTERVAL = 5
27
+ # Number of consecutive connection errors before reporting an error
28
+ CONNECTION_ERRORS_REPORTING_THRESHOLD = 6
29
+
27
30
  # Connection errors that will wait {RECONNECT_INTERVAL} before reconnecting
28
31
  CONNECTION_ERRORS = %w[
29
32
  ActiveRecord::ConnectionNotEstablished
@@ -31,7 +34,6 @@ module GoodJob # :nodoc:
31
34
  PG::UnableToSend
32
35
  PG::Error
33
36
  ].freeze
34
- CONNECTION_ERRORS_REPORTING_THRESHOLD = 3
35
37
 
36
38
  # @!attribute [r] instances
37
39
  # @!scope class
@@ -69,8 +71,8 @@ module GoodJob # :nodoc:
69
71
  @mutex = Mutex.new
70
72
  @shutdown_event = Concurrent::Event.new.tap(&:set)
71
73
  @running = Concurrent::AtomicBoolean.new(false)
72
- @connected = Concurrent::AtomicBoolean.new(false)
73
- @listening = Concurrent::AtomicBoolean.new(false)
74
+ @connected = Concurrent::Event.new
75
+ @listening = Concurrent::Event.new
74
76
  @connection_errors_count = Concurrent::AtomicFixnum.new(0)
75
77
  @connection_errors_reported = Concurrent::AtomicBoolean.new(false)
76
78
  @enable_listening = enable_listening
@@ -85,15 +87,25 @@ module GoodJob # :nodoc:
85
87
  end
86
88
 
87
89
  # Tests whether the notifier is active and has acquired a dedicated database connection.
90
+ # @param timeout [Numeric, nil] Seconds to wait for condition to be true, -1 is forever
88
91
  # @return [true, false, nil]
89
- def connected?
90
- @connected.true?
92
+ def connected?(timeout: nil)
93
+ if timeout.nil?
94
+ @connected.set?
95
+ else
96
+ @connected.wait(timeout == -1 ? nil : timeout)
97
+ end
91
98
  end
92
99
 
93
100
  # Tests whether the notifier is listening for new messages.
101
+ # @param timeout [Numeric, nil] Seconds to wait for condition to be true, -1 is forever
94
102
  # @return [true, false, nil]
95
- def listening?
96
- @listening.true?
103
+ def listening?(timeout: nil)
104
+ if timeout.nil?
105
+ @listening.set?
106
+ else
107
+ @listening.wait(timeout == -1 ? nil : timeout)
108
+ end
97
109
  end
98
110
 
99
111
  def shutdown?
@@ -114,11 +126,12 @@ module GoodJob # :nodoc:
114
126
 
115
127
  if @executor.shutdown? || @task&.complete?
116
128
  # clean up in the even the executor is killed
117
- @connected.make_false
118
- @listening.make_false
129
+ @connected.reset
130
+ @listening.reset
119
131
  @shutdown_event.set
120
132
  else
121
133
  @shutdown_event.wait(timeout == -1 ? nil : timeout) unless timeout.nil?
134
+ @connected.reset if @shutdown_event.set?
122
135
  end
123
136
  @shutdown_event.set?
124
137
  end
@@ -152,6 +165,7 @@ module GoodJob # :nodoc:
152
165
  if connection_error
153
166
  @connection_errors_count.increment
154
167
  if @connection_errors_reported.false? && @connection_errors_count.value >= CONNECTION_ERRORS_REPORTING_THRESHOLD
168
+ @connected.reset
155
169
  GoodJob._on_thread_error(thread_error)
156
170
  @connection_errors_reported.make_true
157
171
  end
@@ -180,15 +194,17 @@ module GoodJob # :nodoc:
180
194
  end
181
195
 
182
196
  def create_listen_task(delay: 0)
183
- @task = Concurrent::ScheduledTask.new(delay, args: [@recipients, @running, @executor, @enable_listening, @listening], executor: @executor) do |thr_recipients, thr_running, thr_executor, thr_enable_listening, thr_listening|
197
+ @task = Concurrent::ScheduledTask.new(delay, args: [@recipients, @running, @executor, @enable_listening, @connected, @listening], executor: @executor) do |thr_recipients, thr_running, thr_executor, thr_enable_listening, thr_connected, thr_listening|
184
198
  with_connection do
199
+ thr_connected.set
200
+
185
201
  begin
186
202
  Rails.application.executor.wrap do
187
203
  run_callbacks :listen do
188
204
  if thr_enable_listening
189
205
  ActiveSupport::Notifications.instrument("notifier_listen.good_job") do
190
206
  connection.execute("LISTEN #{CHANNEL}")
191
- thr_listening.make_true
207
+ thr_listening.set
192
208
  end
193
209
  end
194
210
  end
@@ -216,7 +232,7 @@ module GoodJob # :nodoc:
216
232
  run_callbacks :unlisten do
217
233
  if thr_enable_listening
218
234
  ActiveSupport::Notifications.instrument("notifier_unlisten.good_job") do
219
- thr_listening.make_false
235
+ thr_listening.reset
220
236
  connection.execute("UNLISTEN *")
221
237
  end
222
238
  end
@@ -237,11 +253,9 @@ module GoodJob # :nodoc:
237
253
  end
238
254
  end
239
255
  connection.execute("SET application_name = #{connection.quote(self.class.name)}")
240
- @connected.make_true
241
256
 
242
257
  yield
243
258
  ensure
244
- @connected.make_false
245
259
  connection&.disconnect!
246
260
  self.connection = nil
247
261
  end
@@ -37,7 +37,7 @@ module GoodJob
37
37
  started ? [200, {}, ["Started"]] : [503, {}, ["Not started"]]
38
38
  when '/status/connected'
39
39
  connected = GoodJob::Scheduler.instances.any? && GoodJob::Scheduler.instances.all?(&:running?) &&
40
- GoodJob::Notifier.instances.any? && GoodJob::Notifier.instances.all?(&:listening?)
40
+ GoodJob::Notifier.instances.any? && GoodJob::Notifier.instances.all?(&:connected?)
41
41
  connected ? [200, {}, ["Connected"]] : [503, {}, ["Not connected"]]
42
42
  else
43
43
  [404, {}, ["Not found"]]
@@ -2,7 +2,7 @@
2
2
 
3
3
  module GoodJob
4
4
  # GoodJob gem version.
5
- VERSION = '3.18.2'
5
+ VERSION = '3.19.0'
6
6
 
7
7
  # GoodJob version as Gem::Version object
8
8
  GEM_VERSION = Gem::Version.new(VERSION)
data/lib/good_job.rb CHANGED
@@ -16,6 +16,7 @@ require "good_job/active_job_extensions/notify_options"
16
16
 
17
17
  require "good_job/assignable_connection"
18
18
  require "good_job/bulk"
19
+ require "good_job/callable"
19
20
  require "good_job/capsule"
20
21
  require "good_job/cleanup_tracker"
21
22
  require "good_job/cli"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: good_job
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.18.2
4
+ version: 3.19.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ben Sheldon
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-09-02 00:00:00.000000000 Z
11
+ date: 2023-09-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activejob
@@ -122,62 +122,6 @@ dependencies:
122
122
  - - ">="
123
123
  - !ruby/object:Gem::Version
124
124
  version: '0'
125
- - !ruby/object:Gem::Dependency
126
- name: dotenv
127
- requirement: !ruby/object:Gem::Requirement
128
- requirements:
129
- - - "~>"
130
- - !ruby/object:Gem::Version
131
- version: 2.7.6
132
- type: :development
133
- prerelease: false
134
- version_requirements: !ruby/object:Gem::Requirement
135
- requirements:
136
- - - "~>"
137
- - !ruby/object:Gem::Version
138
- version: 2.7.6
139
- - !ruby/object:Gem::Dependency
140
- name: foreman
141
- requirement: !ruby/object:Gem::Requirement
142
- requirements:
143
- - - ">="
144
- - !ruby/object:Gem::Version
145
- version: '0'
146
- type: :development
147
- prerelease: false
148
- version_requirements: !ruby/object:Gem::Requirement
149
- requirements:
150
- - - ">="
151
- - !ruby/object:Gem::Version
152
- version: '0'
153
- - !ruby/object:Gem::Dependency
154
- name: gem-release
155
- requirement: !ruby/object:Gem::Requirement
156
- requirements:
157
- - - ">="
158
- - !ruby/object:Gem::Version
159
- version: '0'
160
- type: :development
161
- prerelease: false
162
- version_requirements: !ruby/object:Gem::Requirement
163
- requirements:
164
- - - ">="
165
- - !ruby/object:Gem::Version
166
- version: '0'
167
- - !ruby/object:Gem::Dependency
168
- name: github_changelog_generator
169
- requirement: !ruby/object:Gem::Requirement
170
- requirements:
171
- - - ">="
172
- - !ruby/object:Gem::Version
173
- version: '0'
174
- type: :development
175
- prerelease: false
176
- version_requirements: !ruby/object:Gem::Requirement
177
- requirements:
178
- - - ">="
179
- - !ruby/object:Gem::Version
180
- version: '0'
181
125
  - !ruby/object:Gem::Dependency
182
126
  name: kramdown
183
127
  requirement: !ruby/object:Gem::Requirement
@@ -369,6 +313,7 @@ files:
369
313
  - app/views/good_job/shared/icons/_clock.html.erb
370
314
  - app/views/good_job/shared/icons/_dash_circle.html.erb
371
315
  - app/views/good_job/shared/icons/_dots.html.erb
316
+ - app/views/good_job/shared/icons/_eject.html.erb
372
317
  - app/views/good_job/shared/icons/_exclamation.html.erb
373
318
  - app/views/good_job/shared/icons/_info.html.erb
374
319
  - app/views/good_job/shared/icons/_moon_stars_fill.html.erb
@@ -408,6 +353,7 @@ files:
408
353
  - lib/good_job/adapter.rb
409
354
  - lib/good_job/assignable_connection.rb
410
355
  - lib/good_job/bulk.rb
356
+ - lib/good_job/callable.rb
411
357
  - lib/good_job/capsule.rb
412
358
  - lib/good_job/cleanup_tracker.rb
413
359
  - lib/good_job/cli.rb