good_job 4.23.1 → 4.24.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 (101) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +25 -0
  3. data/README.md +5 -1
  4. data/app/charts/good_job/base_chart.rb +1 -0
  5. data/app/charts/good_job/latency_metric/base.rb +2 -1
  6. data/app/charts/good_job/latency_metric/execution.rb +1 -0
  7. data/app/charts/good_job/latency_metric/queue.rb +1 -0
  8. data/app/charts/good_job/latency_metric/total.rb +1 -0
  9. data/app/charts/good_job/latency_metric.rb +1 -0
  10. data/app/charts/good_job/performance_index_chart.rb +1 -0
  11. data/app/charts/good_job/performance_range.rb +5 -3
  12. data/app/charts/good_job/performance_show_chart.rb +1 -0
  13. data/app/charts/good_job/scheduled_by_queue_chart.rb +1 -0
  14. data/app/controllers/good_job/application_controller.rb +7 -4
  15. data/app/controllers/good_job/batches_controller.rb +1 -0
  16. data/app/controllers/good_job/cleaner_controller.rb +1 -0
  17. data/app/controllers/good_job/cron_entries_controller.rb +1 -0
  18. data/app/controllers/good_job/executions_controller.rb +1 -0
  19. data/app/controllers/good_job/frontends_controller.rb +9 -4
  20. data/app/controllers/good_job/jobs_controller.rb +1 -0
  21. data/app/controllers/good_job/metrics_controller.rb +1 -0
  22. data/app/controllers/good_job/pauses_controller.rb +1 -0
  23. data/app/controllers/good_job/performance_controller.rb +1 -0
  24. data/app/controllers/good_job/processes_controller.rb +1 -0
  25. data/app/filters/good_job/base_filter.rb +10 -1
  26. data/app/filters/good_job/batches_filter.rb +1 -0
  27. data/app/filters/good_job/jobs_filter.rb +4 -1
  28. data/app/helpers/good_job/application_helper.rb +1 -0
  29. data/app/helpers/good_job/icons_helper.rb +1 -0
  30. data/app/models/concerns/good_job/advisory_lockable.rb +23 -5
  31. data/app/models/concerns/good_job/error_events.rb +1 -0
  32. data/app/models/concerns/good_job/filterable.rb +1 -0
  33. data/app/models/concerns/good_job/filtered_arguments.rb +1 -0
  34. data/app/models/concerns/good_job/reportable.rb +1 -0
  35. data/app/models/good_job/active_record_parent_class.rb +1 -0
  36. data/app/models/good_job/base_record.rb +1 -0
  37. data/app/models/good_job/batch.rb +17 -16
  38. data/app/models/good_job/batch_record.rb +10 -8
  39. data/app/models/good_job/concurrency_claim.rb +1 -0
  40. data/app/models/good_job/cron_entry.rb +1 -0
  41. data/app/models/good_job/discrete_execution.rb +1 -0
  42. data/app/models/good_job/execution.rb +6 -3
  43. data/app/models/good_job/execution_result.rb +1 -0
  44. data/app/models/good_job/i18n_config.rb +1 -0
  45. data/app/models/good_job/job/lockable.rb +1 -0
  46. data/app/models/good_job/job.rb +20 -16
  47. data/app/models/good_job/process.rb +5 -5
  48. data/app/models/good_job/setting.rb +5 -2
  49. data/config/brakeman.ignore +3 -3
  50. data/lib/active_job/queue_adapters/good_job_adapter.rb +1 -0
  51. data/lib/generators/good_job/install_generator.rb +1 -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 +1 -0
  55. data/lib/good_job/active_job_extensions/interrupt_errors.rb +1 -0
  56. data/lib/good_job/active_job_extensions/labels.rb +1 -0
  57. data/lib/good_job/active_job_extensions/notify_options.rb +1 -0
  58. data/lib/good_job/adapter/inline_buffer.rb +1 -0
  59. data/lib/good_job/adapter.rb +25 -32
  60. data/lib/good_job/bulk.rb +1 -0
  61. data/lib/good_job/callable.rb +1 -0
  62. data/lib/good_job/capsule.rb +4 -0
  63. data/lib/good_job/capsule_tracker.rb +6 -6
  64. data/lib/good_job/cleanup_tracker.rb +1 -0
  65. data/lib/good_job/cli.rb +5 -0
  66. data/lib/good_job/configuration/validator.rb +1 -0
  67. data/lib/good_job/configuration.rb +13 -0
  68. data/lib/good_job/cron_manager.rb +1 -0
  69. data/lib/good_job/current_thread.rb +2 -1
  70. data/lib/good_job/daemon.rb +1 -0
  71. data/lib/good_job/dependencies.rb +1 -0
  72. data/lib/good_job/engine.rb +3 -1
  73. data/lib/good_job/instance_registry.rb +5 -2
  74. data/lib/good_job/interrupt_error.rb +1 -0
  75. data/lib/good_job/interrupted_error.rb +1 -0
  76. data/lib/good_job/job_performer/metrics.rb +1 -0
  77. data/lib/good_job/job_performer.rb +5 -4
  78. data/lib/good_job/lifecycle_hooks.rb +1 -0
  79. data/lib/good_job/log_subscriber.rb +1 -0
  80. data/lib/good_job/multi_scheduler.rb +1 -0
  81. data/lib/good_job/notifier/process_heartbeat.rb +1 -0
  82. data/lib/good_job/notifier.rb +1 -0
  83. data/lib/good_job/poller.rb +1 -0
  84. data/lib/good_job/probe_server/cluster_healthcheck_middleware.rb +1 -0
  85. data/lib/good_job/probe_server/healthcheck_middleware.rb +1 -0
  86. data/lib/good_job/probe_server/not_found_app.rb +1 -0
  87. data/lib/good_job/probe_server/simple_handler.rb +48 -11
  88. data/lib/good_job/probe_server/webrick_handler.rb +1 -0
  89. data/lib/good_job/probe_server.rb +1 -0
  90. data/lib/good_job/ractors.rb +1 -0
  91. data/lib/good_job/safe_state.rb +1 -0
  92. data/lib/good_job/scheduler.rb +1 -0
  93. data/lib/good_job/sd_notify.rb +1 -0
  94. data/lib/good_job/shared_executor.rb +1 -0
  95. data/lib/good_job/subprocess.rb +1 -0
  96. data/lib/good_job/supervisor.rb +22 -15
  97. data/lib/good_job/systemd_service.rb +1 -0
  98. data/lib/good_job/thread_status.rb +1 -0
  99. data/lib/good_job/version.rb +2 -1
  100. data/lib/good_job.rb +47 -21
  101. metadata +2 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 62018fa8fb4313f6aa3e6ef7531ac3073adc80449fd28a03f655014ce0c1922b
