good_job 4.1.0 → 4.1.1

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.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +41 -0
  3. data/app/charts/good_job/performance_index_chart.rb +1 -1
  4. data/app/charts/good_job/performance_show_chart.rb +1 -1
  5. data/app/controllers/good_job/application_controller.rb +1 -1
  6. data/app/controllers/good_job/frontends_controller.rb +6 -2
  7. data/app/controllers/good_job/performance_controller.rb +1 -1
  8. data/app/frontend/good_job/icons.svg +79 -0
  9. data/app/frontend/good_job/style.css +5 -0
  10. data/app/helpers/good_job/icons_helper.rb +8 -5
  11. data/app/models/concerns/good_job/advisory_lockable.rb +17 -7
  12. data/app/models/concerns/good_job/reportable.rb +8 -12
  13. data/app/models/good_job/batch.rb +10 -5
  14. data/app/models/good_job/batch_record.rb +18 -15
  15. data/app/models/good_job/discrete_execution.rb +6 -59
  16. data/app/models/good_job/execution.rb +59 -4
  17. data/app/models/good_job/execution_result.rb +6 -6
  18. data/app/models/good_job/job.rb +567 -12
  19. data/app/views/good_job/batches/_jobs.erb +1 -1
  20. data/app/views/good_job/batches/_table.erb +1 -1
  21. data/app/views/good_job/jobs/index.html.erb +1 -1
  22. data/app/views/layouts/good_job/application.html.erb +7 -7
  23. data/config/brakeman.ignore +75 -0
  24. data/config/locales/de.yml +49 -49
  25. data/config/locales/es.yml +14 -14
  26. data/config/routes.rb +3 -3
  27. data/lib/good_job/active_job_extensions/concurrency.rb +105 -98
  28. data/lib/good_job/adapter/inline_buffer.rb +73 -0
  29. data/lib/good_job/adapter.rb +59 -53
  30. data/lib/good_job/configuration.rb +3 -4
  31. data/lib/good_job/cron_manager.rb +1 -3
  32. data/lib/good_job/current_thread.rb +4 -4
  33. data/lib/good_job/version.rb +1 -1
  34. data/lib/good_job.rb +6 -5
  35. metadata +6 -20
  36. data/app/models/good_job/base_execution.rb +0 -605
  37. data/app/views/good_job/shared/icons/_arrow_clockwise.html.erb +0 -5
  38. data/app/views/good_job/shared/icons/_check.html.erb +0 -5
  39. data/app/views/good_job/shared/icons/_circle_half.html.erb +0 -4
  40. data/app/views/good_job/shared/icons/_clock.html.erb +0 -5
  41. data/app/views/good_job/shared/icons/_dash_circle.html.erb +0 -5
  42. data/app/views/good_job/shared/icons/_dots.html.erb +0 -3
  43. data/app/views/good_job/shared/icons/_eject.html.erb +0 -4
  44. data/app/views/good_job/shared/icons/_exclamation.html.erb +0 -5
  45. data/app/views/good_job/shared/icons/_globe.html.erb +0 -3
  46. data/app/views/good_job/shared/icons/_info.html.erb +0 -4
  47. data/app/views/good_job/shared/icons/_moon_stars_fill.html.erb +0 -5
  48. data/app/views/good_job/shared/icons/_pause.html.erb +0 -4
  49. data/app/views/good_job/shared/icons/_play.html.erb +0 -4
  50. data/app/views/good_job/shared/icons/_skip_forward.html.erb +0 -4
  51. data/app/views/good_job/shared/icons/_stop.html.erb +0 -4
  52. data/app/views/good_job/shared/icons/_sun_fill.html.erb +0 -4
