good_job 4.18.2 → 4.19.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9d2eb2f4b216901a3f6abb9197facd9d9738c13fa4b61fcca2ec3ef4f2b64a6a
4
- data.tar.gz: 7324f38c349116f108cbf216bffdc1207c2f0fd06ff136b7c0f0a8e0059d16a3
3
+ metadata.gz: 7afb27ab67bbbe17bf951784355a40adf87515486c7fd28c5cb12af3e2135005
4
+ data.tar.gz: 9f1da0f02768f9131a3c75ff1d181170a169662f631a83f05862608c579a30e6
5
5
  SHA512:
6
- metadata.gz: 62c30d3afcfa5c7f697f5b156cdced9cf45757f2e05e5e53657fbdf11c1fb0bab0e0e63dad516206737e697bb39d3985c4d0b2fcfb0933c6225fc724a6aca4c7
7
- data.tar.gz: 0d5174c12282329ec248573bfd28a0a86538f7b65b94bf9918c7434bce691a43711017e595b47afb3e200125df5fc33c63b307a545c3d1c7ffd2eb11405741af
6
+ metadata.gz: 4e9e6c72eba0c338334e2acb94cc87e2b5f433ac2b36fd2ec05a1a58656dc5c6ff7d5f4d22f218aeaa04b0f432a44d908654ab0c25dd7c82194acb505539952f
7
+ data.tar.gz: 912da2530fec05469474232f8246f9370dd1da24e822bcf6bcb37b6f649b43ab6d0836d5999a6105eb06f999cc26bde9e94601280093451becf34c2daeb76598
data/CHANGELOG.md CHANGED
@@ -1,5 +1,56 @@
1
1
  # Changelog
2
2
 