4
- data.tar.gz: 478315a41ea1d69a9d15781b4b57ecd099504c3da754605b12fd752a10cb3ad0
3
+ metadata.gz: ca3ea73eb3548ddaa1b84dab18b341c9c8182f91d52b82a41681cfa2080550da
4
+ data.tar.gz: 9fc5590d811d1b04c847054d685d9f911910356c33d0726d1ba1620a2228caf5
5
5
  SHA512:
6
- metadata.gz: 2f3f11b6633b90116e2cbe15057032cdd49609c074a80c21ef496ec250dea79f9ce5f3b3eda27db5c36bfd4cd1c262fda7b41301a168772148d70d35ca3f7dc7
7
- data.tar.gz: 33ef016c46794b39cd7006b16073849c76d62116ab2c42030087b91286ebfd20887bf8735c7f0183fcd8a5acb4c599ae7eabbddcb36420f3a2484044301fc231
6
+ metadata.gz: 2bca41349e888ed1e16ad8a3da30cfb17390eb1451803aa14b2bed95c39494a6cf83b7eb29a6cb39d86b1545756d58ced8c3cbf4acd5d53d56873d2652ce5766
7
+ data.tar.gz: f10b1e0bd0f51edce6879785642730d96367fe2f4fdd5d4206576c510993950fb278db1e38d3ed6c3a2bd1a9050aa99502a4e81a2e8dd3f2a78806c5c509b0a0
data/CHANGELOG.md CHANGED
@@ -1,5 +1,30 @@
1
1
  # Changelog
2
2
 