@@ -0,0 +1,75 @@
1
+ {
2
+ "ignored_warnings": [
3
+ {
4
+ "warning_type": "Dynamic Render Path",
5
+ "warning_code": 15,
6
+ "fingerprint": "041ae0dc908151bac0ef0952c625f0dce3a05d2c01a710397a613ef10083f7ae",
7
+ "check_name": "Render",
8
+ "message": "Render path contains parameter value",
9
+ "file": "app/controllers/good_job/frontends_controller.rb",
10
+ "line": 47,
11
+ "link": "https://brakemanscanner.org/docs/warning_types/dynamic_render_path/",
12
+ "code": "render(file => (self.class.js_modules[params[:id].to_sym] or raise(ActionController::RoutingError, \"Not Found\")), {})",
13
+ "render_path": null,
14
+ "location": {
15
+ "type": "method",
16
+ "class": "GoodJob::FrontendsController",
17
+ "method": "module"
18
+ },
19
+ "user_input": "params[:id].to_sym",
20
+ "confidence": "Weak",
21
+ "cwe_id": [
22
+ 22
23
+ ],
24
+ "note": "Files are explicitly enumerated in the array"
25
+ },
26
+ {
27
+ "warning_type": "Dynamic Render Path",
28
+ "warning_code": 15,
29
+ "fingerprint": "b0c2888c9b217671d90d0141b49b036af3b2a70c63b02968cc97ae2052c86272",
30
+ "check_name": "Render",
31
+ "message": "Render path contains parameter value",
32
+ "file": "app/controllers/good_job/frontends_controller.rb",
33
+ "line": 41,
34
+ "link": "https://brakemanscanner.org/docs/warning_types/dynamic_render_path/",
35
+ "code": "render(file => ({ :css => ({ :bootstrap => GoodJob::Engine.root.join(\"app\", \"frontend\", \"good_job\", \"vendor\", \"bootstrap\", \"bootstrap.min.css\"), :style => GoodJob::Engine.root.join(\"app\", \"frontend\", \"good_job\", \"style.css\") }), :js => ({ :bootstrap => GoodJob::Engine.root.join(\"app\", \"frontend\", \"good_job\", \"vendor\", \"bootstrap\", \"bootstrap.bundle.min.js\"), :chartjs => GoodJob::Engine.root.join(\"app\", \"frontend\", \"good_job\", \"vendor\", \"chartjs\", \"chart.min.js\"), :es_module_shims => GoodJob::Engine.root.join(\"app\", \"frontend\", \"good_job\", \"vendor\", \"es_module_shims.js\"), :rails_ujs => GoodJob::Engine.root.join(\"app\", \"frontend\", \"good_job\", \"vendor\", \"rails_ujs.js\") }), :svg => ({ :icons => GoodJob::Engine.root.join(\"app\", \"frontend\", \"good_job\", \"icons.svg\") }) }.dig(params[:format].to_sym, params[:id].to_sym) or raise(ActionController::RoutingError, \"Not Found\")), {})",
36
+ "render_path": null,
37
+ "location": {
38
+ "type": "method",
39
+ "class": "GoodJob::FrontendsController",
40
+ "method": "static"
41
+ },
42
+ "user_input": "params[:id].to_sym",
43
+ "confidence": "Weak",
44
+ "cwe_id": [
45
+ 22
46
+ ],
47
+ "note": "Files are explicitly enumerated in the array"
48
+ },
49
+ {
50
+ "warning_type": "SQL Injection",
51
+ "warning_code": 0,
52
+ "fingerprint": "c837568c590d9608a8bb9927b31b9597aaacc72053b6482e1a54bd02aa0dd2d7",
53
+ "check_name": "SQL",
54
+ "message": "Possible SQL injection",
55
+ "file": "app/models/good_job/job.rb",
56
+ "line": 140,
57
+ "link": "https://brakemanscanner.org/docs/warning_types/sql_injection/",
58
+ "code": "Arel.sql(\"(CASE #{queues.map.with_index do\n sanitize_sql_array([\"WHEN queue_name = ? THEN ?\", queue_name, index])\n end.join(\" \")} ELSE #{queues.size} END)\")",
59
+ "render_path": null,
60
+ "location": {
61
+ "type": "method",
62
+ "class": "Job",
63
+ "method": null
64
+ },
65
+ "user_input": "queues.map.with_index do\n sanitize_sql_array([\"WHEN queue_name = ? THEN ?\", queue_name, index])\n end.join(\" \")",
66
+ "confidence": "Medium",
67
+ "cwe_id": [
68
+ 89
69
+ ],
70
+ "note": "Developer provided value, queue_name, is sanitized."
71
+ }
72
+ ],
73
+ "updated": "2024-07-18 18:05:56 -0700",
74
+ "brakeman_version": "6.1.2"
75
+ }
@@ -10,17 +10,17 @@ de:
10
10
  retry: Wiederholen
11
11
  batches:
12
12
  index:
13
- older_batches: Ältere Chargen
14
- title: Chargen
13
+ older_batches: Ältere Batches
14
+ title: Batches
15
15
  jobs:
16
16
  actions:
17
- confirm_destroy: Sind Sie sicher, dass Sie diesen Job zerstören wollen?
18
- confirm_discard: Möchten Sie diesen Job wirklich verwerfen?
19
- confirm_reschedule: Möchten Sie diesen Auftrag wirklich verschieben?
20
- confirm_retry: Möchten Sie diesen Job wirklich wiederholen?
21
- destroy: Zerstöre Job
22
- discard: Auftrag verwerfen
23
- reschedule: Auftrag neu planen
17
+ confirm_destroy: Bist du sicher, dass du diesen Job zerstören wilst?
18
+ confirm_discard: Bist du sicher, dass du diesen Job verwerfen willst?
19
+ confirm_reschedule: Bist du sicher, dass du diesen Job verschieben willst?
20
+ confirm_retry: Bist du sicher, dass du diesen Job erneut versuchen willst?
21
+ destroy: Job zerstören
22
+ discard: Job verwerfen
23
+ reschedule: Job neu planen
24
24
  retry: Job wiederholen
25
25
  title: Aktionen
26
26
  no_jobs_found: Keine Jobs gefunden.
@@ -29,12 +29,12 @@ de:
29
29
  batched_jobs: Batch-Jobs
30
30
  callback_jobs: Callback-Jobs
31
31
  table:
32
- no_batches_found: Keine Chargen gefunden.
32
+ no_batches_found: Keine Batches gefunden.
33
33
  cron_entries:
34
34
  actions:
35
- confirm_disable: Möchten Sie diesen Cron-Eintrag wirklich deaktivieren?
36
- confirm_enable: Möchten Sie diesen Cron-Eintrag wirklich aktivieren?
37
- confirm_enqueue: Möchten Sie diesen Cron-Eintrag wirklich in die Warteschlange stellen?
35
+ confirm_disable: Bist du sicher, dass du diesen Cron-Eintrag deaktivieren willst?
36
+ confirm_enable: Bist du sicher, dass du diesen Cron-Eintrag aktivieren willst?
37
+ confirm_enqueue: Bist du sicher, dass du diesen Cron-Eintrag in die Warteschlange stellen willst?
38
38
  disable: Cron-Eintrag deaktivieren
39
39
  enable: Cron-Eintrag aktivieren
40
40
  enqueue: Cron-Eintrag jetzt einreihen
@@ -48,7 +48,7 @@ de:
48
48
  no_cron_schedules_found: Keine Cron-Zeitpläne gefunden.
49
49
  title: Cron-Zeitpläne
50
50
  show:
51
- cron_entry_key: Cron-Eingabetaste
51
+ cron_entry_key: Cron-Schlüssel
52
52
  datetime:
53
53
  distance_in_words:
54
54
  about_x_hours:
@@ -95,11 +95,11 @@ de:
95
95
  minutes: "%{min}m %{sec}s"
96
96
  seconds: "%{sec}s"
97
97
  error_event:
98
- discarded: Weggeworfen
98
+ discarded: Verworfen
99
99
  handled: Abgewickelt
100
100
  interrupted: Unterbrochen
101
101
  retried: Wiederholt
102
- retry_stopped: Der Wiederholungsversuch wurde gestoppt
102
+ retry_stopped: Wiederholung gestoppt
103
103
  unhandled: Unbehandelt
104
104
  helpers:
105
105
  relative_time:
@@ -107,20 +107,20 @@ de:
107
107
  past: Vor %{time}
108
108
  jobs:
109
109
  actions:
110
- confirm_destroy: Sind Sie sicher, dass Sie den Job zerstören wollen?
111
- confirm_discard: Sind Sie sicher, dass Sie den Job verwerfen wollen?
112
- 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.
113
- confirm_reschedule: Möchten Sie den Auftrag wirklich verschieben?
114
- confirm_retry: Möchten Sie den Job wirklich wiederholen?
115
- destroy: Arbeit vernichten
116
- discard: Auftrag verwerfen
110
+ confirm_destroy: Bist du sicher, dass du diesen Job zerstören willst?
111
+ confirm_discard: Bist du sicher, dass du diesen Job verwerfen willst?
112
+ confirm_force_discard: Bist du sicher, dass du 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.
113
+ confirm_reschedule: Bist du sicher, dass du diesen Job verschieben willst?
114
+ confirm_retry: Bist du sicher, dass du diesen Job wiederholen willst?
115
+ destroy: Job zerstören
116
+ discard: Job verwerfen
117
117
  force_discard: Job verwerfen erzwingen
118
- reschedule: Auftrag neu planen
118
+ reschedule: Job neu planen
119
119
  retry: Job wiederholen
