good_job 4.18.2 → 4.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9d2eb2f4b216901a3f6abb9197facd9d9738c13fa4b61fcca2ec3ef4f2b64a6a
4
- data.tar.gz: 7324f38c349116f108cbf216bffdc1207c2f0fd06ff136b7c0f0a8e0059d16a3
3
+ metadata.gz: d91dd892cc8a4110a371e8539b48200a20bb34bdbb08c3f5adc19ae96d2a9374
4
+ data.tar.gz: bea93501defb6c13acb563afced0a7ac340ce58f0567ad12143a7e82fbfd282f
5
5
  SHA512:
6
- metadata.gz: 62c30d3afcfa5c7f697f5b156cdced9cf45757f2e05e5e53657fbdf11c1fb0bab0e0e63dad516206737e697bb39d3985c4d0b2fcfb0933c6225fc724a6aca4c7
7
- data.tar.gz: 0d5174c12282329ec248573bfd28a0a86538f7b65b94bf9918c7434bce691a43711017e595b47afb3e200125df5fc33c63b307a545c3d1c7ffd2eb11405741af
6
+ metadata.gz: 2e4b30ddb0ef27b8409325dc98b775a46cb8398d5422c6387917f7d22df81ef5b5206ea4c23c9f8c2d9fd4e56b8ed0c27a46d26a021296e5f7c656ecb9ada865
7
+ data.tar.gz: 316857dc028b6d98db33e26fd68e5246659f0e79a92ae6fd923726e84770f8c069ef80ee73efdf608d478d4ed107f85a4afd4a0a45deb8145ed1147e43a9bf8a
data/CHANGELOG.md CHANGED
@@ -1,5 +1,39 @@
1
1
  # Changelog
2
2
 
