good_job 3.28.2 → 3.29.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +28 -0
- data/README.md +1 -1
- data/app/models/good_job/base_execution.rb +7 -0
- data/app/models/good_job/execution.rb +34 -10
- 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 +4 -4
- 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 +12 -3
- metadata +6 -3
- /data/lib/generators/good_job/templates/update/migrations/{10_create_good_job_execution_error_backtrace.rb.erb → 12_create_good_job_execution_error_backtrace.rb.erb} +0 -0
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,33 @@
|
|
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
|
+
|
11
|
+
## [v3.28.3](https://github.com/bensheldon/good_job/tree/v3.28.3) (2024-05-18)
|
12
|
+
|
13
|
+
[Full Changelog](https://github.com/bensheldon/good_job/compare/v3.28.2...v3.28.3)
|
14
|
+
|
15
|
+
**Fixed bugs:**
|
16
|
+
|
17
|
+
- Strip more whitespace when parsing queues string [\#1352](https://github.com/bensheldon/good_job/pull/1352) ([bensheldon](https://github.com/bensheldon))
|
18
|
+
- Fix latest migration not affecting `GoodJob.migrated?` [\#1345](https://github.com/bensheldon/good_job/pull/1345) ([Earlopain](https://github.com/Earlopain))
|
19
|
+
|
20
|
+
**Closed issues:**
|
21
|
+
|
22
|
+
- Whitespace in `queues` configuration can cause issues. [\#1351](https://github.com/bensheldon/good_job/issues/1351)
|
23
|
+
- How to properly handle interrupts [\#1343](https://github.com/bensheldon/good_job/issues/1343)
|
24
|
+
- ActiveSupport::CurrentAttributes Compatibility [\#1341](https://github.com/bensheldon/good_job/issues/1341)
|
25
|
+
|
26
|
+
**Merged pull requests:**
|
27
|
+
|
28
|
+
- Don't abort CI jobs when a single one fails [\#1346](https://github.com/bensheldon/good_job/pull/1346) ([Earlopain](https://github.com/Earlopain))
|
29
|
+
- Clarify PgBouncer Compatibility [\#1338](https://github.com/bensheldon/good_job/pull/1338) ([isaac](https://github.com/isaac))
|
30
|
+
|
3
31
|
## [v3.28.2](https://github.com/bensheldon/good_job/tree/v3.28.2) (2024-04-26)
|
4
32
|
|
5
33
|
[Full Changelog](https://github.com/bensheldon/good_job/compare/v3.28.1...v3.28.2)
|
data/README.md
CHANGED
@@ -1383,7 +1383,7 @@ _Note: Rails `travel`/`travel_to` time helpers do not have millisecond precision
|
|
1383
1383
|
|
1384
1384
|
GoodJob is not compatible with PgBouncer in _transaction_ mode, but is compatible with PgBouncer's _connection_ mode. GoodJob uses connection-based advisory locks and LISTEN/NOTIFY, both of which require full database connections.
|
1385
1385
|
|
1386
|
-
|
1386
|
+
If you want to use PgBouncer with the rest of your Rails app you can workaround this limitation by making a direct database connection available to GoodJob. With Rails 6.0's support for [multiple databases](https://guides.rubyonrails.org/active_record_multiple_databases.html), a direct connection to the database can be configured by following the three steps below.
|
1387
1387
|
|
1388
1388
|
1. Define a direct connection to your database that is not proxied through PgBouncer, for example:
|
1389
1389
|
|
@@ -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
|
@@ -40,7 +40,7 @@ module GoodJob
|
|
40
40
|
# GoodJob::Execution.queue_parser('-queue1,queue2')
|
41
41
|
# => { exclude: [ 'queue1', 'queue2' ] }
|
42
42
|
def self.queue_parser(string)
|
43
|
-
string = string.presence || '*'
|
43
|
+
string = string.strip.presence || '*'
|
44
44
|
|
45
45
|
case string.first
|
46
46
|
when '-'
|
@@ -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)
|
11
|
-
schedulers = configuration.queue_string.split(';').map do |queue_string_and_max_threads|
|
12
|
-
queue_string, max_threads = queue_string_and_max_threads.split(':')
|
10
|
+
def self.from_configuration(configuration, capsule: GoodJob.capsule, warm_cache_on_initialize: false)
|
11
|
+
schedulers = configuration.queue_string.split(';').map(&:strip).map do |queue_string_and_max_threads|
|
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.
|
@@ -278,7 +287,7 @@ module GoodJob
|
|
278
287
|
def self.migrated?
|
279
288
|
# Always update with the most recent migration check
|
280
289
|
GoodJob::Execution.reset_column_information
|
281
|
-
GoodJob::Execution.
|
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-
|
11
|
+
date: 2024-05-22 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activejob
|
@@ -357,9 +357,11 @@ files:
|
|
357
357
|
- lib/generators/good_job/templates/update/migrations/07_recreate_good_job_cron_indexes_with_conditional.rb.erb
|
358
358
|
- lib/generators/good_job/templates/update/migrations/08_create_good_job_labels.rb.erb
|
359
359
|
- lib/generators/good_job/templates/update/migrations/09_create_good_job_labels_index.rb.erb
|
360
|
-
- lib/generators/good_job/templates/update/migrations/10_create_good_job_execution_error_backtrace.rb.erb
|
361
360
|
- lib/generators/good_job/templates/update/migrations/10_remove_good_job_active_id_index.rb.erb
|
362
361
|
- lib/generators/good_job/templates/update/migrations/11_create_index_good_job_jobs_for_candidate_lookup.rb.erb
|
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
|