120
120
  destroy:
121
121
  notice: Hiob wurde zerstört
122
122
  discard:
123
- notice: Auftrag wurde verworfen
123
+ notice: Job wurde verworfen
124
124
  executions:
125
125
  application_trace: Application Trace
126
126
  full_trace: Full Trace
@@ -131,25 +131,25 @@ de:
131
131
  notice: Der Job wurde zwangsweise verworfen. Die Ausführung wird fortgesetzt, bei Fehlern wird der Vorgang jedoch nicht wiederholt
132
132
  index:
133
133
  job_pagination: Job-Paginierung
134
- older_jobs: Ältere Berufe
134
+ older_jobs: Ältere Jobs
135
135
  reschedule:
136
- notice: Auftrag wurde verschoben
136
+ notice: Job wurde neu planen
137
137
  retry:
138
- notice: Auftrag wurde wiederholt
138
+ notice: Job wurde wiederholt
139
139
  show:
140
- jobs: Arbeitsplätze
140
+ jobs: Jobs
141
141
  table:
142
142
  actions:
143
143
  apply_to_all:
144
- one: Bewerben Sie sich auf alle 1 Stelle.
145
- other: Bewerben Sie sich auf alle %{count} Stellen.
146
- confirm_destroy_all: Möchten Sie die ausgewählten Jobs wirklich löschen?
147
- confirm_discard_all: Möchten Sie die ausgewählten Jobs wirklich verwerfen?
148
- confirm_reschedule_all: Möchten Sie die ausgewählten Jobs wirklich neu planen?
149
- confirm_retry_all: Möchten Sie die ausgewählten Jobs wirklich wiederholen?
150
- destroy_all: Zerstöre alle
144
+ one: Auf einen Job anwenden.
145
+ other: Auf alle %{count} Jobs anwenden.
146
+ confirm_destroy_all: Bist du sicher, dass du die ausgewählten Jobs löschen willst?
147
+ confirm_discard_all: Bist du sicher, dass du die ausgewählten Jobs verferfen willst?
148
+ confirm_reschedule_all: Bist du sicher, dass du die ausgewählten Jobs neu planen willst?
149
+ confirm_retry_all: Bist du sicher, dass du die ausgewählten Jobs wiederholen willst?
150
+ destroy_all: Alle zerstören
151
151
  discard_all: Alle verwerfen
152
- reschedule_all: Alles neu planen
152
+ reschedule_all: Alle neu planen
153
153
  retry_all: Alle wiederholen
154
154
  title: Aktionen
155
155
  no_jobs_found: Keine Jobs gefunden.
@@ -165,7 +165,7 @@ de:
165
165
  enqueued_at: Eingereiht bei
166
166
  finished: Fertig
167
167
  finished_at: Fertig um
168
- jobs: Arbeitsplätze
168
+ jobs: Jobs
169
169
  name: Name
170
170
  cron:
171
171
  class: Klasse
@@ -197,9 +197,9 @@ de:
197
197
  performance:
198
198
  index:
199
199
  average_duration: Durchschnittliche Dauer
200
- chart_title: Gesamtdauer der Auftragsausführung in Sekunden
201
- executions: Hinrichtungen
202
- job_class: Berufsklasse
200
+ chart_title: Gesamtdauer der Jobs in Sekunden
201
+ executions: Ausführungen
202
+ job_class: Job-Klasse
203
203
  maximum_duration: Maximale Dauer
204
204
  minimum_duration: Mindestdauer
205
205
  title: Leistung
@@ -210,8 +210,8 @@ de:
210
210
  index:
211
211
  cron_enabled: Cron aktiviert
212
212
  no_good_job_processes_found: Keine GoodJob-Prozesse gefunden.
213
- process: Verfahren
214
- schedulers: Planer
213
+ process: Prozess
214
+ schedulers: Scheduler
215
215
  started: Gestartet
216
216
  title: Prozesse
217
217
  updated: Aktualisiert
@@ -224,9 +224,9 @@ de:
224
224
  all: Alle
225
225
  all_jobs: Alle Jobs
226
226
  all_queues: Alle Warteschlangen
227
- clear: Klar
228
- job_name: Berufsbezeichnung
229
- placeholder: Suchen Sie nach Klasse, Job-ID, Jobparametern und Fehlertext.
227
+ clear: Löschen
228
+ job_name: Job-Name
229
+ placeholder: Suche nach Klasse, Job-ID, Jobparameter und Fehlertext.
230
230
  queue_name: Warteschlangenname
