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 +4 -4
- data/CHANGELOG.md +34 -18
- data/README.md +4 -4
- data/app/frontend/good_job/modules/bootstrap_init.js +7 -0
- data/app/frontend/good_job/style.css +0 -21
- data/app/models/concerns/good_job/filterable.rb +15 -1
- data/app/models/good_job/job/lockable.rb +2 -2
- data/app/models/good_job/job.rb +5 -4
- data/app/models/good_job/process.rb +2 -0
- data/app/views/good_job/batches/_jobs.erb +2 -1
- data/app/views/good_job/jobs/_table.erb +2 -1
- data/config/brakeman.ignore +5 -5
- data/lib/good_job/active_job_extensions/concurrency.rb +75 -32
- data/lib/good_job/cli.rb +1 -1
- data/lib/good_job/configuration.rb +7 -4
- data/lib/good_job/safe_state.rb +34 -0
- data/lib/good_job/scheduler.rb +1 -1
- data/lib/good_job/thread_status.rb +2 -2
- data/lib/good_job/version.rb +1 -1
- data/lib/good_job.rb +1 -0
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d91dd892cc8a4110a371e8539b48200a20bb34bdbb08c3f5adc19ae96d2a9374
|
|
4
|
+
data.tar.gz: bea93501defb6c13acb563afced0a7ac340ce58f0567ad12143a7e82fbfd282f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
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
|
|
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] -- <=
|
|
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 =
|
|
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
|
data/app/models/good_job/job.rb
CHANGED
|
@@ -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
|
|
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?(
|
|
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?(
|
|
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?(
|
|
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
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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">
|
data/config/brakeman.ignore
CHANGED
|
@@ -90,25 +90,25 @@
|
|
|
90
90
|
{
|
|
91
91
|
"warning_type": "SQL Injection",
|
|
92
92
|
"warning_code": 0,
|
|
93
|
-
"fingerprint": "
|
|
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":
|
|
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
|
-
@
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
return nil if
|
|
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 =
|
|
30
|
-
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,
|
|
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 =
|
|
36
|
-
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,
|
|
51
|
+
check_perform(perform_limit, perform_throttle, job, resolved_key, resolved_label)
|
|
40
52
|
end
|
|
41
53
|
end
|
|
42
54
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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 =
|
|
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
|
|
56
|
-
|
|
72
|
+
def resolve_label(job)
|
|
73
|
+
return if @label.blank?
|
|
57
74
|
|
|
58
|
-
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
273
|
-
|
|
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
|
|
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
|
|
281
|
-
def good_job_concurrency_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:
|
|
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
|
|
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
|
-
|
|
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['
|
|
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
|
data/lib/good_job/scheduler.rb
CHANGED
|
@@ -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
|
-
|
|
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 =
|
|
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 =
|
|
22
|
+
scheduler = GoodJob::SafeState[:good_job_scheduler]
|
|
23
23
|
scheduler && !scheduler.running?
|
|
24
24
|
end
|
|
25
25
|
end
|
data/lib/good_job/version.rb
CHANGED
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.
|
|
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.
|
|
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: []
|