good_job 3.15.13 → 3.16.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.
Files changed (79) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +56 -4
  3. data/README.md +27 -10
  4. data/app/controllers/good_job/application_controller.rb +16 -9
  5. data/app/controllers/good_job/batches_controller.rb +1 -0
  6. data/app/controllers/good_job/cron_entries_controller.rb +2 -1
  7. data/app/controllers/good_job/frontends_controller.rb +1 -0
  8. data/app/controllers/good_job/jobs_controller.rb +1 -0
  9. data/app/controllers/good_job/processes_controller.rb +1 -0
  10. data/app/filters/good_job/base_filter.rb +1 -0
  11. data/app/filters/good_job/batches_filter.rb +1 -0
  12. data/app/filters/good_job/jobs_filter.rb +1 -0
  13. data/app/helpers/good_job/application_helper.rb +1 -1
  14. data/app/models/concerns/good_job/error_events.rb +28 -0
  15. data/app/models/concerns/good_job/filterable.rb +1 -0
  16. data/app/models/{good_job → concerns/good_job}/lockable.rb +1 -0
  17. data/app/models/concerns/good_job/reportable.rb +1 -0
  18. data/app/models/good_job/active_record_parent_class.rb +9 -0
  19. data/app/models/good_job/base_execution.rb +10 -1
  20. data/app/models/good_job/base_record.rb +4 -1
  21. data/app/models/good_job/cron_entry.rb +1 -0
  22. data/app/models/good_job/discrete_execution.rb +9 -0
  23. data/app/models/good_job/execution.rb +41 -8
  24. data/app/models/good_job/execution_result.rb +17 -2
  25. data/app/models/good_job/i18n_config.rb +25 -0
  26. data/app/models/good_job/job.rb +4 -1
  27. data/app/models/good_job/process.rb +51 -18
  28. data/app/views/good_job/jobs/_executions.erb +1 -1
  29. data/app/views/good_job/jobs/_table.erb +17 -4
  30. data/app/views/good_job/processes/index.html.erb +6 -2
  31. data/app/views/good_job/shared/_navbar.erb +1 -1
  32. data/config/locales/de.yml +9 -1
  33. data/config/locales/en.yml +9 -1
  34. data/config/locales/es.yml +9 -1
  35. data/config/locales/fr.yml +9 -1
  36. data/config/locales/ja.yml +9 -1
  37. data/config/locales/nl.yml +9 -1
  38. data/config/locales/ru.yml +9 -1
  39. data/config/locales/tr.yml +227 -0
  40. data/config/locales/{ua.yml → uk.yml} +34 -2
  41. data/config/routes.rb +1 -0
  42. data/exe/good_job +1 -0
  43. data/lib/active_job/queue_adapters/good_job_adapter.rb +1 -0
  44. data/lib/generators/good_job/install_generator.rb +1 -0
  45. data/lib/generators/good_job/templates/install/migrations/create_good_jobs.rb.erb +5 -1
  46. data/lib/generators/good_job/templates/update/migrations/01_create_good_jobs.rb.erb +3 -1
  47. data/lib/generators/good_job/templates/update/migrations/02_create_good_job_settings.rb.erb +1 -0
  48. data/lib/generators/good_job/templates/update/migrations/03_create_index_good_jobs_jobs_on_priority_created_at_when_unfinished.rb.erb +1 -0
  49. data/lib/generators/good_job/templates/update/migrations/04_create_good_job_batches.rb.erb +1 -0
  50. data/lib/generators/good_job/templates/update/migrations/05_create_good_job_executions.rb.erb +1 -0
  51. data/lib/generators/good_job/templates/update/migrations/06_create_good_jobs_error_event.rb.erb +16 -0
  52. data/lib/generators/good_job/update_generator.rb +1 -0
  53. data/lib/good_job/active_job_extensions/batches.rb +1 -0
  54. data/lib/good_job/active_job_extensions/concurrency.rb +2 -1
  55. data/lib/good_job/active_job_extensions/interrupt_errors.rb +1 -0
  56. data/lib/good_job/active_job_extensions/notify_options.rb +1 -0
  57. data/lib/good_job/adapter.rb +3 -2
  58. data/lib/good_job/assignable_connection.rb +1 -0
  59. data/lib/good_job/bulk.rb +1 -0
  60. data/lib/good_job/capsule.rb +5 -4
  61. data/lib/good_job/cleanup_tracker.rb +2 -1
  62. data/lib/good_job/cli.rb +1 -0
  63. data/lib/good_job/configuration.rb +7 -0
  64. data/lib/good_job/cron_manager.rb +3 -3
  65. data/lib/good_job/current_thread.rb +8 -0
  66. data/lib/good_job/daemon.rb +1 -0
  67. data/lib/good_job/engine.rb +13 -0
  68. data/lib/good_job/interrupt_error.rb +1 -0
  69. data/lib/good_job/job_performer.rb +1 -0
  70. data/lib/good_job/log_subscriber.rb +1 -0
  71. data/lib/good_job/metrics.rb +57 -0
  72. data/lib/good_job/multi_scheduler.rb +1 -0
  73. data/lib/good_job/notifier/{process_registration.rb → process_heartbeat.rb} +10 -1
  74. data/lib/good_job/notifier.rb +18 -16
  75. data/lib/good_job/poller.rb +3 -3
  76. data/lib/good_job/scheduler.rb +30 -11
  77. data/lib/good_job/version.rb +2 -1
  78. data/lib/good_job.rb +15 -1
  79. metadata +11 -5
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ce708c401123eb5129c55505dbc02c6e7bca76f6e3d0886a43a359fb1861d99f
4
- data.tar.gz: 477481c84a743ba4f3a07e7940c8a0cd21c0ec99b4f86277bf86db5e1529a2ad
3
+ metadata.gz: 20bb64485159969378a5abf5d960de13a3f27df63846f85cda6e4ef410c9af23
4
+ data.tar.gz: de0d39b68577c973ca4a4c9d9bccd8bda29bad7e6ca5a936112db7c8df6b038b
5
5
  SHA512:
6
- metadata.gz: 50152f710bbb121123e76a5ced97b06c0b0427cc6eefe6229d9fc6756cb586cba1f1d5624ddea198879a461909e34e631e8f5147940fcef8daf4615fd5c32311
7
- data.tar.gz: a7981cac24a946d8edfc7a892a35383376325071bf89da5321f2c05138907d90ac0ca4e3220277ecc3d32a709abb9b63262c7d97581a3514e64488a645994ff2
6
+ metadata.gz: 9fa9474d0ee2ff9e8e47aa5e3cc31d6cd1924c7d00276a9da2bc4611b970ea80d68a0bc79c9da7f4266c85e0bedfc491860dabff4d4e1088d485a11a21a895f5
7
+ data.tar.gz: eb6a2ea5cdacd25fd930aab7eba6b498ffb5998fa82c78e552f7acaebf59f6f6cd950fb6488e4c317b461ff8931df5250820cdeb46d40a4047a1e8c24495756b
data/CHANGELOG.md CHANGED
@@ -1,5 +1,61 @@
1
1
  # Changelog
2
2
 
3
+ ## [v3.16.0](https://github.com/bensheldon/good_job/tree/v3.16.0) (2023-07-10)
4
+
5
+ [Full Changelog](https://github.com/bensheldon/good_job/compare/v3.15.14...v3.16.0)
6
+
7
+ **Implemented enhancements:**
8
+
9
+ - Add `GoodJob.configure_active_record` as alternative to `GoodJob.active_record_parent_class` [\#1004](https://github.com/bensheldon/good_job/pull/1004) ([bensheldon](https://github.com/bensheldon))
10
+ - Configure `dashboard_default_locale` using custom subclass of `I18n::Config` to isolate I18n configuration from parent application [\#1001](https://github.com/bensheldon/good_job/pull/1001) ([bensheldon](https://github.com/bensheldon))
11
+ - Create `error_event` column to track the context of an error \(discarded, retried, retry\_stopped, etc\) [\#995](https://github.com/bensheldon/good_job/pull/995) ([bensheldon](https://github.com/bensheldon))
12
+ - Added metrics to Scheduler and track in Process state [\#984](https://github.com/bensheldon/good_job/pull/984) ([AndersGM](https://github.com/AndersGM))
13
+
14
+ **Fixed bugs:**
15
+
16
+ - Use Concurrent::Array for class `instances` to avoid JRuby synchronization errors [\#1002](https://github.com/bensheldon/good_job/pull/1002) ([bensheldon](https://github.com/bensheldon))
17
+ - Add test to assert enqueuing behavior within transactions [\#998](https://github.com/bensheldon/good_job/pull/998) ([bensheldon](https://github.com/bensheldon))
18
+ - Fix Ukrainian language code [\#996](https://github.com/bensheldon/good_job/pull/996) ([bensheldon](https://github.com/bensheldon))
19
+
20
+ **Closed issues:**
21
+
22
+ - Is `pgcrypto` necessary? [\#805](https://github.com/bensheldon/good_job/issues/805)
23
+ - Integrate Sorbet type checking [\#404](https://github.com/bensheldon/good_job/issues/404)
24
+
25
+ **Merged pull requests:**
26
+
27
+ - Unify `frozen_string_literal` comment style [\#1003](https://github.com/bensheldon/good_job/pull/1003) ([dixpac](https://github.com/dixpac))
28
+ - Add more execution mode details and caveats to Readme Set Up section [\#997](https://github.com/bensheldon/good_job/pull/997) ([bensheldon](https://github.com/bensheldon))
29
+ - Add note in migrations that `pgcrypto` extension isn't necessary in PG 13+ [\#837](https://github.com/bensheldon/good_job/pull/837) ([bensheldon](https://github.com/bensheldon))
30
+ - Add Sorbet to linter [\#760](https://github.com/bensheldon/good_job/pull/760) ([sam1el](https://github.com/sam1el))
31
+
32
+ ## [v3.15.14](https://github.com/bensheldon/good_job/tree/v3.15.14) (2023-07-03)
33
+
34
+ [Full Changelog](https://github.com/bensheldon/good_job/compare/v3.15.13...v3.15.14)
35
+
36
+ **Implemented enhancements:**
37
+
38
+ - Add Process heartbeat that is updated inside of Notifier [\#977](https://github.com/bensheldon/good_job/pull/977) ([bensheldon](https://github.com/bensheldon))
39
+
40
+ **Fixed bugs:**
41
+
42
+ - Dashboard error when trying to display 0 running/queued/retried jobs due to missing 'zero' translation [\#990](https://github.com/bensheldon/good_job/issues/990)
43
+ - Add explicit namespace back to `GoodJob::DiscreteExecution` [\#983](https://github.com/bensheldon/good_job/pull/983) ([bensheldon](https://github.com/bensheldon))
44
+
45
+ **Closed issues:**
46
+
47
+ - Persisting ActiveSupport::CurrentAttributes [\#981](https://github.com/bensheldon/good_job/issues/981)
48
+ - uninitialized constant DiscreteExecution [\#962](https://github.com/bensheldon/good_job/issues/962)
49
+ - Hard kill resilience with execution counts [\#922](https://github.com/bensheldon/good_job/issues/922)
50
+
51
+ **Merged pull requests:**
52
+
53
+ - Bump rubocop from 1.53.0 to 1.54.0 [\#994](https://github.com/bensheldon/good_job/pull/994) ([dependabot[bot]](https://github.com/apps/dependabot))
54
+ - Bump rails from 7.0.5 to 7.0.6 [\#993](https://github.com/bensheldon/good_job/pull/993) ([dependabot[bot]](https://github.com/apps/dependabot))
55
+ - Fix CI: Lock traces version for Ruby 2.6 compatible version [\#987](https://github.com/bensheldon/good_job/pull/987) ([bensheldon](https://github.com/bensheldon))
56
+ - Turkish Language support [\#986](https://github.com/bensheldon/good_job/pull/986) ([SemihCag](https://github.com/SemihCag))
57
+ - Use generic error reporter in Readme examples [\#964](https://github.com/bensheldon/good_job/pull/964) ([shouichi](https://github.com/shouichi))
58
+
3
59
  ## [v3.15.13](https://github.com/bensheldon/good_job/tree/v3.15.13) (2023-06-14)
4
60
 
5
61
  [Full Changelog](https://github.com/bensheldon/good_job/compare/v3.15.12...v3.15.13)
@@ -43,10 +99,6 @@
43
99
 
44
100
  - Ensure migration warning for `DiscreteExecution` constant is in explicit `GoodJob::` namespace [\#963](https://github.com/bensheldon/good_job/pull/963) ([bensheldon](https://github.com/bensheldon))
45
101
 
46
- **Closed issues:**
47
-
48
- - uninitialized constant DiscreteExecution [\#962](https://github.com/bensheldon/good_job/issues/962)
49
-
50
102
  ## [v3.15.9](https://github.com/bensheldon/good_job/tree/v3.15.9) (2023-05-21)
51
103
 
52
104
  [Full Changelog](https://github.com/bensheldon/good_job/compare/v3.15.8...v3.15.9)
data/README.md CHANGED
@@ -121,8 +121,13 @@ For more of the story of GoodJob, read the [introductory blog post](https://isla
121
121
  YourJob.set(queue: :some_queue, wait: 5.minutes, priority: 10).perform_later
122
122
  ```
123
123
 
124
- 1. In development, GoodJob executes jobs immediately in a separate thread ("async" mode). In production, GoodJob provides different options:
125
-
124
+ 1. **In Rails' development environment**, by default, GoodJob's Adapter executes jobs `async` in a background thread pool in `rails server`.
125
+ - Because of Rails deferred autoloading, jobs enqueued via the `rails console` may not begin executing on a separate server process until the Rails application is fully initialized by loading a web page once.
126
+ - Remember, only Active Job's `perform_later` sends jobs to the queue adapter; Active Job's `perform_now` executes the job immediately and does not invoke the queue adapter. GoodJob is not involved in `perform_now` jobs.
127
+ 1. **In Rails' test environment**, by default, GoodJob's Adapter executes jobs `inline` immediately in the current thread.
128
+ - Future-scheduled jobs can be executed with `GoodJob.perform_inline` using using a tool like Timecop or `ActiveSupport::Testing::TimeHelpers`.
129
+ - Note that Active Job's TestAdapter, which powers test helpers (e.g. `assert_enqueued_with`), may override GoodJob's Adapter in [some configurations](https://github.com/rails/rails/issues/37270).
130
+ 1. **In Rails' production environment**, by default, GoodJob's Adapter enqueues jobs in `external` mode to be executed by a separate execution process:
126
131
  - By default, GoodJob separates job enqueuing from job execution so that jobs can be scaled independently of the web server. Use the GoodJob command-line tool to execute jobs:
127
132
 
128
133
  ```bash
@@ -231,7 +236,7 @@ Rails.application.configure do
231
236
  # Configure options individually...
232
237
  config.good_job.preserve_job_records = true
233
238
  config.good_job.retry_on_unhandled_error = false
234
- config.good_job.on_thread_error = -> (exception) { Sentry.capture_exception(exception) }
239
+ config.good_job.on_thread_error = -> (exception) { Rails.error.report(exception) }
235
240
  config.good_job.execution_mode = :async
236
241
  config.good_job.queues = '*'
237
242
  config.good_job.max_threads = 5
@@ -239,12 +244,13 @@ Rails.application.configure do
239
244
  config.good_job.shutdown_timeout = 25 # seconds
240
245
  config.good_job.enable_cron = true
241
246
  config.good_job.cron = { example: { cron: '0 * * * *', class: 'ExampleJob' } }
247
+ config.good_job.dashboard_default_locale = :en
242
248
 
243
249
  # ...or all at once.
244
250
  config.good_job = {
245
251
  preserve_job_records: true,
246
252
  retry_on_unhandled_error: false,
247
- on_thread_error: -> (exception) { Sentry.capture_exception(exception) },
253
+ on_thread_error: -> (exception) { Rails.error.report(exception) },
248
254
  execution_mode: :async,
249
255
  queues: '*',
250
256
  max_threads: 5,
@@ -257,6 +263,7 @@ Rails.application.configure do
257
263
  class: 'ExampleJob'
258
264
  },
259
265
  },
266
+ dashboard_default_locale: :en,
260
267
  }
261
268
  end
262
269
  ```
@@ -287,7 +294,7 @@ Available configuration options are:
287
294
  - `on_thread_error` (proc, lambda, or callable) will be called when there is an Exception. It can be useful for logging errors to bug tracking services, like Sentry or Airbrake. Example:
288
295
 
289
296
  ```ruby
290
- config.good_job.on_thread_error = -> (exception) { Sentry.capture_exception(exception) }
297
+ config.good_job.on_thread_error = -> (exception) { Rails.error.report(exception) }
291
298
  ```
292
299
 
293
300
  By default, GoodJob configures the following execution modes per environment:
@@ -311,7 +318,17 @@ config.good_job.execution_mode = :external
311
318
 
312
319
  Good Job’s general behavior can also be configured via attributes directly on the `GoodJob` module:
313
320
 
314
- - **`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._
321
+ - **`GoodJob.configure_active_record { ... }`** Inject Active Record configuration into GoodJob's base model, for example, 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. Example:
322
+
323
+ ```ruby
324
+ # config/initializers/good_job.rb
325
+ GoodJob.configure_active_record do
326
+ connects_to database: :special_database
327
+ self.table_name_prefix = "special_application_"
328
+ end
329
+ ```
330
+
331
+ - **`GoodJob.active_record_parent_class`** (string) Alternatively, modify the ActiveRecord parent class inherited by GoodJob's Active Record model `GoodJob::Job` (defaults to `"ActiveRecord::Base"`). Configure this _The value must be a String to avoid premature initialization of ActiveRecord._
315
332
 
316
333
  You’ll generally want to configure these in `config/initializers/good_job.rb`, like so:
317
334
 
@@ -760,7 +777,7 @@ If errors do reach GoodJob, you can assign a callable to `GoodJob.on_thread_erro
760
777
 
761
778
  ```ruby
762
779
  # config/initializers/good_job.rb
763
- GoodJob.on_thread_error = -> (exception) { Sentry.capture_exception(exception) }
780
+ GoodJob.on_thread_error = -> (exception) { Rails.error.report(exception) }
764
781
  ```
765
782
 
766
783
  #### Retries
@@ -794,13 +811,13 @@ class ApplicationJob < ActiveJob::Base
794
811
  retry_on StandardError, wait: :exponentially_longer, attempts: Float::INFINITY
795
812
 
796
813
  retry_on SpecialError, attempts: 5 do |_job, exception|
797
- Sentry.capture_exception(exception)
814
+ Rails.error.report(exception)
798
815
  end
799
816
 
800
817
  around_perform do |_job, block|
801
818
  block.call
802
819
  rescue StandardError => e
803
- Sentry.capture_exception(e)
820
+ Rails.error.report(e)
804
821
  raise
805
822
  end
806
823
  # ...
@@ -823,7 +840,7 @@ ActionMailer::MailDeliveryJob.retry_on StandardError, wait: :exponentially_longe
823
840
  ActionMailer::MailDeliveryJob.around_perform do |_job, block|
824
841
  block.call
825
842
  rescue StandardError => e
826
- Sentry.capture_exception(e)
843
+ Rails.error.report(e)
827
844
  raise
828
845
  end
829
846
  ```
@@ -1,9 +1,10 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module GoodJob
3
4
  class ApplicationController < ActionController::Base
4
5
  protect_from_forgery with: :exception
5
6
 
6
- around_action :switch_locale
7
+ around_action :use_good_job_locale
7
8
 
8
9
  content_security_policy do |policy|
9
10
  policy.default_src(:none) if policy.default_src(*policy.default_src).blank?
@@ -30,8 +31,21 @@ module GoodJob
30
31
  { locale: I18n.locale }.merge(options)
31
32
  end
32
33
 
33
- def switch_locale(&action)
34
+ def use_good_job_locale(&action)
35
+ @original_i18n_config = I18n.config
36
+ I18n.config = ::GoodJob::I18nConfig.new
34
37
  I18n.with_locale(current_locale, &action)
38
+ ensure
39
+ I18n.config = @original_i18n_config
40
+ @original_i18n_config = nil
41
+ end
42
+
43
+ def use_original_locale
44
+ prev_config = I18n.config
45
+ I18n.config = @original_i18n_config if @original_i18n_config
46
+ yield
47
+ ensure
48
+ I18n.config = prev_config
35
49
  end
36
50
 
37
51
  def current_locale
@@ -39,16 +53,9 @@ module GoodJob
39
53
  request.GET['locale']
40
54
  elsif params[:locale]
41
55
  params[:locale]
42
- elsif good_job_available_locales.exclude?(I18n.default_locale) && I18n.available_locales.include?(:en)
43
- :en
44
56
  else
45
57
  I18n.default_locale
46
58
  end
47
59
  end
48
-
49
- def good_job_available_locales
50
- @_good_job_available_locales ||= GoodJob::Engine.root.join("config/locales").glob("*.yml").map { |path| File.basename(path, ".yml").to_sym }.uniq
51
- end
52
- helper_method :good_job_available_locales
53
60
  end
54
61
  end
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module GoodJob
3
4
  class BatchesController < GoodJob::ApplicationController
4
5
  def index
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module GoodJob
3
4
  class CronEntriesController < GoodJob::ApplicationController
4
5
  before_action :check_settings_migration!, only: [:enable, :disable]
@@ -14,7 +15,7 @@ module GoodJob
14
15
 
15
16
  def enqueue
16
17
  @cron_entry = CronEntry.find(params[:cron_key])
17
- @cron_entry.enqueue(Time.current)
18
+ use_original_locale { @cron_entry.enqueue(Time.current) }
18
19
  redirect_back(fallback_location: cron_entries_path, notice: t(".notice"))
19
20
  end
20
21
 
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module GoodJob
3
4
  class FrontendsController < ActionController::Base # rubocop:disable Rails/ApplicationController
4
5
  skip_after_action :verify_same_origin_request, raise: false
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module GoodJob
3
4
  class JobsController < GoodJob::ApplicationController
4
5
  DISCARD_MESSAGE = "Discarded through dashboard"
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module GoodJob
3
4
  class ProcessesController < GoodJob::ApplicationController
4
5
  def index
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module GoodJob
3
4
  class BaseFilter
4
5
  DEFAULT_LIMIT = 25
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module GoodJob
3
4
  class BatchesFilter < BaseFilter
4
5
  def records
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module GoodJob
3
4
  class JobsFilter < BaseFilter
4
5
  def states
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module GoodJob
3
4
  module ApplicationHelper
4
5
  def format_duration(sec)
@@ -63,7 +64,6 @@ module GoodJob
63
64
  end
64
65
 
65
66
  def translation_exists?(key, **options)
66
- true if good_job_available_locales.include?(I18n.locale)
67
67
  I18n.exists?(scope_key_by_partial(key), **options)
68
68
  end
69
69
  end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GoodJob
4
+ # Shared methods for filtering Execution/Job records from the +good_jobs+ table.
5
+ module ErrorEvents
6
+ extend ActiveSupport::Concern
7
+
8
+ ERROR_EVENTS = [
9
+ ERROR_EVENT_INTERRUPTED = 'interrupted',
10
+ ERROR_EVENT_UNHANDLED = 'unhandled',
11
+ ERROR_EVENT_HANDLED = 'handled',
12
+ ERROR_EVENT_RETRIED = 'retried',
13
+ ERROR_EVENT_RETRY_STOPPED = 'retry_stopped',
14
+ ERROR_EVENT_DISCARDED = 'discarded',
15
+ ].freeze
16
+
17
+ included do
18
+ enum error_event: {
19
+ ERROR_EVENT_INTERRUPTED => 0,
20
+ ERROR_EVENT_UNHANDLED => 1,
21
+ ERROR_EVENT_HANDLED => 2,
22
+ ERROR_EVENT_RETRIED => 3,
23
+ ERROR_EVENT_RETRY_STOPPED => 4,
24
+ ERROR_EVENT_DISCARDED => 5,
25
+ }.freeze, _prefix: :error_event
26
+ end
27
+ end
28
+ end
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module GoodJob
3
4
  # Shared methods for filtering Execution/Job records from the +good_jobs+ table.
4
5
  module Filterable
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module GoodJob
3
4
  #
4
5
  # Adds Postgres advisory locking capabilities to an ActiveRecord record.
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module GoodJob
3
4
  module Reportable
4
5
  # There are 3 buckets of non-overlapping statuses:
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GoodJob
4
+ ActiveRecordParentClass = if GoodJob.active_record_parent_class
5
+ Object.const_get(GoodJob.active_record_parent_class)
6
+ else
7
+ ActiveRecord::Base
8
+ end
9
+ end
@@ -4,6 +4,8 @@ module GoodJob
4
4
  # ActiveRecord model to share behavior between {Job} and {Execution} models
5
5
  # which both read out of the same table.
6
6
  class BaseExecution < BaseRecord
7
+ include ErrorEvents
8
+
7
9
  self.table_name = 'good_jobs'
8
10
 
9
11
  # With a given class name
@@ -35,7 +37,14 @@ module GoodJob
35
37
  end
36
38
 
37
39
  def discrete_support?
38
- DiscreteExecution.migrated?
40
+ GoodJob::DiscreteExecution.migrated?
41
+ end
42
+
43
+ def error_event_migrated?
44
+ return true if columns_hash["error_event"].present?
45
+
46
+ migration_pending_warning!
47
+ false
39
48
  end
40
49
  end
41
50
 
@@ -1,10 +1,11 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module GoodJob
3
4
  # Base ActiveRecord class that all GoodJob models inherit from.
4
5
  # Parent class can be configured with +GoodJob.active_record_parent_class+.
5
6
  # @!parse
6
7
  # class BaseRecord < ActiveRecord::Base; end
7
- class BaseRecord < Object.const_get(GoodJob.active_record_parent_class)
8
+ class BaseRecord < ActiveRecordParentClass
8
9
  self.abstract_class = true
9
10
 
10
11
  def self.migration_pending_warning!
@@ -26,5 +27,7 @@ module GoodJob
26
27
  migration_pending_warning!
27
28
  false
28
29
  end
30
+
31
+ ActiveSupport.run_load_hooks(:good_job_base_record, self)
29
32
  end
30
33
  end
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  require "concurrent/hash"
3
4
  require "concurrent/scheduled_task"
4
5
  require "fugit"
@@ -2,6 +2,8 @@
2
2
 
3
3
  module GoodJob # :nodoc:
4
4
  class DiscreteExecution < BaseRecord
5
+ include ErrorEvents
6
+
5
7
  self.table_name = 'good_job_executions'
6
8
 
7
9
  belongs_to :execution, class_name: 'GoodJob::Execution', foreign_key: 'active_job_id', primary_key: 'active_job_id', inverse_of: :discrete_executions, optional: true
@@ -11,6 +13,13 @@ module GoodJob # :nodoc:
11
13
 
12
14
  alias_attribute :performed_at, :created_at
13
15
 
16
+ def self.error_event_migrated?
17
+ return true if columns_hash["error_event"].present?
18
+
19
+ migration_pending_warning!
20
+ false
21
+ end
22
+
14
23
  def number
15
24
  serialized_params.fetch('executions', 0) + 1
16
25
  end
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module GoodJob
3
4
  # ActiveRecord model that represents an +ActiveJob+ job.
4
5
  class Execution < BaseExecution
@@ -262,7 +263,11 @@ module GoodJob
262
263
  unfinished.dequeueing_ordered(parsed_queues).only_scheduled.limit(1).with_advisory_lock(unlock_session: true, select_limit: queue_select_limit) do |executions|
263
264
  execution = executions.first
264
265
  break if execution.blank?
265
- break :unlocked unless execution&.executable?
266
+
267
+ unless execution.executable?
268
+ result = ExecutionResult.new(value: nil, unexecutable: true)
269
+ break
270
+ end
266
271
 
267
272
  yield(execution) if block_given?
268
273
  result = execution.perform
@@ -373,10 +378,14 @@ module GoodJob
373
378
  if discrete?
374
379
  interrupt_error_string = self.class.format_error(GoodJob::InterruptError.new("Interrupted after starting perform at '#{performed_at}'"))
375
380
  self.error = interrupt_error_string
376
- discrete_executions.where(finished_at: nil).where.not(performed_at: nil).update_all( # rubocop:disable Rails/SkipsModelValidations
381
+ self.error_event = ERROR_EVENT_INTERRUPTED if self.class.error_event_migrated?
382
+
383
+ discrete_execution_attrs = {
377
384
  error: interrupt_error_string,
378
- finished_at: Time.current
379
- )
385
+ finished_at: Time.current,
386
+ }
387
+ discrete_execution_attrs[:error_event] = GoodJob::DiscreteExecution.error_events[GoodJob::DiscreteExecution::ERROR_EVENT_INTERRUPTED] if self.class.error_event_migrated?
388
+ discrete_executions.where(finished_at: nil).where.not(performed_at: nil).update_all(discrete_execution_attrs) # rubocop:disable Rails/SkipsModelValidations
380
389
  end
381
390
  end
382
391
 
@@ -405,15 +414,34 @@ module GoodJob
405
414
  end
406
415
  handled_error ||= current_thread.error_on_retry || current_thread.error_on_discard
407
416
 
417
+ error_event = if handled_error == current_thread.error_on_discard
418
+ ERROR_EVENT_DISCARDED
419
+ elsif handled_error == current_thread.error_on_retry
420
+ ERROR_EVENT_RETRIED
421
+ elsif handled_error == current_thread.error_on_retry_stopped
422
+ ERROR_EVENT_RETRY_STOPPED
423
+ elsif handled_error
424
+ ERROR_EVENT_HANDLED
425
+ end
426
+
408
427
  instrument_payload.merge!(
409
428
  value: value,
410
429
  handled_error: handled_error,
411
- retried: current_thread.execution_retried
430
+ retried: current_thread.execution_retried,
431
+ error_event: error_event
412
432
  )
413
- ExecutionResult.new(value: value, handled_error: handled_error, retried: current_thread.execution_retried)
433
+ ExecutionResult.new(value: value, handled_error: handled_error, error_event: error_event, retried: current_thread.execution_retried)
414
434
  rescue StandardError => e
435
+ error_event = if e.is_a?(GoodJob::InterruptError)
436
+ ERROR_EVENT_INTERRUPTED
437
+ elsif e == current_thread.error_on_retry_stopped
438
+ ERROR_EVENT_RETRY_STOPPED
439
+ else
440
+ ERROR_EVENT_UNHANDLED
441
+ end
442
+
415
443
  instrument_payload[:unhandled_error] = e
416
- ExecutionResult.new(value: nil, unhandled_error: e)
444
+ ExecutionResult.new(value: nil, unhandled_error: e, error_event: error_event)
417
445
  end
418
446
  end
419
447
 
@@ -422,9 +450,14 @@ module GoodJob
422
450
  if job_error
423
451
  error_string = self.class.format_error(job_error)
424
452
  self.error = error_string
425
- discrete_execution.error = error_string if discrete_execution
453
+ self.error_event = result.error_event if self.class.error_event_migrated?
454
+ if discrete_execution
455
+ discrete_execution.error = error_string
456
+ discrete_execution.error_event = result.error_event if discrete_execution.class.error_event_migrated?
457
+ end
426
458
  else
427
459
  self.error = nil
460
+ self.error_event = nil if self.class.error_event_migrated?
428
461
  end
429
462
 
430
463
  reenqueued = result.retried? || retried_good_job_id.present?
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module GoodJob
3
4
  # Stores the results of job execution
4
5
  class ExecutionResult
@@ -8,18 +9,32 @@ module GoodJob
8
9
  attr_reader :handled_error
9
10
  # @return [Exception, nil]
10
11
  attr_reader :unhandled_error
11
- # @return [Exception, nil]
12
+ # @return [String, nil]
13
+ attr_reader :error_event
14
+ # @return [Boolean, nil]
15
+ attr_reader :unexecutable
16
+ # @return [Boolean, nil]
12
17
  attr_reader :retried
13
18
  alias retried? retried
14
19
 
15
20
  # @param value [Object, nil]
16
21
  # @param handled_error [Exception, nil]
17
22
  # @param unhandled_error [Exception, nil]
18
- def initialize(value:, handled_error: nil, unhandled_error: nil, retried: false)
23
+ # @param error_event [String, nil]
24
+ # @param unexecutable [Boolean, nil]
25
+ # @param retried [Boolean, nil]
26
+ def initialize(value:, handled_error: nil, unhandled_error: nil, error_event: nil, unexecutable: nil, retried: false)
19
27
  @value = value
20
28
  @handled_error = handled_error
21
29
  @unhandled_error = unhandled_error
30
+ @error_event = error_event
31
+ @unexecutable = unexecutable
22
32
  @retried = retried
23
33
  end
34
+
35
+ # @return [Boolean]
36
+ def succeeded?
37
+ !(handled_error || unhandled_error || unexecutable || retried)
38
+ end
24
39
  end
25
40
  end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GoodJob
4
+ class I18nConfig < ::I18n::Config
5
+ BACKEND = I18n::Backend::Simple.new
6
+ AVAILABLE_LOCALES = GoodJob::Engine.root.join("config/locales").glob("*.yml").map { |path| File.basename(path, ".yml").to_sym }.uniq
7
+ AVAILABLE_LOCALES_SET = AVAILABLE_LOCALES.inject(Set.new) { |set, locale| set << locale.to_s << locale.to_sym }
8
+
9
+ def backend
10
+ BACKEND
11
+ end
12
+
13
+ def available_locales
14
+ AVAILABLE_LOCALES
15
+ end
16
+
17
+ def available_locales_set
18
+ AVAILABLE_LOCALES_SET
19
+ end
20
+
21
+ def default_locale
22
+ GoodJob.configuration.dashboard_default_locale
23
+ end
24
+ end
25
+ end
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module GoodJob
3
4
  # ActiveRecord model that represents an +ActiveJob+ job.
4
5
  # There is not a table in the database whose discrete rows represents "Jobs".
@@ -198,6 +199,7 @@ module GoodJob
198
199
 
199
200
  execution.class.transaction(joinable: false, requires_new: true) do
200
201
  new_active_job = active_job.retry_job(wait: 0, error: execution.error)
202
+ execution.error_event = ERROR_EVENT_RETRIED if execution.error && execution.class.error_event_migrated?
201
203
  execution.save!
202
204
  end
203
205
  end
@@ -221,7 +223,8 @@ module GoodJob
221
223
  update_execution = proc do
222
224
  execution.update(
223
225
  finished_at: Time.current,
224
- error: GoodJob::Execution.format_error(job_error)
226
+ error: GoodJob::Execution.format_error(job_error),
227
+ error_event: :discarded
225
228
  )
226
229
  end
227
230