231
231
  search: Suchen
232
232
  navbar:
@@ -240,14 +240,14 @@ de:
240
240
  theme:
241
241
  auto: Auto
242
242
  dark: Dunkel
243
- light: Licht
244
- theme: Thema
243
+ light: Hell
244
+ theme: Theme
245
245
  pending_migrations: GoodJob hat ausstehende Datenbankmigrationen.
246
246
  secondary_navbar:
247
247
  inspiration: Denk daran, auch du machst einen guten Job!
248
248
  last_updated: Zuletzt aktualisiert
249
249
  status:
250
- discarded: Ausrangiert
250
+ discarded: Verworfen
251
251
  queued: In der Warteschlange
252
252
  retried: Wiederholt
253
253
  running: Laufend
@@ -10,8 +10,8 @@ es:
10
10
  retry: Reintentar
11
11
  batches:
12
12
  index:
13
- older_batches: Batches anteriores
14
- title: Batches
13
+ older_batches: Lotes anteriores
14
+ title: Lotes
15
15
  jobs:
16
16
  actions:
17
17
  confirm_destroy: "¿Estás seguro que querés eliminar esta tarea?"
@@ -26,10 +26,10 @@ es:
26
26
  no_jobs_found: No hay tareas.
27
27
  show:
28
28
  attributes: Atributos
29
- batched_jobs: Batched Jobs
29
+ batched_jobs: Tareas en lote
30
30
  callback_jobs: Callback Jobs
31
31
  table:
32
- no_batches_found: No hay batches.
32
+ no_batches_found: No hay lotes.
33
33
  cron_entries:
34
34
  actions:
35
35
  confirm_disable: "¿Estás seguro que querés deshabilitar esta tarea programada?"
@@ -109,12 +109,12 @@ es:
109
109
  actions:
110
110
  confirm_destroy: "¿Estás seguro que querés eliminar esta tarea?"
111
111
  confirm_discard: "¿Estás seguro que querés descartar esta tarea?"
112
- 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"
112
+ confirm_force_discard: "¿Está seguro de que desea forzar el descarte de esta tarea? La tarea se marcará como descartada, pero la tarea en ejecución no se detendrá; sin embargo, no se volverá a intentar en caso de falla.\n"
113
113
  confirm_reschedule: "¿Estás seguro que querés reprogramar esta tarea?"
114
114
  confirm_retry: "¿Estás seguro que querés reintentar esta tarea?"
115
115
  destroy: Eliminar tarea
116
116
  discard: Descartar tarea
117
- force_discard: Forzar el descarte del trabajo
117
+ force_discard: Forzar el descarte de la tarea
118
118
  reschedule: Reprogramar tarea
119
119
  retry: Reiuntentar tarea
120
120
  destroy:
@@ -122,13 +122,13 @@ es:
122
122
  discard:
123
123
  notice: La tarea ha sido descartada
124
124
  executions:
125
- application_trace: Application Trace
126
- full_trace: Full Trace
125
+ application_trace: Traza de la aplicación
126
+ full_trace: Traza completa
127
127
  in_queue: en cola
128
128
  runtime: en ejecución
129
129
  title: Ejecuciones
130
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.
131
+ notice: La tarea ha sido descartada a la fuerza. Continuará ejecutándose pero no se volverá a intentar en caso de fallas.
132
132
  index:
133
133
  job_pagination: Paginación de tareas
134
134
  older_jobs: Tareas anteriores
@@ -197,12 +197,12 @@ es:
197
197
  performance:
198
198
  index:
199
199
  average_duration: Duración promedio
200
- chart_title: Tiempo total de ejecución del trabajo en segundos
200
+ chart_title: Tiempo total de ejecución de la tarea en segundos
201
201
  executions: Ejecuciones
202
- job_class: clase de trabajo
202
+ job_class: clase de tarea
203
203
  maximum_duration: Duración máxima
204
204
  minimum_duration: Duración mínima
205
- title: Actuación
205
+ title: Rendimiento
206
206
  show:
207
207
  slow: Lento
208
208
  title: Actuación
@@ -230,12 +230,12 @@ es:
230
230
  queue_name: Nombre de la cola
231
231
  search: Buscar
232
232
  navbar:
233
- batches: Batches
233
+ batches: Lotes
234
234
  cron_schedules: Cron
235
235
  jobs: Tareas
236
236
  live_poll: En vivo
237
237
  name: "GoodJob 👍"
