good_job 2.99.0 → 3.0.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 +15 -0
- data/README.md +27 -25
- data/app/charts/good_job/scheduled_by_queue_chart.rb +1 -1
- data/app/controllers/good_job/jobs_controller.rb +11 -11
- data/app/controllers/good_job/processes_controller.rb +1 -1
- data/app/filters/good_job/jobs_filter.rb +2 -2
- data/app/views/good_job/processes/index.html.erb +1 -9
- data/app/views/layouts/good_job/application.html.erb +1 -1
- data/lib/generators/good_job/templates/update/migrations/01_create_good_jobs.rb.erb +9 -0
- data/lib/good_job/adapter.rb +6 -49
- data/lib/good_job/configuration.rb +4 -4
- data/lib/good_job/current_thread.rb +2 -2
- data/lib/good_job/notifier/process_registration.rb +0 -4
- data/lib/good_job/version.rb +1 -1
- data/lib/good_job.rb +6 -8
- data/lib/models/good_job/active_job_job.rb +6 -219
- data/lib/models/good_job/cron_entry.rb +3 -3
- data/lib/models/good_job/execution.rb +3 -11
- data/lib/models/good_job/job.rb +219 -6
- data/lib/models/good_job/process.rb +0 -9
- metadata +7 -11
- data/lib/generators/good_job/templates/update/migrations/02_add_cron_at_to_good_jobs.rb.erb +0 -14
- data/lib/generators/good_job/templates/update/migrations/03_add_cron_key_cron_at_index_to_good_jobs.rb.erb +0 -20
- data/lib/generators/good_job/templates/update/migrations/04_create_good_job_processes.rb.erb +0 -17
- data/lib/generators/good_job/templates/update/migrations/04_index_good_job_jobs_on_finished_at.rb.erb +0 -25
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a4f4e14bae83760bdeb35557a727116d053e47a9b7dbf501914606dbc45c2ef7
|
4
|
+
data.tar.gz: c06e9165b7ac5d87af0ab75291fedd0194018752f34332c1a16930ddca53e3dd
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 949cfcf1506ff658666113e035a875ed4ab44fa3e2c674e4d5d1440bf484d39793a1ee595ae0cb6f719136ce5541aae09a7fc2d66a049ea1bcf973b0165dc8a4
|
7
|
+
data.tar.gz: 9c9ff56251e01d60823a50f4edcea74034a36ef93929cf59adc588273da86d1e04f67c1b25d7d35efecda9309acf7f49f81d29fbc7f94249be0e3732087f9081
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,20 @@
|
|
1
1
|
# Changelog
|
2
2
|
|
3
|
+
## [v3.0.0](https://github.com/bensheldon/good_job/tree/v3.0.0) (2022-06-26)
|
4
|
+
|
5
|
+
[Full Changelog](https://github.com/bensheldon/good_job/compare/v2.99.0...v3.0.0)
|
6
|
+
|
7
|
+
**Implemented enhancements:**
|
8
|
+
|
9
|
+
- By default, preserve job records and automatically them clean up [\#545](https://github.com/bensheldon/good_job/pull/545) ([bensheldon](https://github.com/bensheldon))
|
10
|
+
|
11
|
+
**Merged pull requests:**
|
12
|
+
|
13
|
+
- Update tests to reflect default of `GoodJob.preserve_job_records = true`; update appraisal Gemfiles too [\#643](https://github.com/bensheldon/good_job/pull/643) ([bensheldon](https://github.com/bensheldon))
|
14
|
+
- Remove database migration shims and old migrations [\#642](https://github.com/bensheldon/good_job/pull/642) ([bensheldon](https://github.com/bensheldon))
|
15
|
+
- Remove support for EOL Rails 5.2 [\#637](https://github.com/bensheldon/good_job/pull/637) ([bensheldon](https://github.com/bensheldon))
|
16
|
+
- Remove/rename deprecated behavior and constants for GoodJob v3 [\#633](https://github.com/bensheldon/good_job/pull/633) ([bensheldon](https://github.com/bensheldon))
|
17
|
+
|
3
18
|
## [v2.99.0](https://github.com/bensheldon/good_job/tree/v2.99.0) (2022-06-26)
|
4
19
|
|
5
20
|
[Full Changelog](https://github.com/bensheldon/good_job/compare/v2.17.1...v2.99.0)
|
data/README.md
CHANGED
@@ -144,9 +144,9 @@ For more of the story of GoodJob, read the [introductory blog post](https://isla
|
|
144
144
|
|
145
145
|
## Compatibility
|
146
146
|
|
147
|
-
- **Ruby on Rails:**
|
148
|
-
- **Ruby:**
|
149
|
-
- **Postgres:**
|
147
|
+
- **Ruby on Rails:** 6.0+
|
148
|
+
- **Ruby:** Ruby 2.5+. JRuby 9.2.13+
|
149
|
+
- **Postgres:** 10.0+
|
150
150
|
|
151
151
|
## Configuration
|
152
152
|
|
@@ -280,7 +280,7 @@ Available configuration options are:
|
|
280
280
|
- `cleanup_interval_seconds` (integer) Number of seconds a Scheduler will wait before cleaning up preserved jobs. Defaults to `nil`. Can also be set with the environment variable `GOOD_JOB_CLEANUP_INTERVAL_SECONDS`.
|
281
281
|
- `inline_execution_respects_schedule` (boolean) Opt-in to future behavior of inline execution respecting scheduled jobs. Defaults to `false`.
|
282
282
|
- `logger` ([Rails Logger](https://api.rubyonrails.org/classes/ActiveSupport/Logger.html)) lets you set a custom logger for GoodJob. It should be an instance of a Rails `Logger` (Default: `Rails.logger`).
|
283
|
-
- `preserve_job_records` (boolean) keeps job records in your database even after jobs are completed. (Default: `
|
283
|
+
- `preserve_job_records` (boolean) keeps job records in your database even after jobs are completed. (Default: `true`)
|
284
284
|
- `retry_on_unhandled_error` (boolean) causes jobs to be re-queued and retried if they raise an instance of `StandardError`. Be advised this may lead to jobs being repeated infinitely ([see below for more on retries](#retries)). Instances of `Exception`, like SIGINT, will *always* be retried, regardless of this attribute’s value. (Default: `true`)
|
285
285
|
- `on_thread_error` (proc, lambda, or callable) will be called when an Exception. It can be useful for logging errors to bug tracking services, like Sentry or Airbrake. Example:
|
286
286
|
|
@@ -311,8 +311,8 @@ Good Job’s general behavior can also be configured via attributes directly on
|
|
311
311
|
|
312
312
|
- **`GoodJob.active_record_parent_class`** (string) The ActiveRecord parent class inherited by GoodJob's ActiveRecord model `GoodJob::Job` (defaults to `"ActiveRecord::Base"`). Configure this when using [multiple databases with ActiveRecord](https://guides.rubyonrails.org/active_record_multiple_databases.html) or when other custom configuration is necessary for the ActiveRecord model to connect to the Postgres database. _The value must be a String to avoid premature initialization of ActiveRecord._
|
313
313
|
- **`GoodJob.logger`** ([Rails Logger](https://api.rubyonrails.org/classes/ActiveSupport/Logger.html)) lets you set a custom logger for GoodJob. It should be an instance of a Rails `Logger`.
|
314
|
-
- **`GoodJob.preserve_job_records`** (boolean) keeps job records in your database even after jobs are completed. (Default: `
|
315
|
-
- **`GoodJob.retry_on_unhandled_error`** (boolean) causes jobs to be re-queued and retried if they raise an instance of `StandardError`. Be advised this may lead to jobs being repeated infinitely ([see below for more on retries](#retries)). Instances of `Exception`, like SIGINT, will *always* be retried, regardless of this attribute’s value. (Default: `
|
314
|
+
- **`GoodJob.preserve_job_records`** (boolean) keeps job records in your database even after jobs are completed. (Default: `true`)
|
315
|
+
- **`GoodJob.retry_on_unhandled_error`** (boolean) causes jobs to be re-queued and retried if they raise an instance of `StandardError`. Be advised this may lead to jobs being repeated infinitely ([see below for more on retries](#retries)). Instances of `Exception`, like SIGINT, will *always* be retried, regardless of this attribute’s value. (Default: `false`)
|
316
316
|
- **`GoodJob.on_thread_error`** (proc, lambda, or callable) will be called when an Exception. It can be useful for logging errors to bug tracking services, like Sentry or Airbrake.
|
317
317
|
|
318
318
|
You’ll generally want to configure these in `config/initializers/good_job.rb`, like so:
|
@@ -395,8 +395,6 @@ The Dashboard can be set to automatically refresh by checking "Live Poll" in the
|
|
395
395
|
|
396
396
|
GoodJob can extend ActiveJob to provide limits on concurrently running jobs, either at time of _enqueue_ or at _perform_. Limiting concurrency can help prevent duplicate, double or unecessary jobs from being enqueued, or race conditions when performing, for example when interacting with 3rd-party APIs.
|
397
397
|
|
398
|
-
**Note:** Limiting concurrency at _enqueue_ requires Rails 6.0+ because Rails 5.2 cannot halt ActiveJob callbacks.
|
399
|
-
|
400
398
|
```ruby
|
401
399
|
class MyJob < ApplicationJob
|
402
400
|
include GoodJob::ActiveJobExtensions::Concurrency
|
@@ -564,9 +562,9 @@ GoodJob.on_thread_error = -> (exception) { Raven.capture_exception(exception) }
|
|
564
562
|
|
565
563
|
#### Retries
|
566
564
|
|
567
|
-
By default, GoodJob
|
565
|
+
By default, GoodJob relies on ActiveJob's retry functionality.
|
568
566
|
|
569
|
-
|
567
|
+
ActiveJob can be configured to retry an infinite number of times, with an exponential backoff. Using ActiveJob's `retry_on` prevents exceptions from reaching GoodJob:
|
570
568
|
|
571
569
|
```ruby
|
572
570
|
class ApplicationJob < ActiveJob::Base
|
@@ -575,14 +573,7 @@ class ApplicationJob < ActiveJob::Base
|
|
575
573
|
end
|
576
574
|
```
|
577
575
|
|
578
|
-
When using `retry_on` with _a limited number of retries_, the final exception will not be rescued and will raise to GoodJob
|
579
|
-
|
580
|
-
```ruby
|
581
|
-
# config/initializers/good_job.rb
|
582
|
-
GoodJob.retry_on_unhandled_error = false
|
583
|
-
```
|
584
|
-
|
585
|
-
Alternatively, pass a block to `retry_on` to handle the final exception instead of raising it to GoodJob:
|
576
|
+
When using `retry_on` with _a limited number of retries_, the final exception will not be rescued and will raise to GoodJob's error handler. To avoid this, pass a block to `retry_on` to handle the final exception instead of raising it to GoodJob:
|
586
577
|
|
587
578
|
```ruby
|
588
579
|
class ApplicationJob < ActiveJob::Base
|
@@ -613,9 +604,11 @@ class ApplicationJob < ActiveJob::Base
|
|
613
604
|
end
|
614
605
|
```
|
615
606
|
|
607
|
+
By default, jobs will not be retried unless `retry_on` is configured. This can be overridden by setting `GoodJob.retry_on_unhandled_error` to `true`; GoodJob will then retry the failing job immediately and infinitely, potentially causing high load.
|
608
|
+
|
616
609
|
#### ActionMailer retries
|
617
610
|
|
618
|
-
Any configuration in `ApplicationJob` will have to be duplicated on `ActionMailer::MailDeliveryJob`
|
611
|
+
Any configuration in `ApplicationJob` will have to be duplicated on `ActionMailer::MailDeliveryJob` because ActionMailer uses that custom class which inherits from `ActiveJob::Base`, rather than your application's `ApplicationJob`.
|
619
612
|
|
620
613
|
You can use an initializer to configure `ActionMailer::MailDeliveryJob`, for example:
|
621
614
|
|
@@ -869,22 +862,31 @@ If your application is already using an ActiveJob backend, you will need to inst
|
|
869
862
|
|
870
863
|
GoodJob is fully instrumented with [`ActiveSupport::Notifications`](https://edgeguides.rubyonrails.org/active_support_instrumentation.html#introduction-to-instrumentation).
|
871
864
|
|
872
|
-
By default, GoodJob will
|
865
|
+
By default, GoodJob will preserve job records for 14 days after they are run, regardless of whether they succeed or not (raising a kind of `StandardError`), unless they are interrupted (raising a kind of `Exception`).
|
873
866
|
|
874
|
-
To preserve job records for later inspection, set an initializer:
|
867
|
+
To not preserve job records for later inspection, set an initializer:
|
875
868
|
|
876
869
|
```ruby
|
877
870
|
# config/initializers/good_job.rb
|
878
|
-
GoodJob.preserve_job_records = true
|
871
|
+
GoodJob.preserve_job_records = false # defaults to true, or `false` or `:on_unhandled_error`
|
872
|
+
```
|
873
|
+
|
874
|
+
GoodJob will automatically delete these job records after 14 days. The retention period, as well as the frequency GoodJob checks for deletable records can be configured:
|
875
|
+
|
876
|
+
```ruby
|
877
|
+
|
878
|
+
config.cleanup_preserved_jobs_before_seconds_ago = 14.days.to_i
|
879
|
+
config.cleanup_interval_jobs = 1_000 # Number of executed jobs between deletion sweeps.
|
880
|
+
config.cleanup_interval_seconds = 10.minutes.to_i # Number of seconds between deletion sweeps.
|
879
881
|
```
|
880
882
|
|
881
|
-
It is also
|
883
|
+
It is also possible to manually trigger a cleanup:
|
882
884
|
|
883
885
|
- For example, in a Rake task:
|
884
886
|
|
885
887
|
```ruby
|
886
|
-
GoodJob.cleanup_preserved_jobs # Will
|
887
|
-
GoodJob.cleanup_preserved_jobs(older_than: 7.days) #
|
888
|
+
GoodJob.cleanup_preserved_jobs # Will use default retention period
|
889
|
+
GoodJob.cleanup_preserved_jobs(older_than: 7.days) # custom retention period
|
888
890
|
```
|
889
891
|
|
890
892
|
- For example, using the `good_job` command-line utility:
|
@@ -9,7 +9,7 @@ module GoodJob
|
|
9
9
|
def data
|
10
10
|
end_time = Time.current
|
11
11
|
start_time = end_time - 1.day
|
12
|
-
table_name = GoodJob::
|
12
|
+
table_name = GoodJob::Job.table_name
|
13
13
|
|
14
14
|
count_query = Arel.sql(GoodJob::Execution.pg_or_jdbc_query(<<~SQL.squish))
|
15
15
|
SELECT *
|
@@ -10,8 +10,8 @@ module GoodJob
|
|
10
10
|
destroy: "destroyed",
|
11
11
|
}.freeze
|
12
12
|
|
13
|
-
rescue_from GoodJob::
|
14
|
-
GoodJob::
|
13
|
+
rescue_from GoodJob::Job::AdapterNotGoodJobError,
|
14
|
+
GoodJob::Job::ActionForStateMismatchError,
|
15
15
|
with: :redirect_on_error
|
16
16
|
|
17
17
|
def index
|
@@ -26,7 +26,7 @@ module GoodJob
|
|
26
26
|
JobsFilter.new(params).filtered_query
|
27
27
|
else
|
28
28
|
job_ids = params.fetch(:job_ids, [])
|
29
|
-
|
29
|
+
Job.where(active_job_id: job_ids)
|
30
30
|
end
|
31
31
|
|
32
32
|
processed_jobs = jobs.map do |job|
|
@@ -42,7 +42,7 @@ module GoodJob
|
|
42
42
|
end
|
43
43
|
|
44
44
|
job
|
45
|
-
rescue GoodJob::
|
45
|
+
rescue GoodJob::Job::ActionForStateMismatchError
|
46
46
|
nil
|
47
47
|
end.compact
|
48
48
|
|
@@ -56,29 +56,29 @@ module GoodJob
|
|
56
56
|
end
|
57
57
|
|
58
58
|
def show
|
59
|
-
@job =
|
59
|
+
@job = Job.find(params[:id])
|
60
60
|
end
|
61
61
|
|
62
62
|
def discard
|
63
|
-
@job =
|
63
|
+
@job = Job.find(params[:id])
|
64
64
|
@job.discard_job(DISCARD_MESSAGE)
|
65
65
|
redirect_back(fallback_location: jobs_path, notice: "Job has been discarded")
|
66
66
|
end
|
67
67
|
|
68
68
|
def reschedule
|
69
|
-
@job =
|
69
|
+
@job = Job.find(params[:id])
|
70
70
|
@job.reschedule_job
|
71
71
|
redirect_back(fallback_location: jobs_path, notice: "Job has been rescheduled")
|
72
72
|
end
|
73
73
|
|
74
74
|
def retry
|
75
|
-
@job =
|
75
|
+
@job = Job.find(params[:id])
|
76
76
|
@job.retry_job
|
77
77
|
redirect_back(fallback_location: jobs_path, notice: "Job has been retried")
|
78
78
|
end
|
79
79
|
|
80
80
|
def destroy
|
81
|
-
@job =
|
81
|
+
@job = Job.find(params[:id])
|
82
82
|
@job.destroy_job
|
83
83
|
redirect_back(fallback_location: jobs_path, notice: "Job has been destroyed")
|
84
84
|
end
|
@@ -87,9 +87,9 @@ module GoodJob
|
|
87
87
|
|
88
88
|
def redirect_on_error(exception)
|
89
89
|
alert = case exception
|
90
|
-
when GoodJob::
|
90
|
+
when GoodJob::Job::AdapterNotGoodJobError
|
91
91
|
"ActiveJob Queue Adapter must be GoodJob."
|
92
|
-
when GoodJob::
|
92
|
+
when GoodJob::Job::ActionForStateMismatchError
|
93
93
|
"Job is not in an appropriate state for this action."
|
94
94
|
else
|
95
95
|
exception.to_s
|
@@ -31,7 +31,7 @@ module GoodJob
|
|
31
31
|
when 'scheduled'
|
32
32
|
query = query.scheduled
|
33
33
|
when 'running'
|
34
|
-
query = query.running.select("#{GoodJob::
|
34
|
+
query = query.running.select("#{GoodJob::Job.table_name}.*", 'pg_locks.locktype')
|
35
35
|
when 'queued'
|
36
36
|
query = query.queued
|
37
37
|
end
|
@@ -47,7 +47,7 @@ module GoodJob
|
|
47
47
|
private
|
48
48
|
|
49
49
|
def default_base_query
|
50
|
-
GoodJob::
|
50
|
+
GoodJob::Job.all
|
51
51
|
end
|
52
52
|
end
|
53
53
|
end
|
@@ -3,15 +3,7 @@
|
|
3
3
|
</div>
|
4
4
|
|
5
5
|
<div data-live-poll-region="processes">
|
6
|
-
<% if
|
7
|
-
<div class="card my-3">
|
8
|
-
<div class="card-body">
|
9
|
-
<p class="card-text">
|
10
|
-
<em>Feature unavailable because of pending database migration.</em>
|
11
|
-
</p>
|
12
|
-
</div>
|
13
|
-
</div>
|
14
|
-
<% elsif @processes.present? %>
|
6
|
+
<% if @processes.present? %>
|
15
7
|
<div class="card my-3">
|
16
8
|
<div class="table-responsive">
|
17
9
|
<table class="table card-table table-bordered table-hover table-sm mb-0">
|
@@ -16,7 +16,7 @@
|
|
16
16
|
<%= tag.script "", src: rails_ujs_path(format: :js, v: GoodJob::VERSION, locale: nil), nonce: content_security_policy_nonce %>
|
17
17
|
|
18
18
|
<%= tag.script "", src: es_module_shims_path(format: :js, v: GoodJob::VERSION, locale: nil), async: true, nonce: content_security_policy_nonce %>
|
19
|
-
<% importmaps = { imports: GoodJob::AssetsController.js_modules.keys.
|
19
|
+
<% importmaps = { imports: GoodJob::AssetsController.js_modules.keys.index_with { |module_name| modules_path(module_name, format: :js, locale: nil, v: GoodJob::VERSION) } } %>
|
20
20
|
<%= tag.script importmaps.to_json.html_safe, type: "importmap", nonce: content_security_policy_nonce %>
|
21
21
|
<%= tag.script "", src: scripts_path(format: :js, v: GoodJob::VERSION, locale: nil), type: "module", nonce: content_security_policy_nonce %>
|
22
22
|
</head>
|
@@ -18,6 +18,12 @@ class CreateGoodJobs < ActiveRecord::Migration<%= migration_version %>
|
|
18
18
|
t.text :concurrency_key
|
19
19
|
t.text :cron_key
|
20
20
|
t.uuid :retried_good_job_id
|
21
|
+
t.timestamp :cron_at
|
22
|
+
end
|
23
|
+
|
24
|
+
create_table :good_job_processes, id: :uuid do |t|
|
25
|
+
t.timestamps
|
26
|
+
t.jsonb :state
|
21
27
|
end
|
22
28
|
|
23
29
|
add_index :good_jobs, :scheduled_at, where: "(finished_at IS NULL)", name: "index_good_jobs_on_scheduled_at"
|
@@ -25,5 +31,8 @@ class CreateGoodJobs < ActiveRecord::Migration<%= migration_version %>
|
|
25
31
|
add_index :good_jobs, [:active_job_id, :created_at], name: :index_good_jobs_on_active_job_id_and_created_at
|
26
32
|
add_index :good_jobs, :concurrency_key, where: "(finished_at IS NULL)", name: :index_good_jobs_on_concurrency_key_when_unfinished
|
27
33
|
add_index :good_jobs, [:cron_key, :created_at], name: :index_good_jobs_on_cron_key_and_created_at
|
34
|
+
add_index :good_jobs, [:cron_key, :cron_at], name: :index_good_jobs_on_cron_key_and_cron_at, unique: true
|
35
|
+
add_index :good_jobs, [:active_job_id], name: :index_good_jobs_on_active_job_id
|
36
|
+
add_index :good_jobs, [:finished_at], where: "retried_good_job_id IS NULL AND finished_at IS NOT NULL", name: :index_good_jobs_jobs_on_finished_at
|
28
37
|
end
|
29
38
|
end
|
data/lib/good_job/adapter.rb
CHANGED
@@ -20,32 +20,16 @@ module GoodJob
|
|
20
20
|
#
|
21
21
|
# The default value depends on the Rails environment:
|
22
22
|
#
|
23
|
-
# - +development
|
23
|
+
# - +development+: +:async:+
|
24
|
+
# -+test+: +:inline+
|
24
25
|
# - +production+ and all other environments: +:external+
|
25
26
|
#
|
26
|
-
|
27
|
-
|
28
|
-
# @param poll_interval [Integer, nil] sets the number of seconds between polls for jobs when +execution_mode+ is set to +:async+. You can also set this with the environment variable +GOOD_JOB_POLL_INTERVAL+. Defaults to +1+.
|
29
|
-
# @param start_async_on_initialize [Boolean] whether to start the async scheduler when the adapter is initialized.
|
30
|
-
def initialize(execution_mode: nil, queues: nil, max_threads: nil, poll_interval: nil, start_async_on_initialize: nil)
|
31
|
-
if queues || max_threads || poll_interval || start_async_on_initialize
|
32
|
-
ActiveSupport::Deprecation.warn(
|
33
|
-
"GoodJob::Adapter's execution-related arguments (queues, max_threads, poll_interval, start_async_on_initialize) have been deprecated and will be removed in GoodJob v3. These options should be configured through GoodJob global configuration instead."
|
34
|
-
)
|
35
|
-
end
|
36
|
-
|
37
|
-
@configuration = GoodJob::Configuration.new(
|
38
|
-
{
|
39
|
-
execution_mode: execution_mode,
|
40
|
-
queues: queues,
|
41
|
-
max_threads: max_threads,
|
42
|
-
poll_interval: poll_interval,
|
43
|
-
}
|
44
|
-
)
|
27
|
+
def initialize(execution_mode: nil)
|
28
|
+
@configuration = GoodJob::Configuration.new({ execution_mode: execution_mode })
|
45
29
|
@configuration.validate!
|
46
30
|
self.class.instances << self
|
47
31
|
|
48
|
-
start_async if
|
32
|
+
start_async if GoodJob.async_ready?
|
49
33
|
end
|
50
34
|
|
51
35
|
# Enqueues the ActiveJob job to be performed.
|
@@ -63,11 +47,7 @@ module GoodJob
|
|
63
47
|
# @return [GoodJob::Execution]
|
64
48
|
def enqueue_at(active_job, timestamp)
|
65
49
|
scheduled_at = timestamp ? Time.zone.at(timestamp) : nil
|
66
|
-
|
67
|
-
if execute_inline?
|
68
|
-
future_scheduled = scheduled_at && scheduled_at > Time.current
|
69
|
-
will_execute_inline = !future_scheduled || (future_scheduled && !@configuration.inline_execution_respects_schedule?)
|
70
|
-
end
|
50
|
+
will_execute_inline = execute_inline? && (scheduled_at.nil? || scheduled_at <= Time.current)
|
71
51
|
|
72
52
|
execution = GoodJob::Execution.enqueue(
|
73
53
|
active_job,
|
@@ -76,29 +56,6 @@ module GoodJob
|
|
76
56
|
)
|
77
57
|
|
78
58
|
if will_execute_inline
|
79
|
-
if future_scheduled && !@configuration.inline_execution_respects_schedule?
|
80
|
-
ActiveSupport::Deprecation.warn(<<~DEPRECATION)
|
81
|
-
In the next major release, GoodJob will not *inline* execute
|
82
|
-
future-scheduled jobs.
|
83
|
-
|
84
|
-
To opt into this behavior immediately set:
|
85
|
-
`config.good_job.inline_execution_respects_schedule = true`
|
86
|
-
|
87
|
-
To perform jobs inline at any time, use `GoodJob.perform_inline`.
|
88
|
-
|
89
|
-
For example, using time helpers within an integration test:
|
90
|
-
|
91
|
-
```
|
92
|
-
MyJob.set(wait: 10.minutes).perform_later
|
93
|
-
travel_to(15.minutes.from_now) { GoodJob.perform_inline }
|
94
|
-
```
|
95
|
-
|
96
|
-
Note: Rails `travel`/`travel_to` time helpers do not have millisecond
|
97
|
-
precision, so you must leave at least 1 second between the schedule
|
98
|
-
and time traveling for the job to be executed.
|
99
|
-
DEPRECATION
|
100
|
-
end
|
101
|
-
|
102
59
|
begin
|
103
60
|
result = execution.perform
|
104
61
|
ensure
|
@@ -15,13 +15,13 @@ module GoodJob
|
|
15
15
|
# Default poll interval for async in development environment
|
16
16
|
DEFAULT_DEVELOPMENT_ASYNC_POLL_INTERVAL = -1
|
17
17
|
# Default number of threads to use per {Scheduler}
|
18
|
-
DEFAULT_MAX_CACHE =
|
18
|
+
DEFAULT_MAX_CACHE = 10_000
|
19
19
|
# Default number of seconds to preserve jobs for {CLI#cleanup_preserved_jobs} and {GoodJob.cleanup_preserved_jobs}
|
20
|
-
DEFAULT_CLEANUP_PRESERVED_JOBS_BEFORE_SECONDS_AGO =
|
20
|
+
DEFAULT_CLEANUP_PRESERVED_JOBS_BEFORE_SECONDS_AGO = 14.days.to_i
|
21
21
|
# Default number of jobs to execute between preserved job cleanup runs
|
22
|
-
DEFAULT_CLEANUP_INTERVAL_JOBS =
|
22
|
+
DEFAULT_CLEANUP_INTERVAL_JOBS = 1_000
|
23
23
|
# Default number of seconds to wait between preserved job cleanup runs
|
24
|
-
DEFAULT_CLEANUP_INTERVAL_SECONDS =
|
24
|
+
DEFAULT_CLEANUP_INTERVAL_SECONDS = 10.minutes.to_i
|
25
25
|
# Default to always wait for jobs to finish for {Adapter#shutdown}
|
26
26
|
DEFAULT_SHUTDOWN_TIMEOUT = -1
|
27
27
|
# Default to not running cron
|
@@ -14,8 +14,6 @@ module GoodJob # :nodoc:
|
|
14
14
|
# Registers the current process.
|
15
15
|
def register_process
|
16
16
|
GoodJob::Process.with_connection(connection) do
|
17
|
-
next unless Process.migrated?
|
18
|
-
|
19
17
|
GoodJob::Process.cleanup
|
20
18
|
@process = GoodJob::Process.register
|
21
19
|
end
|
@@ -24,8 +22,6 @@ module GoodJob # :nodoc:
|
|
24
22
|
# Deregisters the current process.
|
25
23
|
def deregister_process
|
26
24
|
GoodJob::Process.with_connection(connection) do
|
27
|
-
next unless Process.migrated?
|
28
|
-
|
29
25
|
@process&.deregister
|
30
26
|
end
|
31
27
|
end
|
data/lib/good_job/version.rb
CHANGED
data/lib/good_job.rb
CHANGED
@@ -43,23 +43,21 @@ module GoodJob
|
|
43
43
|
|
44
44
|
# @!attribute [rw] preserve_job_records
|
45
45
|
# @!scope class
|
46
|
-
# Whether to preserve job records in the database after they have finished (default: +
|
47
|
-
# By default, GoodJob
|
46
|
+
# Whether to preserve job records in the database after they have finished (default: +true+).
|
47
|
+
# By default, GoodJob deletes job records after the job is completed successfully.
|
48
48
|
# If you want to preserve jobs for latter inspection, set this to +true+.
|
49
49
|
# If you want to preserve only jobs that finished with error for latter inspection, set this to +:on_unhandled_error+.
|
50
|
-
# If +true+, you will need to clean out jobs using the +good_job cleanup_preserved_jobs+ CLI command or
|
51
|
-
# by using +Goodjob.cleanup_preserved_jobs+.
|
52
50
|
# @return [Boolean, nil]
|
53
|
-
mattr_accessor :preserve_job_records, default:
|
51
|
+
mattr_accessor :preserve_job_records, default: true
|
54
52
|
|
55
53
|
# @!attribute [rw] retry_on_unhandled_error
|
56
54
|
# @!scope class
|
57
|
-
# Whether to re-perform a job when a type of +StandardError+ is raised to GoodJob (default: +
|
55
|
+
# Whether to re-perform a job when a type of +StandardError+ is raised to GoodJob (default: +false+).
|
58
56
|
# If +true+, causes jobs to be re-queued and retried if they raise an instance of +StandardError+.
|
59
57
|
# If +false+, jobs will be discarded or marked as finished if they raise an instance of +StandardError+.
|
60
58
|
# Instances of +Exception+, like +SIGINT+, will *always* be retried, regardless of this attribute's value.
|
61
59
|
# @return [Boolean, nil]
|
62
|
-
mattr_accessor :retry_on_unhandled_error, default:
|
60
|
+
mattr_accessor :retry_on_unhandled_error, default: false
|
63
61
|
|
64
62
|
# @!attribute [rw] on_thread_error
|
65
63
|
# @!scope class
|
@@ -143,7 +141,7 @@ module GoodJob
|
|
143
141
|
include_discarded = configuration.cleanup_discarded_jobs?
|
144
142
|
|
145
143
|
ActiveSupport::Notifications.instrument("cleanup_preserved_jobs.good_job", { older_than: older_than, timestamp: timestamp }) do |payload|
|
146
|
-
old_jobs = GoodJob::
|
144
|
+
old_jobs = GoodJob::Job.where('finished_at <= ?', timestamp)
|
147
145
|
old_jobs = old_jobs.not_discarded unless include_discarded
|
148
146
|
old_jobs_count = old_jobs.count
|
149
147
|
|
@@ -1,224 +1,11 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
module GoodJob
|
3
|
-
#
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
include Lockable
|
10
|
-
|
11
|
-
# Raised when an inappropriate action is applied to a Job based on its state.
|
12
|
-
ActionForStateMismatchError = Class.new(StandardError)
|
13
|
-
# Raised when an action requires GoodJob to be the ActiveJob Queue Adapter but GoodJob is not.
|
14
|
-
AdapterNotGoodJobError = Class.new(StandardError)
|
15
|
-
# Attached to a Job's Execution when the Job is discarded.
|
16
|
-
DiscardJobError = Class.new(StandardError)
|
17
|
-
|
18
|
-
class << self
|
19
|
-
delegate :table_name, to: GoodJob::Execution
|
20
|
-
|
21
|
-
def table_name=(_value)
|
22
|
-
raise NotImplementedError, 'Assign GoodJob::Execution.table_name directly'
|
23
|
-
end
|
24
|
-
end
|
25
|
-
|
26
|
-
self.primary_key = 'active_job_id'
|
27
|
-
self.advisory_lockable_column = 'active_job_id'
|
28
|
-
|
29
|
-
has_many :executions, -> { order(created_at: :asc) }, class_name: 'GoodJob::Execution', foreign_key: 'active_job_id', inverse_of: :job
|
30
|
-
|
31
|
-
# Only the most-recent unretried execution represents a "Job"
|
32
|
-
default_scope { where(retried_good_job_id: nil) }
|
33
|
-
|
34
|
-
# Get Jobs with given class name
|
35
|
-
# @!method job_class
|
36
|
-
# @!scope class
|
37
|
-
# @param string [String] Execution class name
|
38
|
-
# @return [ActiveRecord::Relation]
|
39
|
-
scope :job_class, ->(job_class) { where("serialized_params->>'job_class' = ?", job_class) }
|
40
|
-
|
41
|
-
# Get Jobs finished before the given timestamp.
|
42
|
-
# @!method finished_before(timestamp)
|
43
|
-
# @!scope class
|
44
|
-
# @param timestamp (DateTime, Time)
|
45
|
-
# @return [ActiveRecord::Relation]
|
46
|
-
scope :finished_before, ->(timestamp) { where(arel_table['finished_at'].lteq(timestamp)) }
|
47
|
-
|
48
|
-
# First execution will run in the future
|
49
|
-
scope :scheduled, -> { where(finished_at: nil).where('COALESCE(scheduled_at, created_at) > ?', DateTime.current).where("(serialized_params->>'executions')::integer < 2") }
|
50
|
-
# Execution errored, will run in the future
|
51
|
-
scope :retried, -> { where(finished_at: nil).where('COALESCE(scheduled_at, created_at) > ?', DateTime.current).where("(serialized_params->>'executions')::integer > 1") }
|
52
|
-
# Immediate/Scheduled time to run has passed, waiting for an available thread run
|
53
|
-
scope :queued, -> { where(finished_at: nil).where('COALESCE(scheduled_at, created_at) <= ?', DateTime.current).joins_advisory_locks.where(pg_locks: { locktype: nil }) }
|
54
|
-
# Advisory locked and executing
|
55
|
-
scope :running, -> { where(finished_at: nil).joins_advisory_locks.where.not(pg_locks: { locktype: nil }) }
|
56
|
-
# Completed executing successfully
|
57
|
-
scope :finished, -> { not_discarded.where.not(finished_at: nil) }
|
58
|
-
# Errored but will not be retried
|
59
|
-
scope :discarded, -> { where.not(finished_at: nil).where.not(error: nil) }
|
60
|
-
# Not errored
|
61
|
-
scope :not_discarded, -> { where(error: nil) }
|
62
|
-
|
63
|
-
# The job's ActiveJob UUID
|
64
|
-
# @return [String]
|
65
|
-
def id
|
66
|
-
active_job_id
|
67
|
-
end
|
68
|
-
|
69
|
-
# The ActiveJob job class, as a string
|
70
|
-
# @return [String]
|
71
|
-
def job_class
|
72
|
-
serialized_params['job_class']
|
73
|
-
end
|
74
|
-
|
75
|
-
# The status of the Job, based on the state of its most recent execution.
|
76
|
-
# @return [Symbol]
|
77
|
-
delegate :status, :last_status_at, to: :head_execution
|
78
|
-
|
79
|
-
# This job's most recent {Execution}
|
80
|
-
# @param reload [Booelan] whether to reload executions
|
81
|
-
# @return [Execution]
|
82
|
-
def head_execution(reload: false)
|
83
|
-
executions.reload if reload
|
84
|
-
executions.load # memoize the results
|
85
|
-
executions.last
|
86
|
-
end
|
87
|
-
|
88
|
-
# This job's initial/oldest {Execution}
|
89
|
-
# @return [Execution]
|
90
|
-
def tail_execution
|
91
|
-
executions.first
|
92
|
-
end
|
93
|
-
|
94
|
-
# The number of times this job has been executed, according to ActiveJob's serialized state.
|
95
|
-
# @return [Numeric]
|
96
|
-
def executions_count
|
97
|
-
aj_count = head_execution.serialized_params.fetch('executions', 0)
|
98
|
-
# The execution count within serialized_params is not updated
|
99
|
-
# once the underlying execution has been executed.
|
100
|
-
if status.in? [:discarded, :finished, :running]
|
101
|
-
aj_count + 1
|
102
|
-
else
|
103
|
-
aj_count
|
104
|
-
end
|
105
|
-
end
|
106
|
-
|
107
|
-
# The number of times this job has been executed, according to the number of GoodJob {Execution} records.
|
108
|
-
# @return [Numeric]
|
109
|
-
def preserved_executions_count
|
110
|
-
executions.size
|
111
|
-
end
|
112
|
-
|
113
|
-
# The most recent error message.
|
114
|
-
# If the job has been retried, the error will be fetched from the previous {Execution} record.
|
115
|
-
# @return [String]
|
116
|
-
def recent_error
|
117
|
-
head_execution.error || executions[-2]&.error
|
118
|
-
end
|
119
|
-
|
120
|
-
# Tests whether the job is being executed right now.
|
121
|
-
# @return [Boolean]
|
122
|
-
def running?
|
123
|
-
# Avoid N+1 Query: `.includes_advisory_locks`
|
124
|
-
if has_attribute?(:locktype)
|
125
|
-
self['locktype'].present?
|
126
|
-
else
|
127
|
-
advisory_locked?
|
128
|
-
end
|
129
|
-
end
|
130
|
-
|
131
|
-
# Retry a job that has errored and been discarded.
|
132
|
-
# This action will create a new {Execution} record for the job.
|
133
|
-
# @return [ActiveJob::Base]
|
134
|
-
def retry_job
|
135
|
-
with_advisory_lock do
|
136
|
-
execution = head_execution(reload: true)
|
137
|
-
active_job = execution.active_job
|
138
|
-
|
139
|
-
raise AdapterNotGoodJobError unless active_job.class.queue_adapter.is_a? GoodJob::Adapter
|
140
|
-
raise ActionForStateMismatchError if execution.finished_at.blank? || execution.error.blank?
|
141
|
-
|
142
|
-
# Update the executions count because the previous execution will not have been preserved
|
143
|
-
# Do not update `exception_executions` because that comes from rescue_from's arguments
|
144
|
-
active_job.executions = (active_job.executions || 0) + 1
|
145
|
-
|
146
|
-
new_active_job = nil
|
147
|
-
GoodJob::CurrentThread.within do |current_thread|
|
148
|
-
current_thread.execution = execution
|
149
|
-
|
150
|
-
execution.class.transaction(joinable: false, requires_new: true) do
|
151
|
-
new_active_job = active_job.retry_job(wait: 0, error: execution.error)
|
152
|
-
execution.save
|
153
|
-
end
|
154
|
-
end
|
155
|
-
new_active_job
|
156
|
-
end
|
157
|
-
end
|
158
|
-
|
159
|
-
# Discard a job so that it will not be executed further.
|
160
|
-
# This action will add a {DiscardJobError} to the job's {Execution} and mark it as finished.
|
161
|
-
# @return [void]
|
162
|
-
def discard_job(message)
|
163
|
-
with_advisory_lock do
|
164
|
-
execution = head_execution(reload: true)
|
165
|
-
active_job = execution.active_job
|
166
|
-
|
167
|
-
raise ActionForStateMismatchError if execution.finished_at.present?
|
168
|
-
|
169
|
-
job_error = GoodJob::ActiveJobJob::DiscardJobError.new(message)
|
170
|
-
|
171
|
-
update_execution = proc do
|
172
|
-
execution.update(
|
173
|
-
finished_at: Time.current,
|
174
|
-
error: [job_error.class, GoodJob::Execution::ERROR_MESSAGE_SEPARATOR, job_error.message].join
|
175
|
-
)
|
176
|
-
end
|
177
|
-
|
178
|
-
if active_job.respond_to?(:instrument)
|
179
|
-
active_job.send :instrument, :discard, error: job_error, &update_execution
|
180
|
-
else
|
181
|
-
update_execution.call
|
182
|
-
end
|
183
|
-
end
|
184
|
-
end
|
185
|
-
|
186
|
-
# Reschedule a scheduled job so that it executes immediately (or later) by the next available execution thread.
|
187
|
-
# @param scheduled_at [DateTime, Time] When to reschedule the job
|
188
|
-
# @return [void]
|
189
|
-
def reschedule_job(scheduled_at = Time.current)
|
190
|
-
with_advisory_lock do
|
191
|
-
execution = head_execution(reload: true)
|
192
|
-
|
193
|
-
raise ActionForStateMismatchError if execution.finished_at.present?
|
194
|
-
|
195
|
-
execution = head_execution(reload: true)
|
196
|
-
execution.update(scheduled_at: scheduled_at)
|
197
|
-
end
|
198
|
-
end
|
199
|
-
|
200
|
-
# Destroy all of a discarded or finished job's executions from the database so that it will no longer appear on the dashboard.
|
201
|
-
# @return [void]
|
202
|
-
def destroy_job
|
203
|
-
with_advisory_lock do
|
204
|
-
execution = head_execution(reload: true)
|
205
|
-
|
206
|
-
raise ActionForStateMismatchError if execution.finished_at.blank?
|
207
|
-
|
208
|
-
destroy
|
209
|
-
end
|
210
|
-
end
|
211
|
-
|
212
|
-
# Utility method to determine which execution record is used to represent this job
|
213
|
-
# @return [String]
|
214
|
-
def _execution_id
|
215
|
-
attributes['id']
|
216
|
-
end
|
217
|
-
|
218
|
-
# Utility method to test whether this job's underlying attributes represents its most recent execution.
|
219
|
-
# @return [Boolean]
|
220
|
-
def _head?
|
221
|
-
_execution_id == head_execution(reload: true).id
|
3
|
+
# @deprecated Use {GoodJob::Job} instead.
|
4
|
+
class ActiveJobJob < Execution
|
5
|
+
after_initialize do |_job|
|
6
|
+
ActiveSupport::Deprecation.warn(
|
7
|
+
"The `GoodJob::ActiveJobJob` class name is deprecated. Replace with `GoodJob::Job`."
|
8
|
+
)
|
222
9
|
end
|
223
10
|
end
|
224
11
|
end
|
@@ -73,13 +73,13 @@ module GoodJob # :nodoc:
|
|
73
73
|
end
|
74
74
|
|
75
75
|
def jobs
|
76
|
-
GoodJob::
|
76
|
+
GoodJob::Job.where(cron_key: key)
|
77
77
|
end
|
78
78
|
|
79
79
|
def last_at
|
80
80
|
return if last_job.blank?
|
81
81
|
|
82
|
-
if GoodJob::
|
82
|
+
if GoodJob::Job.column_names.include?('cron_at')
|
83
83
|
(last_job.cron_at || last_job.created_at).localtime
|
84
84
|
else
|
85
85
|
last_job.created_at
|
@@ -99,7 +99,7 @@ module GoodJob # :nodoc:
|
|
99
99
|
end
|
100
100
|
|
101
101
|
def last_job
|
102
|
-
if GoodJob::
|
102
|
+
if GoodJob::Job.column_names.include?('cron_at')
|
103
103
|
jobs.order("cron_at DESC NULLS LAST").first
|
104
104
|
else
|
105
105
|
jobs.order(created_at: :asc).last
|
@@ -51,7 +51,7 @@ module GoodJob
|
|
51
51
|
end
|
52
52
|
end
|
53
53
|
|
54
|
-
belongs_to :job, class_name: 'GoodJob::
|
54
|
+
belongs_to :job, class_name: 'GoodJob::Job', foreign_key: 'active_job_id', primary_key: 'active_job_id', optional: true, inverse_of: :executions
|
55
55
|
|
56
56
|
# Get Jobs with given ActiveJob ID
|
57
57
|
# @!method active_job_id
|
@@ -207,14 +207,7 @@ module GoodJob
|
|
207
207
|
|
208
208
|
if CurrentThread.cron_key
|
209
209
|
execution_args[:cron_key] = CurrentThread.cron_key
|
210
|
-
|
211
|
-
@cron_at_index = column_names.include?('cron_at') && connection.index_name_exists?(:good_jobs, :index_good_jobs_on_cron_key_and_cron_at) unless instance_variable_defined?(:@cron_at_index)
|
212
|
-
|
213
|
-
if @cron_at_index
|
214
|
-
execution_args[:cron_at] = CurrentThread.cron_at
|
215
|
-
else
|
216
|
-
migration_pending_warning!
|
217
|
-
end
|
210
|
+
execution_args[:cron_at] = CurrentThread.cron_at
|
218
211
|
elsif CurrentThread.active_job_id && CurrentThread.active_job_id == active_job.job_id
|
219
212
|
execution_args[:cron_key] = CurrentThread.execution.cron_key
|
220
213
|
end
|
@@ -346,8 +339,7 @@ module GoodJob
|
|
346
339
|
current_thread.reset
|
347
340
|
current_thread.execution = self
|
348
341
|
|
349
|
-
|
350
|
-
ActiveSupport::Notifications.instrument("perform_job.good_job", { good_job: self, execution: self, process_id: current_thread.process_id, thread_name: current_thread.thread_name }) do
|
342
|
+
ActiveSupport::Notifications.instrument("perform_job.good_job", { execution: self, process_id: current_thread.process_id, thread_name: current_thread.thread_name }) do
|
351
343
|
value = ActiveJob::Base.execute(active_job_data)
|
352
344
|
|
353
345
|
if value.is_a?(Exception)
|
data/lib/models/good_job/job.rb
CHANGED
@@ -1,11 +1,224 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
module GoodJob
|
3
|
-
#
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
3
|
+
# ActiveRecord model that represents an +ActiveJob+ job.
|
4
|
+
# There is not a table in the database whose discrete rows represents "Jobs".
|
5
|
+
# The +good_jobs+ table is a table of individual {GoodJob::Execution}s that share the same +active_job_id+.
|
6
|
+
# A single row from the +good_jobs+ table of executions is fetched to represent an Job
|
7
|
+
class Job < BaseRecord
|
8
|
+
include Filterable
|
9
|
+
include Lockable
|
10
|
+
|
11
|
+
# Raised when an inappropriate action is applied to a Job based on its state.
|
12
|
+
ActionForStateMismatchError = Class.new(StandardError)
|
13
|
+
# Raised when an action requires GoodJob to be the ActiveJob Queue Adapter but GoodJob is not.
|
14
|
+
AdapterNotGoodJobError = Class.new(StandardError)
|
15
|
+
# Attached to a Job's Execution when the Job is discarded.
|
16
|
+
DiscardJobError = Class.new(StandardError)
|
17
|
+
|
18
|
+
class << self
|
19
|
+
delegate :table_name, to: GoodJob::Execution
|
20
|
+
|
21
|
+
def table_name=(_value)
|
22
|
+
raise NotImplementedError, 'Assign GoodJob::Execution.table_name directly'
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
self.primary_key = 'active_job_id'
|
27
|
+
self.advisory_lockable_column = 'active_job_id'
|
28
|
+
|
29
|
+
has_many :executions, -> { order(created_at: :asc) }, class_name: 'GoodJob::Execution', foreign_key: 'active_job_id', inverse_of: :job
|
30
|
+
|
31
|
+
# Only the most-recent unretried execution represents a "Job"
|
32
|
+
default_scope { where(retried_good_job_id: nil) }
|
33
|
+
|
34
|
+
# Get Jobs with given class name
|
35
|
+
# @!method job_class
|
36
|
+
# @!scope class
|
37
|
+
# @param string [String] Execution class name
|
38
|
+
# @return [ActiveRecord::Relation]
|
39
|
+
scope :job_class, ->(job_class) { where("serialized_params->>'job_class' = ?", job_class) }
|
40
|
+
|
41
|
+
# Get Jobs finished before the given timestamp.
|
42
|
+
# @!method finished_before(timestamp)
|
43
|
+
# @!scope class
|
44
|
+
# @param timestamp (DateTime, Time)
|
45
|
+
# @return [ActiveRecord::Relation]
|
46
|
+
scope :finished_before, ->(timestamp) { where(arel_table['finished_at'].lteq(timestamp)) }
|
47
|
+
|
48
|
+
# First execution will run in the future
|
49
|
+
scope :scheduled, -> { where(finished_at: nil).where('COALESCE(scheduled_at, created_at) > ?', DateTime.current).where("(serialized_params->>'executions')::integer < 2") }
|
50
|
+
# Execution errored, will run in the future
|
51
|
+
scope :retried, -> { where(finished_at: nil).where('COALESCE(scheduled_at, created_at) > ?', DateTime.current).where("(serialized_params->>'executions')::integer > 1") }
|
52
|
+
# Immediate/Scheduled time to run has passed, waiting for an available thread run
|
53
|
+
scope :queued, -> { where(finished_at: nil).where('COALESCE(scheduled_at, created_at) <= ?', DateTime.current).joins_advisory_locks.where(pg_locks: { locktype: nil }) }
|
54
|
+
# Advisory locked and executing
|
55
|
+
scope :running, -> { where(finished_at: nil).joins_advisory_locks.where.not(pg_locks: { locktype: nil }) }
|
56
|
+
# Completed executing successfully
|
57
|
+
scope :finished, -> { not_discarded.where.not(finished_at: nil) }
|
58
|
+
# Errored but will not be retried
|
59
|
+
scope :discarded, -> { where.not(finished_at: nil).where.not(error: nil) }
|
60
|
+
# Not errored
|
61
|
+
scope :not_discarded, -> { where(error: nil) }
|
62
|
+
|
63
|
+
# The job's ActiveJob UUID
|
64
|
+
# @return [String]
|
65
|
+
def id
|
66
|
+
active_job_id
|
67
|
+
end
|
68
|
+
|
69
|
+
# The ActiveJob job class, as a string
|
70
|
+
# @return [String]
|
71
|
+
def job_class
|
72
|
+
serialized_params['job_class']
|
73
|
+
end
|
74
|
+
|
75
|
+
# The status of the Job, based on the state of its most recent execution.
|
76
|
+
# @return [Symbol]
|
77
|
+
delegate :status, :last_status_at, to: :head_execution
|
78
|
+
|
79
|
+
# This job's most recent {Execution}
|
80
|
+
# @param reload [Booelan] whether to reload executions
|
81
|
+
# @return [Execution]
|
82
|
+
def head_execution(reload: false)
|
83
|
+
executions.reload if reload
|
84
|
+
executions.load # memoize the results
|
85
|
+
executions.last
|
86
|
+
end
|
87
|
+
|
88
|
+
# This job's initial/oldest {Execution}
|
89
|
+
# @return [Execution]
|
90
|
+
def tail_execution
|
91
|
+
executions.first
|
92
|
+
end
|
93
|
+
|
94
|
+
# The number of times this job has been executed, according to ActiveJob's serialized state.
|
95
|
+
# @return [Numeric]
|
96
|
+
def executions_count
|
97
|
+
aj_count = head_execution.serialized_params.fetch('executions', 0)
|
98
|
+
# The execution count within serialized_params is not updated
|
99
|
+
# once the underlying execution has been executed.
|
100
|
+
if status.in? [:discarded, :finished, :running]
|
101
|
+
aj_count + 1
|
102
|
+
else
|
103
|
+
aj_count
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
# The number of times this job has been executed, according to the number of GoodJob {Execution} records.
|
108
|
+
# @return [Numeric]
|
109
|
+
def preserved_executions_count
|
110
|
+
executions.size
|
111
|
+
end
|
112
|
+
|
113
|
+
# The most recent error message.
|
114
|
+
# If the job has been retried, the error will be fetched from the previous {Execution} record.
|
115
|
+
# @return [String]
|
116
|
+
def recent_error
|
117
|
+
head_execution.error || executions[-2]&.error
|
118
|
+
end
|
119
|
+
|
120
|
+
# Tests whether the job is being executed right now.
|
121
|
+
# @return [Boolean]
|
122
|
+
def running?
|
123
|
+
# Avoid N+1 Query: `.includes_advisory_locks`
|
124
|
+
if has_attribute?(:locktype)
|
125
|
+
self['locktype'].present?
|
126
|
+
else
|
127
|
+
advisory_locked?
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
# Retry a job that has errored and been discarded.
|
132
|
+
# This action will create a new {Execution} record for the job.
|
133
|
+
# @return [ActiveJob::Base]
|
134
|
+
def retry_job
|
135
|
+
with_advisory_lock do
|
136
|
+
execution = head_execution(reload: true)
|
137
|
+
active_job = execution.active_job
|
138
|
+
|
139
|
+
raise AdapterNotGoodJobError unless active_job.class.queue_adapter.is_a? GoodJob::Adapter
|
140
|
+
raise ActionForStateMismatchError if execution.finished_at.blank? || execution.error.blank?
|
141
|
+
|
142
|
+
# Update the executions count because the previous execution will not have been preserved
|
143
|
+
# Do not update `exception_executions` because that comes from rescue_from's arguments
|
144
|
+
active_job.executions = (active_job.executions || 0) + 1
|
145
|
+
|
146
|
+
new_active_job = nil
|
147
|
+
GoodJob::CurrentThread.within do |current_thread|
|
148
|
+
current_thread.execution = execution
|
149
|
+
|
150
|
+
execution.class.transaction(joinable: false, requires_new: true) do
|
151
|
+
new_active_job = active_job.retry_job(wait: 0, error: execution.error)
|
152
|
+
execution.save
|
153
|
+
end
|
154
|
+
end
|
155
|
+
new_active_job
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
# Discard a job so that it will not be executed further.
|
160
|
+
# This action will add a {DiscardJobError} to the job's {Execution} and mark it as finished.
|
161
|
+
# @return [void]
|
162
|
+
def discard_job(message)
|
163
|
+
with_advisory_lock do
|
164
|
+
execution = head_execution(reload: true)
|
165
|
+
active_job = execution.active_job
|
166
|
+
|
167
|
+
raise ActionForStateMismatchError if execution.finished_at.present?
|
168
|
+
|
169
|
+
job_error = GoodJob::Job::DiscardJobError.new(message)
|
170
|
+
|
171
|
+
update_execution = proc do
|
172
|
+
execution.update(
|
173
|
+
finished_at: Time.current,
|
174
|
+
error: [job_error.class, GoodJob::Execution::ERROR_MESSAGE_SEPARATOR, job_error.message].join
|
175
|
+
)
|
176
|
+
end
|
177
|
+
|
178
|
+
if active_job.respond_to?(:instrument)
|
179
|
+
active_job.send :instrument, :discard, error: job_error, &update_execution
|
180
|
+
else
|
181
|
+
update_execution.call
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
# Reschedule a scheduled job so that it executes immediately (or later) by the next available execution thread.
|
187
|
+
# @param scheduled_at [DateTime, Time] When to reschedule the job
|
188
|
+
# @return [void]
|
189
|
+
def reschedule_job(scheduled_at = Time.current)
|
190
|
+
with_advisory_lock do
|
191
|
+
execution = head_execution(reload: true)
|
192
|
+
|
193
|
+
raise ActionForStateMismatchError if execution.finished_at.present?
|
194
|
+
|
195
|
+
execution = head_execution(reload: true)
|
196
|
+
execution.update(scheduled_at: scheduled_at)
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
# Destroy all of a discarded or finished job's executions from the database so that it will no longer appear on the dashboard.
|
201
|
+
# @return [void]
|
202
|
+
def destroy_job
|
203
|
+
with_advisory_lock do
|
204
|
+
execution = head_execution(reload: true)
|
205
|
+
|
206
|
+
raise ActionForStateMismatchError if execution.finished_at.blank?
|
207
|
+
|
208
|
+
destroy
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
# Utility method to determine which execution record is used to represent this job
|
213
|
+
# @return [String]
|
214
|
+
def _execution_id
|
215
|
+
attributes['id']
|
216
|
+
end
|
217
|
+
|
218
|
+
# Utility method to test whether this job's underlying attributes represents its most recent execution.
|
219
|
+
# @return [Boolean]
|
220
|
+
def _head?
|
221
|
+
_execution_id == head_execution(reload: true).id
|
9
222
|
end
|
10
223
|
end
|
11
224
|
end
|
@@ -25,15 +25,6 @@ module GoodJob # :nodoc:
|
|
25
25
|
# @return [ActiveRecord::Relation]
|
26
26
|
scope :inactive, -> { advisory_unlocked }
|
27
27
|
|
28
|
-
# Whether the +good_job_processes+ table exsists.
|
29
|
-
# @return [Boolean]
|
30
|
-
def self.migrated?
|
31
|
-
return true if connection.table_exists?(table_name)
|
32
|
-
|
33
|
-
migration_pending_warning!
|
34
|
-
false
|
35
|
-
end
|
36
|
-
|
37
28
|
# UUID that is unique to the current process and changes when forked.
|
38
29
|
# @return [String]
|
39
30
|
def self.current_id
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: good_job
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 3.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ben Sheldon
|
@@ -16,28 +16,28 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - ">="
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version:
|
19
|
+
version: 6.0.0
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
24
|
- - ">="
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version:
|
26
|
+
version: 6.0.0
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: activerecord
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
31
|
- - ">="
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version:
|
33
|
+
version: 6.0.0
|
34
34
|
type: :runtime
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
38
|
- - ">="
|
39
39
|
- !ruby/object:Gem::Version
|
40
|
-
version:
|
40
|
+
version: 6.0.0
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
42
|
name: concurrent-ruby
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
@@ -72,14 +72,14 @@ dependencies:
|
|
72
72
|
requirements:
|
73
73
|
- - ">="
|
74
74
|
- !ruby/object:Gem::Version
|
75
|
-
version:
|
75
|
+
version: 6.0.0
|
76
76
|
type: :runtime
|
77
77
|
prerelease: false
|
78
78
|
version_requirements: !ruby/object:Gem::Requirement
|
79
79
|
requirements:
|
80
80
|
- - ">="
|
81
81
|
- !ruby/object:Gem::Version
|
82
|
-
version:
|
82
|
+
version: 6.0.0
|
83
83
|
- !ruby/object:Gem::Dependency
|
84
84
|
name: thor
|
85
85
|
requirement: !ruby/object:Gem::Requirement
|
@@ -414,10 +414,6 @@ files:
|
|
414
414
|
- lib/generators/good_job/install_generator.rb
|
415
415
|
- lib/generators/good_job/templates/install/migrations/create_good_jobs.rb.erb
|
416
416
|
- lib/generators/good_job/templates/update/migrations/01_create_good_jobs.rb.erb
|
417
|
-
- lib/generators/good_job/templates/update/migrations/02_add_cron_at_to_good_jobs.rb.erb
|
418
|
-
- lib/generators/good_job/templates/update/migrations/03_add_cron_key_cron_at_index_to_good_jobs.rb.erb
|
419
|
-
- lib/generators/good_job/templates/update/migrations/04_create_good_job_processes.rb.erb
|
420
|
-
- lib/generators/good_job/templates/update/migrations/04_index_good_job_jobs_on_finished_at.rb.erb
|
421
417
|
- lib/generators/good_job/update_generator.rb
|
422
418
|
- lib/good_job.rb
|
423
419
|
- lib/good_job/active_job_extensions.rb
|
@@ -1,14 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
class AddCronAtToGoodJobs < 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, :cron_at)
|
9
|
-
end
|
10
|
-
end
|
11
|
-
|
12
|
-
add_column :good_jobs, :cron_at, :timestamp
|
13
|
-
end
|
14
|
-
end
|
@@ -1,20 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
class AddCronKeyCronAtIndexToGoodJobs < ActiveRecord::Migration<%= migration_version %>
|
3
|
-
disable_ddl_transaction!
|
4
|
-
|
5
|
-
def change
|
6
|
-
reversible do |dir|
|
7
|
-
dir.up do
|
8
|
-
# Ensure this incremental update migration is idempotent
|
9
|
-
# with monolithic install migration.
|
10
|
-
return if connection.index_name_exists?(:good_jobs, :index_good_jobs_on_cron_key_and_cron_at)
|
11
|
-
end
|
12
|
-
end
|
13
|
-
|
14
|
-
add_index :good_jobs,
|
15
|
-
[:cron_key, :cron_at],
|
16
|
-
algorithm: :concurrently,
|
17
|
-
name: :index_good_jobs_on_cron_key_and_cron_at,
|
18
|
-
unique: true
|
19
|
-
end
|
20
|
-
end
|
data/lib/generators/good_job/templates/update/migrations/04_create_good_job_processes.rb.erb
DELETED
@@ -1,17 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
class CreateGoodJobProcesses < 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.table_exists?(:good_job_processes)
|
9
|
-
end
|
10
|
-
end
|
11
|
-
|
12
|
-
create_table :good_job_processes, id: :uuid do |t|
|
13
|
-
t.timestamps
|
14
|
-
t.jsonb :state
|
15
|
-
end
|
16
|
-
end
|
17
|
-
end
|
@@ -1,25 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
class IndexGoodJobJobsOnFinishedAt < ActiveRecord::Migration<%= migration_version %>
|
3
|
-
disable_ddl_transaction!
|
4
|
-
|
5
|
-
def change
|
6
|
-
reversible do |dir|
|
7
|
-
dir.up do
|
8
|
-
# Ensure this incremental update migration is idempotent
|
9
|
-
# with monolithic install migration.
|
10
|
-
return if connection.index_name_exists?(:good_jobs, :index_good_jobs_on_active_job_id)
|
11
|
-
end
|
12
|
-
end
|
13
|
-
|
14
|
-
add_index :good_jobs,
|
15
|
-
[:active_job_id],
|
16
|
-
name: :index_good_jobs_on_active_job_id,
|
17
|
-
algorithm: :concurrently
|
18
|
-
|
19
|
-
add_index :good_jobs,
|
20
|
-
[:finished_at],
|
21
|
-
where: "retried_good_job_id IS NULL AND finished_at IS NOT NULL",
|
22
|
-
name: :index_good_jobs_jobs_on_finished_at,
|
23
|
-
algorithm: :concurrently
|
24
|
-
end
|
25
|
-
end
|