3
+ ## [v4.24.0](https://github.com/bensheldon/good_job/tree/v4.24.0) (2026-10-29)
4
+
5
+ [Full Changelog](https://github.com/bensheldon/good_job/compare/v4.23.1...v4.24.0)
6
+
7
+ **Implemented enhancements:**
8
+
9
+ - Add a separate retention period for discarded jobs [\#1897](https://github.com/bensheldon/good_job/pull/1897) ([IslamElsayed](https://github.com/IslamElsayed))
10
+
11
+ **Fixed bugs:**
12
+
13
+ - Prevent idle or malformed connections from stalling or stopping the probe server [\#1902](https://github.com/bensheldon/good_job/pull/1902) ([bensheldon](https://github.com/bensheldon))
14
+ - Validate advisory lock SQL identifiers and harden dashboard filter params [\#1901](https://github.com/bensheldon/good_job/pull/1901) ([bensheldon](https://github.com/bensheldon))
15
+ - Don't retain or report schedulers replaced by a capsule restart [\#1894](https://github.com/bensheldon/good_job/pull/1894) ([bensheldon](https://github.com/bensheldon))
16
+
17
+ **Closed issues:**
18
+
19
+ - \[feature request\] Cleanup older discarded jobs [\#1597](https://github.com/bensheldon/good_job/issues/1597)
20
+
21
+ **Merged pull requests:**
22
+
23
+ - Restructure code so it type-checks under Sorbet without runtime assertions [\#1899](https://github.com/bensheldon/good_job/pull/1899) ([bensheldon](https://github.com/bensheldon))
24
+ - Upgrade development/demo Ruby to 4.0.9 and update dependencies [\#1898](https://github.com/bensheldon/good_job/pull/1898) ([bensheldon](https://github.com/bensheldon))
25
+ - Fix flaky JRuby FrozenError by preloading record schemas in specs [\#1896](https://github.com/bensheldon/good_job/pull/1896) ([bensheldon](https://github.com/bensheldon))
26
+ - Demo: shorten database connection lifetimes [\#1895](https://github.com/bensheldon/good_job/pull/1895) ([bensheldon](https://github.com/bensheldon))
27
+
3
28
  ## [v4.23.1](https://github.com/bensheldon/good_job/tree/v4.23.1) (2026-10-27)
4
29
 
5
30
  [Full Changelog](https://github.com/bensheldon/good_job/compare/v4.23.0...v4.23.1)
data/README.md CHANGED
@@ -236,7 +236,8 @@ Usage:
236
236
  good_job cleanup_preserved_jobs
237
237
 
238
238
  Options:
239
- [--before-seconds-ago=SECONDS] # Destroy records finished more than this many seconds ago (env var: GOOD_JOB_CLEANUP_PRESERVED_JOBS_BEFORE_SECONDS_AGO, default: 1209600 (14 days))
239
+ [--before-seconds-ago=SECONDS] # Destroy records finished more than this many seconds ago (env var: GOOD_JOB_CLEANUP_PRESERVED_JOBS_BEFORE_SECONDS_AGO, default: 1209600 (14 days))
240
+ [--discarded-before-seconds-ago=SECONDS] # Destroy discarded records finished more than this many seconds ago (env var: GOOD_JOB_CLEANUP_DISCARDED_JOBS_BEFORE_SECONDS_AGO, default: same as --before-seconds-ago)
240
241
 
241
242
  Manually destroys preserved job records.
242
243
 
@@ -317,6 +318,7 @@ Available configuration options are:
317
318
  - `cron` (hash) cron configuration. Defaults to `{}`. You can also set this as a JSON string with the environment variable `GOOD_JOB_CRON`
318
319
  - `cleanup_discarded_jobs` (boolean) whether to destroy discarded jobs when cleaning up preserved jobs using the `$ good_job cleanup_preserved_jobs` CLI command or calling `GoodJob.cleanup_preserved_jobs`. Defaults to `true`. Can also be set with the environment variable `GOOD_JOB_CLEANUP_DISCARDED_JOBS`.
319
320
  - `cleanup_preserved_jobs_before_seconds_ago` (integer) number of seconds to preserve jobs when using the `$ good_job cleanup_preserved_jobs` CLI command or calling `GoodJob.cleanup_preserved_jobs`. Defaults to `1209600` (14 days). Can also be set with the environment variable `GOOD_JOB_CLEANUP_PRESERVED_JOBS_BEFORE_SECONDS_AGO`.
321
+ - `cleanup_discarded_jobs_before_seconds_ago` (integer) number of seconds to preserve discarded jobs, when they should be kept longer (or shorter) than other jobs. Has no effect when `cleanup_discarded_jobs` is `false`. Defaults to `nil`, which uses `cleanup_preserved_jobs_before_seconds_ago`. Can also be set with the environment variable `GOOD_JOB_CLEANUP_DISCARDED_JOBS_BEFORE_SECONDS_AGO`.
320
322
  - `cleanup_interval_jobs` (integer) Number of jobs a Scheduler will execute before cleaning up preserved jobs. Defaults to `1000`. Disable with `false`. Can also be set with the environment variable `GOOD_JOB_CLEANUP_INTERVAL_JOBS` and disabled with `0`).
321
323
  - `cleanup_interval_seconds` (integer) Number of seconds a Scheduler will wait before cleaning up preserved jobs. Defaults to `600` (10 minutes). Disable with `false`. Can also be set with the environment variable `GOOD_JOB_CLEANUP_INTERVAL_SECONDS` and disabled with `0`).
322
324
  - `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`).
@@ -1600,6 +1602,7 @@ GoodJob will automatically delete preserved job records after 14 days. The reten
1600
1602
 
1601
1603
  ```ruby
1602
1604
  config.good_job.cleanup_preserved_jobs_before_seconds_ago = 14.days
1605
+ config.good_job.cleanup_discarded_jobs_before_seconds_ago = 90.days # Optional: keep discarded jobs longer than the others.
1603
1606
  config.good_job.cleanup_interval_jobs = 1_000 # Number of executed jobs between deletion sweeps.
1604
1607
  config.good_job.cleanup_interval_seconds = 10.minutes # Number of seconds between deletion sweeps.
1605
1608
  ```
@@ -1611,6 +1614,7 @@ It is also possible to manually trigger a cleanup of preserved job records:
1611
1614
  ```ruby
1612
1615
  GoodJob.cleanup_preserved_jobs # Will use default retention period
1613
1616
  GoodJob.cleanup_preserved_jobs(older_than: 7.days) # custom retention period
1617
+ GoodJob.cleanup_preserved_jobs(older_than: 7.days, discarded_older_than: 90.days) # keep discarded jobs longer
1614
1618
  ```
1615
1619
 
1616
1620
  - For example, using the `good_job` command-line utility:
@@ -1,3 +1,4 @@
1
+ # typed: true
1
2
  # frozen_string_literal: true
2
3
 
3
4
  module GoodJob
@@ -1,3 +1,4 @@
1
+ # typed: true
1
2
  # frozen_string_literal: true
2
3
 
3
4
  module GoodJob
@@ -6,7 +7,7 @@ module GoodJob
6
7
  # expression over +good_job_executions+, the aggregate that summarizes it per
7
8
  # time bucket, and their titles.
8
9
  class Base
9
- def expression = self.class::EXPRESSION
10
+ def expression = self.class.const_get(:EXPRESSION)
10
11
 
11
12
  def to_arel = Arel.sql(expression)
12
13
 
@@ -1,3 +1,4 @@
1
+ # typed: true
1
2
  # frozen_string_literal: true
2
3
 
3
4
  module GoodJob
@@ -1,3 +1,4 @@
1
+ # typed: true
1
2
  # frozen_string_literal: true
2
3
 
3
4
  module GoodJob
@@ -1,3 +1,4 @@
1
+ # typed: true
1
2
  # frozen_string_literal: true
2
3
 
3
4
  module GoodJob
@@ -1,3 +1,4 @@
1
+ # typed: true
1
2
  # frozen_string_literal: true
2
3
 
3
4
  module GoodJob
@@ -1,3 +1,4 @@
1
+ # typed: true
1
2
  # frozen_string_literal: true
2
3
 
3
4
  module GoodJob
@@ -1,3 +1,4 @@
1
+ # typed: true
1
2
  # frozen_string_literal: true
2
3
 
3
4
  module GoodJob
@@ -281,8 +282,9 @@ module GoodJob
281
282
  components = match.captures
282
283
  components[-1] ||= "0"
283
284
  numeric_components = components.map { |component| Integer(component, 10) }
284
- local_time = Time.utc(*numeric_components)
285
- normalized_value = format("%04d-%02d-%02dT%02d:%02d:%02d", *numeric_components)
285
+ year, month, day, hour, minute, second = numeric_components
286
+ local_time = Time.utc(year, month, day, hour, minute, second)
287
+ normalized_value = format("%04d-%02d-%02dT%02d:%02d:%02d", year, month, day, hour, minute, second)
286
288
  return unless local_time.strftime("%Y-%m-%dT%H:%M:%S") == normalized_value
287
289
 
288
290
  periods = @local_time_zone.tzinfo.periods_for_local(local_time)
@@ -291,7 +293,7 @@ module GoodJob
291
293
  # A local input cannot carry an offset. Include both repeated fall-back occurrences
292
294
  # by resolving starts to the earlier instant and ends to the later instant.
293
295
  instants = periods.map do |period|
294
- (local_time - period.utc_total_offset).in_time_zone
296
+ local_time.ago(period.utc_total_offset).in_time_zone
295
297
  end
296
298
  parameter_key == "chart_end" ? instants.max : instants.min
297
299
  end
@@ -1,3 +1,4 @@
1
+ # typed: true
1
2
  # frozen_string_literal: true
2
3
 
3
4
  module GoodJob
@@ -1,3 +1,4 @@
1
+ # typed: true
1
2
  # frozen_string_literal: true
2
3
 
3
4
  module GoodJob
@@ -1,3 +1,4 @@
1
+ # typed: true
1
2
  # frozen_string_literal: true
2
3
 
3
4
  module GoodJob
@@ -19,14 +20,16 @@ module GoodJob
19
20
  policy.frame_ancestors(:none) if policy.frame_ancestors(*policy.frame_ancestors).blank?
20
21
  end
21
22
 
22
- before_action do
23
- next if request.content_security_policy_nonce_generator
23
+ before_action :set_content_security_policy_nonce_generator
24
+
25
+ private
26
+
27
+ def set_content_security_policy_nonce_generator
28
+ return if request.content_security_policy_nonce_generator
24
29
 
25
30
  request.content_security_policy_nonce_generator = ->(_request) { SecureRandom.base64(16) }
26
31
  end
27
32
 
28
- private
29
-
30
33
  def default_url_options(options = {})
31
34
  { locale: I18n.locale }.merge(options)
32
35
  end
@@ -1,3 +1,4 @@
1
+ # typed: true
1
2
  # frozen_string_literal: true
2
3
 
3
4
  module GoodJob
@@ -1,3 +1,4 @@
1
+ # typed: true
1
2
  # frozen_string_literal: true
2
3
 
3
4
  module GoodJob
@@ -1,3 +1,4 @@
1
+ # typed: true
1
2
  # frozen_string_literal: true
2
3
 
3
4
  module GoodJob
@@ -1,3 +1,4 @@
1
+ # typed: true
1
2
  # frozen_string_literal: true
2
3
 
3
4
  module GoodJob
@@ -1,3 +1,4 @@
1
+ # typed: true
1
2
  # frozen_string_literal: true
2
3
 
3
4
  module GoodJob
@@ -6,7 +7,7 @@ module GoodJob
6
7
  skip_after_action :verify_same_origin_request, raise: false
7
8
 
8
9
  def self.asset_path(*path)
9
- Engine.root.join("app/frontend/good_job", *path)
10
+ path.reduce(Engine.root.join("app/frontend/good_job")) { |asset_path, segment| asset_path.join(segment) }
10
11
  end
11
12
 
12
13
  STATIC_ASSETS = {
@@ -38,9 +39,7 @@ module GoodJob
38
39
  end.merge(MODULE_OVERRIDES)
39
40
  end
40
41
 
41
- before_action do
42
- expires_in 1.year, public: true
43
- end
42
+ before_action :set_cache_headers
44
43
 
45
44
  def static
46
45
  render file: STATIC_ASSETS.dig(params[:format]&.to_sym, params[:id]&.to_sym) || raise(ActionController::RoutingError, 'Not Found')
@@ -51,5 +50,11 @@ module GoodJob
51
50
 
52
51
  render file: self.class.js_modules[params[:id]&.to_sym] || raise(ActionController::RoutingError, 'Not Found')
53
52
  end
53
+
54
+ private
55
+
56
+ def set_cache_headers
57
+ expires_in 1.year, public: true
58
+ end
54
59
  end
55
60
  end
@@ -1,3 +1,4 @@
1
+ # typed: true
1
2
  # frozen_string_literal: true
2
3
 
3
4
  module GoodJob
@@ -1,3 +1,4 @@
1
+ # typed: true
1
2
  # frozen_string_literal: true
2
3
 
3
4
  module GoodJob
@@ -1,3 +1,4 @@
1
+ # typed: true
1
2
  # frozen_string_literal: true
2
3
 
3
4
  module GoodJob
@@ -1,3 +1,4 @@
1
+ # typed: true
1
2
  # frozen_string_literal: true
2
3
 
3
4
  module GoodJob
@@ -1,3 +1,4 @@
1
+ # typed: true
1
2
  # frozen_string_literal: true
2
3
 
3
4
  module GoodJob
@@ -1,3 +1,4 @@
1
+ # typed: true
1
2
  # frozen_string_literal: true
2
3
 
3
4
  module GoodJob
@@ -15,7 +16,7 @@ module GoodJob
15
16
  def records
16
17
  @_records ||= begin
17
18
  after_id = params[:after_id]
18
- after_at = params[:after_at].present? ? Time.zone.parse(params[:after_at]) : nil
19
+ after_at = parse_after_at(params[:after_at])
19
20
  limit = params.fetch(:limit, DEFAULT_LIMIT)
20
21
 
21
22
  query_for_records.display_all(
@@ -88,6 +89,14 @@ module GoodJob
88
89
 
89
90
  private
90
91
 
92
+ def parse_after_at(value)
93
+ return unless value.is_a?(String) && value.present?
94
+
95
+ Time.zone.parse(value)
96
+ rescue ArgumentError
97
+ nil
98
+ end
99
+
91
100
  def query_for_records
92
101
  raise NotImplementedError
93
102
  end
@@ -1,3 +1,4 @@
1
+ # typed: true
1
2
  # frozen_string_literal: true
2
3
 
3
4
  module GoodJob
@@ -1,3 +1,4 @@
1
+ # typed: true
1
2
  # frozen_string_literal: true
2
3
 
3
4
  module GoodJob
@@ -92,7 +93,9 @@ module GoodJob
92
93
  end
93
94
 
94
95
  def filter_by_search_query(query)
95
- search_query = query&.strip
96
+ return {} unless query.is_a?(String)
97
+
98
+ search_query = query.strip
96
99
  return {} if search_query.blank?
97
100
 
98
101
  if query_is_uuid_and_job_exists?(search_query)
@@ -1,3 +1,4 @@
1
+ # typed: true
1
2
  # frozen_string_literal: true
2
3
 
3
4
  module GoodJob
@@ -1,3 +1,4 @@
1
+ # typed: true
1
2
  # frozen_string_literal: true
2
3
 
3
4
  module GoodJob
@@ -1,3 +1,4 @@
1
+ # typed: false
1
2
  # frozen_string_literal: true
2
3
 
3
4
  module GoodJob
@@ -30,6 +31,10 @@ module GoodJob
30
31
  #
31
32
  # @example
32
33
  # GoodJob::AdvisoryLockable.hash_function = "sha256"
34
+ # pgcrypto digest() types; "md5", "hashtext", and "uuid_v5" have dedicated branches in +advisory_lock_bigint_sql+.
35
+ DIGEST_HASH_FUNCTIONS = %w[sha1 sha224 sha256 sha384 sha512].freeze
36
+ ADVISORY_LOCK_FUNCTION_PATTERN = /\Apg_(try_)?advisory_(xact_)?(un)?lock(_shared)?\z/
37
+
33
38
  class << self
34
39
  def hash_function=(value)
35
40
  @_hash_function = value
@@ -386,7 +391,7 @@ module GoodJob
386
391
  cte_type = supports_cte_materialization_specifiers? ? :MATERIALIZED : :""
387
392
  composed_cte = Arel::Nodes::As.new(cte_table, Arel::Nodes::UnaryOperation.new(cte_type, cte_query.arel))
388
393
 
389
- lock_condition = "#{function}(#{advisory_lock_bigint_sql("#{_quoted_table_name_string} || '-' || #{adapter_class.quote_table_name(cte_table.name)}.#{adapter_class.quote_column_name(column)}::text")})"
394
+ lock_condition = "#{advisory_lock_function_sql(function)}(#{advisory_lock_bigint_sql("#{_quoted_table_name_string} || '-' || #{adapter_class.quote_table_name(cte_table.name)}.#{adapter_class.quote_column_name(column)}::text")})"
390
395
  query = cte_table.project(cte_table[:id])
391
396
  .with(composed_cte)
392
397
  .where(defined?(Arel::Nodes::BoundSqlLiteral) ? Arel::Nodes::BoundSqlLiteral.new(lock_condition, [], {}) : Arel::Nodes::SqlLiteral.new(lock_condition))
@@ -414,11 +419,11 @@ module GoodJob
414
419
  def advisory_lock_key(key, function: advisory_lockable_function, connection: nil)
415
420
  query = if function.include? "_try_"
416
421
  <<~SQL.squish
417
- SELECT #{function}(#{advisory_lock_bigint_sql('$1::text')}) AS locked
422
+ SELECT #{advisory_lock_function_sql(function)}(#{advisory_lock_bigint_sql('$1::text')}) AS locked
418
423
  SQL
419
424
  else
420
425
  <<~SQL.squish
421
- SELECT #{function}(#{advisory_lock_bigint_sql('$1::text')})::text AS locked
426
+ SELECT #{advisory_lock_function_sql(function)}(#{advisory_lock_bigint_sql('$1::text')})::text AS locked
422
427
  SQL
423
428
  end
424
429
 
@@ -522,7 +527,7 @@ module GoodJob
522
527
  raise ArgumentError, "No unlock function provided" if function.blank?
523
528
 
524
529
  query = <<~SQL.squish
525
- SELECT #{function}(#{advisory_lock_bigint_sql('$1::text')}) AS unlocked
530
+ SELECT #{advisory_lock_function_sql(function)}(#{advisory_lock_bigint_sql('$1::text')}) AS unlocked
526
531
  SQL
527
532
  binds = [
528
533
  ActiveRecord::Relation::QueryAttribute.new('key', key, ActiveRecord::Type::String.new),
@@ -608,10 +613,23 @@ module GoodJob
608
613
  "('x' || substr(replace(uuid_generate_v5('6ba7b810-9dad-11d1-80b4-00c04fd430c8'::uuid, (#{value_sql})::text)::text, '-', ''), 1, 16))::bit(64)::bigint"
609
614
  else
610
615
  # pgcrypto's digest() supports sha1, sha224, sha256, sha384, sha512
611
- "('x' || substr(encode(digest((#{value_sql})::text, '#{GoodJob::AdvisoryLockable.hash_function}'), 'hex'), 1, 16))::bit(64)::bigint"
616
+ digest_type = GoodJob::AdvisoryLockable.hash_function.to_s.downcase
617
+ raise ArgumentError, "Unsupported advisory lock hash function: #{digest_type.inspect}" unless DIGEST_HASH_FUNCTIONS.include?(digest_type)
618
+
619
+ "('x' || substr(encode(digest((#{value_sql})::text, '#{digest_type}'), 'hex'), 1, 16))::bit(64)::bigint"
612
620
  end
613
621
  end
614
622
 
623
+ # Validates a Postgres advisory lock function name before it is interpolated into SQL.
624
+ # @param function [String, Symbol]
625
+ # @return [String]
626
+ def advisory_lock_function_sql(function)
627
+ function = function.to_s
628
+ raise ArgumentError, "Unsupported advisory lock function: #{function.inspect}" unless ADVISORY_LOCK_FUNCTION_PATTERN.match?(function)
629
+
630
+ function
631
+ end
632
+
615
633
  # Extracts the upper 32 bits of the advisory lock bigint, used as +classid+ in pg_locks.
616
634
  def advisory_lock_classid_sql(value_sql)
617
635
  "substring((#{advisory_lock_bigint_sql(value_sql)})::bit(64) from 1 for 32)::bit(32)::int"
@@ -1,3 +1,4 @@
1
+ # typed: false
1
2
  # frozen_string_literal: true
2
3
 
3
4
  module GoodJob
@@ -1,3 +1,4 @@
1
+ # typed: false
1
2
  # frozen_string_literal: true
2
3
 
3
4
  module GoodJob
@@ -1,3 +1,4 @@
1
+ # typed: true
1
2
  # frozen_string_literal: true
2
3
 
3
4
  module GoodJob
@@ -1,3 +1,4 @@
1
+ # typed: false
1
2
  # frozen_string_literal: true
2
3
 
3
4
  module GoodJob
@@ -1,3 +1,4 @@
1
+ # typed: ignore
1
2
  # frozen_string_literal: true
2
3
 
3
4
  module GoodJob
@@ -1,3 +1,4 @@
1
+ # typed: true
1
2
  # frozen_string_literal: true
2
3
 
3
4
  module GoodJob
@@ -1,3 +1,4 @@
1
+ # typed: true
1
2
  # frozen_string_literal: true
2
3
 
3
4
  module GoodJob
@@ -109,22 +110,22 @@ module GoodJob
109
110
 
110
111
  begin
111
112
  if build_result[:bulkable].any?
112
- Job.transaction(requires_new: true, joinable: false) do
113
- persisted_jobs = _insert_jobs(build_result[:bulkable], build_result[:active_jobs_by_job_id])
114
-
115
- if execute_inline
116
- inline_jobs = persisted_jobs.select { |job| job.scheduled_at.nil? || job.scheduled_at <= current_time }
117
- if lock_strategy != :advisory && lock_id && inline_jobs.any?
118
- Job.where(id: inline_jobs.map(&:id)).update_all( # rubocop:disable Rails/SkipsModelValidations
119
- locked_by_id: lock_id, locked_at: current_time, lock_type: Job.lock_types[lock_strategy.to_s]
120
- )
121
- inline_jobs.each { |j| j.assign_attributes(locked_by_id: lock_id, locked_at: current_time, lock_type: lock_strategy) }
122
- end
123
- case lock_strategy
124
- when :advisory, :hybrid
125
- inline_jobs.each(&:advisory_lock!)
126
- end
113
+ persisted_jobs, inline_jobs = Job.transaction(requires_new: true, joinable: false) do
114
+ inserted_jobs = _insert_jobs(build_result[:bulkable], build_result[:active_jobs_by_job_id])
115
+ next [inserted_jobs, []] unless execute_inline
116
+
117
+ jobs_to_inline = inserted_jobs.select { |job| job.scheduled_at.nil? || job.scheduled_at <= current_time }
118
+ if lock_strategy != :advisory && lock_id && jobs_to_inline.any?
119
+ Job.where(id: jobs_to_inline.map(&:id)).update_all( # rubocop:disable Rails/SkipsModelValidations
120
+ locked_by_id: lock_id, locked_at: current_time, lock_type: Job.lock_types[lock_strategy.to_s]
121
+ )
122
+ jobs_to_inline.each { |j| j.assign_attributes(locked_by_id: lock_id, locked_at: current_time, lock_type: lock_strategy) }
127
123
  end
124
+ case lock_strategy
125
+ when :advisory, :hybrid
126
+ jobs_to_inline.each(&:advisory_lock!)
127
+ end
128
+ [inserted_jobs, jobs_to_inline]
128
129
  end
129
130
  end
130
131
 
@@ -444,7 +445,7 @@ module GoodJob
444
445
  state = { queue_name: queue_name, count: grouped_jobs.size }
445
446
  state[:scheduled_at] = scheduled_at if scheduled_at
446
447
 
447
- executed_locally = adapter.respond_to?(:execute_async?) && adapter.execute_async? && GoodJob.capsule&.create_thread(state)
448
+ executed_locally = adapter.respond_to?(:execute_async?) && adapter.execute_async? && GoodJob.capsule.create_thread(state)
448
449
  unless executed_locally
449
450
  state[:count] = grouped_jobs.count { |job| _send_notify?(active_jobs_by_job_id[job.active_job_id]) }
450
451
  Notifier.notify(state) unless state[:count].zero?
@@ -1,3 +1,4 @@
1
+ # typed: true
1
2
  # frozen_string_literal: true
2
3
 
3
4
  require 'active_job/arguments'
@@ -63,8 +64,8 @@ module GoodJob
63
64
  if job_discarded && !discarded_at
64
65
  update(discarded_at: Time.current)
65
66
 
66
- if on_discard.present?
67
- discard_job_class = on_discard.constantize
67
+ discard_job_class = on_discard.presence&.constantize
68
+ if discard_job_class
68
69
  Job.defer_after_commit_maybe(discard_job_class) do
69
70
  Batch.within_thread(batch_id: nil, batch_callback_id: id) do
70
71
  discard_job_class.set(priority: callback_priority, queue: callback_queue_name).perform_later(to_batch, { event: :discard })
@@ -76,8 +77,8 @@ module GoodJob
76
77
  if enqueued_at && !(self.class.jobs_finished_at_migrated? ? jobs_finished_at : finished_at) && jobs.where(finished_at: nil).none?
77
78
  self.class.jobs_finished_at_migrated? ? update(jobs_finished_at: Time.current) : update(finished_at: Time.current)
78
79
 
79
- if !discarded_at && on_success.present?
80
- success_job_class = on_success.constantize
80
+ success_job_class = on_success.presence&.constantize unless discarded_at
81
+ if success_job_class
81
82
  Job.defer_after_commit_maybe(success_job_class) do
82
83
  Batch.within_thread(batch_id: nil, batch_callback_id: id) do
83
84
  success_job_class.set(priority: callback_priority, queue: callback_queue_name).perform_later(to_batch, { event: :success })
@@ -85,11 +86,11 @@ module GoodJob
85
86
  end
86
87
  end
87
88
 
88
- if on_finish.present?
89
- finish_job_class = on_finish.constantize
89
+ finish_job_class = on_finish.presence&.constantize
90
+ if finish_job_class
90
91
  Job.defer_after_commit_maybe(finish_job_class) do
91
92
  Batch.within_thread(batch_id: nil, batch_callback_id: id) do
92
- on_finish.constantize.set(priority: callback_priority, queue: callback_queue_name).perform_later(to_batch, { event: :finish })
93
+ finish_job_class.set(priority: callback_priority, queue: callback_queue_name).perform_later(to_batch, { event: :finish })
93
94
  end
94
95
  end
95
96
  end
@@ -113,7 +114,8 @@ module GoodJob
113
114
  end
114
115
 
115
116
  if Rails.gem_version < Gem::Version.new('7.1.0.alpha')
116
- serialize :serialized_properties, PropertySerializer, default: -> { {} }
117
+ # Positional coder argument; Sorbet only knows the Rails 7.1+ signature
118
+ public_send(:serialize, :serialized_properties, PropertySerializer, default: -> { {} }) # rubocop:disable Style/SendWithLiteralMethodName
117
119
  else
118
120
  serialize :serialized_properties, coder: PropertySerializer, default: -> { {} }
119
121
  end
@@ -1,3 +1,4 @@
1
+ # typed: true
1
2
  # frozen_string_literal: true
2
3
 
3
4
  module GoodJob
@@ -1,3 +1,4 @@
1
+ # typed: true
1
2
  # frozen_string_literal: true
2
3
 
3
4
  require "concurrent/hash"
@@ -1,3 +1,4 @@
1
+ # typed: true
1
2
  # frozen_string_literal: true
2
3
 
3
4
  module GoodJob
@@ -1,3 +1,4 @@
1
+ # typed: true
1
2
  # frozen_string_literal: true
2
3
 
3
4
  module GoodJob # :nodoc:
@@ -44,7 +45,7 @@ module GoodJob # :nodoc:
44
45
 
45
46
  def status
46
47
  if finished_at.present?
47
- if error.present? && job.finished_at.present?
48
+ if error.present? && job&.finished_at.present?
48
49
  :discarded
49
50
  elsif error.present?
50
51
  :retried
@@ -57,9 +58,11 @@ module GoodJob # :nodoc:
57
58
  end
58
59
 
59
60
  def interrupted_duration
60
- return unless error_event == "interrupted" && finished_at && performed_at
61
+ interrupted_at = finished_at
62
+ started_at = performed_at
63
+ return unless error_event == "interrupted" && interrupted_at && started_at
61
64
 
62
- finished_at - performed_at
65
+ interrupted_at - started_at
63
66
  end
64
67
 
65
68
  def display_serialized_params
@@ -1,3 +1,4 @@
1
+ # typed: true
1
2
  # frozen_string_literal: true
2
3
 
3
4
  module GoodJob
@@ -1,3 +1,4 @@
1
+ # typed: true
1
2
  # frozen_string_literal: true
2
3
 
3
4
  module GoodJob
@@ -1,3 +1,4 @@
1
+ # typed: false
1
2
  # frozen_string_literal: true
2
3
 
3
4
  module GoodJob