238
- performance: Actuación
238
+ performance: Rendimiento
239
239
  processes: Procesos
240
240
  theme:
241
241
  auto: Auto
data/config/routes.rb CHANGED
@@ -33,8 +33,8 @@ GoodJob::Engine.routes.draw do
33
33
 
34
34
  resources :performance, only: %i[index show]
35
35
 
36
- scope :frontend, controller: :frontends do
37
- get "modules/:name", action: :module, as: :frontend_module, constraints: { format: 'js' }
38
- get "static/:name", action: :static, as: :frontend_static, constraints: { format: %w[css js] }
36
+ scope :frontend, controller: :frontends, defaults: { version: GoodJob::VERSION.tr(".", "-") } do
37
+ get "modules/:version/:id", action: :module, as: :frontend_module, constraints: { format: 'js' }
38
+ get "static/:version/:id", action: :static, as: :frontend_static, constraints: { format: %w[css js svg] }
39
39
  end
40
40
  end
@@ -28,28 +28,88 @@ module GoodJob
28
28
  class_attribute :good_job_concurrency_config, instance_accessor: false, default: {}
29
29
  attr_writer :good_job_concurrency_key
30
30
 
31
- if ActiveJob.gem_version >= Gem::Version.new("6.1.0")
32
- before_enqueue do |job|
33
- good_job_enqueue_concurrency_check(job, on_abort: -> { throw(:abort) }, on_enqueue: nil)
34
- end
35
- else
36
- around_enqueue do |job, block|
37
- good_job_enqueue_concurrency_check(job, on_abort: nil, on_enqueue: block)
38
- end
39
- end
40
-
41
31
  wait_key = if ActiveJob.gem_version >= Gem::Version.new("7.1.0.a")
42
32
  :polynomially_longer
43
33
  else
44
34
  :exponentially_longer
45
35
  end
46
-
47
36
  retry_on(
48
37
  GoodJob::ActiveJobExtensions::Concurrency::ConcurrencyExceededError,
49
38
  attempts: Float::INFINITY,
50
39
  wait: wait_key
51
40
  )
52
41
 
42
+ before_enqueue do |job|
43
+ # Don't attempt to enforce concurrency limits with other queue adapters.
44
+ next unless job.class.queue_adapter.is_a?(GoodJob::Adapter)
45
+
46
+ # Always allow jobs to be retried because the current job's execution will complete momentarily
47
+ next if CurrentThread.active_job_id == job.job_id
48
+
49
+ # Only generate the concurrency key on the initial enqueue in case it is dynamic
50
+ job.good_job_concurrency_key ||= job._good_job_concurrency_key
51
+ key = job.good_job_concurrency_key
52
+ next if key.blank?
53
+
54
+ enqueue_limit = job.class.good_job_concurrency_config[:enqueue_limit]
55
+ enqueue_limit = instance_exec(&enqueue_limit) if enqueue_limit.respond_to?(:call)
56
+ enqueue_limit = nil unless enqueue_limit.present? && (0...Float::INFINITY).cover?(enqueue_limit)
57
+
58
+ unless enqueue_limit
59
+ total_limit = job.class.good_job_concurrency_config[:total_limit]
60
+ total_limit = instance_exec(&total_limit) if total_limit.respond_to?(:call)
61
+ total_limit = nil unless total_limit.present? && (0...Float::INFINITY).cover?(total_limit)
62
+ end
63
+
64
+ enqueue_throttle = job.class.good_job_concurrency_config[:enqueue_throttle]
65
+ enqueue_throttle = instance_exec(&enqueue_throttle) if enqueue_throttle.respond_to?(:call)
66
+ enqueue_throttle = nil unless enqueue_throttle.present? && enqueue_throttle.is_a?(Array) && enqueue_throttle.size == 2
67
+
68
+ limit = enqueue_limit || total_limit
69
+ throttle = enqueue_throttle
70
+ next unless limit || throttle
71
+
72
+ exceeded = nil
73
+ GoodJob::Job.transaction(requires_new: true, joinable: false) do
74
+ GoodJob::Job.advisory_lock_key(key, function: "pg_advisory_xact_lock") do
75
+ if limit
76
+ enqueue_concurrency = if enqueue_limit
77
+ GoodJob::Job.where(concurrency_key: key).unfinished.advisory_unlocked.count
78
+ else
79
+ GoodJob::Job.where(concurrency_key: key).unfinished.count
80
+ end
81
+
82
+ # The job has not yet been enqueued, so check if adding it will go over the limit
83
+ if (enqueue_concurrency + 1) > limit
84
+ logger.info "Aborted enqueue of #{job.class.name} (Job ID: #{job.job_id}) because the concurrency key '#{key}' has reached its enqueue limit of #{limit} #{'job'.pluralize(limit)}"
85
+ exceeded = :limit
86
+ next
87
+ end
88
+ end
89
+
90
+ if throttle
91
+ throttle_limit = throttle[0]
92
+ throttle_period = throttle[1]
93
+ enqueued_within_period = GoodJob::Job.where(concurrency_key: key)
94
+ .where(GoodJob::Job.arel_table[:created_at].gt(throttle_period.ago))
95
+ .count
96
+
97
+ if (enqueued_within_period + 1) > throttle_limit
98
+ logger.info "Aborted enqueue of #{job.class.name} (Job ID: #{job.job_id}) because the concurrency key '#{key}' has reached its throttle limit of #{limit} #{'job'.pluralize(limit)}"
99
+ exceeded = :throttle
100
+ next
101
+ end
102
+ end
103
+ end
104
+
105
+ # Rollback the transaction because it's potentially less expensive than committing it
106
+ # even though nothing has been altered in the transaction.
107
+ raise ActiveRecord::Rollback
108
+ end
109
+
110
+ throw :abort if exceeded
111
+ end
112
+
53
113
  before_perform do |job|
