good_job 2.99.0 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2a19a18e7245448f7f44c99a492f4ca08f311cc96da9a50f4ba4d7b704685017
4
- data.tar.gz: 07330c1a31b873b172066d9e7946b0184b250f7276fe041617c378b35ce8beca
3
+ metadata.gz: a4f4e14bae83760bdeb35557a727116d053e47a9b7dbf501914606dbc45c2ef7
4
+ data.tar.gz: c06e9165b7ac5d87af0ab75291fedd0194018752f34332c1a16930ddca53e3dd
5
5
  SHA512:
6
- metadata.gz: 7a38a4a1bc73cf7378f24850b1e58b9b7cf6fbf83ef7aff8fcdba7d2c77ce782e273a3bc946efcf21d98157523b68183bb36efcbd570d253eab73cc7e8542c99
7
- data.tar.gz: c97a8235d5b0041237909d6bb551bcccf2de5a4e2fa07bbbb12496a245aa6c4408896baa2a752b0a9f3eb6a1eb037189baa68e6d9979b57ef640e08750609d27
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:** 5.2+
148
- - **Ruby:** MRI 2.5+. JRuby 9.2.13+
149
- - **Postgres:** 9.6+
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: `false`)
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: `false`)
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: `true`)
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 will automatically and immediately retry a job when an exception is raised to GoodJob.
565
+ By default, GoodJob relies on ActiveJob's retry functionality.
568
566
 
569
- However, 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:
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. GoodJob can be configured to discard un-handled exceptions instead of retrying them. Be aware that if NOT setting `retry_on_unhandled_error` to `false` good_job will by default retry the failing job and may do this infinitely without pause thereby at least causing high load. In most cases `retry_on_unhandled_error` should be set as following:
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` (`ActionMailer::DeliveryJob` in Rails 5.2 or earlier) because ActionMailer uses a custom class, `ActionMailer::MailDeliveryJob`, which inherits from `ActiveJob::Base`, rather than your applications `ApplicationJob`.
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 destroy job records 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`).
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 necessary to destroy these preserved jobs from the database after a certain time period:
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 keep 1 day of job records by default.
887
- GoodJob.cleanup_preserved_jobs(older_than: 7.days) # It also takes custom arguments.
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::ActiveJobJob.table_name
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::ActiveJobJob::AdapterNotGoodJobError,
14
- GoodJob::ActiveJobJob::ActionForStateMismatchError,
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
- ActiveJobJob.where(active_job_id: job_ids)
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::ActiveJobJob::ActionForStateMismatchError
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 = ActiveJobJob.find(params[:id])
59
+ @job = Job.find(params[:id])
60
60
  end
61
61
 
62
62
  def discard
63
- @job = ActiveJobJob.find(params[:id])
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 = ActiveJobJob.find(params[:id])
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 = ActiveJobJob.find(params[:id])
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 = ActiveJobJob.find(params[:id])
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::ActiveJobJob::AdapterNotGoodJobError
90
+ when GoodJob::Job::AdapterNotGoodJobError
91
91
  "ActiveJob Queue Adapter must be GoodJob."
92
- when GoodJob::ActiveJobJob::ActionForStateMismatchError
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
@@ -2,7 +2,7 @@
2
2
  module GoodJob
3
3
  class ProcessesController < GoodJob::ApplicationController
4
4
  def index
5
- @processes = GoodJob::Process.active.order(created_at: :desc) if GoodJob::Process.migrated?
5
+ @processes = GoodJob::Process.active.order(created_at: :desc)
6
6
  end
7
7
  end
8
8
  end
@@ -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::ActiveJobJob.table_name}.*", 'pg_locks.locktype')
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::ActiveJobJob.all
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 !GoodJob::Process.migrated? %>
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.each_with_object({}) { |module_name, imports| imports[module_name] = modules_path(module_name, format: :js, locale: nil, v: GoodJob::VERSION) } } %>
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
@@ -20,32 +20,16 @@ module GoodJob
20
20
  #
21
21
  # The default value depends on the Rails environment:
22
22
  #
23
- # - +development+ and +test+: +:inline+
23
+ # - +development+: +:async:+
24
+ # -+test+: +:inline+
24
25
  # - +production+ and all other environments: +:external+
25
26
  #
26
- # @param max_threads [Integer, nil] sets the number of threads per scheduler to use when +execution_mode+ is set to +:async+. The +queues+ parameter can specify a number of threads for each group of queues which will override this value. You can also set this with the environment variable +GOOD_JOB_MAX_THREADS+. Defaults to +5+.
27
- # @param queues [String, nil] determines which queues to execute jobs from when +execution_mode+ is set to +:async+. See {file:README.md#optimize-queues-threads-and-processes} for more details on the format of this string. You can also set this with the environment variable +GOOD_JOB_QUEUES+. Defaults to +"*"+.
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 start_async_on_initialize || GoodJob.async_ready?
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 = 10000
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 = 24 * 60 * 60
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 = nil
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 = nil
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
@@ -56,8 +56,8 @@ module GoodJob
56
56
  # Exports values to hash
57
57
  # @return [Hash]
58
58
  def self.to_h
59
- ACCESSORS.each_with_object({}) do |accessor, hash|
60
- hash[accessor] = send(accessor)
59
+ ACCESSORS.index_with do |accessor|
60
+ send(accessor)
61
61
  end
62
62
  end
63
63
 
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
  module GoodJob
3
3
  # GoodJob gem version.
4
- VERSION = '2.99.0'
4
+ VERSION = '3.0.0'
5
5
  end
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: +false+).
47
- # By default, GoodJob destroys job records after the job is completed successfully.
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: false
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: +true+).
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: true
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::ActiveJobJob.where('finished_at <= ?', timestamp)
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
- # 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 ActiveJobJob
7
- class ActiveJobJob < 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::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::ActiveJobJob.where(cron_key: key)
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::ActiveJobJob.column_names.include?('cron_at')
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::ActiveJobJob.column_names.include?('cron_at')
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::ActiveJobJob', foreign_key: 'active_job_id', primary_key: 'active_job_id', optional: true, inverse_of: :executions
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
- # DEPRECATION: Remove deprecated `good_job:` parameter in GoodJob v3
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)
@@ -1,11 +1,224 @@
1
1
  # frozen_string_literal: true
2
2
  module GoodJob
3
- # @deprecated Use {GoodJob::Execution} instead.
4
- class Job < Execution
5
- after_initialize do |_job|
6
- ActiveSupport::Deprecation.warn(
7
- "The `GoodJob::Job` class name is deprecated. Replace with `GoodJob::Execution`."
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: 2.99.0
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: 5.2.0
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: 5.2.0
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: 5.2.0
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: 5.2.0
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: 5.2.0
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: 5.2.0
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
@@ -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