good_job 4.1.0 → 4.1.1

Sign up to get free protection for your applications and to get access to all the features.
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