54
114
  # Don't attempt to enforce concurrency limits with other queue adapters.
55
115
  next unless job.class.queue_adapter.is_a?(GoodJob::Adapter)
@@ -80,30 +140,43 @@ module GoodJob
80
140
  next
81
141
  end
82
142
 
83
- GoodJob::Job.advisory_lock_key(key, function: "pg_advisory_lock") do
84
- if limit
85
- allowed_active_job_ids = GoodJob::Job.unfinished.where(concurrency_key: key)
86
- .advisory_locked
87
- .order(Arel.sql("COALESCE(performed_at, scheduled_at, created_at) ASC"))
88
- .limit(limit).pluck(:active_job_id)
89
- # The current job has already been locked and will appear in the previous query
90
- raise GoodJob::ActiveJobExtensions::Concurrency::ConcurrencyExceededError unless allowed_active_job_ids.include?(job.job_id)
143
+ exceeded = nil
144
+ GoodJob::Job.transaction(requires_new: true, joinable: false) do
145
+ GoodJob::Job.advisory_lock_key(key, function: "pg_advisory_xact_lock") do
146
+ if limit
147
+ allowed_active_job_ids = GoodJob::Job.unfinished.where(concurrency_key: key)
148
+ .advisory_locked
149
+ .order(Arel.sql("COALESCE(performed_at, scheduled_at, created_at) ASC"))
150
+ .limit(limit).pluck(:active_job_id)
151
+ # The current job has already been locked and will appear in the previous query
152
+ exceeded = :limit unless allowed_active_job_ids.include?(job.job_id)
153
+ next
154
+ end
155
+
156
+ if throttle
157
+ throttle_limit = throttle[0]
158
+ throttle_period = throttle[1]
159
+
160
+ query = Execution.joins(:job)
161
+ .where(GoodJob::Job.table_name => { concurrency_key: key })
162
+ .where(Execution.arel_table[:created_at].gt(Execution.bind_value('created_at', throttle_period.ago, ActiveRecord::Type::DateTime)))
163
+ allowed_active_job_ids = query.where(error: nil).or(query.where.not(error: "GoodJob::ActiveJobExtensions::Concurrency::ThrottleExceededError: GoodJob::ActiveJobExtensions::Concurrency::ThrottleExceededError"))
164
+ .order(created_at: :asc)
165
+ .limit(throttle_limit)
166
+ .pluck(:active_job_id)
167
+
168
+ exceeded = :throttle unless allowed_active_job_ids.include?(job.job_id)
169
+ next
170
+ end
91
171
  end
92
172
 
93
- if throttle
94
- throttle_limit = throttle[0]
95
- throttle_period = throttle[1]
96
-
97
- query = DiscreteExecution.joins(:job)
98
- .where(GoodJob::Job.table_name => { concurrency_key: key })
99
- .where(DiscreteExecution.arel_table[:created_at].gt(DiscreteExecution.bind_value('created_at', throttle_period.ago, ActiveRecord::Type::DateTime)))
100
- allowed_active_job_ids = query.where(error: nil).or(query.where.not(error: "GoodJob::ActiveJobExtensions::Concurrency::ThrottleExceededError: GoodJob::ActiveJobExtensions::Concurrency::ThrottleExceededError"))
101
- .order(created_at: :asc)
102
- .limit(throttle_limit)
103
- .pluck(:active_job_id)
173
+ raise ActiveRecord::Rollback
174
+ end
104
175
 