3
+ ## [v4.19.1](https://github.com/bensheldon/good_job/tree/v4.19.1) (2026-06-25)
4
+
5
+ [Full Changelog](https://github.com/bensheldon/good_job/compare/v4.19.0...v4.19.1)
6
+
7
+ **Fixed bugs:**
8
+
9
+ - Support Rails Edge version of Arel::Table [\#1783](https://github.com/bensheldon/good_job/pull/1783) ([luizkowalski](https://github.com/luizkowalski))
10
+ - Emit ActiveSupport::Notifications for concurrency-aborted enqueues [\#1779](https://github.com/bensheldon/good_job/pull/1779) ([bdewater-thatch](https://github.com/bdewater-thatch))
11
+
12
+ **Closed issues:**
13
+
14
+ - The probe server doesn't handle SIGINT/SIGTERM [\#1632](https://github.com/bensheldon/good_job/issues/1632)
15
+
16
+ ## [v4.19.0](https://github.com/bensheldon/good_job/tree/v4.19.0) (2026-05-27)
17
+
18
+ [Full Changelog](https://github.com/bensheldon/good_job/compare/v4.18.3...v4.19.0)
19
+
20
+ **Implemented enhancements:**
21
+
22
+ - Truncate long labels in dashboard badges [\#1766](https://github.com/bensheldon/good_job/pull/1766) ([MahmoudBakr23](https://github.com/MahmoudBakr23))
23
+ - Set default queue\_select\_limit to 1000 [\#1762](https://github.com/bensheldon/good_job/pull/1762) ([MahmoudBakr23](https://github.com/MahmoudBakr23))
24
+
25
+ **Fixed bugs:**
26
+
27
+ - Use table\_name instead of hardcoding :good\_jobs in schema introspection [\#1774](https://github.com/bensheldon/good_job/pull/1774) ([eidarus](https://github.com/eidarus))
28
+ - Fix incorrect ENV variable guard for GOOD\_JOB\_ENABLE\_PAUSES [\#1772](https://github.com/bensheldon/good_job/pull/1772) ([jqr](https://github.com/jqr))
29
+ - Fix `PG::ProgramLimitExceeded` in jobs index search for large error payloads [\#1769](https://github.com/bensheldon/good_job/pull/1769) ([createdbypete](https://github.com/createdbypete))
30
+ - Fix PG::AmbiguousColumn in skiplocked/hybrid claim with ordered queues + concurrency rules [\#1768](https://github.com/bensheldon/good_job/pull/1768) ([createdbypete](https://github.com/createdbypete))
31
+ - Make execution state completely Fiber-safe via Rails isolated execution state [\#1765](https://github.com/bensheldon/good_job/pull/1765) ([ollym](https://github.com/ollym))
32
+ - Handle nil updated\_at in stale? method [\#1764](https://github.com/bensheldon/good_job/pull/1764) ([gavinballard](https://github.com/gavinballard))
33
+
34
+ **Merged pull requests:**
35
+
36
+ - Prune CI test matrix to boundary Ruby versions per Rails version [\#1777](https://github.com/bensheldon/good_job/pull/1777) ([bensheldon](https://github.com/bensheldon))
37
+ - Refactor Concurrency::Rule to use explicit ivars instead of options hash [\#1776](https://github.com/bensheldon/good_job/pull/1776) ([bensheldon](https://github.com/bensheldon))
38
+ - Fix typo in README.md [\#1773](https://github.com/bensheldon/good_job/pull/1773) ([NobodysNightmare](https://github.com/NobodysNightmare))
39
+ - Use explicit keyword arguments for concurrency controls [\#1770](https://github.com/bensheldon/good_job/pull/1770) ([bdewater-thatch](https://github.com/bdewater-thatch))
40
+
41
+ ## [v4.18.3](https://github.com/bensheldon/good_job/tree/v4.18.3) (2026-05-27)
42
+
43
+ [Full Changelog](https://github.com/bensheldon/good_job/compare/v4.18.2...v4.18.3)
44
+
45
+ **Closed issues:**
46
+
47
+ - enable\_pauses ENV var check has singular/plural mismatch [\#1771](https://github.com/bensheldon/good_job/issues/1771)
48
+ - Raises `PG::AmbiguousColumn` under hybrid lock strategy on ordered queues with throttle [\#1767](https://github.com/bensheldon/good_job/issues/1767)
49
+ - Handle long labels more gracefully [\#1674](https://github.com/bensheldon/good_job/issues/1674)
50
+ - Drop Duplicate index [\#1661](https://github.com/bensheldon/good_job/issues/1661)
51
+ - Job runner process enters a loop on create\_listen\_task - stale check fails [\#1649](https://github.com/bensheldon/good_job/issues/1649)
52
+ - Set a default queue\_select\_limit [\#1596](https://github.com/bensheldon/good_job/issues/1596)
53
+
3
54
  ## [v4.18.2](https://github.com/bensheldon/good_job/tree/v4.18.2) (2026-04-20)
4
55
 
5
56
  [Full Changelog](https://github.com/bensheldon/good_job/compare/v4.18.1...v4.18.2)
@@ -83,10 +134,6 @@
83
134
 
84
135
  [Full Changelog](https://github.com/bensheldon/good_job/compare/v4.16.0...v4.17.0)
85
136
 
86
- **Implemented enhancements:**
87
-
88
- - Introduce advisory lock key customization support methods [\#1722](https://github.com/bensheldon/good_job/pull/1722) ([amkisko](https://github.com/amkisko))
89
-
90
137
  **Merged pull requests:**
91
138
 
92
139
  - Convert UI JavaScript modules to Stimulus controllers [\#1743](https://github.com/bensheldon/good_job/pull/1743) ([bensheldon](https://github.com/bensheldon))
@@ -102,11 +149,6 @@
102
149
  - Allow filtering by label on dashboard [\#1739](https://github.com/bensheldon/good_job/pull/1739) ([bensheldon](https://github.com/bensheldon))
103
150
  - Allow multiple concurrency rules per job via labels [\#1700](https://github.com/bensheldon/good_job/pull/1700) ([bscofield](https://github.com/bscofield))
104
151
 
105
- **Fixed bugs:**
106
-
107
- - Fix advisory lock connection stickiness in block contexts [\#1736](https://github.com/bensheldon/good_job/pull/1736) ([bensheldon](https://github.com/bensheldon))
108
- - Add JRuby 10 to testing matrix [\#1559](https://github.com/bensheldon/good_job/pull/1559) ([bensheldon](https://github.com/bensheldon))
109
-
110
152
  **Closed issues:**
111
153
 
112
154
  - Job duration misreported if interrupted [\#1723](https://github.com/bensheldon/good_job/issues/1723)
@@ -116,7 +158,6 @@
116
158
  - Use annotated git tag in release script [\#1741](https://github.com/bensheldon/good_job/pull/1741) ([bensheldon](https://github.com/bensheldon))
117
159
  - Double single-thread scheduler integration test timeout on JRuby [\#1738](https://github.com/bensheldon/good_job/pull/1738) ([bensheldon](https://github.com/bensheldon))
118
160
  - Fix JRuby test flakes for scheduler timeout and interrupted execution duration [\#1737](https://github.com/bensheldon/good_job/pull/1737) ([bensheldon](https://github.com/bensheldon))
119
- - Count Advisory Locks and refactor advisory lock lifecycle [\#1735](https://github.com/bensheldon/good_job/pull/1735) ([bensheldon](https://github.com/bensheldon))
120
161
  - Show interrupted execution recovery duration in dashboard [\#1733](https://github.com/bensheldon/good_job/pull/1733) ([bensheldon](https://github.com/bensheldon))
121
162
  - chore: use `merge` to avoid mutate the query object [\#1717](https://github.com/bensheldon/good_job/pull/1717) ([luizkowalski](https://github.com/luizkowalski))
122
163
 
@@ -132,7 +173,6 @@
132
173
 
133
174
  **Merged pull requests:**
134
175
 
135
- - Fix JRuby in development lockfile, with test [\#1734](https://github.com/bensheldon/good_job/pull/1734) ([bensheldon](https://github.com/bensheldon))
136
176
  - Add herb to linter [\#1732](https://github.com/bensheldon/good_job/pull/1732) ([bensheldon](https://github.com/bensheldon))
137
177
  - Update development dependencies; apply Rubocop to\_h lints [\#1728](https://github.com/bensheldon/good_job/pull/1728) ([bensheldon](https://github.com/bensheldon))
138
178
 
@@ -194,7 +234,6 @@
194
234
 
195
235
  **Fixed bugs:**
196
236
 
197
- - Check for graceful shutdown inside job cleanup loops [\#1711](https://github.com/bensheldon/good_job/pull/1711) ([bdewater-thatch](https://github.com/bdewater-thatch))
198
237
  - Add title to Good Job Dashboard layout [\#1701](https://github.com/bensheldon/good_job/pull/1701) ([mockdeep](https://github.com/mockdeep))
199
238
 
200
239
  **Closed issues:**
@@ -290,7 +329,6 @@
290
329
  **Merged pull requests:**
291
330
 
292
331
  - Update sorbet/tapioca [\#1681](https://github.com/bensheldon/good_job/pull/1681) ([bensheldon](https://github.com/bensheldon))
293
- - Remove obsolete property from tests [\#1676](https://github.com/bensheldon/good_job/pull/1676) ([RDIL](https://github.com/RDIL))
294
332
  - Bump actions/checkout from 4 to 5 [\#1673](https://github.com/bensheldon/good_job/pull/1673) ([dependabot[bot]](https://github.com/apps/dependabot))
295
333
 
296
334
  ## [v4.11.2](https://github.com/bensheldon/good_job/tree/v4.11.2) (2025-08-06)
@@ -413,10 +451,6 @@
413
451
 
414
452
  [Full Changelog](https://github.com/bensheldon/good_job/compare/v4.9.0...v4.9.1)
415
453
 
416
- **Implemented enhancements:**
417
-
418
- - Order Dashboard jobs in more "natural" order [\#1604](https://github.com/bensheldon/good_job/pull/1604) ([francois](https://github.com/francois))
419
-
420
454
  **Fixed bugs:**
421
455
 
422
456
  - \[dashboard\] Scheduled tasks are shown "backwards" [\#1580](https://github.com/bensheldon/good_job/issues/1580)
@@ -3015,7 +3049,6 @@
3015
3049
 
3016
3050
  **Fixed bugs:**
3017
3051
 
3018
- - Transactions in "aborting" threads do not commit; causes GoodJob::Process record not destroyed on exit [\#489](https://github.com/bensheldon/good_job/issues/489)
3019
3052
  - Deserialize ActiveJob arguments when manually retrying a job [\#513](https://github.com/bensheldon/good_job/pull/513) ([bensheldon](https://github.com/bensheldon))
3020
3053
 
3021
3054
  **Closed issues:**
data/README.md CHANGED
@@ -726,7 +726,7 @@ config.good_job.cron = {
726
726
 
727
727
  ### Bulk enqueue
728
728
 
729
- GoodJob's Bulk-enqueue functionality can buffer and enqueue multiple jobs at once, using a single INSERT statement. This can more performant when enqueuing a large number of jobs.
729
+ GoodJob's Bulk-enqueue functionality can buffer and enqueue multiple jobs at once, using a single INSERT statement. This can be more performant when enqueuing a large number of jobs.
730
730
 
731
731
  ```ruby
732
732
  # Capture jobs using `.perform_later`:
@@ -1292,7 +1292,7 @@ The recommended way to monitor the queue in production is:
1292
1292
 
1293
1293
  GoodJob’s advisory locking strategy uses a materialized CTE (Common Table Expression). This strategy can be non-performant when querying a very large queue of executable jobs (100,000+) because the database query must materialize all executable jobs before acquiring an advisory lock.
1294
1294
 
1295
- GoodJob offers an optional optimization to limit the number of jobs that are queried: Queue Select Limit.
1295
+ GoodJob's Queue Select Limit restricts how many jobs are materialized per advisory lock cycle. **It defaults to `1000` no configuration needed.** Override it if your deployment requires a different value:
1296
1296
 
1297
1297
  ```none
1298
1298
  # CLI option
@@ -1305,7 +1305,7 @@ config.good_job.queue_select_limit = 1000
1305
1305
  GOOD_JOB_QUEUE_SELECT_LIMIT=1000
1306
1306
  ```
1307
1307
 
1308
- The Queue Select Limit value should be set to a rough upper-bound that exceeds all GoodJob execution threads / database connections. `1000` is a number that likely exceeds the available database connections on most PaaS offerings, but still offers a performance boost for GoodJob when executing very large queues.
1308
+ The default of `1000` is a rough upper-bound that exceeds the available database connections on most PaaS offerings, while still offering a significant performance boost for GoodJob when executing very large queues.
1309
1309
 
1310
1310
  To explain where this value is used, here is the pseudo-query that GoodJob uses to find executable jobs:
1311
1311
 
@@ -1318,7 +1318,7 @@ To explain where this value is used, here is the pseudo-query that GoodJob uses
1318
1318
  FROM good_jobs
1319
1319
  WHERE (scheduled_at <= NOW() OR scheduled_at IS NULL) AND finished_at IS NULL
1320
1320
  ORDER BY priority DESC NULLS LAST, created_at ASC
1321
- [LIMIT 1000] -- <= introduced when queue_select_limit is set
1321
+ [LIMIT 1000] -- <= applied by default; configurable via queue_select_limit
1322
1322
  )
1323
1323
  SELECT id
1324
1324
  FROM rows
@@ -15,7 +15,14 @@ function setupPopovers() {
15
15
  })
16
16
  }
17
17
 
18
+ function setupTooltips() {
19
+ document.querySelectorAll('[data-bs-toggle="tooltip"]').forEach((el) => {
20
+ new bootstrap.Tooltip(el)
21
+ })
22
+ }
23
+
18
24
  window.document.addEventListener("turbo:load", function() {
19
25
  showToasts();
20
26
  setupPopovers();
27
+ setupTooltips();
21
28
  });
@@ -1,24 +1,3 @@
1
- .tooltip {
2
- position: absolute;
3
- z-index: 1;
4
- padding: 5px;
5
- background: rgba(0, 0, 0, 0.3);
6
- opacity: 1;
7
- border-radius: 3px;
8
- text-align: center;
9
- pointer-events: none;
10
- color: white;
11
- transition: opacity .1s ease-out;
12
- }
13
-
14
- .tooltip.tooltip-hidden {
15
- opacity: 0;
16
- }
17
-
18
- .ct-label.ct-horizontal {
19
- white-space: nowrap;
20
- }
21
-
22
1
  .chart-wrapper {
23
2
  position: relative;
24
3
  height: 200px;
@@ -207,6 +207,7 @@ module GoodJob
207
207
  end
208
208
 
209
209
  ADVISORY_LOCK_COUNTS = AdvisoryLockCounter.new
210
+ AREL_TABLE_NEW_KWARGS = Arel::Table.instance_method(:initialize).parameters.any? { |type, name| type == :key && name == :name }
210
211
 
211
212
  included do
212
213
  # Default column to be used when creating Advisory Locks
@@ -357,7 +358,7 @@ module GoodJob
357
358
  primary_key_for_select = primary_key.to_sym
358
359
  column_for_select = column.to_sym
359
360
 
360
- cte_table = Arel::Table.new(:rows)
361
+ cte_table = arel_table_for(:rows)
361
362
  cte_query = original_query.except(:limit)
362
363
  cte_query = if primary_key_for_select == column_for_select
363
364
  cte_query.select(primary_key_for_select)
@@ -485,6 +486,13 @@ module GoodJob
485
486
 
486
487
  private
487
488
 
489
+ # Arel::Table.new has different parameters depending on Rails version.
490
+ # New version expects keyword arguments for the name while the old version expects a positional argument.
491
+ # This method creates a new Arel::Table instance with the correct parameters.
492
+ def arel_table_for(name)
493
+ AREL_TABLE_NEW_KWARGS ? Arel::Table.new(name: name) : Arel::Table.new(name)
494
+ end
495
+
488
496
  # Executes a single pg_advisory_unlock call and updates bookkeeping.
489
497
  # Used by {.advisory_unlock_key} and {.advisory_unlock_key!} to avoid
490
498
  # mutual recursion when CTE-aware cleanup is needed.
@@ -5,6 +5,11 @@ module GoodJob
5
5
  module Filterable
6
6
  extend ActiveSupport::Concern
7
7
 
8
+ # Caps the largest free-form columns (error, serialized_params->>'arguments')
9
+ # fed to to_tsvector() so the combined tsvector stays under PG's ~1 MB limit
10
+ # (PG::ProgramLimitExceeded: "string is too long for tsvector").
11
+ MAX_SEARCH_COLUMN_CHARS = 262_144
12
+
8
13
  included do
9
14
  # Get records in display order with optional keyset pagination.
10
15
  # @!method display_all(ordered_by: ["created_at", "desc"], after_at: nil, after_id: nil)
@@ -65,7 +70,16 @@ module GoodJob
65
70
  next if query.blank?
66
71
 
67
72
  # TODO: turn this into proper bind parameters in Arel
68
- tsvector = "(to_tsvector('english', id::text) || to_tsvector('english', COALESCE(active_job_id::text, '')) || to_tsvector('english', serialized_params) || to_tsvector('english', COALESCE(serialized_params->>'arguments', '')) || to_tsvector('english', COALESCE(error, '')) || to_tsvector('english', COALESCE(array_to_string(labels, ' '), '')))"
73
+ tsvector = <<~SQL.squish
74
+ (
75
+ to_tsvector('english', id::text) ||
76
+ to_tsvector('english', COALESCE(active_job_id::text, '')) ||
77
+ to_tsvector('english', serialized_params) ||
78
+ to_tsvector('english', COALESCE(LEFT(serialized_params->>'arguments', #{MAX_SEARCH_COLUMN_CHARS}), '')) ||
79
+ to_tsvector('english', COALESCE(LEFT(error, #{MAX_SEARCH_COLUMN_CHARS}), '')) ||
80
+ to_tsvector('english', COALESCE(array_to_string(labels, ' '), ''))
81
+ )
82
+ SQL
69
83
  to_tsquery_function = database_supports_websearch_to_tsquery? ? 'websearch_to_tsquery' : 'plainto_tsquery'
70
84
  where("#{tsvector} @@ #{to_tsquery_function}('english', CAST(? AS text))", query)
71
85
  .order(sanitize_sql_for_order([Arel.sql("ts_rank(#{tsvector}, #{to_tsquery_function}('english', CAST(? AS text)))"), query]) => 'DESC')
@@ -59,7 +59,7 @@ module GoodJob
59
59
  record = unscoped.find_by_sql([sql, locked_by_id, locked_at, lock_types[lock_type.to_s]]).first
60
60
 
61
61
  begin
62
- yield(record)
62
+ unscoped { yield(record) }
63
63
  ensure
64
64
  record.update(locked_by_id: nil, locked_at: nil, lock_type: nil) if record && !record.destroyed? && record.locked_by_id.present?
65
65
  record&.run_callbacks(:perform_unlocked)
@@ -103,7 +103,7 @@ module GoodJob
103
103
  record_advisory_lock(lease_connection, record.lockable_column_key, cte: true) if record
104
104
 
105
105
  begin
106
- yield(record)
106
+ unscoped { yield(record) }
107
107
  ensure
108
108
  if record
109
109
  record.advisory_unlock
@@ -187,8 +187,9 @@ module GoodJob
187
187
  # @param queues [Array<string] ordered names of queues
188
188
  # @return [ActiveRecord::Relation]
189
189
  scope :queue_ordered, (lambda do |queues|
190
+ qualified_queue_name = "#{quoted_table_name}.#{adapter_class.quote_column_name('queue_name')}"
190
191
  clauses = queues.map.with_index do |queue_name, index|
191
- sanitize_sql_array(["WHEN queue_name = ? THEN ?", queue_name, index])
192
+ sanitize_sql_array(["WHEN #{qualified_queue_name} = ? THEN ?", queue_name, index])
192
193
  end
193
194
  order(Arel.sql("(CASE #{clauses.join(' ')} ELSE #{queues.size} END)"))
194
195
  end)
@@ -286,14 +287,14 @@ module GoodJob
286
287
  end
287
288
 
288
289
  def historic_finished_at_index_migrated?
289
- return true if connection.index_name_exists?(:good_jobs, "index_good_jobs_on_queue_name_priority_scheduled_at_unfinished")
290
+ return true if connection.index_name_exists?(table_name, "index_good_jobs_on_queue_name_priority_scheduled_at_unfinished")
290
291
 
291
292
  migration_pending_warning!
292
293
  false
293
294
  end
294
295
 
295
296
  def lock_type_migrated?
296
- return true if connection.index_name_exists?(:good_jobs, :index_good_jobs_for_candidate_dequeue_unlocked)
297
+ return true if connection.index_name_exists?(table_name, :index_good_jobs_for_candidate_dequeue_unlocked)
297
298
 
298
299
  migration_pending_warning!
299
300
  false
@@ -305,7 +306,7 @@ module GoodJob
305
306
  def lock_type_column_exists?
306
307
  return @_lock_type_column_exists if defined?(@_lock_type_column_exists)
307
308
 
308
- @_lock_type_column_exists = connection_pool.with_connection { |conn| conn.column_exists?(:good_jobs, :lock_type) }
309
+ @_lock_type_column_exists = connection_pool.with_connection { |conn| conn.column_exists?(table_name, :lock_type) }
309
310
  end
310
311
 
311
312
  def reset_column_information
@@ -151,6 +151,8 @@ module GoodJob # :nodoc:
151
151
  end
152
152
 
153
153
  def stale?
154
+ return true if updated_at.nil?
155
+
154
156
  updated_at < STALE_INTERVAL.ago
155
157
  end
156
158
 
@@ -37,7 +37,8 @@
37
37
  <div class="col-4 col-lg-1 text-lg-end">
38
38
  <div class="d-lg-none small text-muted mt-1"><%= t "good_job.models.job.labels" %></div>
39
39
  <% job.labels&.each do |label| %>
40
- <span class="badge rounded-pill text-bg-secondary font-monospace"><%= label %></span>
40
+ <% truncated_label = truncate(label, length: 15) %>
41
+ <%= tag.span truncated_label, class: "badge rounded-pill text-bg-secondary font-monospace", **(truncated_label == label ? {} : { title: label, data: { bs_toggle: "tooltip" } }) %>
41
42
  <% end %>
42
43
  </div>
43
44
  <div class="col-4 col-lg-1 text-lg-end">
@@ -104,7 +104,8 @@
104
104
  <div class="col-4 col-lg-1 text-wrap text-lg-end">
105
105
  <div class="d-lg-none small text-muted mt-1"><%= t "good_job.models.job.labels" %></div>
106
106
  <% job.labels&.each do |label| %>
107
- <%= link_to label, filter.to_params(label: label), class: "badge rounded-pill text-bg-secondary font-monospace text-decoration-none" %>
107
+ <% truncated_label = truncate(label, length: 15) %>
108
+ <%= link_to truncated_label, filter.to_params(label: label), class: "badge rounded-pill text-bg-secondary font-monospace text-decoration-none", **(truncated_label == label ? {} : { title: label, data: { bs_toggle: "tooltip" } }) %>
108
109
  <% end %>
109
110
  </div>
110
111
  <div class="col-4 col-lg-1 text-lg-end">
@@ -66,13 +66,13 @@
66
66
  {
67
67
  "warning_type": "Dangerous Eval",
68
68
  "warning_code": 13,
69
- "fingerprint": "c4c3e1b8b28ebbfd4672cf3e8f0022a27aff3c12dc4fea750b412de1387f91e6",
69
+ "fingerprint": "c01ff32ee40cc694540aaa76e3fdab8f330babd9d6dbf2564c32aef1cfd877fa",
70
70
  "check_name": "Evaluation",
71
71
  "message": "Dynamic string evaluated as code",
72
72
  "file": "lib/good_job/log_subscriber.rb",
73
- "line": 256,
73
+ "line": 278,
74
74
  "link": "https://brakemanscanner.org/docs/warning_types/dangerous_eval/",
75
- "code": "class_eval(\" def #{level}(progname = nil, tags: [], &block) # def info(progname = nil, tags: [], &block)\\n return unless logger # return unless logger\\n #\\n tag_logger(*tags) do # tag_logger(*tags) do\\n logger.#{level}(progname, &block) # logger.info(progname, &block)\\n end # end\\n end #\\n\", \"lib/good_job/log_subscriber.rb\", (256 + 1))",
75
+ "code": "class_eval(\" def #{level}(progname = nil, tags: [], &block) # def info(progname = nil, tags: [], &block)\\n return unless logger # return unless logger\\n #\\n tag_logger(*tags) do # tag_logger(*tags) do\\n logger.#{level}(progname, &block) # logger.info(progname, &block)\\n end # end\\n end #\\n\", \"lib/good_job/log_subscriber.rb\", (278 + 1))",
76
76
  "render_path": null,
77
77
  "location": {
78
78
  "type": "method",
@@ -90,25 +90,25 @@
90
90
  {
91
91
  "warning_type": "SQL Injection",
92
92
  "warning_code": 0,
93
- "fingerprint": "c837568c590d9608a8bb9927b31b9597aaacc72053b6482e1a54bd02aa0dd2d7",
93
+ "fingerprint": "c57f2525e5d0780a0bcb460502f1b3ebceb9992885466f45b4035ab670c628d9",
94
94
  "check_name": "SQL",
95
95
  "message": "Possible SQL injection",
96
96
  "file": "app/models/good_job/job.rb",
97
- "line": 162,
97
+ "line": 194,
98
98
  "link": "https://brakemanscanner.org/docs/warning_types/sql_injection/",
99
- "code": "Arel.sql(\"(CASE #{queues.map.with_index do\n sanitize_sql_array([\"WHEN queue_name = ? THEN ?\", queue_name, index])\n end.join(\" \")} ELSE #{queues.size} END)\")",
99
+ "code": "Arel.sql(\"(CASE #{queues.map.with_index do\n sanitize_sql_array([\"WHEN #{\"#{quoted_table_name}.#{adapter_class.quote_column_name(\"queue_name\")}\"} = ? THEN ?\", queue_name, index])\n end.join(\" \")} ELSE #{queues.size} END)\")",
100
100
  "render_path": null,
101
101
  "location": {
102
102
  "type": "method",
103
103
  "class": "Job",
104
104
  "method": null
105
105
  },
106
- "user_input": "queues.map.with_index do\n sanitize_sql_array([\"WHEN queue_name = ? THEN ?\", queue_name, index])\n end.join(\" \")",
106
+ "user_input": "queues.map.with_index do\n sanitize_sql_array([\"WHEN #{\"#{quoted_table_name}.#{adapter_class.quote_column_name(\"queue_name\")}\"} = ? THEN ?\", queue_name, index])\n end.join(\" \")",
107
107
  "confidence": "Medium",
108
108
  "cwe_id": [
109
109
  89
110
110
  ],
111
- "note": "Developer provided value, queue_name, is sanitized."
111
+ "note": "Developer provided value, queue_name, is sanitized; identifier is quoted via Rails quoted_table_name and quote_column_name."
112
112
  },
113
113
  {
114
114
  "warning_type": "Command Injection",
@@ -16,52 +16,66 @@ module GoodJob
16
16
  ThrottleExceededError = Class.new(ConcurrencyExceededError)
17
17
 
18
18
  class Rule
19
+ attr_reader :label, :total_limit, :enqueue_limit, :perform_limit, :enqueue_throttle, :perform_throttle
20
+
19
21
  def initialize(config)
20
- @config = config
22
+ @label = config[:label]
23
+ @key = config.key?(:key) ? config[:key] : GoodJob::NONE
24
+ @total_limit = config[:total_limit]
25
+ @enqueue_limit = config[:enqueue_limit]
26
+ @perform_limit = config[:perform_limit]
27
+ @enqueue_throttle = config[:enqueue_throttle]
28
+ @perform_throttle = config[:perform_throttle]
29
+ end
30
+
31
+ def key
32
+ @key.equal?(GoodJob::NONE) ? nil : @key
21
33
  end
22
34
 
23
35
  def evaluate(job, stage)
24
- key = key(job)
25
- label = label(job)
26
- return nil if key.blank? && label.blank?
36
+ resolved_key = resolve_key(job)
37
+ resolved_label = resolve_label(job)
38
+ return nil if resolved_key.blank? && resolved_label.blank?
27
39
 
28
40
  if stage == :enqueue
29
- enqueue_limit = limit(job, :enqueue_limit) || limit(job, :total_limit)
30
- enqueue_throttle = throttle(job, :enqueue_throttle)
41
+ enqueue_limit = resolve_limit(job, @enqueue_limit) || resolve_limit(job, @total_limit)
42
+ enqueue_throttle = resolve_throttle(job, @enqueue_throttle)
31
43
  return nil unless enqueue_limit || enqueue_throttle
32
44
 
33
- check_enqueue(enqueue_limit, enqueue_throttle, job, key, label, enqueue_limit_flag: @config[:enqueue_limit].present?)
45
+ check_enqueue(enqueue_limit, enqueue_throttle, job, resolved_key, resolved_label, enqueue_limit_flag: @enqueue_limit.present?)
34
46
  elsif stage == :perform
35
- perform_limit = limit(job, :perform_limit) || limit(job, :total_limit)
36
- perform_throttle = throttle(job, :perform_throttle)
47
+ perform_limit = resolve_limit(job, @perform_limit) || resolve_limit(job, @total_limit)
48
+ perform_throttle = resolve_throttle(job, @perform_throttle)
37
49
  return nil unless perform_limit || perform_throttle
38
50
 
39
- check_perform(perform_limit, perform_throttle, job, key, label)
51
+ check_perform(perform_limit, perform_throttle, job, resolved_key, resolved_label)
40
52
  end
41
53
  end
42
54
 
43
- def key(job)
44
- key_spec = @config[:key]
45
- if key_spec.blank?
46
- "label:#{label(job)}"
55
+ private
56
+
57
+ def key_explicit?
58
+ !@key.equal?(GoodJob::NONE)
59
+ end
60
+
61
+ def resolve_key(job)
62
+ if key.blank?
63
+ "label:#{resolve_label(job)}"
47
64
  else
48
- key_value = key_spec.respond_to?(:call) ? job.instance_exec(&key_spec) : key_spec
65
+ key_value = @key.respond_to?(:call) ? job.instance_exec(&@key) : @key
49
66
  raise TypeError, "Concurrency key must be a String; was a #{key_value.class}" if key_value.present? && VALID_TYPES.none? { |type| key_value.is_a?(type) }
50
67
 
51
68
  key_value
52
69
  end
53
70
  end
54
71
 
55
- def label(job)
56
- label_spec = @config[:label]
72
+ def resolve_label(job)
73
+ return if @label.blank?
57
74
 
58
- return if label_spec.blank?
59
-
60
- label_spec.respond_to?(:call) ? job.instance_exec(&label_spec) : label_spec
75
+ @label.respond_to?(:call) ? job.instance_exec(&@label) : @label
61
76
  end
62
77
 
63
- def limit(job, limit_name)
64
- value = @config[limit_name]
78
+ def resolve_limit(job, value)
65
79
  return nil if value.nil?
66
80
 
67
81
  value = job.instance_exec(&value) if value.respond_to?(:call)
@@ -69,8 +83,7 @@ module GoodJob
69
83
  value
70
84
  end
71
85
 
72
- def throttle(job, throttle_name)
73
- value = @config[throttle_name]
86
+ def resolve_throttle(job, value)
74
87
  return nil if value.nil?
75
88
 
76
89
  value = job.instance_exec(&value) if value.respond_to?(:call)
@@ -78,12 +91,10 @@ module GoodJob
78
91
  value
79
92
  end
80
93
 
81
- private
82
-
83
94
  def query_scope(label, key)
84
95
  if label.present?
85
96
  GoodJob::Job.labeled(label)
86
- elsif @config.key?(:key) && key.present?
97
+ elsif key_explicit? && key.present?
87
98
  GoodJob::Job.where(concurrency_key: key)
88
99
  else
89
100
  GoodJob::Job.all
@@ -109,7 +120,10 @@ module GoodJob
109
120
  end
110
121
 
111
122
  if (enqueue_concurrency + 1) > limit
112
- job.logger.info "Aborted enqueue of #{job.class.name} (Job ID: #{job.job_id}) because the concurrency key '#{key}' has reached its enqueue limit of #{limit} #{'job'.pluralize(limit)}"
123
+ ActiveSupport::Notifications.instrument(
124
+ "enqueue_concurrency_limit_exceeded.good_job",
125
+ { job: job, key: key, limit: limit }
126
+ )
113
127
  exceeded = :limit
114
128
  next
115
129
  end
@@ -123,7 +137,10 @@ module GoodJob
123
137
  .count
124
138
 
125
139
  if (enqueued_within_period + 1) > throttle_limit
126
- job.logger.info "Aborted enqueue of #{job.class.name} (Job ID: #{job.job_id}) because the concurrency key '#{key}' has reached its throttle limit of #{throttle_limit} #{'job'.pluralize(throttle_limit)}"
140
+ ActiveSupport::Notifications.instrument(
141
+ "enqueue_concurrency_throttle_exceeded.good_job",
142
+ { job: job, key: key, limit: throttle_limit }
143
+ )
127
144
  exceeded = :throttle
128
145
  next
129
146
  end
@@ -269,16 +286,48 @@ module GoodJob
269
286
  end
270
287
 
271
288
  class_methods do
272
- def good_job_control_concurrency_with(config)
273
- self.good_job_concurrency_config = config
289
+ def good_job_control_concurrency_with(
290
+ total_limit: NONE,
291
+ enqueue_limit: NONE,
292
+ perform_limit: NONE,
293
+ enqueue_throttle: NONE,
294
+ perform_throttle: NONE,
295
+ key: NONE
296
+ )
297
+ self.good_job_concurrency_config = {
298
+ total_limit: total_limit,
299
+ enqueue_limit: enqueue_limit,
300
+ perform_limit: perform_limit,
301
+ enqueue_throttle: enqueue_throttle,
302
+ perform_throttle: perform_throttle,
303
+ key: key,
304
+ }.reject { |_key, value| value.equal?(NONE) }
274
305
  end
275
306
 
276
307
  # Define a concurrency rule. Rules are appended to the class-level
277
- # `good_job_concurrency_rules` array. Each rule is a Hash that may
308
+ # `good_job_concurrency_rules` array. Each rule uses keyword arguments that may
278
309
  # include keys such as :label, :key (optional lock key), and
279
310
  # stage-specific settings like :enqueue_limit, :enqueue_throttle,
280
- # :perform_limit, :perform_throttle, and :total_limit/:total_throttle.
281
- def good_job_concurrency_rule(rule)
311
+ # :perform_limit, :perform_throttle, and :total_limit.
312
+ def good_job_concurrency_rule(
313
+ label: NONE,
314
+ key: NONE,
315
+ total_limit: NONE,
316
+ enqueue_limit: NONE,
317
+ perform_limit: NONE,
318
+ enqueue_throttle: NONE,
319
+ perform_throttle: NONE
320
+ )
321
+ rule = {
322
+ label: label,
323
+ key: key,
324
+ total_limit: total_limit,
325
+ enqueue_limit: enqueue_limit,
326
+ perform_limit: perform_limit,
327
+ enqueue_throttle: enqueue_throttle,
328
+ perform_throttle: perform_throttle,
329
+ }.reject { |_key, value| value.equal?(NONE) }
330
+
282
331
  self.good_job_concurrency_rules = Array(good_job_concurrency_rules) + [Rule.new(rule)]
283
332
  end
284
333
  end
data/lib/good_job/cli.rb CHANGED
@@ -105,7 +105,7 @@ module GoodJob
105
105
  method_option :queue_select_limit,
106
106
  type: :numeric,
107
107
  banner: 'COUNT',
108
- desc: "The number of queued jobs to select when polling for a job to run. (env var: GOOD_JOB_QUEUE_SELECT_LIMIT, default: nil)"
108
+ desc: "The number of queued jobs to select when polling for a job to run. (env var: GOOD_JOB_QUEUE_SELECT_LIMIT, default: 1000)"
109
109
 
110
110
  def start
111
111
  set_up_application!
@@ -39,6 +39,8 @@ module GoodJob
39
39
  DEFAULT_ENQUEUE_AFTER_TRANSACTION_COMMIT = false
40
40
  # Default enable_pauses setting
41
41
  DEFAULT_ENABLE_PAUSES = false
42
+ # Default number of candidate jobs to query when polling
43
+ DEFAULT_QUEUE_SELECT_LIMIT = 1_000
42
44
  # Valid dequeue query sorts
43
45
  DEQUEUE_QUERY_SORTS = [:created_at, :scheduled_at].freeze
44
46
  # Valid lock strategies
@@ -242,13 +244,14 @@ module GoodJob
242
244
  # This limit is intended to avoid locking a large number of rows when selecting eligible jobs
243
245
  # from the queue. This value should be higher than the total number of threads across all good_job
244
246
  # processes to ensure a thread can retrieve an eligible and unlocked job.
245
- # @return [Integer, nil]
247
+ # @return [Integer]
246
248
  def queue_select_limit
247
249
  (
248
250
  options[:queue_select_limit] ||
249
251
  rails_config[:queue_select_limit] ||
250
- env['GOOD_JOB_QUEUE_SELECT_LIMIT']
251
- )&.to_i
252
+ env['GOOD_JOB_QUEUE_SELECT_LIMIT'] ||
253
+ DEFAULT_QUEUE_SELECT_LIMIT
254
+ ).to_i
252
255
  end
253
256
 
254
257
  # The number of seconds that a good_job process will idle with out running a job before exiting
@@ -388,7 +391,7 @@ module GoodJob
388
391
  def enable_pauses
389
392
  return options[:enable_pauses] unless options[:enable_pauses].nil?
390
393
  return rails_config[:enable_pauses] unless rails_config[:enable_pauses].nil?
391
- return ActiveModel::Type::Boolean.new.cast(env['GOOD_JOB_ENABLE_PAUSES']) unless env['GOOD_JOB_ENABLE_PAUSE'].nil?
394
+ return ActiveModel::Type::Boolean.new.cast(env['GOOD_JOB_ENABLE_PAUSES']) unless env['GOOD_JOB_ENABLE_PAUSES'].nil?
392
395
 
393
396
  DEFAULT_ENABLE_PAUSES
394
397
  end
@@ -153,6 +153,28 @@ module GoodJob
153
153
  end
154
154
  end
155
155
 
156
+ # @!macro notification_responder
157
+ def enqueue_concurrency_limit_exceeded(event)
158
+ job = event.payload[:job]
159
+ key = event.payload[:key]
160
+ limit = event.payload[:limit]
161
+
162
+ info do
163
+ "Aborted enqueue of #{job.class.name} (Job ID: #{job.job_id}) because the concurrency key '#{key}' has reached its enqueue limit of #{limit} #{'job'.pluralize(limit)}"
164
+ end
165
+ end
166
+
167
+ # @!macro notification_responder
168
+ def enqueue_concurrency_throttle_exceeded(event)
169
+ job = event.payload[:job]
170
+ key = event.payload[:key]
171
+ limit = event.payload[:limit]
172
+
173
+ info do
174
+ "Aborted enqueue of #{job.class.name} (Job ID: #{job.job_id}) because the concurrency key '#{key}' has reached its throttle limit of #{limit} #{'job'.pluralize(limit)}"
175
+ end
176
+ end
177
+
156
178
  # @!macro notification_responder
157
179
  def systemd_watchdog_start(event)
158
180
  interval = event.payload[:interval]
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GoodJob
4
+ # Execution-local storage with a compatibility fallback.
5
+ # Uses ActiveSupport::IsolatedExecutionState on Rails 7.0+ (which respects
6
+ # config.active_support.isolation_level = :fiber for Fiber-aware servers).
7
+ # Falls back to Thread.current on older Rails.
8
+ module SafeState
9
+ def self.[](key)
10
+ state[key]
11
+ end
12
+
13
+ def self.[]=(key, value)
14
+ state[key] = value
15
+ end
16
+
17
+ def self.delete(key)
18
+ if defined?(ActiveSupport::IsolatedExecutionState)
19
+ ActiveSupport::IsolatedExecutionState.delete(key)
20
+ else
21
+ Thread.current[key] = nil
22
+ end
23
+ end
24
+
25
+ def self.state
26
+ if defined?(ActiveSupport::IsolatedExecutionState)
27
+ ActiveSupport::IsolatedExecutionState
28
+ else
29
+ Thread.current
30
+ end
31
+ end
32
+ private_class_method :state
33
+ end
34
+ end
@@ -282,7 +282,7 @@ module GoodJob # :nodoc:
282
282
  def create_task(delay = 0, fanout: false)
283
283
  future = Concurrent::ScheduledTask.new(delay, args: [self, performer], executor: executor, timer_set: timer_set) do |thr_scheduler, thr_performer|
284
284
  Thread.current.name = Thread.current.name.sub("-worker-", "-thread-") if Thread.current.name
285
- Thread.current[:good_job_scheduler] = thr_scheduler
285
+ GoodJob::SafeState[:good_job_scheduler] = thr_scheduler
286
286
  Thread.current.priority = -3 if thr_scheduler.lower_thread_priority
287
287
 
288
288
  Rails.application.reloader.wrap do
@@ -11,7 +11,7 @@ module GoodJob
11
11
  # Whether the current job execution thread is in a running state.
12
12
  # @return [Boolean]
13
13
  def current_thread_running?
14
- scheduler = Thread.current[:good_job_scheduler]
14
+ scheduler = GoodJob::SafeState[:good_job_scheduler]
15
15
  scheduler ? scheduler.running? : true
16
16
  end
17
17
 
@@ -19,7 +19,7 @@ module GoodJob
19
19
  # (the opposite of running).
20
20
  # @return [Boolean]
21
21
  def current_thread_shutting_down?
22
- scheduler = Thread.current[:good_job_scheduler]
22
+ scheduler = GoodJob::SafeState[:good_job_scheduler]
23
23
  scheduler && !scheduler.running?
24
24
  end
25
25
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  module GoodJob
4
4
  # GoodJob gem version.
5
- VERSION = '4.18.2'
5
+ VERSION = '4.19.1'
6
6
 
7
7
  # GoodJob version as Gem::Version object
8
8
  GEM_VERSION = Gem::Version.new(VERSION)
data/lib/good_job.rb CHANGED
@@ -40,6 +40,7 @@ require_relative "good_job/probe_server/healthcheck_middleware"
40
40
  require_relative "good_job/probe_server/not_found_app"
41
41
  require_relative "good_job/probe_server/simple_handler"
42
42
  require_relative "good_job/probe_server/webrick_handler"
43
+ require_relative "good_job/safe_state"
43
44
  require_relative "good_job/scheduler"
44
45
  require_relative "good_job/shared_executor"
45
46
  require_relative "good_job/systemd_service"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: good_job
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.18.2
4
+ version: 4.19.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ben Sheldon
@@ -396,6 +396,7 @@ files:
396
396
  - lib/good_job/probe_server/not_found_app.rb
397
397
  - lib/good_job/probe_server/simple_handler.rb
398
398
  - lib/good_job/probe_server/webrick_handler.rb
399
+ - lib/good_job/safe_state.rb
399
400
  - lib/good_job/scheduler.rb
400
401
  - lib/good_job/sd_notify.rb
401
402
  - lib/good_job/shared_executor.rb
@@ -433,7 +434,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
433
434
  - !ruby/object:Gem::Version
434
435
  version: '0'
435
436
  requirements: []
436
- rubygems_version: 4.0.10
437
+ rubygems_version: 4.0.11
437
438
  specification_version: 4
438
439
  summary: A multithreaded, Postgres-based ActiveJob backend for Ruby on Rails
439
440
  test_files: []