3
+ ## [v4.19.0](https://github.com/bensheldon/good_job/tree/v4.19.0) (2026-05-27)
4
+
5
+ [Full Changelog](https://github.com/bensheldon/good_job/compare/v4.18.2...v4.19.0)
6
+
7
+ **Implemented enhancements:**
8
+
9
+ - Truncate long labels in dashboard badges [\#1766](https://github.com/bensheldon/good_job/pull/1766) ([MahmoudBakr23](https://github.com/MahmoudBakr23))
10
+ - Set default queue\_select\_limit to 1000 [\#1762](https://github.com/bensheldon/good_job/pull/1762) ([MahmoudBakr23](https://github.com/MahmoudBakr23))
11
+
12
+ **Fixed bugs:**
13
+
14
+ - 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))
15
+ - Fix incorrect ENV variable guard for GOOD\_JOB\_ENABLE\_PAUSES [\#1772](https://github.com/bensheldon/good_job/pull/1772) ([jqr](https://github.com/jqr))
16
+ - 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))
17
+ - 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))
18
+ - 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))
19
+ - Handle nil updated\_at in stale? method [\#1764](https://github.com/bensheldon/good_job/pull/1764) ([gavinballard](https://github.com/gavinballard))
20
+
21
+ **Closed issues:**
22
+
23
+ - enable\_pauses ENV var check has singular/plural mismatch [\#1771](https://github.com/bensheldon/good_job/issues/1771)
24
+ - Raises `PG::AmbiguousColumn` under hybrid lock strategy on ordered queues with throttle [\#1767](https://github.com/bensheldon/good_job/issues/1767)
25
+ - Handle long labels more gracefully [\#1674](https://github.com/bensheldon/good_job/issues/1674)
26
+ - Drop Duplicate index [\#1661](https://github.com/bensheldon/good_job/issues/1661)
27
+ - Job runner process enters a loop on create\_listen\_task - stale check fails [\#1649](https://github.com/bensheldon/good_job/issues/1649)
28
+ - Set a default queue\_select\_limit [\#1596](https://github.com/bensheldon/good_job/issues/1596)
29
+
30
+ **Merged pull requests:**
31
+
32
+ - 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))
33
+ - 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))
34
+ - Fix typo in README.md [\#1773](https://github.com/bensheldon/good_job/pull/1773) ([NobodysNightmare](https://github.com/NobodysNightmare))
35
+ - Use explicit keyword arguments for concurrency controls [\#1770](https://github.com/bensheldon/good_job/pull/1770) ([bdewater-thatch](https://github.com/bdewater-thatch))
36
+
3
37
  ## [v4.18.2](https://github.com/bensheldon/good_job/tree/v4.18.2) (2026-04-20)
4
38
 
5
39
  [Full Changelog](https://github.com/bensheldon/good_job/compare/v4.18.1...v4.18.2)
@@ -83,10 +117,6 @@
83
117
 
84
118
  [Full Changelog](https://github.com/bensheldon/good_job/compare/v4.16.0...v4.17.0)
85
119
 
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
120
  **Merged pull requests:**
91
121
 
92
122
  - Convert UI JavaScript modules to Stimulus controllers [\#1743](https://github.com/bensheldon/good_job/pull/1743) ([bensheldon](https://github.com/bensheldon))
@@ -102,11 +132,6 @@
102
132
  - Allow filtering by label on dashboard [\#1739](https://github.com/bensheldon/good_job/pull/1739) ([bensheldon](https://github.com/bensheldon))
103
133
  - Allow multiple concurrency rules per job via labels [\#1700](https://github.com/bensheldon/good_job/pull/1700) ([bscofield](https://github.com/bscofield))
104
134
 
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
135
  **Closed issues:**
111
136
 
112
137
  - Job duration misreported if interrupted [\#1723](https://github.com/bensheldon/good_job/issues/1723)
@@ -116,7 +141,6 @@
116
141
  - Use annotated git tag in release script [\#1741](https://github.com/bensheldon/good_job/pull/1741) ([bensheldon](https://github.com/bensheldon))
117
142
  - Double single-thread scheduler integration test timeout on JRuby [\#1738](https://github.com/bensheldon/good_job/pull/1738) ([bensheldon](https://github.com/bensheldon))
118
143
  - 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
144
  - Show interrupted execution recovery duration in dashboard [\#1733](https://github.com/bensheldon/good_job/pull/1733) ([bensheldon](https://github.com/bensheldon))
121
145
  - chore: use `merge` to avoid mutate the query object [\#1717](https://github.com/bensheldon/good_job/pull/1717) ([luizkowalski](https://github.com/luizkowalski))
122
146
 
@@ -132,7 +156,6 @@
132
156
 
133
157
  **Merged pull requests:**
134
158
 
135
- - Fix JRuby in development lockfile, with test [\#1734](https://github.com/bensheldon/good_job/pull/1734) ([bensheldon](https://github.com/bensheldon))
136
159
  - Add herb to linter [\#1732](https://github.com/bensheldon/good_job/pull/1732) ([bensheldon](https://github.com/bensheldon))
137
160
  - Update development dependencies; apply Rubocop to\_h lints [\#1728](https://github.com/bensheldon/good_job/pull/1728) ([bensheldon](https://github.com/bensheldon))
138
161
 
@@ -194,7 +217,6 @@
194
217
 
195
218
  **Fixed bugs:**
196
219
 
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
220
  - Add title to Good Job Dashboard layout [\#1701](https://github.com/bensheldon/good_job/pull/1701) ([mockdeep](https://github.com/mockdeep))
199
221
 
200
222
  **Closed issues:**
@@ -290,7 +312,6 @@
290
312
  **Merged pull requests:**
291
313
 
292
314
  - 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
315
  - Bump actions/checkout from 4 to 5 [\#1673](https://github.com/bensheldon/good_job/pull/1673) ([dependabot[bot]](https://github.com/apps/dependabot))
295
316
 
296
317
  ## [v4.11.2](https://github.com/bensheldon/good_job/tree/v4.11.2) (2025-08-06)
@@ -413,10 +434,6 @@
413
434
 
414
435
  [Full Changelog](https://github.com/bensheldon/good_job/compare/v4.9.0...v4.9.1)
415
436
 
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
437
  **Fixed bugs:**
421
438
 
422
439
  - \[dashboard\] Scheduled tasks are shown "backwards" [\#1580](https://github.com/bensheldon/good_job/issues/1580)
@@ -3015,7 +3032,6 @@
3015
3032
 
3016
3033
  **Fixed bugs:**
3017
3034
 
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
3035
  - Deserialize ActiveJob arguments when manually retrying a job [\#513](https://github.com/bensheldon/good_job/pull/513) ([bensheldon](https://github.com/bensheldon))
3020
3036
 
3021
3037
  **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;
@@ -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">
@@ -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
@@ -269,16 +280,48 @@ module GoodJob
269
280
  end
270
281
 
271
282
  class_methods do
272
- def good_job_control_concurrency_with(config)
273
- self.good_job_concurrency_config = config
283
+ def good_job_control_concurrency_with(
284
+ total_limit: NONE,
285
+ enqueue_limit: NONE,
286
+ perform_limit: NONE,
287
+ enqueue_throttle: NONE,
288
+ perform_throttle: NONE,
289
+ key: NONE
290
+ )
291
+ self.good_job_concurrency_config = {
292
+ total_limit: total_limit,
293
+ enqueue_limit: enqueue_limit,
294
+ perform_limit: perform_limit,
295
+ enqueue_throttle: enqueue_throttle,
296
+ perform_throttle: perform_throttle,
297
+ key: key,
298
+ }.reject { |_key, value| value.equal?(NONE) }
274
299
  end
275
300
 
276
301
  # Define a concurrency rule. Rules are appended to the class-level
277
- # `good_job_concurrency_rules` array. Each rule is a Hash that may
302
+ # `good_job_concurrency_rules` array. Each rule uses keyword arguments that may
278
303
  # include keys such as :label, :key (optional lock key), and
279
304
  # 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)
305
+ # :perform_limit, :perform_throttle, and :total_limit.
306
+ def good_job_concurrency_rule(
307
+ label: NONE,
308
+ key: NONE,
309
+ total_limit: NONE,
310
+ enqueue_limit: NONE,
311
+ perform_limit: NONE,
312
+ enqueue_throttle: NONE,
313
+ perform_throttle: NONE
314
+ )
315
+ rule = {
316
+ label: label,
317
+ key: key,
318
+ total_limit: total_limit,
319
+ enqueue_limit: enqueue_limit,
320
+ perform_limit: perform_limit,
321
+ enqueue_throttle: enqueue_throttle,
322
+ perform_throttle: perform_throttle,
323
+ }.reject { |_key, value| value.equal?(NONE) }
324
+
282
325
  self.good_job_concurrency_rules = Array(good_job_concurrency_rules) + [Rule.new(rule)]
283
326
  end
284
327
  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
@@ -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.0'
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.0
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: []