good_job 4.22.1 → 4.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +38 -0
  3. data/README.md +11 -1
  4. data/app/controllers/good_job/executions_controller.rb +11 -0
  5. data/app/models/concerns/good_job/filtered_arguments.rb +27 -0
  6. data/app/models/good_job/concurrency_claim.rb +2 -0
  7. data/app/models/good_job/cron_entry.rb +9 -11
  8. data/app/models/good_job/execution.rb +2 -0
  9. data/app/models/good_job/job.rb +2 -0
  10. data/app/views/good_job/executions/show.html.erb +11 -0
  11. data/app/views/good_job/jobs/_executions.erb +2 -6
  12. data/app/views/good_job/jobs/show.html.erb +1 -1
  13. data/config/locales/pt-BR.yml +25 -23
  14. data/config/routes.rb +3 -1
  15. data/lib/generators/good_job/templates/install/migrations/create_good_jobs.rb.erb +1 -0
  16. data/lib/generators/good_job/templates/update/migrations/01_create_good_jobs.rb.erb +1 -0
  17. data/lib/generators/good_job/templates/update/migrations/22_add_index_good_job_executions_on_created_at.rb.erb +12 -0
  18. data/lib/good_job/adapter.rb +27 -27
  19. data/lib/good_job/capsule.rb +8 -8
  20. data/lib/good_job/capsule_tracker.rb +29 -11
  21. data/lib/good_job/cron_manager.rb +5 -6
  22. data/lib/good_job/instance_registry.rb +45 -0
  23. data/lib/good_job/job_performer.rb +2 -1
  24. data/lib/good_job/notifier.rb +5 -6
  25. data/lib/good_job/poller.rb +4 -6
  26. data/lib/good_job/probe_server/simple_handler.rb +3 -0
  27. data/lib/good_job/probe_server/webrick_handler.rb +2 -0
  28. data/lib/good_job/probe_server.rb +2 -0
  29. data/lib/good_job/ractors.rb +2 -0
  30. data/lib/good_job/scheduler.rb +3 -6
  31. data/lib/good_job/shared_executor.rb +4 -6
  32. data/lib/good_job/systemd_service.rb +1 -0
  33. data/lib/good_job/version.rb +1 -1
  34. data/lib/good_job.rb +13 -2
  35. metadata +6 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9ce4bd5cfbb7e4fcc44f90099bc1aa892ef59e1294c69eac5d4279c3968db9c0
4
- data.tar.gz: '093ba2cc69d8d51f38bbf4cd93d9bf97aa65d995768f00a6b0d27dbe1aacdd08'
3
+ metadata.gz: 50f42b2481a308af99413d1a89c0b7d3d4c2e07cc48cc29a323c3c42a528e610
4
+ data.tar.gz: a55f5f59b5758614fa38c79b603d4f6298b6e79de40931e05fc4cc4fa0e68f56
5
5
  SHA512:
6
- metadata.gz: bcbcad67274883a3cb3d4405d7d43015c5c154241dab9c1d136ccbcae7ea53bf9e6606d4713761b7c565826487f042c0e869ecc14eafacfa46ce3bfb9de6dc26
7
- data.tar.gz: 56b86db61ba722ebbe770f0f0be2f20eaea6a132af9a3d9e5f688a2c2138d3782d92a3a425ff47b0e1881539c7bfe8135c18444b887665105edc385d3f3aa3a7
6
+ metadata.gz: adacf056f5e11131e0bb704e5b830a11a4a64b0d0a06339c0edaa13b1d8c94bf8b7e734e0df79d3ccf0230e7ceba3da4f220d66aa4eece5dd4dd5e961af294e1
7
+ data.tar.gz: 7b954a6a98b208038575c4d05e6f6e18d219125daf42f66ca8d1e0223f5e18eb1250fd3c608eddeb4f8b78d94ba84f3f141805ccf24f81dedeec0a37bac808ff
data/CHANGELOG.md CHANGED
@@ -1,5 +1,43 @@
1
1
  # Changelog
2
2
 
