good_job 4.22.0 → 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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +55 -0
- data/README.md +11 -1
- data/app/controllers/good_job/executions_controller.rb +11 -0
- data/app/frontend/good_job/style.css +5 -0
- data/app/helpers/good_job/application_helper.rb +5 -4
- data/app/models/concerns/good_job/advisory_lockable.rb +33 -17
- data/app/models/concerns/good_job/filtered_arguments.rb +27 -0
- data/app/models/good_job/batch_record.rb +0 -1
- data/app/models/good_job/concurrency_claim.rb +3 -1
- data/app/models/good_job/cron_entry.rb +9 -11
- data/app/models/good_job/execution.rb +2 -0
- data/app/models/good_job/job.rb +2 -0
- data/app/models/good_job/process.rb +1 -0
- data/app/views/good_job/batches/_jobs.erb +9 -9
- data/app/views/good_job/executions/show.html.erb +11 -0
- data/app/views/good_job/jobs/_executions.erb +2 -6
- data/app/views/good_job/jobs/_table.erb +10 -10
- data/app/views/good_job/jobs/show.html.erb +1 -1
- data/config/brakeman.ignore +3 -3
- data/config/locales/pt-BR.yml +25 -23
- data/config/routes.rb +3 -1
- data/lib/generators/good_job/templates/install/migrations/create_good_jobs.rb.erb +1 -0
- data/lib/generators/good_job/templates/update/migrations/01_create_good_jobs.rb.erb +1 -0
- data/lib/generators/good_job/templates/update/migrations/22_add_index_good_job_executions_on_created_at.rb.erb +12 -0
- data/lib/good_job/active_job_extensions/concurrency.rb +9 -2
- data/lib/good_job/adapter.rb +72 -29
- data/lib/good_job/capsule.rb +8 -8
- data/lib/good_job/capsule_tracker.rb +29 -11
- data/lib/good_job/configuration.rb +10 -1
- data/lib/good_job/cron_manager.rb +5 -6
- data/lib/good_job/engine.rb +4 -0
- data/lib/good_job/instance_registry.rb +45 -0
- data/lib/good_job/job_performer.rb +2 -1
- data/lib/good_job/log_subscriber.rb +9 -0
- data/lib/good_job/notifier.rb +5 -6
- data/lib/good_job/poller.rb +4 -6
- data/lib/good_job/probe_server/simple_handler.rb +3 -0
- data/lib/good_job/probe_server/webrick_handler.rb +2 -0
- data/lib/good_job/probe_server.rb +2 -0
- data/lib/good_job/ractors.rb +99 -0
- data/lib/good_job/scheduler.rb +3 -6
- data/lib/good_job/shared_executor.rb +4 -6
- data/lib/good_job/systemd_service.rb +1 -0
- data/lib/good_job/version.rb +1 -1
- data/lib/good_job.rb +81 -11
- metadata +7 -1
data/config/locales/pt-BR.yml
CHANGED
|
@@ -37,17 +37,17 @@ pt-BR:
|
|
|
37
37
|
no_batches_found: Nenhum lote encontrado.
|
|
38
38
|
cleaner:
|
|
39
39
|
index:
|
|
40
|
-
all:
|
|
41
|
-
class:
|
|
42
|
-
exception:
|
|
43
|
-
grouped_by_class:
|
|
44
|
-
grouped_by_exception:
|
|
45
|
-
last_1_hour:
|
|
46
|
-
last_24_hours:
|
|
47
|
-
last_3_days:
|
|
48
|
-
last_3_hours:
|
|
49
|
-
last_7_days:
|
|
50
|
-
title:
|
|
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
|
|
145
|
+
notice: A tarefa foi excluída.
|
|
146
146
|
discard:
|
|
147
147
|
notice: A tarefa foi descartada.
|
|
148
148
|
executions:
|
|
149
|
-
application_trace:
|
|
150
|
-
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
|
|
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:
|
|
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
|
|
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
|
|
249
|
-
jobs:
|
|
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:
|
|
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:
|
|
321
|
-
cron_schedules:
|
|
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
|
|
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:
|
|
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
|
|
@@ -6,6 +6,8 @@ module GoodJob
|
|
|
6
6
|
extend ActiveSupport::Concern
|
|
7
7
|
|
|
8
8
|
VALID_TYPES = [String, Symbol, Numeric, Date, Time, TrueClass, FalseClass, NilClass].freeze
|
|
9
|
+
# The lock and claim key for a rule without a label or key, which counts all jobs.
|
|
10
|
+
ALL_KEY = "all"
|
|
9
11
|
|
|
10
12
|
class ConcurrencyExceededError < StandardError
|
|
11
13
|
def backtrace
|
|
@@ -52,6 +54,9 @@ module GoodJob
|
|
|
52
54
|
|
|
53
55
|
def evaluate(job, stage)
|
|
54
56
|
resolved_label = resolve_label(job)
|
|
57
|
+
# A rule with a label that resolves to blank does not apply
|
|
58
|
+
return nil if @label.present? && resolved_label.blank?
|
|
59
|
+
|
|
55
60
|
resolved_key = resolve_key(job, resolved_label)
|
|
56
61
|
return nil if resolved_key.blank? && resolved_label.blank?
|
|
57
62
|
|
|
@@ -77,8 +82,10 @@ module GoodJob
|
|
|
77
82
|
end
|
|
78
83
|
|
|
79
84
|
def resolve_key(job, label)
|
|
80
|
-
if label.present?
|
|
85
|
+
if label.present?
|
|
81
86
|
"label:#{label}"
|
|
87
|
+
elsif key.blank?
|
|
88
|
+
ALL_KEY
|
|
82
89
|
else
|
|
83
90
|
key_value = @key.respond_to?(:call) ? job.instance_exec(&@key) : @key
|
|
84
91
|
raise TypeError, "Concurrency key must be a String; was a #{key_value.class}" if key_value.present? && VALID_TYPES.none? { |type| key_value.is_a?(type) }
|
|
@@ -117,7 +124,7 @@ module GoodJob
|
|
|
117
124
|
elsif key_explicit? && key.present?
|
|
118
125
|
"key:#{key}"
|
|
119
126
|
else
|
|
120
|
-
|
|
127
|
+
ALL_KEY
|
|
121
128
|
end
|
|
122
129
|
end
|
|
123
130
|
|
data/lib/good_job/adapter.rb
CHANGED
|
@@ -5,11 +5,7 @@ module GoodJob
|
|
|
5
5
|
# ActiveJob Adapter.
|
|
6
6
|
#
|
|
7
7
|
class Adapter
|
|
8
|
-
|
|
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
|
|
8
|
+
include InstanceRegistry
|
|
13
9
|
|
|
14
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+.
|
|
15
11
|
#
|
|
@@ -25,13 +21,15 @@ module GoodJob
|
|
|
25
21
|
# -+test+: +:inline+
|
|
26
22
|
# - +production+ and all other environments: +:external+
|
|
27
23
|
#
|
|
28
|
-
|
|
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
|
|
29
27
|
@_execution_mode_override = execution_mode
|
|
30
28
|
GoodJob::Configuration.validate_execution_mode(@_execution_mode_override) if @_execution_mode_override
|
|
31
|
-
@
|
|
29
|
+
@_capsule = _capsule
|
|
32
30
|
|
|
33
31
|
start_async if GoodJob.async_ready?
|
|
34
|
-
|
|
32
|
+
register_instance
|
|
35
33
|
end
|
|
36
34
|
|
|
37
35
|
# Enqueues the ActiveJob job to be performed.
|
|
@@ -75,16 +73,19 @@ module GoodJob
|
|
|
75
73
|
lock_id = nil
|
|
76
74
|
|
|
77
75
|
if execute_inline?
|
|
78
|
-
|
|
76
|
+
lock_id = register_tracker
|
|
79
77
|
tracker_registered = true
|
|
80
|
-
lock_id = @capsule.tracker.id_for_lock
|
|
81
78
|
end
|
|
82
79
|
|
|
83
80
|
begin
|
|
84
81
|
GoodJob::Job.transaction(requires_new: true, joinable: false) do
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
82
|
+
results = if GoodJob::Ractors.main?
|
|
83
|
+
column_names = GoodJob::Job.column_names
|
|
84
|
+
job_attributes = jobs.map { |job| job.attributes.slice(*column_names) }
|
|
85
|
+
GoodJob::Job.insert_all(job_attributes, returning: %w[id active_job_id]) # rubocop:disable Rails/SkipsModelValidations
|
|
86
|
+
else
|
|
87
|
+
insert_individually(jobs)
|
|
88
|
+
end
|
|
88
89
|
|
|
89
90
|
job_id_to_provider_job_id = results.to_h { |result| [result['active_job_id'], result['id']] }
|
|
90
91
|
active_jobs.each do |active_job|
|
|
@@ -120,16 +121,16 @@ module GoodJob
|
|
|
120
121
|
end
|
|
121
122
|
ensure
|
|
122
123
|
inline_jobs.each(&:advisory_unlock)
|
|
123
|
-
|
|
124
|
+
unregister_tracker if tracker_registered
|
|
124
125
|
tracker_registered = false
|
|
125
126
|
end
|
|
126
127
|
elsif tracker_registered
|
|
127
|
-
|
|
128
|
+
unregister_tracker
|
|
128
129
|
tracker_registered = false
|
|
129
130
|
end
|
|
130
131
|
rescue StandardError
|
|
131
132
|
if tracker_registered
|
|
132
|
-
|
|
133
|
+
unregister_tracker
|
|
133
134
|
tracker_registered = false
|
|
134
135
|
end
|
|
135
136
|
raise
|
|
@@ -147,7 +148,7 @@ module GoodJob
|
|
|
147
148
|
state = { queue_name: queue_name, count: jobs_by_queue_and_scheduled_at.size }
|
|
148
149
|
state[:scheduled_at] = scheduled_at if scheduled_at
|
|
149
150
|
|
|
150
|
-
executed_locally = execute_async? &&
|
|
151
|
+
executed_locally = execute_async? && create_capsule_thread(state)
|
|
151
152
|
unless executed_locally
|
|
152
153
|
state[:count] = job_id_to_active_jobs.values_at(*jobs_by_queue_and_scheduled_at.map(&:active_job_id)).count { |active_job| send_notify?(active_job) }
|
|
153
154
|
Notifier.notify(state) unless state[:count].zero?
|
|
@@ -183,8 +184,7 @@ module GoodJob
|
|
|
183
184
|
)
|
|
184
185
|
elsif will_execute_inline
|
|
185
186
|
lock_strategy = GoodJob::Job.effective_lock_strategy
|
|
186
|
-
|
|
187
|
-
lock_id = @capsule.tracker.id_for_lock
|
|
187
|
+
lock_id = register_tracker
|
|
188
188
|
tracker_registered = true
|
|
189
189
|
begin
|
|
190
190
|
job = case lock_strategy
|
|
@@ -198,12 +198,12 @@ module GoodJob
|
|
|
198
198
|
InlineBuffer.perform_now_or_defer do
|
|
199
199
|
perform_inline(job, notify: send_notify?(active_job), already_claimed: lock_strategy != :advisory, advisory_unlock: lock_strategy != :skiplocked)
|
|
200
200
|
ensure
|
|
201
|
-
|
|
201
|
+
unregister_tracker if tracker_registered
|
|
202
202
|
tracker_registered = false
|
|
203
203
|
end
|
|
204
204
|
rescue StandardError
|
|
205
205
|
if tracker_registered
|
|
206
|
-
|
|
206
|
+
unregister_tracker
|
|
207
207
|
tracker_registered = false
|
|
208
208
|
end
|
|
209
209
|
raise
|
|
@@ -215,7 +215,7 @@ module GoodJob
|
|
|
215
215
|
)
|
|
216
216
|
|
|
217
217
|
if job
|
|
218
|
-
executed_locally = execute_async? &&
|
|
218
|
+
executed_locally = execute_async? && create_capsule_thread(job.job_state)
|
|
219
219
|
Notifier.notify(job.job_state) if !executed_locally && send_notify?(active_job)
|
|
220
220
|
end
|
|
221
221
|
end
|
|
@@ -246,8 +246,7 @@ module GoodJob
|
|
|
246
246
|
# * A positive number will wait that many seconds before stopping any remaining active threads.
|
|
247
247
|
# @return [void]
|
|
248
248
|
def shutdown(timeout: NONE)
|
|
249
|
-
|
|
250
|
-
@_async_started = false
|
|
249
|
+
capsule.shutdown(timeout: timeout)
|
|
251
250
|
end
|
|
252
251
|
|
|
253
252
|
# This adapter's execution mode
|
|
@@ -282,19 +281,63 @@ module GoodJob
|
|
|
282
281
|
def start_async
|
|
283
282
|
return unless execute_async?
|
|
284
283
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
@_async_started = true
|
|
284
|
+
capsule.start
|
|
285
|
+
capsule.lower_thread_priority = true if GoodJob.configuration.lower_thread_priority.in?([true, nil])
|
|
288
286
|
end
|
|
289
287
|
|
|
290
288
|
# Whether the async executors are running
|
|
291
289
|
# @return [Boolean]
|
|
292
290
|
def async_started?
|
|
293
|
-
|
|
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?
|
|
294
298
|
end
|
|
295
299
|
|
|
296
300
|
private
|
|
297
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
|
+
# The capsule is not Ractor-shareable, so it is always used on the main Ractor.
|
|
310
|
+
def create_capsule_thread(state)
|
|
311
|
+
job_state = GoodJob::Ractors.main? ? state : GoodJob::Ractors.make_shareable(state.dup)
|
|
312
|
+
GoodJob::Ractors.on_main(self) { capsule&.create_thread(job_state) }
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
def register_tracker
|
|
316
|
+
GoodJob::Ractors.on_main(self) do
|
|
317
|
+
tracker = capsule.tracker
|
|
318
|
+
tracker.register
|
|
319
|
+
tracker.id_for_lock
|
|
320
|
+
end
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
def unregister_tracker
|
|
324
|
+
GoodJob::Ractors.on_main(self) { capsule.tracker.unregister }
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
def tracker_id_for_lock
|
|
328
|
+
GoodJob::Ractors.on_main(self) { capsule.tracker.id_for_lock }
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
# Active Record does not support +insert_all+ outside of the main Ractor.
|
|
332
|
+
def insert_individually(jobs)
|
|
333
|
+
jobs.filter_map do |job|
|
|
334
|
+
GoodJob::Job.transaction(requires_new: true) { job.save!(validate: false) }
|
|
335
|
+
{ 'id' => job.id, 'active_job_id' => job.active_job_id }
|
|
336
|
+
rescue ActiveRecord::RecordNotUnique
|
|
337
|
+
nil
|
|
338
|
+
end
|
|
339
|
+
end
|
|
340
|
+
|
|
298
341
|
def send_notify?(active_job)
|
|
299
342
|
return false unless GoodJob.configuration.enable_listen_notify
|
|
300
343
|
return true unless active_job.respond_to?(:good_job_notify)
|
|
@@ -309,7 +352,7 @@ module GoodJob
|
|
|
309
352
|
retried_job = nil
|
|
310
353
|
|
|
311
354
|
loop do
|
|
312
|
-
result = job.perform(lock_id:
|
|
355
|
+
result = job.perform(lock_id: tracker_id_for_lock, already_claimed: already_claimed)
|
|
313
356
|
already_claimed = false # only skip lock-write on first iteration
|
|
314
357
|
retried_job = result.retried_job
|
|
315
358
|
break if retried_job.nil? || retried_job.scheduled_at.nil? || retried_job.scheduled_at > Time.current
|
data/lib/good_job/capsule.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
116
|
-
#
|
|
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
|
|
253
|
+
@mutex.synchronize do
|
|
254
|
+
ns_reset if forked?
|
|
255
|
+
yield
|
|
256
|
+
end
|
|
239
257
|
end
|
|
240
258
|
end
|
|
241
259
|
end
|
|
@@ -528,6 +528,15 @@ module GoodJob
|
|
|
528
528
|
(options[:dequeue_query_sort] || rails_config[:dequeue_query_sort] || :created_at).to_sym
|
|
529
529
|
end
|
|
530
530
|
|
|
531
|
+
# Resolves lazily memoized values so the configuration can be frozen.
|
|
532
|
+
# @return [self]
|
|
533
|
+
def prepare_for_sharing
|
|
534
|
+
in_webserver?
|
|
535
|
+
validator
|
|
536
|
+
@_rails_config = rails_config
|
|
537
|
+
self
|
|
538
|
+
end
|
|
539
|
+
|
|
531
540
|
private
|
|
532
541
|
|
|
533
542
|
# Reads a configuration value from one or more deprecated sources, emitting a
|
|
@@ -586,7 +595,7 @@ module GoodJob
|
|
|
586
595
|
end
|
|
587
596
|
|
|
588
597
|
def rails_config
|
|
589
|
-
Rails.application.config.good_job
|
|
598
|
+
@_rails_config || Rails.application.config.good_job
|
|
590
599
|
end
|
|
591
600
|
end
|
|
592
601
|
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
|
-
|
|
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
|
-
|
|
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?
|
data/lib/good_job/engine.rb
CHANGED
|
@@ -10,6 +10,10 @@ module GoodJob
|
|
|
10
10
|
config.good_job = ActiveSupport::OrderedOptions.new
|
|
11
11
|
config.good_job.cron = {}
|
|
12
12
|
|
|
13
|
+
initializer "good_job.ractors" do |app|
|
|
14
|
+
app.singleton_class.prepend(GoodJob::Ractors::ApplicationExtension) if GoodJob::Ractors.enabled? && app.respond_to?(:ractorize!)
|
|
15
|
+
end
|
|
16
|
+
|
|
13
17
|
initializer "good_job.logger" do |_app|
|
|
14
18
|
ActiveSupport.on_load(:good_job) do
|
|
15
19
|
self.logger = ::Rails.logger if GoodJob.logger == GoodJob::DEFAULT_LOGGER
|
|
@@ -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
|