good_job 3.28.3 → 3.29.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 +8 -0
- data/app/models/good_job/base_execution.rb +7 -0
- data/app/models/good_job/execution.rb +33 -9
- data/app/models/good_job/job.rb +1 -0
- data/app/models/good_job/process.rb +82 -69
- data/lib/generators/good_job/templates/install/migrations/create_good_jobs.rb.erb +9 -0
- data/lib/generators/good_job/templates/update/migrations/13_create_good_job_process_lock_ids.rb.erb +17 -0
- data/lib/generators/good_job/templates/update/migrations/14_create_good_job_process_lock_indexes.rb.erb +37 -0
- data/lib/good_job/adapter.rb +6 -4
- data/lib/good_job/capsule.rb +18 -7
- data/lib/good_job/capsule_tracker.rb +227 -0
- data/lib/good_job/job_performer.rb +18 -15
- data/lib/good_job/multi_scheduler.rb +2 -2
- data/lib/good_job/notifier/process_heartbeat.rb +3 -3
- data/lib/good_job/notifier.rb +18 -23
- data/lib/good_job/shared_executor.rb +19 -1
- data/lib/good_job/version.rb +1 -1
- data/lib/good_job.rb +13 -4
- metadata +5 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: dba2ffb689b77716dac30f6c89c02a8ddd2cad9d340ca72fe86405d4cee446e1
|
|
4
|
+
data.tar.gz: 43c511926416fe1360e312ceb375b7db10e9c95ed9c8db040d9828e497b77eb0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b52422348cb25a90c17b755978b13278591ee5e388e3b16648fcd89fd77f6570a796ec44d7c64d4e9b0111b5102f2b80edfcbcd9236d13a2394a9ea5cc32297a
|
|
7
|
+
data.tar.gz: 91fab8760f5fbf4ff137fc66da09ead108201d0a47dd5d6859a4afed9d66b88f26c758ed95e4243591e6b719d417d7fa1d9ae3a85e5d3619f6c1b374435996c6
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [v3.29.0](https://github.com/bensheldon/good_job/tree/v3.29.0) (2024-05-22)
|
|
4
|
+
|
|
5
|
+
[Full Changelog](https://github.com/bensheldon/good_job/compare/v3.28.3...v3.29.0)
|
|
6
|
+
|
|
7
|
+
**Merged pull requests:**
|
|
8
|
+
|
|
9
|
+
- Add association between Process and Jobs, and add a heartbeat to the Process record [\#999](https://github.com/bensheldon/good_job/pull/999) ([bensheldon](https://github.com/bensheldon))
|
|
10
|
+
|
|
3
11
|
## [v3.28.3](https://github.com/bensheldon/good_job/tree/v3.28.3) (2024-05-18)
|
|
4
12
|
|
|
5
13
|
[Full Changelog](https://github.com/bensheldon/good_job/compare/v3.28.2...v3.28.3)
|
|
@@ -84,6 +84,13 @@ module GoodJob
|
|
|
84
84
|
migration_pending_warning!
|
|
85
85
|
false
|
|
86
86
|
end
|
|
87
|
+
|
|
88
|
+
def process_lock_migrated?
|
|
89
|
+
return true if connection.index_name_exists?(:good_job_executions, :index_good_job_executions_on_process_id_and_created_at)
|
|
90
|
+
|
|
91
|
+
migration_pending_warning!
|
|
92
|
+
false
|
|
93
|
+
end
|
|
87
94
|
end
|
|
88
95
|
|
|
89
96
|
# The ActiveJob job class, as a string
|
|
@@ -262,7 +262,7 @@ module GoodJob
|
|
|
262
262
|
# return value for the job's +#perform+ method, and the exception the job
|
|
263
263
|
# raised, if any (if the job raised, then the second array entry will be
|
|
264
264
|
# +nil+). If there were no jobs to execute, returns +nil+.
|
|
265
|
-
def self.perform_with_advisory_lock(parsed_queues: nil, queue_select_limit: nil)
|
|
265
|
+
def self.perform_with_advisory_lock(lock_id:, parsed_queues: nil, queue_select_limit: nil)
|
|
266
266
|
execution = nil
|
|
267
267
|
result = nil
|
|
268
268
|
|
|
@@ -270,7 +270,7 @@ module GoodJob
|
|
|
270
270
|
execution = executions.first
|
|
271
271
|
if execution&.executable?
|
|
272
272
|
yield(execution) if block_given?
|
|
273
|
-
result = execution.perform
|
|
273
|
+
result = execution.perform(lock_id: lock_id)
|
|
274
274
|
else
|
|
275
275
|
execution = nil
|
|
276
276
|
yield(nil) if block_given?
|
|
@@ -367,7 +367,7 @@ module GoodJob
|
|
|
367
367
|
# An array of the return value of the job's +#perform+ method and the
|
|
368
368
|
# exception raised by the job, if any. If the job completed successfully,
|
|
369
369
|
# the second array entry (the exception) will be +nil+ and vice versa.
|
|
370
|
-
def perform
|
|
370
|
+
def perform(lock_id:)
|
|
371
371
|
run_callbacks(:perform) do
|
|
372
372
|
raise PreviouslyPerformedError, 'Cannot perform a job that has already been performed' if finished_at
|
|
373
373
|
|
|
@@ -397,17 +397,37 @@ module GoodJob
|
|
|
397
397
|
|
|
398
398
|
if discrete?
|
|
399
399
|
transaction do
|
|
400
|
-
|
|
400
|
+
discrete_execution_attrs = {
|
|
401
401
|
job_class: job_class,
|
|
402
402
|
queue_name: queue_name,
|
|
403
403
|
serialized_params: serialized_params,
|
|
404
404
|
scheduled_at: (scheduled_at || created_at),
|
|
405
|
-
created_at: job_performed_at
|
|
406
|
-
|
|
407
|
-
|
|
405
|
+
created_at: job_performed_at,
|
|
406
|
+
}
|
|
407
|
+
discrete_execution_attrs[:process_id] = lock_id if GoodJob::DiscreteExecution.columns_hash.key?("process_id")
|
|
408
|
+
|
|
409
|
+
execution_attrs = {
|
|
410
|
+
performed_at: job_performed_at,
|
|
411
|
+
executions_count: ((executions_count || 0) + 1),
|
|
412
|
+
}
|
|
413
|
+
if GoodJob::Execution.columns_hash.key?("locked_by_id")
|
|
414
|
+
execution_attrs[:locked_by_id] = lock_id
|
|
415
|
+
execution_attrs[:locked_at] = Time.current
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
discrete_execution = discrete_executions.create!(discrete_execution_attrs)
|
|
419
|
+
update!(execution_attrs)
|
|
408
420
|
end
|
|
409
421
|
else
|
|
410
|
-
|
|
422
|
+
execution_attrs = {
|
|
423
|
+
performed_at: job_performed_at,
|
|
424
|
+
}
|
|
425
|
+
if GoodJob::Execution.columns_hash.key?("locked_by_id")
|
|
426
|
+
execution_attrs[:locked_by_id] = lock_id
|
|
427
|
+
execution_attrs[:locked_at] = Time.current
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
update!(execution_attrs)
|
|
411
431
|
end
|
|
412
432
|
|
|
413
433
|
ActiveSupport::Notifications.instrument("perform_job.good_job", { execution: self, process_id: current_thread.process_id, thread_name: current_thread.thread_name }) do |instrument_payload|
|
|
@@ -450,7 +470,11 @@ module GoodJob
|
|
|
450
470
|
end
|
|
451
471
|
end
|
|
452
472
|
|
|
453
|
-
job_attributes =
|
|
473
|
+
job_attributes = if self.class.columns_hash.key?("locked_by_id")
|
|
474
|
+
{ locked_by_id: nil, locked_at: nil }
|
|
475
|
+
else
|
|
476
|
+
{}
|
|
477
|
+
end
|
|
454
478
|
|
|
455
479
|
job_error = result.handled_error || result.unhandled_error
|
|
456
480
|
if job_error
|
data/app/models/good_job/job.rb
CHANGED
|
@@ -29,6 +29,7 @@ module GoodJob
|
|
|
29
29
|
self.implicit_order_column = 'created_at'
|
|
30
30
|
|
|
31
31
|
belongs_to :batch, class_name: 'GoodJob::BatchRecord', inverse_of: :jobs, optional: true
|
|
32
|
+
belongs_to :locked_by_process, class_name: "GoodJob::Process", foreign_key: :locked_by_id, inverse_of: :locked_jobs, optional: true
|
|
32
33
|
has_many :executions, -> { order(created_at: :asc) }, class_name: 'GoodJob::Execution', foreign_key: 'active_job_id', inverse_of: :job # rubocop:disable Rails/HasManyOrHasOneDependent
|
|
33
34
|
has_many :discrete_executions, -> { order(created_at: :asc) }, class_name: 'GoodJob::DiscreteExecution', foreign_key: 'active_job_id', primary_key: :active_job_id, inverse_of: :job # rubocop:disable Rails/HasManyOrHasOneDependent
|
|
34
35
|
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
require 'socket'
|
|
4
4
|
|
|
5
5
|
module GoodJob # :nodoc:
|
|
6
|
-
#
|
|
6
|
+
# Active Record model that represents a GoodJob capsule/process (either async or CLI).
|
|
7
7
|
class Process < BaseRecord
|
|
8
8
|
include AdvisoryLockable
|
|
9
9
|
include OverridableConnection
|
|
@@ -15,50 +15,74 @@ module GoodJob # :nodoc:
|
|
|
15
15
|
|
|
16
16
|
self.table_name = 'good_job_processes'
|
|
17
17
|
self.implicit_order_column = 'created_at'
|
|
18
|
+
LOCK_TYPES = [
|
|
19
|
+
LOCK_TYPE_ADVISORY = 'advisory',
|
|
20
|
+
].freeze
|
|
18
21
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
+
LOCK_TYPE_ENUMS = {
|
|
23
|
+
LOCK_TYPE_ADVISORY => 1,
|
|
24
|
+
}.freeze
|
|
25
|
+
|
|
26
|
+
self.table_name = 'good_job_processes'
|
|
27
|
+
|
|
28
|
+
has_many :locked_jobs, class_name: "GoodJob::Job", foreign_key: :locked_by_id, inverse_of: :locked_by_process, dependent: nil
|
|
29
|
+
after_destroy { locked_jobs.update_all(locked_by_id: nil) if GoodJob::Job.columns_hash.key?("locked_by_id") } # rubocop:disable Rails/SkipsModelValidations
|
|
22
30
|
|
|
23
31
|
# Processes that are active and locked.
|
|
24
32
|
# @!method active
|
|
25
33
|
# @!scope class
|
|
26
34
|
# @return [ActiveRecord::Relation]
|
|
27
|
-
scope :active,
|
|
35
|
+
scope :active, (lambda do
|
|
36
|
+
if lock_type_migrated?
|
|
37
|
+
query = joins_advisory_locks
|
|
38
|
+
query.where(lock_type: LOCK_TYPE_ENUMS[LOCK_TYPE_ADVISORY]).advisory_locked
|
|
39
|
+
.or(query.where(lock_type: nil).where(arel_table[:updated_at].gt(EXPIRED_INTERVAL.ago)))
|
|
40
|
+
else
|
|
41
|
+
advisory_locked
|
|
42
|
+
end
|
|
43
|
+
end)
|
|
28
44
|
|
|
29
45
|
# Processes that are inactive and unlocked (e.g. SIGKILLed)
|
|
30
46
|
# @!method active
|
|
31
47
|
# @!scope class
|
|
32
48
|
# @return [ActiveRecord::Relation]
|
|
33
|
-
scope :inactive,
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
49
|
+
scope :inactive, (lambda do
|
|
50
|
+
if lock_type_migrated?
|
|
51
|
+
query = joins_advisory_locks
|
|
52
|
+
query.where(lock_type: LOCK_TYPE_ENUMS[LOCK_TYPE_ADVISORY]).advisory_unlocked
|
|
53
|
+
.or(query.where(lock_type: nil).where(arel_table[:updated_at].lt(EXPIRED_INTERVAL.ago)))
|
|
54
|
+
else
|
|
55
|
+
advisory_unlocked
|
|
56
|
+
end
|
|
57
|
+
end)
|
|
40
58
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
59
|
+
# Deletes all inactive process records.
|
|
60
|
+
def self.cleanup
|
|
61
|
+
inactive.find_each do |process|
|
|
62
|
+
GoodJob::Job.where(locked_by_id: process.id).update_all(locked_by_id: nil, locked_at: nil) if GoodJob::Job.columns_hash.key?("locked_by_id") # rubocop:disable Rails/SkipsModelValidations
|
|
63
|
+
process.delete
|
|
45
64
|
end
|
|
46
|
-
_current_id
|
|
47
65
|
end
|
|
48
66
|
|
|
49
|
-
#
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
mutex.synchronize { ns_current_state }
|
|
67
|
+
# @return [Boolean]
|
|
68
|
+
def self.lock_type_migrated?
|
|
69
|
+
columns_hash["lock_type"].present?
|
|
53
70
|
end
|
|
54
71
|
|
|
55
|
-
def self.
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
72
|
+
def self.create_record(id:, with_advisory_lock: false)
|
|
73
|
+
attributes = {
|
|
74
|
+
id: id,
|
|
75
|
+
state: process_state,
|
|
76
|
+
}
|
|
77
|
+
if with_advisory_lock
|
|
78
|
+
attributes[:create_with_advisory_lock] = true
|
|
79
|
+
attributes[:lock_type] = LOCK_TYPE_ADVISORY if lock_type_migrated?
|
|
80
|
+
end
|
|
81
|
+
create!(attributes)
|
|
82
|
+
end
|
|
59
83
|
|
|
84
|
+
def self.process_state
|
|
60
85
|
{
|
|
61
|
-
id: ns_current_id,
|
|
62
86
|
hostname: Socket.gethostname,
|
|
63
87
|
pid: ::Process.pid,
|
|
64
88
|
proctitle: $PROGRAM_NAME,
|
|
@@ -66,10 +90,8 @@ module GoodJob # :nodoc:
|
|
|
66
90
|
retry_on_unhandled_error: GoodJob.retry_on_unhandled_error,
|
|
67
91
|
schedulers: GoodJob::Scheduler.instances.map(&:stats),
|
|
68
92
|
cron_enabled: GoodJob.configuration.enable_cron?,
|
|
69
|
-
total_succeeded_executions_count:
|
|
70
|
-
total_errored_executions_count:
|
|
71
|
-
total_executions_count: total_succeeded_executions_count + total_errored_executions_count,
|
|
72
|
-
total_empty_executions_count: total_empty_executions_count,
|
|
93
|
+
total_succeeded_executions_count: GoodJob::Scheduler.instances.sum { |scheduler| scheduler.stats.fetch(:succeeded_executions_count) },
|
|
94
|
+
total_errored_executions_count: GoodJob::Scheduler.instances.sum { |scheduler| scheduler.stats.fetch(:errored_executions_count) },
|
|
73
95
|
database_connection_pool: {
|
|
74
96
|
size: connection_pool.size,
|
|
75
97
|
active: connection_pool.connections.count(&:in_use?),
|
|
@@ -77,45 +99,36 @@ module GoodJob # :nodoc:
|
|
|
77
99
|
}
|
|
78
100
|
end
|
|
79
101
|
|
|
80
|
-
# Deletes all inactive process records.
|
|
81
|
-
def self.cleanup
|
|
82
|
-
inactive.delete_all
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
# Registers the current process in the database
|
|
86
|
-
# @return [GoodJob::Process]
|
|
87
|
-
def self.register
|
|
88
|
-
mutex.synchronize do
|
|
89
|
-
process_state = ns_current_state
|
|
90
|
-
create(id: process_state[:id], state: process_state, create_with_advisory_lock: true)
|
|
91
|
-
rescue ActiveRecord::RecordNotUnique
|
|
92
|
-
find(ns_current_state[:id])
|
|
93
|
-
end
|
|
94
|
-
end
|
|
95
|
-
|
|
96
102
|
def refresh
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
+
self.state = self.class.process_state
|
|
104
|
+
reload.update(state: state, updated_at: Time.current)
|
|
105
|
+
rescue ActiveRecord::RecordNotFound
|
|
106
|
+
@new_record = true
|
|
107
|
+
self.created_at = self.updated_at = nil
|
|
108
|
+
state_will_change!
|
|
109
|
+
save
|
|
103
110
|
end
|
|
104
111
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
return unless owns_advisory_lock?
|
|
112
|
+
def refresh_if_stale(cleanup: false)
|
|
113
|
+
return unless stale?
|
|
108
114
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
end
|
|
115
|
+
result = refresh
|
|
116
|
+
self.class.cleanup if cleanup
|
|
117
|
+
result
|
|
113
118
|
end
|
|
114
119
|
|
|
115
120
|
def state
|
|
116
121
|
super || {}
|
|
117
122
|
end
|
|
118
123
|
|
|
124
|
+
def stale?
|
|
125
|
+
updated_at < STALE_INTERVAL.ago
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def expired?
|
|
129
|
+
updated_at < EXPIRED_INTERVAL.ago
|
|
130
|
+
end
|
|
131
|
+
|
|
119
132
|
def basename
|
|
120
133
|
File.basename(state.fetch("proctitle", ""))
|
|
121
134
|
end
|
|
@@ -124,20 +137,20 @@ module GoodJob # :nodoc:
|
|
|
124
137
|
state.fetch("schedulers", [])
|
|
125
138
|
end
|
|
126
139
|
|
|
127
|
-
def
|
|
128
|
-
return unless
|
|
140
|
+
def lock_type
|
|
141
|
+
return unless self.class.columns_hash['lock_type']
|
|
129
142
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
result
|
|
143
|
+
enum = super
|
|
144
|
+
LOCK_TYPE_ENUMS.key(enum) if enum
|
|
133
145
|
end
|
|
134
146
|
|
|
135
|
-
def
|
|
136
|
-
|
|
137
|
-
end
|
|
147
|
+
def lock_type=(value)
|
|
148
|
+
return unless self.class.columns_hash['lock_type']
|
|
138
149
|
|
|
139
|
-
|
|
140
|
-
|
|
150
|
+
enum = LOCK_TYPE_ENUMS[value]
|
|
151
|
+
raise(ArgumentError, "Invalid error_event: #{value}") if value && !enum
|
|
152
|
+
|
|
153
|
+
super(enum)
|
|
141
154
|
end
|
|
142
155
|
end
|
|
143
156
|
end
|
|
@@ -30,6 +30,8 @@ class CreateGoodJobs < ActiveRecord::Migration<%= migration_version %>
|
|
|
30
30
|
t.text :job_class
|
|
31
31
|
t.integer :error_event, limit: 2
|
|
32
32
|
t.text :labels, array: true
|
|
33
|
+
t.uuid :locked_by_id
|
|
34
|
+
t.datetime :locked_at
|
|
33
35
|
end
|
|
34
36
|
|
|
35
37
|
create_table :good_job_batches, id: :uuid do |t|
|
|
@@ -58,11 +60,13 @@ class CreateGoodJobs < ActiveRecord::Migration<%= migration_version %>
|
|
|
58
60
|
t.text :error
|
|
59
61
|
t.integer :error_event, limit: 2
|
|
60
62
|
t.text :error_backtrace, array: true
|
|
63
|
+
t.uuid :process_id
|
|
61
64
|
end
|
|
62
65
|
|
|
63
66
|
create_table :good_job_processes, id: :uuid do |t|
|
|
64
67
|
t.timestamps
|
|
65
68
|
t.jsonb :state
|
|
69
|
+
t.integer :lock_type, limit: 2
|
|
66
70
|
end
|
|
67
71
|
|
|
68
72
|
create_table :good_job_settings, id: :uuid do |t|
|
|
@@ -88,5 +92,10 @@ class CreateGoodJobs < ActiveRecord::Migration<%= migration_version %>
|
|
|
88
92
|
add_index :good_jobs, :labels, using: :gin, where: "(labels IS NOT NULL)", name: :index_good_jobs_on_labels
|
|
89
93
|
|
|
90
94
|
add_index :good_job_executions, [:active_job_id, :created_at], name: :index_good_job_executions_on_active_job_id_and_created_at
|
|
95
|
+
add_index :good_jobs, [:priority, :scheduled_at], order: { priority: "ASC NULLS LAST", scheduled_at: :asc },
|
|
96
|
+
where: "finished_at IS NULL AND locked_by_id IS NULL", name: :index_good_jobs_on_priority_scheduled_at_unfinished_unlocked
|
|
97
|
+
add_index :good_jobs, :locked_by_id,
|
|
98
|
+
where: "locked_by_id IS NOT NULL", name: "index_good_jobs_on_locked_by_id"
|
|
99
|
+
add_index :good_job_executions, [:process_id, :created_at], name: :index_good_job_executions_on_process_id_and_created_at
|
|
91
100
|
end
|
|
92
101
|
end
|
data/lib/generators/good_job/templates/update/migrations/13_create_good_job_process_lock_ids.rb.erb
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
class CreateGoodJobProcessLockIds < ActiveRecord::Migration<%= migration_version %>
|
|
3
|
+
def change
|
|
4
|
+
reversible do |dir|
|
|
5
|
+
dir.up do
|
|
6
|
+
# Ensure this incremental update migration is idempotent
|
|
7
|
+
# with monolithic install migration.
|
|
8
|
+
return if connection.column_exists?(:good_jobs, :locked_by_id)
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
add_column :good_jobs, :locked_by_id, :uuid
|
|
13
|
+
add_column :good_jobs, :locked_at, :datetime
|
|
14
|
+
add_column :good_job_executions, :process_id, :uuid
|
|
15
|
+
add_column :good_job_processes, :lock_type, :integer, limit: 2
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
class CreateGoodJobProcessLockIndexes < ActiveRecord::Migration<%= migration_version %>
|
|
3
|
+
disable_ddl_transaction!
|
|
4
|
+
|
|
5
|
+
def change
|
|
6
|
+
reversible do |dir|
|
|
7
|
+
dir.up do
|
|
8
|
+
unless connection.index_name_exists?(:good_jobs, :index_good_jobs_on_priority_scheduled_at_unfinished_unlocked)
|
|
9
|
+
add_index :good_jobs, [:priority, :scheduled_at],
|
|
10
|
+
order: { priority: "ASC NULLS LAST", scheduled_at: :asc },
|
|
11
|
+
where: "finished_at IS NULL AND locked_by_id IS NULL",
|
|
12
|
+
name: :index_good_jobs_on_priority_scheduled_at_unfinished_unlocked,
|
|
13
|
+
algorithm: :concurrently
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
unless connection.index_name_exists?(:good_jobs, :index_good_jobs_on_locked_by_id)
|
|
17
|
+
add_index :good_jobs, :locked_by_id,
|
|
18
|
+
where: "locked_by_id IS NOT NULL",
|
|
19
|
+
name: :index_good_jobs_on_locked_by_id,
|
|
20
|
+
algorithm: :concurrently
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
unless connection.index_name_exists?(:good_job_executions, :index_good_job_executions_on_process_id_and_created_at)
|
|
24
|
+
add_index :good_job_executions, [:process_id, :created_at],
|
|
25
|
+
name: :index_good_job_executions_on_process_id_and_created_at,
|
|
26
|
+
algorithm: :concurrently
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
dir.down do
|
|
31
|
+
remove_index(:good_jobs, name: :index_good_jobs_on_priority_scheduled_at_unfinished_unlocked) if connection.index_name_exists?(:good_jobs, :index_good_jobs_on_priority_scheduled_at_unfinished_unlocked)
|
|
32
|
+
remove_index(:good_jobs, name: :index_good_jobs_on_locked_by_id) if connection.index_name_exists?(:good_jobs, :index_good_jobs_on_locked_by_id)
|
|
33
|
+
remove_index(:good_job_executions, name: :index_good_job_executions_on_process_id_and_created_at) if connection.index_name_exists?(:good_job_executions, :index_good_job_executions_on_process_id_and_created_at)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
data/lib/good_job/adapter.rb
CHANGED
|
@@ -97,16 +97,17 @@ module GoodJob
|
|
|
97
97
|
end
|
|
98
98
|
end
|
|
99
99
|
|
|
100
|
+
@capsule.tracker.register
|
|
100
101
|
begin
|
|
101
102
|
until inline_executions.empty?
|
|
102
103
|
begin
|
|
103
104
|
inline_execution = inline_executions.shift
|
|
104
|
-
inline_result = inline_execution.perform
|
|
105
|
+
inline_result = inline_execution.perform(lock_id: @capsule.tracker.id_for_lock)
|
|
105
106
|
|
|
106
107
|
retried_execution = inline_result.retried
|
|
107
108
|
while retried_execution && retried_execution.scheduled_at <= Time.current
|
|
108
109
|
inline_execution = retried_execution
|
|
109
|
-
inline_result = inline_execution.perform
|
|
110
|
+
inline_result = inline_execution.perform(lock_id: @capsule.tracker.id_for_lock)
|
|
110
111
|
retried_execution = inline_result.retried
|
|
111
112
|
end
|
|
112
113
|
ensure
|
|
@@ -116,6 +117,7 @@ module GoodJob
|
|
|
116
117
|
raise inline_result.unhandled_error if inline_result.unhandled_error
|
|
117
118
|
end
|
|
118
119
|
ensure
|
|
120
|
+
@capsule.tracker.unregister
|
|
119
121
|
inline_executions.each(&:advisory_unlock)
|
|
120
122
|
end
|
|
121
123
|
|
|
@@ -168,12 +170,12 @@ module GoodJob
|
|
|
168
170
|
create_with_advisory_lock: true
|
|
169
171
|
)
|
|
170
172
|
begin
|
|
171
|
-
result = execution.perform
|
|
173
|
+
result = @capsule.tracker.register { execution.perform(lock_id: @capsule.tracker.id_for_lock) }
|
|
172
174
|
|
|
173
175
|
retried_execution = result.retried
|
|
174
176
|
while retried_execution && (retried_execution.scheduled_at.nil? || retried_execution.scheduled_at <= Time.current)
|
|
175
177
|
execution = retried_execution
|
|
176
|
-
result = execution.perform
|
|
178
|
+
result = @capsule.tracker.register { execution.perform(lock_id: @capsule.tracker.id_for_lock) }
|
|
177
179
|
retried_execution = result.retried
|
|
178
180
|
end
|
|
179
181
|
|
data/lib/good_job/capsule.rb
CHANGED
|
@@ -11,6 +11,10 @@ module GoodJob
|
|
|
11
11
|
# @return [Array<GoodJob::Capsule>, nil]
|
|
12
12
|
cattr_reader :instances, default: Concurrent::Array.new, instance_reader: false
|
|
13
13
|
|
|
14
|
+
delegate :register, :renew, :unregister, :id_for_lock, to: :@tracker, prefix: :_tracker
|
|
15
|
+
|
|
16
|
+
attr_reader :tracker
|
|
17
|
+
|
|
14
18
|
# @param configuration [GoodJob::Configuration] Configuration to use for this capsule.
|
|
15
19
|
def initialize(configuration: nil)
|
|
16
20
|
@configuration = configuration
|
|
@@ -18,6 +22,9 @@ module GoodJob
|
|
|
18
22
|
@started_at = nil
|
|
19
23
|
@mutex = Mutex.new
|
|
20
24
|
|
|
25
|
+
@shared_executor = GoodJob::SharedExecutor.new
|
|
26
|
+
@tracker = GoodJob::CapsuleTracker.new(executor: @shared_executor)
|
|
27
|
+
|
|
21
28
|
self.class.instances << self
|
|
22
29
|
end
|
|
23
30
|
|
|
@@ -29,15 +36,13 @@ module GoodJob
|
|
|
29
36
|
@mutex.synchronize do
|
|
30
37
|
return unless startable?(force: force)
|
|
31
38
|
|
|
32
|
-
@
|
|
33
|
-
@notifier = GoodJob::Notifier.new(enable_listening: configuration.enable_listen_notify, executor: @shared_executor.executor)
|
|
39
|
+
@notifier = GoodJob::Notifier.new(enable_listening: configuration.enable_listen_notify, capsule: self, executor: @shared_executor)
|
|
34
40
|
@poller = GoodJob::Poller.new(poll_interval: configuration.poll_interval)
|
|
35
|
-
@multi_scheduler = GoodJob::MultiScheduler.from_configuration(configuration, warm_cache_on_initialize: true)
|
|
41
|
+
@multi_scheduler = GoodJob::MultiScheduler.from_configuration(configuration, capsule: self, warm_cache_on_initialize: true)
|
|
36
42
|
@notifier.recipients.push([@multi_scheduler, :create_thread])
|
|
37
43
|
@poller.recipients.push(-> { @multi_scheduler.create_thread({ fanout: true }) })
|
|
38
44
|
|
|
39
|
-
@cron_manager = GoodJob::CronManager.new(configuration.cron_entries, start_on_initialize: true, executor: @shared_executor
|
|
40
|
-
|
|
45
|
+
@cron_manager = GoodJob::CronManager.new(configuration.cron_entries, start_on_initialize: true, executor: @shared_executor) if configuration.enable_cron?
|
|
41
46
|
@startable = false
|
|
42
47
|
@started_at = Time.current
|
|
43
48
|
end
|
|
@@ -52,7 +57,7 @@ module GoodJob
|
|
|
52
57
|
# @return [void]
|
|
53
58
|
def shutdown(timeout: NONE)
|
|
54
59
|
timeout = configuration.shutdown_timeout if timeout == NONE
|
|
55
|
-
GoodJob._shutdown_all([@
|
|
60
|
+
GoodJob._shutdown_all([@notifier, @poller, @multi_scheduler, @cron_manager].compact, after: [@shared_executor], timeout: timeout)
|
|
56
61
|
@startable = false
|
|
57
62
|
@started_at = nil
|
|
58
63
|
end
|
|
@@ -74,7 +79,7 @@ module GoodJob
|
|
|
74
79
|
|
|
75
80
|
# @return [Boolean] Whether the capsule has been shutdown.
|
|
76
81
|
def shutdown?
|
|
77
|
-
[@
|
|
82
|
+
[@notifier, @poller, @multi_scheduler, @cron_manager].compact.all?(&:shutdown?)
|
|
78
83
|
end
|
|
79
84
|
|
|
80
85
|
# @param duration [nil, Numeric] Length of idleness to check for (in seconds).
|
|
@@ -99,6 +104,12 @@ module GoodJob
|
|
|
99
104
|
@multi_scheduler&.create_thread(job_state)
|
|
100
105
|
end
|
|
101
106
|
|
|
107
|
+
# UUID for this capsule; to be used for inspection (not directly for locking jobs).
|
|
108
|
+
# @return [String]
|
|
109
|
+
def process_id
|
|
110
|
+
@tracker.process_id
|
|
111
|
+
end
|
|
112
|
+
|
|
102
113
|
private
|
|
103
114
|
|
|
104
115
|
def configuration
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module GoodJob # :nodoc:
|
|
4
|
+
# CapsuleTracker save a record in the database and periodically refreshes it. The intention is to
|
|
5
|
+
# create a heartbeat that can be used to determine whether a capsule/process is still active
|
|
6
|
+
# and use that to lock (or unlock) jobs.
|
|
7
|
+
class CapsuleTracker
|
|
8
|
+
# The database record used for tracking.
|
|
9
|
+
# @return [GoodJob::Process, nil]
|
|
10
|
+
attr_reader :record
|
|
11
|
+
|
|
12
|
+
# Number of tracked job executions.
|
|
13
|
+
attr_reader :locks
|
|
14
|
+
|
|
15
|
+
# Number of tracked job executions with advisory locks.
|
|
16
|
+
# @return [Integer]
|
|
17
|
+
attr_reader :advisory_locks
|
|
18
|
+
|
|
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
|
|
24
|
+
|
|
25
|
+
# @param executor [Concurrent::AbstractExecutorService] The executor to use for refreshing the process record.
|
|
26
|
+
def initialize(executor: Concurrent.global_io_executor)
|
|
27
|
+
@executor = executor
|
|
28
|
+
@mutex = Mutex.new
|
|
29
|
+
@locks = 0
|
|
30
|
+
@advisory_locked_connection = nil
|
|
31
|
+
@record_id = SecureRandom.uuid
|
|
32
|
+
@record = nil
|
|
33
|
+
@refresh_task = nil
|
|
34
|
+
|
|
35
|
+
# AS::ForkTracker is only present on Rails v6.1+.
|
|
36
|
+
# Fall back to PID checking if ForkTracker is not available
|
|
37
|
+
if defined?(ActiveSupport::ForkTracker)
|
|
38
|
+
ActiveSupport::ForkTracker.after_fork { reset }
|
|
39
|
+
@forktracker = true
|
|
40
|
+
else
|
|
41
|
+
@ruby_pid = ::Process.pid
|
|
42
|
+
@forktracker = false
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
self.class.instances << self
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# The UUID to use for locking. May be nil if the process is not registered or is unusable/expired.
|
|
49
|
+
# If UUID has not yet been persisted to the database, this method will make a query to insert or update it.
|
|
50
|
+
# @return [String, nil]
|
|
51
|
+
def id_for_lock
|
|
52
|
+
value = nil
|
|
53
|
+
synchronize do
|
|
54
|
+
next if @locks.zero?
|
|
55
|
+
|
|
56
|
+
reset_on_fork
|
|
57
|
+
if @record
|
|
58
|
+
@record.refresh_if_stale
|
|
59
|
+
else
|
|
60
|
+
@record = GoodJob::Process.create_record(id: @record_id)
|
|
61
|
+
create_refresh_task
|
|
62
|
+
end
|
|
63
|
+
value = @record&.id
|
|
64
|
+
end
|
|
65
|
+
value
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# The expected UUID of the process for use in inspection.
|
|
69
|
+
# Use {#id_for_lock} if using this as a lock key.
|
|
70
|
+
# @return [String]
|
|
71
|
+
def process_id
|
|
72
|
+
@record_id
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Registers the current process around a job execution site.
|
|
76
|
+
# +register+ is expected to be called multiple times in a process, but should be advisory locked only once (in a single thread).
|
|
77
|
+
# @param with_advisory_lock [Boolean] Whether the lock strategy should us an advisory lock; the connection must be retained to support advisory locks.
|
|
78
|
+
# @yield [void] If a block is given, the process will be unregistered after the block completes.
|
|
79
|
+
# @return [void]
|
|
80
|
+
def register(with_advisory_lock: false)
|
|
81
|
+
synchronize do
|
|
82
|
+
if with_advisory_lock
|
|
83
|
+
if @record
|
|
84
|
+
if !advisory_locked? || !advisory_locked_connection?
|
|
85
|
+
@record.class.transaction do
|
|
86
|
+
@record.advisory_lock!
|
|
87
|
+
@record.update(lock_type: GoodJob::Process::LOCK_TYPE_ADVISORY)
|
|
88
|
+
end
|
|
89
|
+
@advisory_locked_connection = WeakRef.new(@record.class.connection)
|
|
90
|
+
end
|
|
91
|
+
else
|
|
92
|
+
@record = GoodJob::Process.create_record(id: @record_id, with_advisory_lock: true)
|
|
93
|
+
@advisory_locked_connection = WeakRef.new(@record.class.connection)
|
|
94
|
+
create_refresh_task
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
@locks += 1
|
|
99
|
+
end
|
|
100
|
+
return unless block_given?
|
|
101
|
+
|
|
102
|
+
begin
|
|
103
|
+
yield
|
|
104
|
+
ensure
|
|
105
|
+
unregister(with_advisory_lock: with_advisory_lock)
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Unregisters the current process from the database.
|
|
110
|
+
# @param with_advisory_lock [Boolean] Whether the lock strategy should unlock an advisory lock; the connection must be able to support advisory locks.
|
|
111
|
+
# @return [void]
|
|
112
|
+
def unregister(with_advisory_lock: false)
|
|
113
|
+
synchronize do
|
|
114
|
+
if @locks.zero?
|
|
115
|
+
return
|
|
116
|
+
elsif @locks == 1
|
|
117
|
+
if @record
|
|
118
|
+
if with_advisory_lock && advisory_locked? && advisory_locked_connection?
|
|
119
|
+
@record.class.transaction do
|
|
120
|
+
@record.advisory_unlock
|
|
121
|
+
@record.destroy
|
|
122
|
+
end
|
|
123
|
+
@advisory_locked_connection = nil
|
|
124
|
+
else
|
|
125
|
+
@record.destroy
|
|
126
|
+
end
|
|
127
|
+
@record = nil
|
|
128
|
+
end
|
|
129
|
+
cancel_refresh_task
|
|
130
|
+
elsif with_advisory_lock && advisory_locked? && advisory_locked_connection?
|
|
131
|
+
@record.class.transaction do
|
|
132
|
+
@record.advisory_unlock
|
|
133
|
+
@record.update(lock_type: nil)
|
|
134
|
+
end
|
|
135
|
+
@advisory_locked_connection = nil
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
@locks -= 1 unless @locks.zero?
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Refreshes the process record in the database.
|
|
143
|
+
# @param silent [Boolean] Whether to silence logging.
|
|
144
|
+
# @return [void]
|
|
145
|
+
def renew(silent: false)
|
|
146
|
+
GoodJob::Process.with_logger_silenced(silent: silent) do
|
|
147
|
+
@record&.refresh_if_stale(cleanup: true)
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Tests whether an active advisory lock has been taken on the record.
|
|
152
|
+
# @return [Boolean]
|
|
153
|
+
def advisory_locked?
|
|
154
|
+
@advisory_locked_connection&.weakref_alive? && @advisory_locked_connection&.active?
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# @!visibility private
|
|
158
|
+
def task_observer(_time, _output, thread_error)
|
|
159
|
+
GoodJob._on_thread_error(thread_error) if thread_error && !thread_error.is_a?(Concurrent::CancelledOperationError)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
private
|
|
163
|
+
|
|
164
|
+
def advisory_locked_connection?
|
|
165
|
+
@record&.class&.connection && @advisory_locked_connection&.weakref_alive? && @advisory_locked_connection.eql?(@record.class.connection)
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def task_interval
|
|
169
|
+
GoodJob::Process::STALE_INTERVAL + jitter
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def jitter
|
|
173
|
+
GoodJob::Process::STALE_INTERVAL * 0.1 * Kernel.rand
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def create_refresh_task(delay: nil)
|
|
177
|
+
return if @refresh_task
|
|
178
|
+
return unless @executor
|
|
179
|
+
|
|
180
|
+
delay ||= task_interval
|
|
181
|
+
@refresh_task = Concurrent::ScheduledTask.new(delay.to_f, executor: @executor) do
|
|
182
|
+
Rails.application.executor.wrap do
|
|
183
|
+
synchronize do
|
|
184
|
+
next unless @locks.positive?
|
|
185
|
+
|
|
186
|
+
@refresh_task = nil
|
|
187
|
+
create_refresh_task
|
|
188
|
+
renew(silent: true)
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
@refresh_task.add_observer(self, :task_observer)
|
|
193
|
+
@refresh_task.execute
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def cancel_refresh_task
|
|
197
|
+
@refresh_task&.cancel
|
|
198
|
+
@refresh_task = nil
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def reset
|
|
202
|
+
synchronize { ns_reset }
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def reset_on_fork
|
|
206
|
+
return if Concurrent.on_jruby?
|
|
207
|
+
return if @forktracker || ::Process.pid == @ruby_pid
|
|
208
|
+
|
|
209
|
+
@ruby_pid = ::Process.pid
|
|
210
|
+
ns_reset
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def ns_reset
|
|
214
|
+
@record_id = SecureRandom.uuid
|
|
215
|
+
@record = nil
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# Synchronize must always be called from within a Rails Executor; it may deadlock if the order is reversed.
|
|
219
|
+
def synchronize(&block)
|
|
220
|
+
if @mutex.owned?
|
|
221
|
+
yield
|
|
222
|
+
else
|
|
223
|
+
@mutex.synchronize(&block)
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
end
|
|
@@ -14,8 +14,9 @@ module GoodJob
|
|
|
14
14
|
cattr_accessor :performing_active_job_ids, default: Concurrent::Set.new
|
|
15
15
|
|
|
16
16
|
# @param queue_string [String] Queues to execute jobs from
|
|
17
|
-
def initialize(queue_string)
|
|
17
|
+
def initialize(queue_string, capsule: GoodJob.capsule)
|
|
18
18
|
@queue_string = queue_string
|
|
19
|
+
@capsule = capsule
|
|
19
20
|
@metrics = Metrics.new
|
|
20
21
|
end
|
|
21
22
|
|
|
@@ -30,20 +31,22 @@ module GoodJob
|
|
|
30
31
|
# @return [Object, nil] Returns job result or +nil+ if no job was found
|
|
31
32
|
def next
|
|
32
33
|
active_job_id = nil
|
|
33
|
-
|
|
34
|
-
@
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
result
|
|
34
|
+
@capsule.tracker.register do
|
|
35
|
+
job_query.perform_with_advisory_lock(lock_id: @capsule.tracker.id_for_lock, parsed_queues: parsed_queues, queue_select_limit: GoodJob.configuration.queue_select_limit) do |execution|
|
|
36
|
+
@metrics.touch_check_queue_at
|
|
37
|
+
|
|
38
|
+
if execution
|
|
39
|
+
active_job_id = execution.active_job_id
|
|
40
|
+
performing_active_job_ids << active_job_id
|
|
41
|
+
@metrics.touch_execution_at
|
|
42
|
+
yield(execution) if block_given?
|
|
43
|
+
else
|
|
44
|
+
@metrics.increment_empty_executions
|
|
45
|
+
end
|
|
46
|
+
end.tap do |result|
|
|
47
|
+
if result
|
|
48
|
+
result.succeeded? ? @metrics.increment_succeeded_executions : @metrics.increment_errored_executions
|
|
49
|
+
end
|
|
47
50
|
end
|
|
48
51
|
end
|
|
49
52
|
ensure
|
|
@@ -7,12 +7,12 @@ module GoodJob
|
|
|
7
7
|
# @param configuration [GoodJob::Configuration]
|
|
8
8
|
# @param warm_cache_on_initialize [Boolean]
|
|
9
9
|
# @return [GoodJob::MultiScheduler]
|
|
10
|
-
def self.from_configuration(configuration, warm_cache_on_initialize: false)
|
|
10
|
+
def self.from_configuration(configuration, capsule: GoodJob.capsule, warm_cache_on_initialize: false)
|
|
11
11
|
schedulers = configuration.queue_string.split(';').map(&:strip).map do |queue_string_and_max_threads|
|
|
12
12
|
queue_string, max_threads = queue_string_and_max_threads.split(':').map { |str| str.strip.presence }
|
|
13
13
|
max_threads = (max_threads || configuration.max_threads).to_i
|
|
14
14
|
|
|
15
|
-
job_performer = GoodJob::JobPerformer.new(queue_string)
|
|
15
|
+
job_performer = GoodJob::JobPerformer.new(queue_string, capsule: capsule)
|
|
16
16
|
GoodJob::Scheduler.new(
|
|
17
17
|
job_performer,
|
|
18
18
|
max_threads: max_threads,
|
|
@@ -16,7 +16,7 @@ module GoodJob # :nodoc:
|
|
|
16
16
|
def register_process
|
|
17
17
|
GoodJob::Process.override_connection(connection) do
|
|
18
18
|
GoodJob::Process.cleanup
|
|
19
|
-
@
|
|
19
|
+
@capsule.tracker.register(with_advisory_lock: true)
|
|
20
20
|
end
|
|
21
21
|
end
|
|
22
22
|
|
|
@@ -24,7 +24,7 @@ module GoodJob # :nodoc:
|
|
|
24
24
|
Rails.application.executor.wrap do
|
|
25
25
|
GoodJob::Process.override_connection(connection) do
|
|
26
26
|
GoodJob::Process.with_logger_silenced do
|
|
27
|
-
@
|
|
27
|
+
@capsule.tracker.renew
|
|
28
28
|
end
|
|
29
29
|
end
|
|
30
30
|
end
|
|
@@ -33,7 +33,7 @@ module GoodJob # :nodoc:
|
|
|
33
33
|
# Deregisters the current process.
|
|
34
34
|
def deregister_process
|
|
35
35
|
GoodJob::Process.override_connection(connection) do
|
|
36
|
-
@
|
|
36
|
+
@capsule.tracker.unregister(with_advisory_lock: true)
|
|
37
37
|
end
|
|
38
38
|
end
|
|
39
39
|
end
|
data/lib/good_job/notifier.rb
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require 'active_support/core_ext/module/attribute_accessors_per_thread'
|
|
4
4
|
require 'concurrent/atomic/atomic_boolean'
|
|
5
|
+
require "concurrent/scheduled_task"
|
|
5
6
|
require "good_job/notifier/process_heartbeat"
|
|
6
7
|
|
|
7
8
|
module GoodJob # :nodoc:
|
|
@@ -62,13 +63,12 @@ module GoodJob # :nodoc:
|
|
|
62
63
|
|
|
63
64
|
# @param recipients [Array<#call, Array(Object, Symbol)>]
|
|
64
65
|
# @param enable_listening [true, false]
|
|
65
|
-
|
|
66
|
-
def initialize(*recipients, enable_listening: true, executor: Concurrent.global_io_executor)
|
|
66
|
+
def initialize(*recipients, enable_listening: true, capsule: GoodJob.capsule, executor: Concurrent.global_io_executor)
|
|
67
67
|
@recipients = Concurrent::Array.new(recipients)
|
|
68
68
|
@enable_listening = enable_listening
|
|
69
69
|
@executor = executor
|
|
70
70
|
|
|
71
|
-
@
|
|
71
|
+
@monitor = Monitor.new
|
|
72
72
|
@shutdown_event = Concurrent::Event.new.tap(&:set)
|
|
73
73
|
@running = Concurrent::AtomicBoolean.new(false)
|
|
74
74
|
@connected = Concurrent::Event.new
|
|
@@ -77,6 +77,7 @@ module GoodJob # :nodoc:
|
|
|
77
77
|
@connection_errors_reported = Concurrent::AtomicBoolean.new(false)
|
|
78
78
|
@enable_listening = enable_listening
|
|
79
79
|
@task = nil
|
|
80
|
+
@capsule = capsule
|
|
80
81
|
|
|
81
82
|
start
|
|
82
83
|
self.class.instances << self
|
|
@@ -183,6 +184,8 @@ module GoodJob # :nodoc:
|
|
|
183
184
|
|
|
184
185
|
private
|
|
185
186
|
|
|
187
|
+
delegate :synchronize, to: :@monitor
|
|
188
|
+
|
|
186
189
|
def start
|
|
187
190
|
synchronize do
|
|
188
191
|
return if @running.true?
|
|
@@ -211,20 +214,20 @@ module GoodJob # :nodoc:
|
|
|
211
214
|
end
|
|
212
215
|
|
|
213
216
|
while thr_executor.running? && thr_running.true?
|
|
214
|
-
run_callbacks
|
|
215
|
-
wait_for_notify do |channel, payload|
|
|
216
|
-
next unless channel == CHANNEL
|
|
217
|
-
|
|
218
|
-
ActiveSupport::Notifications.instrument("notifier_notified.good_job", { payload: payload })
|
|
219
|
-
parsed_payload = JSON.parse(payload, symbolize_names: true)
|
|
220
|
-
thr_recipients.each do |recipient|
|
|
221
|
-
target, method_name = recipient.is_a?(Array) ? recipient : [recipient, :call]
|
|
222
|
-
target.send(method_name, parsed_payload)
|
|
223
|
-
end
|
|
224
|
-
end
|
|
217
|
+
Rails.application.executor.wrap { run_callbacks(:tick) }
|
|
225
218
|
|
|
226
|
-
|
|
219
|
+
wait_for_notify do |channel, payload|
|
|
220
|
+
next unless channel == CHANNEL
|
|
221
|
+
|
|
222
|
+
ActiveSupport::Notifications.instrument("notifier_notified.good_job", { payload: payload })
|
|
223
|
+
parsed_payload = JSON.parse(payload, symbolize_names: true)
|
|
224
|
+
thr_recipients.each do |recipient|
|
|
225
|
+
target, method_name = recipient.is_a?(Array) ? recipient : [recipient, :call]
|
|
226
|
+
target.send(method_name, parsed_payload)
|
|
227
|
+
end
|
|
227
228
|
end
|
|
229
|
+
|
|
230
|
+
reset_connection_errors
|
|
228
231
|
end
|
|
229
232
|
end
|
|
230
233
|
ensure
|
|
@@ -284,13 +287,5 @@ module GoodJob # :nodoc:
|
|
|
284
287
|
@connection_errors_count.value = 0
|
|
285
288
|
@connection_errors_reported.make_false
|
|
286
289
|
end
|
|
287
|
-
|
|
288
|
-
def synchronize(&block)
|
|
289
|
-
if @mutex.owned?
|
|
290
|
-
yield
|
|
291
|
-
else
|
|
292
|
-
@mutex.synchronize(&block)
|
|
293
|
-
end
|
|
294
|
-
end
|
|
295
290
|
end
|
|
296
291
|
end
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'concurrent/executor/executor_service'
|
|
4
|
+
|
|
3
5
|
module GoodJob
|
|
4
6
|
class SharedExecutor
|
|
7
|
+
# Allow posting tasks directly to instances
|
|
8
|
+
include Concurrent::ExecutorService
|
|
9
|
+
|
|
5
10
|
MAX_THREADS = 2
|
|
6
11
|
|
|
7
12
|
# @!attribute [r] instances
|
|
@@ -13,8 +18,21 @@ module GoodJob
|
|
|
13
18
|
attr_reader :executor
|
|
14
19
|
|
|
15
20
|
def initialize
|
|
21
|
+
@mutex = Mutex.new
|
|
22
|
+
|
|
16
23
|
self.class.instances << self
|
|
17
|
-
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def post(*args, &task)
|
|
27
|
+
unless running?
|
|
28
|
+
@mutex.synchronize do
|
|
29
|
+
next if running?
|
|
30
|
+
|
|
31
|
+
create_executor
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
@executor&.post(*args, &task)
|
|
18
36
|
end
|
|
19
37
|
|
|
20
38
|
def running?
|
data/lib/good_job/version.rb
CHANGED
data/lib/good_job.rb
CHANGED
|
@@ -19,6 +19,7 @@ require_relative "good_job/overridable_connection"
|
|
|
19
19
|
require_relative "good_job/bulk"
|
|
20
20
|
require_relative "good_job/callable"
|
|
21
21
|
require_relative "good_job/capsule"
|
|
22
|
+
require_relative "good_job/capsule_tracker"
|
|
22
23
|
require_relative "good_job/cleanup_tracker"
|
|
23
24
|
require_relative "good_job/cli"
|
|
24
25
|
require_relative "good_job/configuration"
|
|
@@ -169,11 +170,12 @@ module GoodJob
|
|
|
169
170
|
end
|
|
170
171
|
|
|
171
172
|
# Sends +#shutdown+ or +#restart+ to executable objects ({GoodJob::Notifier}, {GoodJob::Poller}, {GoodJob::Scheduler}, {GoodJob::MultiScheduler}, {GoodJob::CronManager})
|
|
172
|
-
# @param executables [Array<Notifier, Poller, Scheduler, MultiScheduler, CronManager>] Objects to shut down.
|
|
173
|
+
# @param executables [Array<Notifier, Poller, Scheduler, MultiScheduler, CronManager, SharedExecutor>] Objects to shut down.
|
|
173
174
|
# @param method_name [:symbol] Method to call, e.g. +:shutdown+ or +:restart+.
|
|
174
175
|
# @param timeout [nil,Numeric]
|
|
176
|
+
# @param after [Array<Notifier, Poller, Scheduler, MultiScheduler, CronManager, SharedExecutor>] Objects to shut down after initial executables shut down.
|
|
175
177
|
# @return [void]
|
|
176
|
-
def self._shutdown_all(executables, method_name = :shutdown, timeout: -1)
|
|
178
|
+
def self._shutdown_all(executables, method_name = :shutdown, timeout: -1, after: [])
|
|
177
179
|
if timeout.is_a?(Numeric) && timeout.positive?
|
|
178
180
|
executables.each { |executable| executable.send(method_name, timeout: nil) }
|
|
179
181
|
|
|
@@ -182,6 +184,13 @@ module GoodJob
|
|
|
182
184
|
else
|
|
183
185
|
executables.each { |executable| executable.send(method_name, timeout: timeout) }
|
|
184
186
|
end
|
|
187
|
+
return unless after.any? && !timeout.nil?
|
|
188
|
+
|
|
189
|
+
if stop_at
|
|
190
|
+
after.each { |executable| executable.shutdown(timeout: [stop_at - Time.current, 0].max) }
|
|
191
|
+
else
|
|
192
|
+
after.each { |executable| executable.shutdown(timeout: timeout) }
|
|
193
|
+
end
|
|
185
194
|
end
|
|
186
195
|
|
|
187
196
|
# Destroys preserved job and batch records.
|
|
@@ -277,8 +286,8 @@ module GoodJob
|
|
|
277
286
|
# @return [Boolean]
|
|
278
287
|
def self.migrated?
|
|
279
288
|
# Always update with the most recent migration check
|
|
280
|
-
GoodJob::
|
|
281
|
-
GoodJob::
|
|
289
|
+
GoodJob::Execution.reset_column_information
|
|
290
|
+
GoodJob::Execution.process_lock_migrated?
|
|
282
291
|
end
|
|
283
292
|
|
|
284
293
|
ActiveSupport.run_load_hooks(:good_job, self)
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: good_job
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 3.
|
|
4
|
+
version: 3.29.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ben Sheldon
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2024-05-
|
|
11
|
+
date: 2024-05-22 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: activejob
|
|
@@ -360,6 +360,8 @@ files:
|
|
|
360
360
|
- lib/generators/good_job/templates/update/migrations/10_remove_good_job_active_id_index.rb.erb
|
|
361
361
|
- lib/generators/good_job/templates/update/migrations/11_create_index_good_job_jobs_for_candidate_lookup.rb.erb
|
|
362
362
|
- lib/generators/good_job/templates/update/migrations/12_create_good_job_execution_error_backtrace.rb.erb
|
|
363
|
+
- lib/generators/good_job/templates/update/migrations/13_create_good_job_process_lock_ids.rb.erb
|
|
364
|
+
- lib/generators/good_job/templates/update/migrations/14_create_good_job_process_lock_indexes.rb.erb
|
|
363
365
|
- lib/generators/good_job/update_generator.rb
|
|
364
366
|
- lib/good_job.rb
|
|
365
367
|
- lib/good_job/active_job_extensions/batches.rb
|
|
@@ -371,6 +373,7 @@ files:
|
|
|
371
373
|
- lib/good_job/bulk.rb
|
|
372
374
|
- lib/good_job/callable.rb
|
|
373
375
|
- lib/good_job/capsule.rb
|
|
376
|
+
- lib/good_job/capsule_tracker.rb
|
|
374
377
|
- lib/good_job/cleanup_tracker.rb
|
|
375
378
|
- lib/good_job/cli.rb
|
|
376
379
|
- lib/good_job/configuration.rb
|