3
+ ## [v4.23.0](https://github.com/bensheldon/good_job/tree/v4.23.0) (2026-10-27)
4
+
5
+ [Full Changelog](https://github.com/bensheldon/good_job/compare/v4.22.1...v4.23.0)
6
+
7
+ **Implemented enhancements:**
8
+
9
+ - Filter sensitive job arguments in the Dashboard [\#1881](https://github.com/bensheldon/good_job/pull/1881) ([bogdan](https://github.com/bogdan))
10
+
11
+ **Fixed bugs:**
12
+
13
+ - Respect enable\_listen\_notify when promoting concurrency claims [\#1892](https://github.com/bensheldon/good_job/pull/1892) ([eidam](https://github.com/eidam))
14
+ - Downgrade lock\_type before releasing advisory lock [\#1883](https://github.com/bensheldon/good_job/pull/1883) ([jmsanders](https://github.com/jmsanders))
15
+
16
+ **Closed issues:**
17
+
18
+ - ConcurrencyClaim sends NOTIFY even when enable\_listen\_notify is false [\#1891](https://github.com/bensheldon/good_job/issues/1891)
19
+
20
+ **Merged pull requests:**
21
+
22
+ - Keep JRuby platform in Gemfile.lock when Bundler re-resolves [\#1893](https://github.com/bensheldon/good_job/pull/1893) ([bensheldon](https://github.com/bensheldon))
23
+ - Make integration specs robust under database load; fix mixed lock strategy specs [\#1890](https://github.com/bensheldon/good_job/pull/1890) ([bensheldon](https://github.com/bensheldon))
24
+ - Pin CI test runners to ubuntu-24.04 [\#1888](https://github.com/bensheldon/good_job/pull/1888) ([bensheldon](https://github.com/bensheldon))
25
+ - Require the concurrent-ruby classes GoodJob uses [\#1885](https://github.com/bensheldon/good_job/pull/1885) ([czj](https://github.com/czj))
26
+ - Add index\_good\_job\_executions\_on\_created\_at [\#1884](https://github.com/bensheldon/good_job/pull/1884) ([clinejj](https://github.com/clinejj))
27
+ - Demo: cap YJIT memory and shrink the Rack::Attack store [\#1880](https://github.com/bensheldon/good_job/pull/1880) ([bensheldon](https://github.com/bensheldon))
28
+ - Demo: enable rbtrace in Puma workers for heap dumps; add sheap [\#1879](https://github.com/bensheldon/good_job/pull/1879) ([bensheldon](https://github.com/bensheldon))
29
+ - Demo: warm up Puma before forking, add Thruster, raise DB pool [\#1878](https://github.com/bensheldon/good_job/pull/1878) ([bensheldon](https://github.com/bensheldon))
30
+ - Return 404 for GET /jobs/mass\_update instead of redirecting [\#1877](https://github.com/bensheldon/good_job/pull/1877) ([bensheldon](https://github.com/bensheldon))
31
+ - Load execution full traces and serialized params on demand [\#1876](https://github.com/bensheldon/good_job/pull/1876) ([bensheldon](https://github.com/bensheldon))
32
+ - Add specs for the demo's Rack::Attack rules [\#1875](https://github.com/bensheldon/good_job/pull/1875) ([bensheldon](https://github.com/bensheldon))
33
+ - Fix demo boot: don't require rails/all in bin/rails [\#1874](https://github.com/bensheldon/good_job/pull/1874) ([bensheldon](https://github.com/bensheldon))
34
+ - Reduce demo app memory and crawler load [\#1873](https://github.com/bensheldon/good_job/pull/1873) ([bensheldon](https://github.com/bensheldon))
35
+ - Fix JRuby flake from inspect in wait\_until predicate expectations [\#1872](https://github.com/bensheldon/good_job/pull/1872) ([bensheldon](https://github.com/bensheldon))
36
+ - Remove Adapter::RUNTIMES so adapters can be garbage collected [\#1870](https://github.com/bensheldon/good_job/pull/1870) ([bensheldon](https://github.com/bensheldon))
37
+ - Add GoodJob.prune to release shut down capsules, schedulers, and notifiers [\#1869](https://github.com/bensheldon/good_job/pull/1869) ([bensheldon](https://github.com/bensheldon))
38
+ - Use resolve\_\[property\] naming for CronEntry configuration resolvers [\#1868](https://github.com/bensheldon/good_job/pull/1868) ([bensheldon](https://github.com/bensheldon))
39
+ - Fix inconsistencies in the Brazilian Portuguese locale [\#1867](https://github.com/bensheldon/good_job/pull/1867) ([tiagoamaro](https://github.com/tiagoamaro))
40
+
3
41
  ## [v4.22.1](https://github.com/bensheldon/good_job/tree/v4.22.1) (2026-10-13)
4
42
 
5
43
  [Full Changelog](https://github.com/bensheldon/good_job/compare/v4.22.0...v4.22.1)
data/README.md CHANGED
@@ -44,6 +44,7 @@ For more of the story of GoodJob, read the [introductory blog post](https://isla
44
44
  - [Dashboard](#dashboard)
45
45
  - [API-only Rails applications](#api-only-rails-applications)
46
46
  - [Live polling](#live-polling)
47
+ - [Filtering sensitive arguments](#filtering-sensitive-arguments)
47
48
  - [Extending dashboard views](#extending-dashboard-views)
48
49
  - [Job priority](#job-priority)
49
50
  - [Concurrency controls](#concurrency-controls)
@@ -479,6 +480,15 @@ end
479
480
 
480
481
  The Dashboard can be set to automatically refresh by checking "Live Poll" in the Dashboard header, or by setting `?poll=10` with the interval in seconds (default 30 seconds).
481
482
 
483
+ #### Filtering sensitive arguments
484
+
485
+ The Dashboard hides job arguments the same way Rails hides them from logs:
486
+
487
+ - Hash values whose keys match `Rails.application.config.filter_parameters` (e.g. `password:`) are displayed as `[FILTERED]`.
488
+ - All arguments are displayed as `[FILTERED]` for jobs with `log_arguments` disabled, either for every job with `config.active_job.log_arguments = false` or per job class with `self.log_arguments = false`.
489
+
490
+ Filtering only applies to how arguments are displayed; arguments are still stored unfiltered in the database and are used when the job is performed or retried.
491
+
482
492
  #### Extending dashboard views
483
493
 
484
494
  GoodJob exposes some views that are intended to be overridden by placing views in your application:
@@ -1644,7 +1654,7 @@ end
1644
1654
  GOOD_JOB_LOCK_STRATEGY=skiplocked
1645
1655
  ```
1646
1656
 
1647
- All three strategies (`:advisory`, `:skiplocked`, `:hybrid`) can coexist safely during a rolling deploy — each strategy excludes jobs that are already locked by another worker regardless of which strategy that worker uses.
1657
+ During a rolling deploy, `:hybrid` workers can safely run alongside either `:advisory` or `:skiplocked` workers. But `:advisory` and `:skiplocked` workers must not run at the same time, because neither sees the other's locks and the same job may be performed twice. To move from `:advisory` to `:skiplocked`, deploy `:hybrid` first, wait until no `:advisory` workers remain, and then deploy `:skiplocked`.
1648
1658
 
1649
1659
  #### PgBouncer configuration
1650
1660
 
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GoodJob
4
+ # Renders an execution's heavier details on demand so they aren't included in every job page.
5
+ class ExecutionsController < GoodJob::ApplicationController
6
+ def show
7
+ @execution = Execution.find_by!(id: params[:id], active_job_id: params[:job_id])
8
+ render layout: false
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GoodJob
4
+ # Hides sensitive Active Job arguments when displaying Job/Execution records in the Dashboard.
5
+ module FilteredArguments
6
+ # Active Job arguments with sensitive values replaced by +[FILTERED]+.
7
+ # Every argument is filtered when the job class has +log_arguments+ disabled
8
+ # (e.g. +config.active_job.log_arguments = false+); otherwise hash values whose
9
+ # keys match +Rails.application.config.filter_parameters+ are filtered.
10
+ # @return [Array]
11
+ def display_arguments
12
+ arguments = serialized_params.fetch("arguments", [])
13
+ return arguments.map { ActiveSupport::ParameterFilter::FILTERED } unless log_arguments?
14
+
15
+ parameter_filter = ActiveSupport::ParameterFilter.new(Rails.application.config.filter_parameters)
16
+ parameter_filter.filter("arguments" => arguments)["arguments"]
17
+ end
18
+
19
+ private
20
+
21
+ def log_arguments?
22
+ active_job_class = job_class&.safe_constantize
23
+ active_job_class = ActiveJob::Base unless active_job_class.respond_to?(:log_arguments?)
24
+ active_job_class.log_arguments?
25
+ end
26
+ end
27
+ end
@@ -172,6 +172,8 @@ module GoodJob
172
172
  end
173
173
 
174
174
  def notify(job_id)
175
+ return unless GoodJob.configuration.enable_listen_notify
176
+
175
177
  queue_name = GoodJob::Job.where(id: job_id).pick(:queue_name)
176
178
  GoodJob::Notifier.notify({ queue_name: queue_name }) if queue_name
177
179
  end
@@ -139,8 +139,10 @@ module GoodJob # :nodoc:
139
139
  job_klass = resolve_job_class
140
140
  next unless job_klass.is_a?(Class)
141
141
 
142
- configured_job = job_klass.set(set_value)
143
- kwargs_value.present? ? configured_job.perform_later(*args_value, **kwargs_value) : configured_job.perform_later(*args_value)
142
+ configured_job = job_klass.set(resolve_set)
143
+ job_args = resolve_args
144
+ job_kwargs = resolve_kwargs
145
+ job_kwargs.present? ? configured_job.perform_later(*job_args, **job_kwargs) : configured_job.perform_later(*job_args)
144
146
  end
145
147
  end
146
148
  end
@@ -213,7 +215,8 @@ module GoodJob # :nodoc:
213
215
  end
214
216
 
215
217
  def resolve_job_class
216
- value = job_class_value
218
+ value = job_class
219
+ value = value.call if value.respond_to?(:call)
217
220
  value.is_a?(String) ? value.constantize : value
218
221
  end
219
222
 
@@ -236,22 +239,17 @@ module GoodJob # :nodoc:
236
239
  schedule if schedule.instance_of?(Fugit::Cron)
237
240
  end
238
241
 
239
- def job_class_value
240
- value = job_class || nil
241
- value.respond_to?(:call) ? value.call : value
242
- end
243
-
244
- def set_value
242
+ def resolve_set
245
243
  value = set || {}
246
244
  value.respond_to?(:call) ? value.call : value
247
245
  end
248
246
 
249
- def args_value
247
+ def resolve_args
250
248
  value = args || []
251
249
  value.respond_to?(:call) ? value.call : value
252
250
  end
253
251
 
254
- def kwargs_value
252
+ def resolve_kwargs
255
253
  value = kwargs || nil
256
254
  value.respond_to?(:call) ? value.call : value
257
255
  end
@@ -3,6 +3,7 @@
3
3
  module GoodJob # :nodoc:
4
4
  class Execution < BaseRecord
5
5
  include ErrorEvents
6
+ include FilteredArguments
6
7
 
7
8
  self.table_name = 'good_job_executions'
8
9
  self.implicit_order_column = 'created_at'
@@ -63,6 +64,7 @@ module GoodJob # :nodoc:
63
64
 
64
65
  def display_serialized_params
65
66
  serialized_params.merge({
67
+ "arguments" => display_arguments,
66
68
  _good_job_execution: attributes.except('serialized_params'),
67
69
  })
68
70
  end
@@ -6,6 +6,7 @@ module GoodJob
6
6
  include AdvisoryLockable
7
7
  include Job::Lockable
8
8
  include ErrorEvents
9
+ include FilteredArguments
9
10
  include Filterable
10
11
  include Reportable
11
12
 
@@ -622,6 +623,7 @@ module GoodJob
622
623
  # @return [Hash]
623
624
  def display_serialized_params
624
625
  serialized_params.merge({
626
+ "arguments" => display_arguments,
625
627
  _good_job: attributes.except('serialized_params', 'locktype', 'owns_advisory_lock'),
626
628
  })
627
629
  end
@@ -0,0 +1,11 @@
1
+ <%= content_tag "turbo-frame", id: dom_id(@execution, :full_trace) do %>
2
+ <div class="small">
3
+ <code class="text-wrap text-break m-0 text-secondary-emphasis">
4
+ <%= safe_join(@execution.error_backtrace || [], tag.br) %>
5
+ </code>
6
+ </div>
7
+ <% end %>
8
+
9
+ <%= content_tag "turbo-frame", id: dom_id(@execution, :serialized_params) do %>
10
+ <%= tag.pre JSON.pretty_generate(truncate_display_value(@execution.display_serialized_params)) %>
11
+ <% end %>
@@ -58,11 +58,7 @@
58
58
  </div>
59
59
  <% end %>
60
60
  <%= tag.div class: "tab-pane fade", id: dom_id(execution, :full_pane), role: "tabpane", aria: { labelledby: dom_id(execution, :full) }, tabindex: 0 do %>
61
- <div class="small">
62
- <code class="text-wrap text-break m-0 text-secondary-emphasis">
63
- <%= safe_join(execution.error_backtrace, tag.br) %>
64
- </code>
65
- </div>
61
+ <%= content_tag "turbo-frame", "", id: dom_id(execution, :full_trace), src: job_execution_path(@job.id, execution.id), loading: "lazy" %>
66
62
  <% end %>
67
63
  <% end %>
68
64
  <% end %>
@@ -70,7 +66,7 @@
70
66
  <% end %>
71
67
  <%= render 'good_job/custom_execution_details', execution: execution, job: @job %>
72
68
  <%= tag.div id: dom_id(execution, "params"), class: "list-group-item collapse small bg-dark text-light" do %>
73
- <%= tag.pre JSON.pretty_generate(truncate_display_value(execution.display_serialized_params)) %>
69
+ <%= content_tag "turbo-frame", "", id: dom_id(execution, :serialized_params), src: job_execution_path(@job.id, execution.id), loading: "lazy" %>
74
70
  <% end %>
75
71
  <% end %>
76
72
  </div>
@@ -85,7 +85,7 @@
85
85
  <% end %>
86
86
  </h5>
87
87
  </div>
88
- <%= tag.pre @job.serialized_params["arguments"].map { |argument| truncate_display_value(argument).inspect }.join(', '), class: 'text-wrap text-break' %>
88
+ <%= tag.pre @job.display_arguments.map { |argument| truncate_display_value(argument).inspect }.join(', '), class: 'text-wrap text-break' %>
89
89
 
90
90
  <%= tag.div id: dom_id(@job, "params"), class: "list-group-item collapse small bg-dark text-light" do %>
91
91
  <%= tag.pre JSON.pretty_generate(truncate_display_value(@job.display_serialized_params)) %>
@@ -37,17 +37,17 @@ pt-BR:
37
37
  no_batches_found: Nenhum lote encontrado.
38
38
  cleaner:
39
39
  index:
40
- all: All
41
- class: Class
42
- exception: Exception
43
- grouped_by_class: Discards grouped by Class
44
- grouped_by_exception: Discards grouped by Exception
45
- last_1_hour: Last 1 hour
46
- last_24_hours: Last 24 hours
47
- last_3_days: Last 3 days
48
- last_3_hours: Last 3 hours
49
- last_7_days: Last 7 days
50
- title: Discard Cleaner
40
+ all: Tudo
41
+ class: Classe
42
+ exception: Exceção
43
+ grouped_by_class: Descartes agrupados por Classe
44
+ grouped_by_exception: Descartes agrupados por Exceção
45
+ last_1_hour: Última hora
46
+ last_24_hours: Últimas 24 horas
47
+ last_3_days: Últimos 3 dias
48
+ last_3_hours: Últimas 3 horas
49
+ last_7_days: Últimos 7 dias
50
+ title: Limpeza de Descartes
51
51
  total: Total
52
52
  concurrency_claims:
53
53
  states:
@@ -142,12 +142,12 @@ pt-BR:
142
142
  reschedule: Reagendar tarefa
143
143
  retry: Tentar tarefa novamente
144
144
  destroy:
145
- notice: A tarefa foi excluida.
145
+ notice: A tarefa foi excluída.
146
146
  discard:
147
147
  notice: A tarefa foi descartada.
148
148
  executions:
149
- application_trace: Application Trace
150
- full_trace: Full Trace
149
+ application_trace: Trace da aplicação
150
+ full_trace: Trace completo
151
151
  in_queue: na fila
152
152
  recovery: recuperação
153
153
  runtime: tempo de execução
@@ -175,7 +175,7 @@ pt-BR:
175
175
  destroy_all: Excluir tudo
176
176
  discard_all: Descartar tudo
177
177
  reschedule_all: Reagendar tudo
178
- retry_all: Tentar novamente tudo
178
+ retry_all: Tentar tudo novamente
179
179
  title: Ações
180
180
  no_jobs_found: Nenhuma tarefa encontrada.
181
181
  toggle_actions: Alternar Ações
@@ -204,7 +204,7 @@ pt-BR:
204
204
  job:
205
205
  arguments: Argumentos
206
206
  attempts: Tentativas
207
- concurrency_claims: Reivindicações de concorrência
207
+ concurrency_claims: Reservas de concorrência
208
208
  labels: Rótulos
209
209
  priority: Prioridade
210
210
  queue: Fila
@@ -230,7 +230,7 @@ pt-BR:
230
230
  confirm_pause: Tem certeza que deseja pausar %{value}?
231
231
  confirm_unpause: Tem certeza que deseja retomar %{value}?
232
232
  disabled: O recurso experimental de Pausas do GoodJob está desativado por padrão porque pode degradar o desempenho. Configure para ativar
233
- job_class: Classe do trabalho
233
+ job_class: Classe da tarefa
234
234
  label: Rótulo
235
235
  pause: Pausar
236
236
  queue: Fila
@@ -245,8 +245,8 @@ pt-BR:
245
245
  chart_title_total: Latência total média em segundos
246
246
  execution_latency: Latência de execução
247
247
  executions: Execuções
248
- job_class: Classe do trabalho
249
- jobs: Trabalhos
248
+ job_class: Classe da tarefa
249
+ jobs: Tarefas
250
250
  no_executions: Nenhuma execução neste intervalo de tempo.
251
251
  queue_latency: Latência na fila
252
252
  queue_name: Nome da fila
@@ -267,7 +267,9 @@ pt-BR:
267
267
  days: "%{count}d"
268
268
  hours: "%{count}h"
269
269
  minutes: "%{count}m"
270
- months: "~%{count} mês"
270
+ months:
271
+ one: "~%{count} mês"
272
+ other: "~%{count} meses"
271
273
  seconds: "%{count}s"
272
274
  years: "~%{count}a"
273
275
  chart_bucket_size: 'Granularidade temporal do gráfico: %{size}'
@@ -317,8 +319,8 @@ pt-BR:
317
319
  search: Pesquisar
318
320
  navbar:
319
321
  batches: Lotes
320
- cleaner: Discard Cleaner
321
- cron_schedules: Agendamentos
322
+ cleaner: Limpeza de Descartes
323
+ cron_schedules: Tarefas Programadas
322
324
  jobs: Tarefas
323
325
  live_poll: Acompanhamento ao Vivo
324
326
  name: "GoodJob 👍"
@@ -332,7 +334,7 @@ pt-BR:
332
334
  theme: Tema
333
335
  pending_migrations: O GoodJob tem migrações pendentes no banco de dados.
334
336
  secondary_navbar:
335
- inspiration: Lembre-se, você também está fazendo uma Boa Tarefa!
337
+ inspiration: Lembre-se, você também está fazendo um Bom Trabalho!
336
338
  last_updated: Última atualização
337
339
  status:
338
340
  discarded: Descartado
data/config/routes.rb CHANGED
@@ -5,7 +5,7 @@ GoodJob::Engine.routes.draw do
5
5
 
6
6
  resources :jobs, only: %i[index show destroy] do
7
7
  collection do
8
- get :mass_update, to: redirect(path: 'jobs')
8
+ get :mass_update, to: ->(_env) { raise ActionController::RoutingError, 'Not Found' }
9
9
  put :mass_update
10
10
  end
11
11
 
@@ -15,6 +15,8 @@ GoodJob::Engine.routes.draw do
15
15
  put :reschedule
16
16
  put :retry
17
17
  end
18
+
19
+ resources :executions, only: %i[show]
18
20
  end
19
21
  get 'jobs/metrics/primary_nav', to: 'metrics#primary_nav', as: :metrics_primary_nav
20
22
  get 'jobs/metrics/job_status', to: 'metrics#job_status', as: :metrics_job_status
@@ -114,6 +114,7 @@ class CreateGoodJobs < ActiveRecord::Migration<%= migration_version %>
114
114
 
115
115
  add_index :good_job_executions, [:active_job_id, :created_at], name: :index_good_job_executions_on_active_job_id_and_created_at
116
116
  add_index :good_job_executions, :scheduled_at, name: :index_good_job_executions_on_scheduled_at
117
+ add_index :good_job_executions, :created_at, name: :index_good_job_executions_on_created_at
117
118
  add_index :good_jobs, [:priority, :scheduled_at, :id],
118
119
  where: "finished_at IS NULL", name: "index_good_jobs_on_priority_scheduled_at_unfinished"
119
120
  add_index :good_jobs, [:queue_name, :scheduled_at, :id],
@@ -97,6 +97,7 @@ class CreateGoodJobs < ActiveRecord::Migration<%= migration_version %>
97
97
 
98
98
  add_index :good_job_executions, [:active_job_id, :created_at], name: :index_good_job_executions_on_active_job_id_and_created_at
99
99
  add_index :good_job_executions, :scheduled_at, name: :index_good_job_executions_on_scheduled_at
100
+ add_index :good_job_executions, :created_at, name: :index_good_job_executions_on_created_at
100
101
  add_index :good_jobs, :locked_by_id,
101
102
  where: "locked_by_id IS NOT NULL", name: "index_good_jobs_on_locked_by_id"
102
103
  add_index :good_jobs, [:priority, :scheduled_at, :id],
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ class AddIndexGoodJobExecutionsOnCreatedAt < ActiveRecord::Migration<%= migration_version %>
4
+ disable_ddl_transaction!
5
+
6
+ def change
7
+ add_index :good_job_executions, :created_at,
8
+ name: :index_good_job_executions_on_created_at,
9
+ algorithm: :concurrently,
10
+ if_not_exists: true
11
+ end
12
+ end
@@ -5,18 +5,7 @@ module GoodJob
5
5
  # ActiveJob Adapter.
6
6
  #
7
7
  class Adapter
8
- # @!attribute [r] instances
9
- # @!scope class
10
- # List of all instantiated Adapters in the current process.
11
- # @return [Array<GoodJob::Adapter>, nil]
12
- cattr_reader :instances, default: Concurrent::Array.new, instance_reader: false
13
-
14
- # Mutable per-adapter state (the capsule and whether it was started), kept
15
- # off the adapter instance so that the adapter itself can be made
16
- # Ractor-shareable. Only accessed from the main Ractor.
17
- Runtime = Struct.new(:capsule, :async_started)
18
- RUNTIMES = Concurrent::Map.new
19
- private_constant :Runtime, :RUNTIMES
8
+ include InstanceRegistry
20
9
 
21
10
  # @param execution_mode [Symbol, nil] specifies how and where jobs should be executed. You can also set this with the environment variable +GOOD_JOB_EXECUTION_MODE+.
22
11
  #
@@ -32,13 +21,15 @@ module GoodJob
32
21
  # -+test+: +:inline+
33
22
  # - +production+ and all other environments: +:external+
34
23
  #
35
- def initialize(execution_mode: nil, _capsule: GoodJob.capsule) # rubocop:disable Lint/UnderscorePrefixedVariableName
24
+ # @param _capsule [GoodJob::Capsule, nil] Capsule to use instead of {GoodJob.capsule}. An adapter with
25
+ # its own capsule cannot be made Ractor-shareable.
26
+ def initialize(execution_mode: nil, _capsule: nil) # rubocop:disable Lint/UnderscorePrefixedVariableName
36
27
  @_execution_mode_override = execution_mode
37
28
  GoodJob::Configuration.validate_execution_mode(@_execution_mode_override) if @_execution_mode_override
38
- RUNTIMES[self] = Runtime.new(_capsule, false)
29
+ @_capsule = _capsule
39
30
 
40
31
  start_async if GoodJob.async_ready?
41
- self.class.instances << self
32
+ register_instance
42
33
  end
43
34
 
44
35
  # Enqueues the ActiveJob job to be performed.
@@ -255,9 +246,7 @@ module GoodJob
255
246
  # * A positive number will wait that many seconds before stopping any remaining active threads.
256
247
  # @return [void]
257
248
  def shutdown(timeout: NONE)
258
- runtime = RUNTIMES[self]
259
- runtime.capsule&.shutdown(timeout: timeout)
260
- runtime.async_started = false
249
+ capsule.shutdown(timeout: timeout)
261
250
  end
262
251
 
263
252
  # This adapter's execution mode
@@ -292,40 +281,51 @@ module GoodJob
292
281
  def start_async
293
282
  return unless execute_async?
294
283
 
295
- runtime = RUNTIMES[self]
296
- runtime.capsule.start
297
- runtime.capsule.lower_thread_priority = true if GoodJob.configuration.lower_thread_priority.in?([true, nil])
298
- runtime.async_started = true
284
+ capsule.start
285
+ capsule.lower_thread_priority = true if GoodJob.configuration.lower_thread_priority.in?([true, nil])
299
286
  end
300
287
 
301
288
  # Whether the async executors are running
302
289
  # @return [Boolean]
303
290
  def async_started?
304
- GoodJob::Ractors.on_main(self) { RUNTIMES[self].async_started }
291
+ GoodJob::Ractors.on_main(self) { capsule.running? }
292
+ end
293
+
294
+ # Adapters are retained until Rails has initialized so that async adapters can be started.
295
+ # @return [Boolean]
296
+ def prunable?
297
+ GoodJob.async_ready?
305
298
  end
306
299
 
307
300
  private
308
301
 
302
+ # The capsule is resolved on each use (rather than stored) so the adapter
303
+ # remains Ractor-shareable. Only call from the main Ractor.
304
+ # @return [GoodJob::Capsule]
305
+ def capsule
306
+ @_capsule || GoodJob.capsule
307
+ end
308
+
309
309
  # The capsule is not Ractor-shareable, so it is always used on the main Ractor.
310
310
  def create_capsule_thread(state)
311
311
  job_state = GoodJob::Ractors.main? ? state : GoodJob::Ractors.make_shareable(state.dup)
312
- GoodJob::Ractors.on_main(self) { RUNTIMES[self].capsule&.create_thread(job_state) }
312
+ GoodJob::Ractors.on_main(self) { capsule&.create_thread(job_state) }
313
313
  end
314
314
 
315
315
  def register_tracker
316
316
  GoodJob::Ractors.on_main(self) do
317
- tracker = RUNTIMES[self].capsule.tracker
317
+ tracker = capsule.tracker
318
318
  tracker.register
319
319
  tracker.id_for_lock
320
320
  end
321
321
  end
322
322
 
323
323
  def unregister_tracker
324
- GoodJob::Ractors.on_main(self) { RUNTIMES[self].capsule.tracker.unregister }
324
+ GoodJob::Ractors.on_main(self) { capsule.tracker.unregister }
325
325
  end
326
326
 
327
327
  def tracker_id_for_lock
328
- GoodJob::Ractors.on_main(self) { RUNTIMES[self].capsule.tracker.id_for_lock }
328
+ GoodJob::Ractors.on_main(self) { capsule.tracker.id_for_lock }
329
329
  end
330
330
 
331
331
  # Active Record does not support +insert_all+ outside of the main Ractor.
@@ -5,11 +5,7 @@ module GoodJob
5
5
  # a {GoodJob::Scheduler}, {GoodJob::Poller}, {GoodJob::Notifier}, and {GoodJob::CronManager}.
6
6
  # GoodJob creates a default capsule on initialization.
7
7
  class Capsule
8
- # @!attribute [r] instances
9
- # @!scope class
10
- # List of all instantiated Capsules in the current process.
11
- # @return [Array<GoodJob::Capsule>, nil]
12
- cattr_reader :instances, default: Concurrent::Array.new, instance_reader: false
8
+ include InstanceRegistry
13
9
 
14
10
  delegate :register, :renew, :unregister, :id_for_lock, to: :@tracker, prefix: :_tracker
15
11
 
@@ -26,7 +22,7 @@ module GoodJob
26
22
  @tracker = GoodJob::CapsuleTracker.new(executor: @shared_executor)
27
23
  @lower_thread_priority = nil
28
24
 
29
- self.class.instances << self
25
+ register_instance
30
26
  end
31
27
 
32
28
  # Start the capsule once. After a shutdown, {#restart} must be used to start again.
@@ -37,6 +33,8 @@ module GoodJob
37
33
  @mutex.synchronize do
38
34
  return unless startable?(force: force)
39
35
 
36
+ register_instance
37
+
40
38
  @notifier = GoodJob::Notifier.new(enable_listening: configuration.enable_listen_notify, capsule: self, executor: @shared_executor)
41
39
  @poller = GoodJob::Poller.new(poll_interval: configuration.poll_interval)
42
40
  @multi_scheduler = GoodJob::MultiScheduler.from_configuration(configuration, capsule: self, warm_cache_on_initialize: true).tap do |multischeduler|
@@ -116,12 +114,14 @@ module GoodJob
116
114
  @multi_scheduler&.lower_thread_priority = value
117
115
  end
118
116
 
119
- private
120
-
117
+ # Configuration for this capsule, falling back to the global configuration.
118
+ # @return [GoodJob::Configuration]
121
119
  def configuration
122
120
  @configuration || GoodJob.configuration
123
121
  end
124
122
 
123
+ private
124
+
125
125
  def startable?(force: false)
126
126
  !@started_at && (@startable || force)
127
127
  end
@@ -1,5 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "concurrent/errors"
4
+ require "concurrent/scheduled_task"
5
+
3
6
  module GoodJob # :nodoc:
4
7
  # CapsuleTracker save a record in the database and periodically refreshes it. The intention is to
5
8
  # create a heartbeat that can be used to determine whether a capsule/process is still active
@@ -16,11 +19,7 @@ module GoodJob # :nodoc:
16
19
  # @return [Integer]
17
20
  attr_reader :advisory_locks
18
21
 
19
- # @!attribute [r] instances
20
- # @!scope class
21
- # List of all instantiated CapsuleTrackers in the current process.
22
- # @return [Array<GoodJob::CapsuleTracker>, nil]
23
- cattr_reader :instances, default: Concurrent::Array.new, instance_reader: false
22
+ include InstanceRegistry
24
23
 
25
24
  # @param executor [Concurrent::AbstractExecutorService] The executor to use for refreshing the process record.
26
25
  def initialize(executor: Concurrent.global_io_executor)
@@ -32,10 +31,9 @@ module GoodJob # :nodoc:
32
31
  @record_id = SecureRandom.uuid
33
32
  @record = nil
34
33
  @refresh_task = nil
34
+ @pid = ::Process.pid
35
35
 
36
- ActiveSupport::ForkTracker.after_fork { reset }
37
-
38
- self.class.instances << self
36
+ register_instance
39
37
  end
40
38
 
41
39
  # The UUID to use for locking. May be nil if the process is not registered or is unusable/expired.
@@ -57,10 +55,18 @@ module GoodJob # :nodoc:
57
55
  value
58
56
  end
59
57
 
58
+ # Whether the tracker has no active registrations and can be removed from {.instances}.
59
+ # It is re-added to {.instances} when it is registered again.
60
+ # @return [Boolean]
61
+ def prunable?
62
+ @locks.zero?
63
+ end
64
+
60
65
  # The expected UUID of the process for use in inspection.
61
66
  # Use {#id_for_lock} if using this as a lock key.
62
67
  # @return [String]
63
68
  def process_id
69
+ reset if forked?
64
70
  @record_id
65
71
  end
66
72
 
@@ -75,6 +81,7 @@ module GoodJob # :nodoc:
75
81
  # where with_connection always returns a newly borrowed connection.
76
82
  GoodJob::Process.lease_connection if with_advisory_lock && advisory_lock_connection.nil?
77
83
 
84
+ register_instance
78
85
  synchronize(with_connection: with_advisory_lock) do
79
86
  if with_advisory_lock && !advisory_locked?
80
87
  if @record
@@ -112,9 +119,10 @@ module GoodJob # :nodoc:
112
119
  if with_advisory_lock && @advisory_locked
113
120
  active = advisory_locked?
114
121
  if !active || matches_advisory_locked_connection?(advisory_lock_connection)
115
- @record.advisory_unlock(connection: advisory_lock_connection) if active
116
- # Remaining jobs need heartbeat liveness once the advisory lock is gone.
122
+ # Move remaining jobs to heartbeat liveness before releasing the lock; otherwise
123
+ # another process's cleanup can see an unlocked advisory record and release them.
117
124
  @record&.update(lock_type: nil) if @locks > 1
125
+ @record.advisory_unlock(connection: advisory_lock_connection) if active
118
126
  @advisory_locked = false
119
127
  @advisory_locked_connection = nil
120
128
  end
@@ -213,7 +221,14 @@ module GoodJob # :nodoc:
213
221
  synchronize { ns_reset }
214
222
  end
215
223
 
224
+ # Reset lazily after a fork (rather than with an ActiveSupport::ForkTracker callback)
225
+ # so that the tracker is not retained after it is no longer used.
226
+ def forked?
227
+ @pid != ::Process.pid
228
+ end
229
+
216
230
  def ns_reset
231
+ @pid = ::Process.pid
217
232
  @record_id = SecureRandom.uuid
218
233
  @record = nil
219
234
  @advisory_locked = false
@@ -235,7 +250,10 @@ module GoodJob # :nodoc:
235
250
  elsif @mutex.owned?
236
251
  yield
237
252
  else
238
- @mutex.synchronize(&block)
253
+ @mutex.synchronize do
254
+ ns_reset if forked?
255
+ yield
256
+ end
239
257
  end
240
258
  end
241
259
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "concurrent/errors"
4
+ require "concurrent/future"
3
5
  require "concurrent/hash"
4
6
  require "concurrent/scheduled_task"
5
7
  require "fugit"
@@ -9,11 +11,7 @@ module GoodJob # :nodoc:
9
11
  # CronManagers enqueue jobs on a repeating schedule.
10
12
  #
11
13
  class CronManager
12
- # @!attribute [r] instances
13
- # @!scope class
14
- # List of all instantiated CronManagers in the current process.
15
- # @return [Array<GoodJob::CronManager>, nil]
16
- cattr_reader :instances, default: Concurrent::Array.new, instance_reader: false
14
+ include InstanceRegistry
17
15
 
18
16
  # Task observer for cron task
19
17
  # @param time [Time]
@@ -41,11 +39,12 @@ module GoodJob # :nodoc:
41
39
  @graceful_restart_period = graceful_restart_period
42
40
 
43
41
  start if start_on_initialize
44
- self.class.instances << self
42
+ register_instance
45
43
  end
46
44
 
47
45
  # Schedule tasks that will enqueue jobs based on their schedule
48
46
  def start
47
+ register_instance
49
48
  ActiveSupport::Notifications.instrument("cron_manager_start.good_job", cron_entries: cron_entries) do
50
49
  @running = true
51
50
  graceful_restart = graceful_restart?
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "concurrent/array"
4
+
5
+ module GoodJob # :nodoc:
6
+ # Tracks instances of a class in a class-level +instances+ registry so they can be
7
+ # shut down or restarted together (e.g. by {GoodJob.shutdown} and {GoodJob.restart}).
8
+ # Instances remain in the registry after they are shut down so that they can be
9
+ # restarted; use +prune+ to remove them so they can be garbage collected.
10
+ module InstanceRegistry
11
+ extend ActiveSupport::Concern
12
+
13
+ included do
14
+ # @!attribute [r] instances
15
+ # @!scope class
16
+ # List of all registered instances in the current process.
17
+ # @return [Array]
18
+ cattr_reader :instances, default: Concurrent::Array.new, instance_reader: false
19
+ end
20
+
21
+ class_methods do
22
+ # Removes prunable (e.g. shut down) instances from the registry so they can be garbage collected.
23
+ # Instances are re-registered if they are started again.
24
+ # @return [Array] The pruned instances.
25
+ def prune
26
+ pruned = instances.select(&:prunable?)
27
+ instances.delete_if { |instance| pruned.include?(instance) }
28
+ pruned
29
+ end
30
+ end
31
+
32
+ # Whether the instance can be removed from the registry.
33
+ # @return [Boolean]
34
+ def prunable?
35
+ shutdown?
36
+ end
37
+
38
+ private
39
+
40
+ def register_instance
41
+ instances = self.class.instances
42
+ instances << self unless instances.include?(self)
43
+ end
44
+ end
45
+ end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'concurrent/delay'
4
+ require "concurrent/set"
4
5
 
5
6
  module GoodJob
6
7
  #
@@ -32,7 +33,7 @@ module GoodJob
32
33
  def next
33
34
  active_job_id = nil
34
35
  @capsule.tracker.register do
35
- job_query.perform_with_lock(lock_id: @capsule.tracker.id_for_lock, parsed_queues: parsed_queues, queue_select_limit: GoodJob.configuration.queue_select_limit, lock_strategy: GoodJob.configuration.lock_strategy) do |execution|
36
+ job_query.perform_with_lock(lock_id: @capsule.tracker.id_for_lock, parsed_queues: parsed_queues, queue_select_limit: @capsule.configuration.queue_select_limit, lock_strategy: @capsule.configuration.lock_strategy) do |execution|
36
37
  @metrics.touch_check_queue_at
37
38
 
38
39
  if execution
@@ -1,7 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'active_support/core_ext/module/attribute_accessors_per_thread'
4
+ require "concurrent/array"
4
5
  require 'concurrent/atomic/atomic_boolean'
6
+ require "concurrent/atomic/atomic_fixnum"
7
+ require "concurrent/atomic/event"
5
8
  require "concurrent/scheduled_task"
6
9
  require "good_job/notifier/process_heartbeat"
7
10
 
@@ -39,11 +42,7 @@ module GoodJob # :nodoc:
39
42
  PG::Error
40
43
  ].freeze
41
44
 
42
- # @!attribute [r] instances
43
- # @!scope class
44
- # List of all instantiated Notifiers in the current process.
45
- # @return [Array<GoodJob::Notifier>, nil]
46
- cattr_reader :instances, default: Concurrent::Array.new, instance_reader: false
45
+ include InstanceRegistry
47
46
 
48
47
  # @!attribute [rw] connection
49
48
  # @!scope class
@@ -85,7 +84,6 @@ module GoodJob # :nodoc:
85
84
  @last_keepalive_time = Time.current
86
85
 
87
86
  start
88
- self.class.instances << self
89
87
  end
90
88
 
91
89
  def running?
@@ -192,6 +190,7 @@ module GoodJob # :nodoc:
192
190
  delegate :synchronize, to: :@monitor
193
191
 
194
192
  def start
193
+ register_instance
195
194
  synchronize do
196
195
  return if @running.true?
197
196
 
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "concurrent/array"
3
4
  require 'concurrent/atomic/atomic_boolean'
5
+ require "concurrent/timer_task"
4
6
 
5
7
  module GoodJob # :nodoc:
6
8
  #
@@ -14,11 +16,7 @@ module GoodJob # :nodoc:
14
16
  run_now: true,
15
17
  }.freeze
16
18
 
17
- # @!attribute [r] instances
18
- # @!scope class
19
- # List of all instantiated Pollers in the current process.
20
- # @return [Array<GoodJob::Poller>, nil]
21
- cattr_reader :instances, default: Concurrent::Array.new, instance_reader: false
19
+ include InstanceRegistry
22
20
 
23
21
  # Creates GoodJob::Poller from a GoodJob::Configuration instance.
24
22
  # @param configuration [GoodJob::Configuration]
@@ -40,7 +38,6 @@ module GoodJob # :nodoc:
40
38
  @timer_options[:execution_interval] = poll_interval if poll_interval.present?
41
39
 
42
40
  create_timer
43
- self.class.instances << self
44
41
  end
45
42
 
46
43
  # Tests whether the timer is running.
@@ -99,6 +96,7 @@ module GoodJob # :nodoc:
99
96
 
100
97
  # @return [void]
101
98
  def create_timer
99
+ register_instance
102
100
  return if @timer_options[:execution_interval] <= 0
103
101
 
104
102
  @timer = Concurrent::TimerTask.new(@timer_options) do
@@ -1,5 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "concurrent/atomic/atomic_boolean"
4
+ require "concurrent/future"
5
+
3
6
  module GoodJob
4
7
  class ProbeServer
5
8
  class SimpleHandler
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "concurrent/future"
4
+
3
5
  module GoodJob
4
6
  class ProbeServer
5
7
  class WebrickHandler
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "concurrent/errors"
4
+
3
5
  module GoodJob
4
6
  class ProbeServer
5
7
  def self.task_observer(time, output, thread_error) # rubocop:disable Lint/UnusedMethodArgument
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "concurrent/map"
4
+
3
5
  module GoodJob
4
6
  # Compatibility layer for running GoodJob in a Ractor-ized Rails application.
5
7
  #
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "concurrent/executor/thread_pool_executor"
4
4
  require "concurrent/executor/timer_set"
5
+ require "concurrent/future"
5
6
  require "concurrent/scheduled_task"
6
7
  require "concurrent/utility/processor_counter"
7
8
 
@@ -32,11 +33,7 @@ module GoodJob # :nodoc:
32
33
  # In CRuby, this sets the thread quantum to ~12.5ms ( 100ms * 2^(-3) ).
33
34
  LOW_THREAD_PRIORITY = -3
34
35
 
35
- # @!attribute [r] instances
36
- # @!scope class
37
- # List of all instantiated Schedulers in the current process.
38
- # @return [Array<GoodJob::Scheduler>, nil]
39
- cattr_reader :instances, default: Concurrent::Array.new, instance_reader: false
36
+ include InstanceRegistry
40
37
 
41
38
  # Human readable name of the scheduler that includes configuration values.
42
39
  # @return [String]
@@ -74,7 +71,6 @@ module GoodJob # :nodoc:
74
71
 
75
72
  create_executor
76
73
  warm_cache if warm_cache_on_initialize
77
- self.class.instances << self
78
74
  end
79
75
 
80
76
  # Tests whether the scheduler is running.
@@ -270,6 +266,7 @@ module GoodJob # :nodoc:
270
266
 
271
267
  # @return [void]
272
268
  def create_executor
269
+ register_instance
273
270
  instrument("scheduler_create_pool", { performer_name: performer.name, max_threads: @executor_options[:max_threads] }) do
274
271
  @timer_set = TimerSet.new
275
272
  @executor = ThreadPoolExecutor.new(@executor_options)
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'concurrent/executor/executor_service'
4
+ require "concurrent/executor/thread_pool_executor"
4
5
 
5
6
  module GoodJob
6
7
  class SharedExecutor
@@ -9,18 +10,14 @@ module GoodJob
9
10
 
10
11
  MAX_THREADS = 2
11
12
 
12
- # @!attribute [r] instances
13
- # @!scope class
14
- # List of all instantiated SharedExecutor in the current process.
15
- # @return [Array<GoodJob::SharedExecutor>, nil]
16
- cattr_reader :instances, default: Concurrent::Array.new, instance_reader: false
13
+ include InstanceRegistry
17
14
 
18
15
  attr_reader :executor
19
16
 
20
17
  def initialize
21
18
  @mutex = Mutex.new
22
19
 
23
- self.class.instances << self
20
+ register_instance
24
21
  end
25
22
 
26
23
  def post(*args, &task)
@@ -75,6 +72,7 @@ module GoodJob
75
72
  private
76
73
 
77
74
  def create_executor
75
+ register_instance
78
76
  @executor = Concurrent::ThreadPoolExecutor.new(
79
77
  # On JRuby, a Java thread pool with an unbounded queue never grows beyond min_threads (except to a single
80
78
  # thread from 0), and the Notifier's listen loop occupies a thread for as long as it runs.
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "concurrent/errors"
3
4
  require 'concurrent/timer_task'
4
5
  require 'good_job/sd_notify'
5
6
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  module GoodJob
4
4
  # GoodJob gem version.
5
- VERSION = '4.22.1'
5
+ VERSION = '4.23.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
@@ -7,6 +7,7 @@ require_relative "good_job/version"
7
7
  require_relative "good_job/engine"
8
8
  require_relative "good_job/ractors"
9
9
 
10
+ require_relative "good_job/instance_registry"
10
11
  require_relative "good_job/adapter"
11
12
  require_relative "good_job/adapter/inline_buffer"
12
13
  require_relative "active_job/queue_adapters/good_job_adapter"
@@ -249,6 +250,16 @@ module GoodJob
249
250
  Capsule.instances.all?(&:shutdown?)
250
251
  end
251
252
 
253
+ # Removes shut down instances (e.g. {GoodJob::Capsule}, {GoodJob::Scheduler}, {GoodJob::Notifier}) from their
254
+ # class-level registries so they can be garbage collected. Shut down instances are otherwise retained so that
255
+ # {.restart} can restart them; pruned instances will not be restarted by {.restart} unless started again directly.
256
+ # This is useful in processes that repeatedly create and shut down capsules, such as test suites.
257
+ # @return [void]
258
+ def self.prune
259
+ [Adapter, Capsule, CronManager, Notifier, Poller, Scheduler, SharedExecutor, CapsuleTracker].each(&:prune)
260
+ nil
261
+ end
262
+
252
263
  # Stops and restarts executing jobs.
253
264
  # GoodJob does its work in pools of background threads.
254
265
  # When forking processes you should shut down these background threads before forking, and restart them after forking.
@@ -387,8 +398,8 @@ module GoodJob
387
398
  # For use in tests/CI to validate GoodJob is up-to-date.
388
399
  # @return [Boolean]
389
400
  def self.migrated?
390
- GoodJob::ConcurrencyClaim.connection_pool.with_connection do |connection|
391
- connection.table_exists?(GoodJob::ConcurrencyClaim.table_name)
401
+ GoodJob::Execution.connection_pool.with_connection do |connection|
402
+ connection.index_name_exists?(GoodJob::Execution.table_name, :index_good_job_executions_on_created_at)
392
403
  end
393
404
  end
394
405
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: good_job
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.22.1
4
+ version: 4.23.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ben Sheldon
@@ -275,6 +275,7 @@ files:
275
275
  - app/controllers/good_job/batches_controller.rb
276
276
  - app/controllers/good_job/cleaner_controller.rb
277
277
  - app/controllers/good_job/cron_entries_controller.rb
278
+ - app/controllers/good_job/executions_controller.rb
278
279
  - app/controllers/good_job/frontends_controller.rb
279
280
  - app/controllers/good_job/jobs_controller.rb
280
281
  - app/controllers/good_job/metrics_controller.rb
@@ -307,6 +308,7 @@ files:
307
308
  - app/models/concerns/good_job/advisory_lockable.rb
308
309
  - app/models/concerns/good_job/error_events.rb
309
310
  - app/models/concerns/good_job/filterable.rb
311
+ - app/models/concerns/good_job/filtered_arguments.rb
310
312
  - app/models/concerns/good_job/reportable.rb
311
313
  - app/models/good_job/active_record_parent_class.rb
312
314
  - app/models/good_job/base_record.rb
@@ -332,6 +334,7 @@ files:
332
334
  - app/views/good_job/cleaner/index.html.erb
333
335
  - app/views/good_job/cron_entries/index.html.erb
334
336
  - app/views/good_job/cron_entries/show.html.erb
337
+ - app/views/good_job/executions/show.html.erb
335
338
  - app/views/good_job/jobs/_executions.erb
336
339
  - app/views/good_job/jobs/_table.erb
337
340
  - app/views/good_job/jobs/index.html.erb
@@ -394,6 +397,7 @@ files:
394
397
  - lib/generators/good_job/templates/update/migrations/19_remove_redundant_good_jobs_indexes.rb.erb
395
398
  - lib/generators/good_job/templates/update/migrations/20_reorder_good_jobs_cron_index.rb.erb
396
399
  - lib/generators/good_job/templates/update/migrations/21_create_good_job_concurrency_claims.rb.erb
400
+ - lib/generators/good_job/templates/update/migrations/22_add_index_good_job_executions_on_created_at.rb.erb
397
401
  - lib/generators/good_job/update_generator.rb
398
402
  - lib/good_job.rb
399
403
  - lib/good_job/active_job_extensions/batches.rb
@@ -416,6 +420,7 @@ files:
416
420
  - lib/good_job/daemon.rb
417
421
  - lib/good_job/dependencies.rb
418
422
  - lib/good_job/engine.rb
423
+ - lib/good_job/instance_registry.rb
419
424
  - lib/good_job/interrupt_error.rb
420
425
  - lib/good_job/interrupted_error.rb
421
426
  - lib/good_job/job_performer.rb