105
- raise ThrottleExceededError unless allowed_active_job_ids.include?(job.job_id)
106
- end
176
+ if exceeded == :limit
177
+ raise GoodJob::ActiveJobExtensions::Concurrency::ConcurrencyExceededError
178
+ elsif exceeded == :throttle
179
+ raise GoodJob::ActiveJobExtensions::Concurrency::ThrottleExceededError
107
180
  end
108
181
  end
109
182
  end
@@ -139,72 +212,6 @@ module GoodJob
139
212
  def _good_job_default_concurrency_key
140
213
  self.class.name.to_s
141
214
  end
142
-
143
- private
144
-
145
- def good_job_enqueue_concurrency_check(job, on_abort:, on_enqueue:)
146
- # Don't attempt to enforce concurrency limits with other queue adapters.
147
- return on_enqueue&.call unless job.class.queue_adapter.is_a?(GoodJob::Adapter)
148
-
149
- # Always allow jobs to be retried because the current job's execution will complete momentarily
150
- return on_enqueue&.call if CurrentThread.active_job_id == job.job_id
151
-
152
- # Only generate the concurrency key on the initial enqueue in case it is dynamic
153
- job.good_job_concurrency_key ||= job._good_job_concurrency_key
154
- key = job.good_job_concurrency_key
155
- return on_enqueue&.call if key.blank?
156
-
157
- enqueue_limit = job.class.good_job_concurrency_config[:enqueue_limit]
158
- enqueue_limit = instance_exec(&enqueue_limit) if enqueue_limit.respond_to?(:call)
159
- enqueue_limit = nil unless enqueue_limit.present? && (0...Float::INFINITY).cover?(enqueue_limit)
160
-
161
- unless enqueue_limit
162
- total_limit = job.class.good_job_concurrency_config[:total_limit]
163
- total_limit = instance_exec(&total_limit) if total_limit.respond_to?(:call)
164
- total_limit = nil unless total_limit.present? && (0...Float::INFINITY).cover?(total_limit)
165
- end
166
-
167
- enqueue_throttle = job.class.good_job_concurrency_config[:enqueue_throttle]
168
- enqueue_throttle = instance_exec(&enqueue_throttle) if enqueue_throttle.respond_to?(:call)
169
- enqueue_throttle = nil unless enqueue_throttle.present? && enqueue_throttle.is_a?(Array) && enqueue_throttle.size == 2
170
-
171
- limit = enqueue_limit || total_limit
172
- throttle = enqueue_throttle
173
- return on_enqueue&.call unless limit || throttle
174
-
175
- GoodJob::Job.advisory_lock_key(key, function: "pg_advisory_lock") do
176
- if limit
177
- enqueue_concurrency = if enqueue_limit
178
- GoodJob::Job.where(concurrency_key: key).unfinished.advisory_unlocked.count
179
- else
180
- GoodJob::Job.where(concurrency_key: key).unfinished.count
181
- end
182
-
183
- # The job has not yet been enqueued, so check if adding it will go over the limit
184
- if (enqueue_concurrency + 1) > limit
185
- logger.info "Aborted enqueue of #{job.class.name} (Job ID: #{job.job_id}) because the concurrency key '#{key}' has reached its enqueue limit of #{limit} #{'job'.pluralize(limit)}"
186
- on_abort&.call
187
- break
188
- end
189
- end
190
-
191
- if throttle
192
- throttle_limit = throttle[0]
193
- throttle_period = throttle[1]
194
- enqueued_within_period = GoodJob::Job.where(concurrency_key: key)
195
- .where(GoodJob::Job.arel_table[:created_at].gt(throttle_period.ago))
196
- .count
197
-
198
- if (enqueued_within_period + 1) > throttle_limit
199
- logger.info "Aborted enqueue of #{job.class.name} (Job ID: #{job.job_id}) because the concurrency key '#{key}' has reached its throttle limit of #{limit} #{'job'.pluralize(limit)}"
200
- on_abort&.call
201
- break
202
- end
203
- end
204
-
205
- on_enqueue&.call
206
- end
207
- end
208
215
  end
209
216
  end
210
217
  end