good_job 3.27.3 → 3.28.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +53 -0
- data/README.md +91 -2
- data/app/frontend/good_job/style.css +4 -0
- data/app/models/concerns/good_job/advisory_lockable.rb +7 -2
- data/app/models/concerns/good_job/filterable.rb +3 -2
- data/app/models/good_job/base_record.rb +4 -0
- data/app/models/good_job/batch_record.rb +8 -2
- data/app/models/good_job/discrete_execution.rb +11 -0
- data/app/models/good_job/execution.rb +6 -4
- data/app/models/good_job/job.rb +4 -4
- data/app/views/good_job/jobs/_executions.erb +27 -1
- data/app/views/good_job/jobs/_table.erb +2 -2
- data/config/locales/de.yml +3 -3
- data/config/locales/en.yml +3 -3
- data/config/locales/es.yml +8 -16
- data/config/locales/fr.yml +7 -7
- data/config/locales/it.yml +3 -3
- data/config/locales/ja.yml +3 -3
- data/config/locales/ko.yml +3 -3
- data/config/locales/nl.yml +4 -4
- data/config/locales/pt-BR.yml +3 -3
- data/config/locales/ru.yml +2 -0
- data/config/locales/tr.yml +3 -3
- data/config/locales/uk.yml +3 -3
- data/lib/generators/good_job/templates/install/migrations/create_good_jobs.rb.erb +1 -0
- data/lib/generators/good_job/templates/update/migrations/10_create_good_job_execution_error_backtrace.rb.erb +15 -0
- data/lib/good_job/active_job_extensions/concurrency.rb +1 -1
- data/lib/good_job/active_job_extensions/notify_options.rb +1 -1
- data/lib/good_job/adapter.rb +6 -0
- data/lib/good_job/bulk.rb +1 -1
- data/lib/good_job/capsule.rb +10 -6
- data/lib/good_job/configuration.rb +10 -0
- data/lib/good_job/version.rb +1 -1
- data/lib/good_job.rb +2 -2
- metadata +3 -30
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b22f482dcf118c82d0b5889c6c37fd72bb65ff9a010f4fcc486bc2f0c4fc0bbe
|
4
|
+
data.tar.gz: 83b1d597eee0f0c9c37b4afdf82f96e2164bef2d3b27181c10678327803f718e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b0b84290323a36b818cf92763e9df745737099f3990c934914e8ae700d6cb3a2ceb237764a8ff24fe923d5a8af0d18394e14a6aec516085a2bf057579341747e
|
7
|
+
data.tar.gz: 030e271e5218499dbbce39e21803a5da6ede65422addb33ba5a0024582ac0996418471664061ce54700b1e9a5819eeeac517c479bb79d329577985e148752811
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,58 @@
|
|
1
1
|
# Changelog
|
2
2
|
|
3
|
+
## [v3.28.0](https://github.com/bensheldon/good_job/tree/v3.28.0) (2024-04-19)
|
4
|
+
|
5
|
+
[Full Changelog](https://github.com/bensheldon/good_job/compare/v3.27.4...v3.28.0)
|
6
|
+
|
7
|
+
**Implemented enhancements:**
|
8
|
+
|
9
|
+
- Store and optionally display the full execution backtrace [\#1328](https://github.com/bensheldon/good_job/pull/1328) ([Earlopain](https://github.com/Earlopain))
|
10
|
+
- Store error backtraces on discrete executions [\#1325](https://github.com/bensheldon/good_job/pull/1325) ([Intrepidd](https://github.com/Intrepidd))
|
11
|
+
|
12
|
+
**Fixed bugs:**
|
13
|
+
|
14
|
+
- add missing dropdown-item class [\#1327](https://github.com/bensheldon/good_job/pull/1327) ([patriciomacadden](https://github.com/patriciomacadden))
|
15
|
+
|
16
|
+
**Closed issues:**
|
17
|
+
|
18
|
+
- Proposal: Migrating Documentation to a Separate Website [\#1324](https://github.com/bensheldon/good_job/issues/1324)
|
19
|
+
- Potential documentation error in GoodJob::ActiveJobExtensions::NotifyOptions [\#1321](https://github.com/bensheldon/good_job/issues/1321)
|
20
|
+
- ActiveSupport::CurrentAttributes reset after `perform_later` [\#1320](https://github.com/bensheldon/good_job/issues/1320)
|
21
|
+
- Storing backtrace in database? [\#1162](https://github.com/bensheldon/good_job/issues/1162)
|
22
|
+
- Potential locking race condition when using cron scheduler across multiple processes [\#731](https://github.com/bensheldon/good_job/issues/731)
|
23
|
+
|
24
|
+
**Merged pull requests:**
|
25
|
+
|
26
|
+
- docs: corrected a typo regarding the use of GoodJob::ActiveJobExtensions::NotifyOptions [\#1322](https://github.com/bensheldon/good_job/pull/1322) ([pgvsalamander](https://github.com/pgvsalamander))
|
27
|
+
- Add "best practices" section to Readme [\#1318](https://github.com/bensheldon/good_job/pull/1318) ([bensheldon](https://github.com/bensheldon))
|
28
|
+
- Change ApplicationRecord to ApplicationJob for label documentation. [\#1317](https://github.com/bensheldon/good_job/pull/1317) ([frans-k](https://github.com/frans-k))
|
29
|
+
- Run test matrix against Ruby 3.3; remove pry [\#1315](https://github.com/bensheldon/good_job/pull/1315) ([bensheldon](https://github.com/bensheldon))
|
30
|
+
- Add `Rails.application.load_server` to Demo `config.ru`; quiet puma web-concurrency warnings [\#1314](https://github.com/bensheldon/good_job/pull/1314) ([bensheldon](https://github.com/bensheldon))
|
31
|
+
- Fix test leakage of configuration double [\#1312](https://github.com/bensheldon/good_job/pull/1312) ([bensheldon](https://github.com/bensheldon))
|
32
|
+
- Rewrite queries to all use bind parameters and prepare: true [\#1308](https://github.com/bensheldon/good_job/pull/1308) ([bensheldon](https://github.com/bensheldon))
|
33
|
+
|
34
|
+
## [v3.27.4](https://github.com/bensheldon/good_job/tree/v3.27.4) (2024-04-04)
|
35
|
+
|
36
|
+
[Full Changelog](https://github.com/bensheldon/good_job/compare/v3.27.3...v3.27.4)
|
37
|
+
|
38
|
+
**Fixed bugs:**
|
39
|
+
|
40
|
+
- Fix status tabs for Spanish translation [\#1307](https://github.com/bensheldon/good_job/pull/1307) ([bensheldon](https://github.com/bensheldon))
|
41
|
+
|
42
|
+
**Closed issues:**
|
43
|
+
|
44
|
+
- Adapter to implement `enqueue_after_transaction_commit` [\#1310](https://github.com/bensheldon/good_job/issues/1310)
|
45
|
+
- PG::SyntaxError: ERROR from timestamp [\#1305](https://github.com/bensheldon/good_job/issues/1305)
|
46
|
+
- Spanish translation while on the dashboard displays hashes instead of the job status name [\#1304](https://github.com/bensheldon/good_job/issues/1304)
|
47
|
+
- Exception raised during asset precompilation when no `config/database.yml` [\#1302](https://github.com/bensheldon/good_job/issues/1302)
|
48
|
+
- Good job is running with standalone action cable server [\#1299](https://github.com/bensheldon/good_job/issues/1299)
|
49
|
+
- Growing memory usage [\#1276](https://github.com/bensheldon/good_job/issues/1276)
|
50
|
+
|
51
|
+
**Merged pull requests:**
|
52
|
+
|
53
|
+
- Implement enqueue\_after\_transaction\_commit? [\#1311](https://github.com/bensheldon/good_job/pull/1311) ([luizkowalski](https://github.com/luizkowalski))
|
54
|
+
- Fix localization formatting of `confirm_force_discard` [\#1309](https://github.com/bensheldon/good_job/pull/1309) ([bensheldon](https://github.com/bensheldon))
|
55
|
+
|
3
56
|
## [v3.27.3](https://github.com/bensheldon/good_job/tree/v3.27.3) (2024-03-29)
|
4
57
|
|
5
58
|
[Full Changelog](https://github.com/bensheldon/good_job/compare/v3.27.2...v3.27.3)
|
data/README.md
CHANGED
@@ -72,6 +72,11 @@ For more of the story of GoodJob, read the [introductory blog post](https://isla
|
|
72
72
|
- [Write tests](#write-tests)
|
73
73
|
- [PgBouncer compatibility](#pgbouncer-compatibility)
|
74
74
|
- [CLI HTTP health check probes](#cli-http-health-check-probes)
|
75
|
+
- [Doing your best job with GoodJob](#doing-your-best-job-with-goodjob)
|
76
|
+
- [Sizing jobs: mice and elephants](#sizing-jobs-mice-and-elephants)
|
77
|
+
- [Isolating by total latency](#isolating-by-total-latency)
|
78
|
+
- [Configuring your queues](#configuring-your-queues)
|
79
|
+
- [Additional observations](#additional-observations)
|
75
80
|
- [Contribute](#contribute)
|
76
81
|
- [Gem development](#gem-development)
|
77
82
|
- [Development setup](#development-setup)
|
@@ -500,12 +505,12 @@ Higher priority numbers run first in all versions of GoodJob v3.x and below. Goo
|
|
500
505
|
Labels are the recommended way to add context or metadata to specific jobs. For example, all jobs that have a dependency on an email service could be labeled `email`. Using labels requires adding the Active Job extension `GoodJob::ActiveJobExtensions::Labels` to your job class.
|
501
506
|
|
502
507
|
```ruby
|
503
|
-
class
|
508
|
+
class ApplicationJob < ActiveJob::Base
|
504
509
|
include GoodJob::ActiveJobExtensions::Labels
|
505
510
|
end
|
506
511
|
|
507
512
|
# Add a default label to every job within the class
|
508
|
-
class WelcomeJob <
|
513
|
+
class WelcomeJob < ApplicationJob
|
509
514
|
self.good_job_labels = ["email"]
|
510
515
|
|
511
516
|
def perform
|
@@ -1527,6 +1532,90 @@ gem 'webrick'
|
|
1527
1532
|
|
1528
1533
|
If WEBrick is configured to be used, but the dependency is not found, GoodJob will log a warning and fallback to the default probe server.
|
1529
1534
|
|
1535
|
+
## Doing your best job with GoodJob
|
1536
|
+
|
1537
|
+
_This section explains how to use GoodJob the most efficiently and performantly, according to its maintainers. GoodJob is very flexible and you don’t necessarily have to use it this way, but the concepts explained here are part of GoodJob’s design intent._
|
1538
|
+
|
1539
|
+
Background jobs are hard. There are two extremes:
|
1540
|
+
|
1541
|
+
- **Throw resources (compute, servers, money) at it** by creating dedicated processes (or servers) for each type of job or queue and scaling them independently to achieve the lowest latency and highest throughput.
|
1542
|
+
- **Do the best you can in a small budget** by creating dedicated _thread pools_ within a process for each type of job or queue to produce quality-of-service and compromise maximum latency (or tail latency) because of shared resources and thread contention. You can even run them in the web process if you’re really cheap.
|
1543
|
+
|
1544
|
+
This section will largely focused on optimizing within the latter small-budget scenario, but the concepts and explanation should help you optimize the big-budget scenario too.
|
1545
|
+
|
1546
|
+
Let’s start with anti-patterns, and then the rest of this section will explain an alternative:
|
1547
|
+
|
1548
|
+
- **Don’t use functional names for your queues** like `mailers` or `sms` or `turbo` or `batch`. Instead name them after the total latency target (the total duration within queue and executing till finish) you expect for that job e.g.`latency_30s` or `latency_5m` or `literally_whenever`.
|
1549
|
+
- **Priority can’t fix a lack of capacity.** Priority rules (i.e. weighing or ordering which jobs or queues execute first) only works when there is capacity available to execute that _next_ job. When all capacity is in-use, priority cannot preempt a job that is already executing ("head-of-line blocking").
|
1550
|
+
|
1551
|
+
The following will explain methods to create homogenous workloads (based on latency) and increase execution capacity when queuing latency causes the jobs to exceed their total latency target.
|
1552
|
+
|
1553
|
+
### Sizing jobs: mice and elephants
|
1554
|
+
|
1555
|
+
Queuing theory will refer to fast/small/low-latency tasks as **Mice** (e.g. a password reset email, an MFA token via SMS) and slow/big/high-latency tasks as **Elephants** (e.g. sending an email newsletter to 10k recipients, a batched update that touches every record in the database).
|
1556
|
+
|
1557
|
+
Explicitly group your jobs by their latency: how quickly you expect them to finish to achieve your expected quality of service. This should be their **total latency** (or duration) which is the sum of: **queuing latency** which is how long the job waits in queue until execution capacity becomes available (which ideally should be zero, because you have idle capacity and can start executing a job immediately as soon as it is enqueued or upon its scheduled time) and **execution latency** which is how long the job’s execution takes (e.g. the email being sent). Example: I expect this Password Reset Email Job to have a total latency of 30 seconds or less.
|
1558
|
+
|
1559
|
+
In a working application, you likely will have more gradations than just small and big or slow and fast (analogously: badgers, wildebeests; maybe even tardigrades or blue whales for tiny and huge, respectively), but there will regardless be a relatively small and countable number of discrete latency buckets to organize your jobs into.
|
1560
|
+
|
1561
|
+
### Isolating by total latency
|
1562
|
+
|
1563
|
+
The most efficient workloads are homogenous (similar) workloads. If you know every job to be executed will take about the same amount of time, you can estimate the maximum delay for a new job at the back of the queue and have that drive decisions about capacity. Alternatively, if those jobs are heterogenous (mixed) it’s possible that a very slow/long-duration job could hold everything back for much longer than anticipated and it’s sorta random. That’s bad!
|
1564
|
+
|
1565
|
+
A fun visual image here for a single-file queue is a doorway: If you only have 1 doorway, it must be big enough to fit an elephant. But if an elephant is going through the door (and it will go through slowly!) no mice can fit through the door until the elephant is fully clear. Your mice will be delayed!
|
1566
|
+
|
1567
|
+
Priority will not help when an elephant is in the doorway. Yes, you could say mice have a higher priority than elephants and always allow any mouse to go _before_ any elephant in queue will start. But once an elephant *has started* going through the door, any subsequent mouse who arrives must wait for the elephant to egress regardless of their priority. In Active Job and Ruby, it’s really hard to stop or cancel or preempt a running job (unless you’ve already architected that into your jobs, like with the [`job-iteration`](https://github.com/Shopify/job-iteration) library)
|
1568
|
+
|
1569
|
+
The best solution is to have a 2nd door, but only sized for mice, so an elephant can’t ever block it. With a mouse-sized doorway _and_ an elephant-sized doorway, mice can still go through the big elephant door when an elephant isn’t using it. Each door has a _maximum_ size (or “latency”) we want it to accept, and smaller is ok, just not larger.
|
1570
|
+
|
1571
|
+
### Configuring your queues
|
1572
|
+
|
1573
|
+
If we wanted to capture the previous 2-door scenario in GoodJob, we’d configure the queues like this;
|
1574
|
+
|
1575
|
+
```ruby
|
1576
|
+
config.good_job.queues = "mice:1; elephant,mice:1"
|
1577
|
+
```
|
1578
|
+
|
1579
|
+
This configuration creates two isolated thread pools (separated by a semicolon) each with 1 thread each (the number after the colon). The 2nd thread pool recognizes that both elephants and mice can use that isolated thread pool; if there is an influx of mice, it's possible to use the elephant’s thread pool if an elephant isn't already in progress.
|
1580
|
+
|
1581
|
+
So what if we add an intermediately-sized `badgers` ? In that case, we can make 3 distinct queues:
|
1582
|
+
|
1583
|
+
```ruby
|
1584
|
+
config.good_job.queues = "mice:1; badgers,mice:1; elephants,badgers,mice:1"
|
1585
|
+
```
|
1586
|
+
|
1587
|
+
In this case, we make a mouse sized queue, a badger sized queue, and an elephant sized queue. We can simplify this even further:
|
1588
|
+
|
1589
|
+
```ruby
|
1590
|
+
config.good_job.queues = "mice:1; badgers,mice:1; *:1"
|
1591
|
+
```
|
1592
|
+
|
1593
|
+
Using the wildcard `*` for any queue also helps ensure that if a job is enqueued to a newly declared queue (maybe via a dependency or just inadvertently) it will still get executed until you notice and decide on its appropriate latency target.
|
1594
|
+
|
1595
|
+
In these examples, the order doesn’t matter; it just is maybe more readable to go from the lowest-latency to largest-latency pool (the semicolon groups), and then within a pool to list the largest allowable latency first (the commas). Nothing here is about “job priority” or “queue priority”, this is wholly about grouping.
|
1596
|
+
|
1597
|
+
In your application, not the zoo, you’ll want to enqueue your `PaswordResetJob` on the `mice` queue, your `CreateComplicatedObjectJob` on the `badger` queue, and your `AuditEveryAccountEverJob` on the `elephant` queue. But you want to name your queues by latency, so that ends up being:
|
1598
|
+
|
1599
|
+
```ruby
|
1600
|
+
config.good_job.queues = "latency_30s:1; latency_2m,latency_30s:1; *:1"
|
1601
|
+
```
|
1602
|
+
|
1603
|
+
And you likely want to have more than one thread (though more than 3-5 threads per process will cause thread contention and slow everything down a bit):
|
1604
|
+
|
1605
|
+
```ruby
|
1606
|
+
config.good_job.queues = "latency_30s:2; latency_2m,latency_30s:2; *:2"
|
1607
|
+
```
|
1608
|
+
|
1609
|
+
### Additional observations
|
1610
|
+
|
1611
|
+
- Unlike GoodJob, other Active Job backends may treat a "queue" and an "isolated execution pool" as one and the same. GoodJob allows composing multiple Active Job queues into the same pool for flexibility and to make it easier to migrate from functionally-named queues to latency-based ones.
|
1612
|
+
- You don't *have* to name your queues explicitly like `latency_30s` but it makes it easier to identify outliers and communicate your operational targets. Many people push back on this; that's ok. An option to capture functional details is to use GoodJob's Labels feature instead of encoding them in the queue name.
|
1613
|
+
- The downside of organizing your jobs like this is that you may have jobs with the same latency target but wildly different operational parameters, like being coupled to another system that has limited throughput or questionable reliability. GoodJob offers Concurrency and Throttling Controls, but isolation is always the most performant and reliable option, though it requires dedicated resources and costs more.
|
1614
|
+
- Observe, monitor, and adapt your job queues over time. You likely have incomplete information about the execution latency of your jobs inclusive of all dependencies across all scenarios. You should expect to adjust your queues and grouping over time as you observe their behavior.
|
1615
|
+
- If you find you have unreliable external dependencies that introduce latency, you may also want to further isolate your jobs based on those dependencies, for example, isolating `latency_10s_email_service` to its own execution pool.
|
1616
|
+
- Scale on queue latency. Per the previous point in which you do not have complete control over execution latency, you do have control over the queue latency. If queue latency is causing your jobs to miss their total latency target, you must add more capacity (e.g. processes or servers.
|
1617
|
+
- This is all largely about latency-based queue design. It’s possible to go further and organize by latency _and_ parallelism. For that I recommend Nate Berkopec’s [*Complete Guide to Rails Performance*](https://www.railsspeed.com/) which covers things like Amdahl’s Law.
|
1618
|
+
|
1530
1619
|
## Contribute
|
1531
1620
|
|
1532
1621
|
<!-- Please keep this section in sync with CONTRIBUTING.md -->
|
@@ -46,14 +46,19 @@ module GoodJob
|
|
46
46
|
cte_query = cte_query.limit(select_limit) if select_limit
|
47
47
|
cte_type = supports_cte_materialization_specifiers? ? :MATERIALIZED : :""
|
48
48
|
composed_cte = Arel::Nodes::As.new(cte_table, Arel::Nodes::UnaryOperation.new(cte_type, cte_query.arel))
|
49
|
+
|
50
|
+
lock_condition = "#{function}(('x' || substr(md5(#{connection.quote(table_name)} || '-' || #{connection.quote_table_name(cte_table.name)}.#{connection.quote_column_name(column)}::text), 1, 16))::bit(64)::bigint)"
|
49
51
|
query = cte_table.project(cte_table[:id])
|
50
52
|
.with(composed_cte)
|
51
|
-
.where(Arel
|
53
|
+
.where(defined?(Arel::Nodes::BoundSqlLiteral) ? Arel::Nodes::BoundSqlLiteral.new(lock_condition, [], {}) : Arel::Nodes::SqlLiteral.new(lock_condition))
|
52
54
|
|
53
55
|
limit = original_query.arel.ast.limit
|
54
56
|
query.limit = limit.value if limit.present?
|
55
57
|
|
56
|
-
|
58
|
+
# Arel.sql and the IN clause prevent this from being preparable
|
59
|
+
# That's why this is manually composed of BoundSqlLiteral's and an InfixOperation
|
60
|
+
# to sidestep anywhere in Arel where the `collector.preparable = false` is set
|
61
|
+
unscoped.where(Arel::Nodes::InfixOperation.new("IN", arel_table[primary_key], query)).merge(original_query.only(:order))
|
57
62
|
end)
|
58
63
|
|
59
64
|
# Joins the current query with Postgres's +pg_locks+ table (it provides
|
@@ -17,9 +17,9 @@ module GoodJob
|
|
17
17
|
scope :display_all, (lambda do |after_scheduled_at: nil, after_id: nil|
|
18
18
|
query = order(Arel.sql('COALESCE(scheduled_at, created_at) DESC, id DESC'))
|
19
19
|
if after_scheduled_at.present? && after_id.present?
|
20
|
-
query = query.where
|
20
|
+
query = query.where Arel::Nodes::Grouping.new([coalesce_scheduled_at_created_at, arel_table["id"]]).lt(Arel::Nodes::Grouping.new([bind_value('coalesce', after_scheduled_at, ActiveRecord::Type::DateTime), bind_value('id', after_id, ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Uuid)]))
|
21
21
|
elsif after_scheduled_at.present?
|
22
|
-
query = query.where
|
22
|
+
query = query.where coalesce_scheduled_at_created_at.lt(bind_value('coalesce', after_scheduled_at, ActiveRecord::Type::DateTime))
|
23
23
|
end
|
24
24
|
query
|
25
25
|
end)
|
@@ -34,6 +34,7 @@ module GoodJob
|
|
34
34
|
query = query.to_s.strip
|
35
35
|
next if query.blank?
|
36
36
|
|
37
|
+
# TODO: turn this into proper bind parameters in Arel
|
37
38
|
tsvector = "(to_tsvector('english', id::text) || to_tsvector('english', COALESCE(active_job_id::text, '')) || to_tsvector('english', serialized_params) || to_tsvector('english', COALESCE(error, ''))#{" || to_tsvector('english', COALESCE(array_to_string(labels, ' '), ''))" if labels_migrated?})"
|
38
39
|
to_tsquery_function = database_supports_websearch_to_tsquery? ? 'websearch_to_tsquery' : 'plainto_tsquery'
|
39
40
|
where("#{tsvector} @@ #{to_tsquery_function}(?)", query)
|
@@ -40,6 +40,10 @@ module GoodJob
|
|
40
40
|
end
|
41
41
|
end
|
42
42
|
|
43
|
+
def self.bind_value(name, value, type_class)
|
44
|
+
Arel::Nodes::BindParam.new(ActiveRecord::Relation::QueryAttribute.new(name, value, type_class.new))
|
45
|
+
end
|
46
|
+
|
43
47
|
ActiveSupport.run_load_hooks(:good_job_base_record, self)
|
44
48
|
end
|
45
49
|
end
|
@@ -18,6 +18,8 @@ module GoodJob
|
|
18
18
|
scope :not_discarded, -> { where(discarded_at: nil) }
|
19
19
|
scope :succeeded, -> { finished.not_discarded }
|
20
20
|
|
21
|
+
scope :finished_before, ->(timestamp) { where(arel_table['finished_at'].lteq(bind_value('finished_at', timestamp, ActiveRecord::Type::DateTime))) }
|
22
|
+
|
21
23
|
alias_attribute :enqueued?, :enqueued_at
|
22
24
|
alias_attribute :discarded?, :discarded_at
|
23
25
|
alias_attribute :finished?, :finished_at
|
@@ -25,9 +27,13 @@ module GoodJob
|
|
25
27
|
scope :display_all, (lambda do |after_created_at: nil, after_id: nil|
|
26
28
|
query = order(created_at: :desc, id: :desc)
|
27
29
|
if after_created_at.present? && after_id.present?
|
28
|
-
query =
|
30
|
+
query = if Gem::Version.new(Rails.version) < Gem::Version.new('7.0.0.a') || Concurrent.on_jruby?
|
31
|
+
query.where(Arel.sql('(created_at, id) < (:after_created_at, :after_id)'), after_created_at: after_created_at, after_id: after_id)
|
32
|
+
else
|
33
|
+
query.where Arel::Nodes::Grouping.new([arel_table["created_at"], arel_table["id"]]).lt(Arel::Nodes::Grouping.new([bind_value('created_at', after_created_at, ActiveRecord::Type::DateTime), bind_value('id', after_id, ActiveRecord::Type::String)]))
|
34
|
+
end
|
29
35
|
elsif after_created_at.present?
|
30
|
-
query = query.where
|
36
|
+
query = query.where arel_table["created_at"].lt(bind_value('created_at', after_created_at, ActiveRecord::Type::DateTime))
|
31
37
|
end
|
32
38
|
query
|
33
39
|
end)
|
@@ -21,6 +21,13 @@ module GoodJob # :nodoc:
|
|
21
21
|
false
|
22
22
|
end
|
23
23
|
|
24
|
+
def self.backtrace_migrated?
|
25
|
+
return true if columns_hash["error_backtrace"].present?
|
26
|
+
|
27
|
+
migration_pending_warning!
|
28
|
+
false
|
29
|
+
end
|
30
|
+
|
24
31
|
def number
|
25
32
|
serialized_params.fetch('executions', 0) + 1
|
26
33
|
end
|
@@ -58,5 +65,9 @@ module GoodJob # :nodoc:
|
|
58
65
|
_good_job_execution: attributes.except('serialized_params'),
|
59
66
|
})
|
60
67
|
end
|
68
|
+
|
69
|
+
def filtered_error_backtrace
|
70
|
+
Rails.backtrace_cleaner.clean(error_backtrace || [])
|
71
|
+
end
|
61
72
|
end
|
62
73
|
end
|
@@ -95,7 +95,7 @@ module GoodJob
|
|
95
95
|
# @!method only_scheduled
|
96
96
|
# @!scope class
|
97
97
|
# @return [ActiveRecord::Relation]
|
98
|
-
scope :only_scheduled, -> { where(arel_table['scheduled_at'].lteq(
|
98
|
+
scope :only_scheduled, -> { where(arel_table['scheduled_at'].lteq(bind_value('scheduled_at', DateTime.current, ActiveRecord::Type::DateTime))).or(where(scheduled_at: nil)) }
|
99
99
|
|
100
100
|
# Order executions by priority (highest priority first).
|
101
101
|
# @!method priority_ordered
|
@@ -161,7 +161,7 @@ module GoodJob
|
|
161
161
|
# @param timestamp (Float)
|
162
162
|
# Get jobs that finished before this time (in epoch time).
|
163
163
|
# @return [ActiveRecord::Relation]
|
164
|
-
scope :finished, ->(timestamp = nil) { timestamp ? where(arel_table['finished_at'].lteq(timestamp)) : where.not(finished_at: nil) }
|
164
|
+
scope :finished, ->(timestamp = nil) { timestamp ? where(arel_table['finished_at'].lteq(bind_value('finished_at', timestamp, ActiveRecord::Type::DateTime))) : where.not(finished_at: nil) }
|
165
165
|
|
166
166
|
# Get Jobs that started but not finished yet.
|
167
167
|
# @!method running
|
@@ -290,11 +290,12 @@ module GoodJob
|
|
290
290
|
query = advisory_unlocked.unfinished.schedule_ordered
|
291
291
|
|
292
292
|
after ||= Time.current
|
293
|
-
|
293
|
+
after_bind = bind_value('scheduled_at', after, ActiveRecord::Type::DateTime)
|
294
|
+
after_query = query.where(arel_table['scheduled_at'].gt(after_bind)).or query.where(scheduled_at: nil).where(arel_table['created_at'].gt(after_bind))
|
294
295
|
after_at = after_query.limit(limit).pluck(:scheduled_at, :created_at).map { |timestamps| timestamps.compact.first }
|
295
296
|
|
296
297
|
if now_limit&.positive?
|
297
|
-
now_query = query.where('scheduled_at
|
298
|
+
now_query = query.where(arel_table['scheduled_at'].lt(bind_value('scheduled_at', Time.current, ActiveRecord::Type::DateTime))).or query.where(scheduled_at: nil)
|
298
299
|
now_at = now_query.limit(now_limit).pluck(:scheduled_at, :created_at).map { |timestamps| timestamps.compact.first }
|
299
300
|
end
|
300
301
|
|
@@ -460,6 +461,7 @@ module GoodJob
|
|
460
461
|
if discrete_execution
|
461
462
|
discrete_execution.error = error_string
|
462
463
|
discrete_execution.error_event = result.error_event
|
464
|
+
discrete_execution.error_backtrace = job_error.backtrace if discrete_execution.class.backtrace_migrated?
|
463
465
|
end
|
464
466
|
else
|
465
467
|
job_attributes[:error] = nil
|
data/app/models/good_job/job.rb
CHANGED
@@ -44,14 +44,14 @@ module GoodJob
|
|
44
44
|
# @!scope class
|
45
45
|
# @param timestamp (DateTime, Time)
|
46
46
|
# @return [ActiveRecord::Relation]
|
47
|
-
scope :finished_before, ->(timestamp) { where(arel_table['finished_at'].lteq(timestamp)) }
|
47
|
+
scope :finished_before, ->(timestamp) { where(arel_table['finished_at'].lteq(bind_value('finished_at', timestamp, ActiveRecord::Type::DateTime))) }
|
48
48
|
|
49
49
|
# First execution will run in the future
|
50
|
-
scope :scheduled, -> { where(finished_at: nil).where(coalesce_scheduled_at_created_at.gt(
|
50
|
+
scope :scheduled, -> { where(finished_at: nil).where(coalesce_scheduled_at_created_at.gt(bind_value('coalesce', Time.current, ActiveRecord::Type::DateTime))).where(params_execution_count.lt(2)) }
|
51
51
|
# Execution errored, will run in the future
|
52
|
-
scope :retried, -> { where(finished_at: nil).where(coalesce_scheduled_at_created_at.gt(
|
52
|
+
scope :retried, -> { where(finished_at: nil).where(coalesce_scheduled_at_created_at.gt(bind_value('coalesce', Time.current, ActiveRecord::Type::DateTime))).where(params_execution_count.gt(1)) }
|
53
53
|
# Immediate/Scheduled time to run has passed, waiting for an available thread run
|
54
|
-
scope :queued, -> { where(finished_at: nil).where(coalesce_scheduled_at_created_at.lteq(
|
54
|
+
scope :queued, -> { where(finished_at: nil).where(coalesce_scheduled_at_created_at.lteq(bind_value('coalesce', Time.current, ActiveRecord::Type::DateTime))).joins_advisory_locks.where(pg_locks: { locktype: nil }) }
|
55
55
|
# Advisory locked and executing
|
56
56
|
scope :running, -> { where(finished_at: nil).joins_advisory_locks.where.not(pg_locks: { locktype: nil }) }
|
57
57
|
# Finished executing (succeeded or discarded)
|
@@ -35,9 +35,35 @@
|
|
35
35
|
</div>
|
36
36
|
<% if execution.error %>
|
37
37
|
<div class="mt-3 small">
|
38
|
-
<strong class="small"><%=t "good_job.shared.error" %>:</strong>
|
38
|
+
<strong class="small text-danger"><%=t "good_job.shared.error" %>:</strong>
|
39
39
|
<code class="text-wrap text-break m-0 text-secondary-emphasis"><%= execution.error %></code>
|
40
40
|
</div>
|
41
|
+
<% if GoodJob::DiscreteExecution.backtrace_migrated? && execution.error_backtrace&.any? %>
|
42
|
+
<%= tag.ul class: "nav nav-tabs small w-fit-content", id: dom_id(execution, :tab), role:"tablist" do %>
|
43
|
+
<li class="nav-item" role="presentation">
|
44
|
+
<%= tag.button t(".application_trace"), class: "nav-link active p-1", id: dom_id(execution, :application), data: { bs_toggle: "tab", bs_target: dom_id(execution, :"#application_pane") }, type: "button", role: "tab", aria: { controls: dom_id(execution, :application_pane), selected: true } %>
|
45
|
+
</li>
|
46
|
+
<li class="nav-item" role="presentation">
|
47
|
+
<%= tag.button t(".full_trace"), class: "nav-link p-1", id: dom_id(execution, :full), data: { bs_toggle: "tab", bs_target: dom_id(execution, :"#full_pane") }, type: "button", role: "tab", aria: { controls: dom_id(execution, :full_pane), selected: false } %>
|
48
|
+
</li>
|
49
|
+
<% end %>
|
50
|
+
<%= tag.div class: "tab-content", id: "#{dom_id(execution, :tab)}Content" do %>
|
51
|
+
<%= tag.div class: "tab-pane fade show active", id: dom_id(execution, :application_pane), role: "tabpane", aria: { labelledby: dom_id(execution, :application) }, tabindex: 0 do %>
|
52
|
+
<div class="small">
|
53
|
+
<code class="text-wrap text-break m-0 text-secondary-emphasis">
|
54
|
+
<%= safe_join(execution.filtered_error_backtrace, tag.br) %>
|
55
|
+
</code>
|
56
|
+
</div>
|
57
|
+
<% end %>
|
58
|
+
<%= tag.div class: "tab-pane fade", id: dom_id(execution, :full_pane), role: "tabpane", aria: { labelledby: dom_id(execution, :full) }, tabindex: 0 do %>
|
59
|
+
<div class="small">
|
60
|
+
<code class="text-wrap text-break m-0 text-secondary-emphasis">
|
61
|
+
<%= safe_join(execution.error_backtrace, tag.br) %>
|
62
|
+
</code>
|
63
|
+
</div>
|
64
|
+
<% end %>
|
65
|
+
<% end %>
|
66
|
+
<% end %>
|
41
67
|
<% end %>
|
42
68
|
<% end %>
|
43
69
|
<%= render 'good_job/custom_execution_details', execution: execution, job: @job %>
|
@@ -25,7 +25,7 @@
|
|
25
25
|
</button>
|
26
26
|
<ul class="dropdown-menu" aria-labelledby="destroy-dropdown-toggle">
|
27
27
|
<li>
|
28
|
-
<%= form.button type: 'submit', name: 'mass_action', value: 'destroy', class: 'btn', title: t(".actions.destroy_all"), data: { confirm: t(".actions.confirm_destroy_all"), disable: true } do %>
|
28
|
+
<%= form.button type: 'submit', name: 'mass_action', value: 'destroy', class: 'btn dropdown-item', title: t(".actions.destroy_all"), data: { confirm: t(".actions.confirm_destroy_all"), disable: true } do %>
|
29
29
|
<span class="me-1"><%= render_icon "trash" %></span> <%=t "good_job.actions.destroy" %>
|
30
30
|
<% end %>
|
31
31
|
</li>
|
@@ -68,7 +68,7 @@
|
|
68
68
|
<%= tag.h5 tag.code(link_to(job.display_name, job_path(job), class: "text-reset text-decoration-none")), class: "text-reset mb-0" %>
|
69
69
|
<% if job.error %>
|
70
70
|
<div class="mt-1 small">
|
71
|
-
<strong class="small"><%=t "good_job.shared.error" %>:</strong>
|
71
|
+
<strong class="small text-danger"><%=t "good_job.shared.error" %>:</strong>
|
72
72
|
<code class="text-wrap text-break m-0 text-secondary-emphasis"><%= job.error %></code>
|
73
73
|
</div>
|
74
74
|
<% end %>
|
data/config/locales/de.yml
CHANGED
@@ -111,9 +111,7 @@ de:
|
|
111
111
|
actions:
|
112
112
|
confirm_destroy: Sind Sie sicher, dass Sie den Job zerstören wollen?
|
113
113
|
confirm_discard: Sind Sie sicher, dass Sie den Job verwerfen wollen?
|
114
|
-
confirm_force_discard:
|
115
|
-
|
116
|
-
'
|
114
|
+
confirm_force_discard: Sind Sie sicher, dass Sie das Verwerfen dieses Jobs erzwingen möchten? Der Job wird als verworfen markiert, aber der laufende Job wird nicht gestoppt – er wird jedoch bei Fehlern nicht erneut versucht.
|
117
115
|
confirm_reschedule: Möchten Sie den Auftrag wirklich verschieben?
|
118
116
|
confirm_retry: Möchten Sie den Job wirklich wiederholen?
|
119
117
|
destroy: Arbeit vernichten
|
@@ -126,6 +124,8 @@ de:
|
|
126
124
|
discard:
|
127
125
|
notice: Auftrag wurde verworfen
|
128
126
|
executions:
|
127
|
+
application_trace: Application Trace
|
128
|
+
full_trace: Full Trace
|
129
129
|
in_queue: in der Warteschlange
|
130
130
|
runtime: Laufzeit
|
131
131
|
title: Hinrichtungen
|
data/config/locales/en.yml
CHANGED
@@ -111,9 +111,7 @@ en:
|
|
111
111
|
actions:
|
112
112
|
confirm_destroy: Are you sure you want to destroy the job?
|
113
113
|
confirm_discard: Are you usure you want to discard the job?
|
114
|
-
confirm_force_discard:
|
115
|
-
|
116
|
-
'
|
114
|
+
confirm_force_discard: Are you sure you want to force discard this job? The job will be marked as discarded but the running job will not be stopped - it will, however, not be retried on failures.
|
117
115
|
confirm_reschedule: Are you sure you want to reschedule the job?
|
118
116
|
confirm_retry: Are you sure you want to retry the job?
|
119
117
|
destroy: Destroy job
|
@@ -126,6 +124,8 @@ en:
|
|
126
124
|
discard:
|
127
125
|
notice: Job has been discarded
|
128
126
|
executions:
|
127
|
+
application_trace: Application Trace
|
128
|
+
full_trace: Full Trace
|
129
129
|
in_queue: in queue
|
130
130
|
runtime: runtime
|
131
131
|
title: Executions
|
data/config/locales/es.yml
CHANGED
@@ -124,6 +124,8 @@ es:
|
|
124
124
|
discard:
|
125
125
|
notice: La tarea ha sido descartada
|
126
126
|
executions:
|
127
|
+
application_trace: Application Trace
|
128
|
+
full_trace: Full Trace
|
127
129
|
in_queue: en cola
|
128
130
|
runtime: en ejecución
|
129
131
|
title: Ejecuciones
|
@@ -233,19 +235,9 @@ es:
|
|
233
235
|
inspiration: "¡Recuerda, también tú estás haciendo un buen trabajo!"
|
234
236
|
last_updated: Última actualización
|
235
237
|
status:
|
236
|
-
discarded:
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
retried:
|
243
|
-
one: Reintentada
|
244
|
-
other: Reintentadas
|
245
|
-
running: Corriendo
|
246
|
-
scheduled:
|
247
|
-
one: Programada
|
248
|
-
other: Programadas
|
249
|
-
succeeded:
|
250
|
-
one: Exitosa
|
251
|
-
other: Exitosas
|
238
|
+
discarded: Descartado
|
239
|
+
queued: Encolado
|
240
|
+
retried: Reintentado
|
241
|
+
running: Ejecutando
|
242
|
+
scheduled: Programado
|
243
|
+
succeeded: Exitoso
|
data/config/locales/fr.yml
CHANGED
@@ -33,9 +33,9 @@ fr:
|
|
33
33
|
no_batches_found: Aucun lot trouvé.
|
34
34
|
cron_entries:
|
35
35
|
actions:
|
36
|
-
confirm_disable: Voulez-vous vraiment désactiver cette entrée cron?
|
37
|
-
confirm_enable: Voulez-vous vraiment activer cette entrée cron?
|
38
|
-
confirm_enqueue: Voulez-vous vraiment mettre en file d'attente cette entrée cron?
|
36
|
+
confirm_disable: Voulez-vous vraiment désactiver cette entrée cron ?
|
37
|
+
confirm_enable: Voulez-vous vraiment activer cette entrée cron ?
|
38
|
+
confirm_enqueue: Voulez-vous vraiment mettre en file d'attente cette entrée cron ?
|
39
39
|
disable: Désactiver l'entrée cron
|
40
40
|
enable: Activer l'entrée cron
|
41
41
|
enqueue: Mettre en file d'attente l'entrée cron maintenant
|
@@ -111,9 +111,7 @@ fr:
|
|
111
111
|
actions:
|
112
112
|
confirm_destroy: Voulez-vous vraiment détruire le job ?
|
113
113
|
confirm_discard: Voulez-vous vraiment mettre au rebut le job ?
|
114
|
-
confirm_force_discard:
|
115
|
-
|
116
|
-
'
|
114
|
+
confirm_force_discard: Êtes-vous sûr de vouloir forcer l'abandon du travail? Le travail sera marqué comme abandonné mais le travail en cours ne sera pas arrêté - cependant, il ne sera pas réessayé en cas d'échec.
|
117
115
|
confirm_reschedule: Voulez-vous vraiment replanifier le job ?
|
118
116
|
confirm_retry: Voulez-vous vraiment réessayer le job ?
|
119
117
|
destroy: Détruire le job
|
@@ -126,6 +124,8 @@ fr:
|
|
126
124
|
discard:
|
127
125
|
notice: Le job a été mis au rebut
|
128
126
|
executions:
|
127
|
+
application_trace: Application Trace
|
128
|
+
full_trace: Full Trace
|
129
129
|
in_queue: Dans la file d'attente
|
130
130
|
runtime: Durée
|
131
131
|
title: Exécutions
|
@@ -146,7 +146,7 @@ fr:
|
|
146
146
|
one: Appliquer à ce job
|
147
147
|
other: Appliquer aux %{count} jobs
|
148
148
|
confirm_destroy_all: Voulez-vous vraiment détruire les jobs sélectionnés ?
|
149
|
-
confirm_discard_all: Voulez-vous vraiment mettre au rebut les jobs sélectionnés?
|
149
|
+
confirm_discard_all: Voulez-vous vraiment mettre au rebut les jobs sélectionnés ?
|
150
150
|
confirm_reschedule_all: Voulez-vous vraiment replanifier les jobs sélectionnées ?
|
151
151
|
confirm_retry_all: Voulez-vous vraiment réessayer les jobs sélectionnées ?
|
152
152
|
destroy_all: Tout détruire
|
data/config/locales/it.yml
CHANGED
@@ -111,9 +111,7 @@ it:
|
|
111
111
|
actions:
|
112
112
|
confirm_destroy: Sei sicuro di voler eliminare il job?
|
113
113
|
confirm_discard: Sei sicuro di voler scartare il job?
|
114
|
-
confirm_force_discard:
|
115
|
-
|
116
|
-
'
|
114
|
+
confirm_force_discard: Sei sicuro di voler forzare lo scarto di questo job? Il job verrà contrassegnato come scartato ma il job in esecuzione non verrà interrotto - tuttavia, non verrà riprovato in caso di fallimento.
|
117
115
|
confirm_reschedule: Sei sicuro di voler riprogrammare il job?
|
118
116
|
confirm_retry: Sei sicuro di voler riprovare il job?
|
119
117
|
destroy: Elimina job
|
@@ -126,6 +124,8 @@ it:
|
|
126
124
|
discard:
|
127
125
|
notice: Il job è stato scartato
|
128
126
|
executions:
|
127
|
+
application_trace: Application Trace
|
128
|
+
full_trace: Full Trace
|
129
129
|
in_queue: in coda
|
130
130
|
runtime: tempo di esecuzione
|
131
131
|
title: Esecuzioni
|
data/config/locales/ja.yml
CHANGED
@@ -111,9 +111,7 @@ ja:
|
|
111
111
|
actions:
|
112
112
|
confirm_destroy: ジョブを削除してもよろしいですか?
|
113
113
|
confirm_discard: ジョブを破棄してもよろしいですか?
|
114
|
-
confirm_force_discard:
|
115
|
-
|
116
|
-
'
|
114
|
+
confirm_force_discard: このジョブを強制的に破棄してもよろしいですか?ジョブは破棄済みとしてマークされますが、実行中のジョブは停止されません。ただし、失敗した場合は再試行されません。
|
117
115
|
confirm_reschedule: ジョブを再スケジュールしてもよろしいですか?
|
118
116
|
confirm_retry: ジョブを再試行してもよろしいですか?
|
119
117
|
destroy: ジョブを削除
|
@@ -126,6 +124,8 @@ ja:
|
|
126
124
|
discard:
|
127
125
|
notice: ジョブが破棄されました
|
128
126
|
executions:
|
127
|
+
application_trace: Application Trace
|
128
|
+
full_trace: Full Trace
|
129
129
|
in_queue: 待機中
|
130
130
|
runtime: 実行時間
|
131
131
|
title: 実行
|
data/config/locales/ko.yml
CHANGED
@@ -111,9 +111,7 @@ ko:
|
|
111
111
|
actions:
|
112
112
|
confirm_destroy: 해당 작업을 삭제하시겠습니까?
|
113
113
|
confirm_discard: 해당 작업을 폐기하시겠습니까?
|
114
|
-
confirm_force_discard:
|
115
|
-
|
116
|
-
'
|
114
|
+
confirm_force_discard: 이 작업을 강제로 폐기하시겠습니까? 작업은 폐기로 표시되지만, 실행 중인 작업은 중지되지 않습니다. 그러나 실패한 경우 재시도되지 않습니다.
|
117
115
|
confirm_reschedule: 해당 작업을 재예약하시겠습니까?
|
118
116
|
confirm_retry: 해당 작업을 다시 시도하시겠습니까?
|
119
117
|
destroy: 작업 삭제
|
@@ -126,6 +124,8 @@ ko:
|
|
126
124
|
discard:
|
127
125
|
notice: 작업이 폐기되었습니다.
|
128
126
|
executions:
|
127
|
+
application_trace: Application Trace
|
128
|
+
full_trace: Full Trace
|
129
129
|
in_queue: 대기 중
|
130
130
|
runtime: 실행 시간
|
131
131
|
title: 실행
|
data/config/locales/nl.yml
CHANGED
@@ -33,7 +33,7 @@ nl:
|
|
33
33
|
no_batches_found: Geen batches gevonden.
|
34
34
|
cron_entries:
|
35
35
|
actions:
|
36
|
-
confirm_disable: Weet u
|
36
|
+
confirm_disable: Weet u zekerf dat u deze cron-vermelding wilt uitschakelen?
|
37
37
|
confirm_enable: Weet u zeker dat u deze cron-invoer wilt inschakelen?
|
38
38
|
confirm_enqueue: Weet u zeker dat u deze cron-vermelding in de wachtrij wilt plaatsen?
|
39
39
|
disable: Schakel cron-invoer uit
|
@@ -111,9 +111,7 @@ nl:
|
|
111
111
|
actions:
|
112
112
|
confirm_destroy: Weet je zeker dat je de baan wilt vernietigen?
|
113
113
|
confirm_discard: Wilt u de baan afwijzen?
|
114
|
-
confirm_force_discard:
|
115
|
-
|
116
|
-
'
|
114
|
+
confirm_force_discard: Weet u zeker dat u deze taak geforceerd wilt weggooien? De taak wordt gemarkeerd als verwijderd, maar de lopende taak wordt niet gestopt. Bij fouten wordt deze echter niet opnieuw geprobeerd.
|
117
115
|
confirm_reschedule: Weet u zeker dat u de taak opnieuw wilt inplannen?
|
118
116
|
confirm_retry: Weet u zeker dat u de taak opnieuw wilt proberen?
|
119
117
|
destroy: Baan vernietigen
|
@@ -126,6 +124,8 @@ nl:
|
|
126
124
|
discard:
|
127
125
|
notice: Job is weggegooid
|
128
126
|
executions:
|
127
|
+
application_trace: Application Trace
|
128
|
+
full_trace: Full Trace
|
129
129
|
in_queue: in de wachtrij
|
130
130
|
runtime: looptijd
|
131
131
|
title: Executies
|
data/config/locales/pt-BR.yml
CHANGED
@@ -111,9 +111,7 @@ pt-BR:
|
|
111
111
|
actions:
|
112
112
|
confirm_destroy: Tem certeza de que deseja excluir a tarefa?
|
113
113
|
confirm_discard: Tem certeza de que deseja descartar a tarefa?
|
114
|
-
confirm_force_discard:
|
115
|
-
|
116
|
-
'
|
114
|
+
confirm_force_discard: Tem certeza de que deseja forçar o descarte desta tarefa? A tarefa será marcada como descartada, mas a tarefa em execução não será interrompida - no entanto, ela não será tentada novamente em falhas.
|
117
115
|
confirm_reschedule: Tem certeza de que deseja reagendar a tarefa?
|
118
116
|
confirm_retry: Tem certeza de que deseja tentar novamente a tarefa?
|
119
117
|
destroy: Excluir tarefa
|
@@ -126,6 +124,8 @@ pt-BR:
|
|
126
124
|
discard:
|
127
125
|
notice: A tarefa foi descartada.
|
128
126
|
executions:
|
127
|
+
application_trace: Application Trace
|
128
|
+
full_trace: Full Trace
|
129
129
|
in_queue: na fila
|
130
130
|
runtime: tempo de execução
|
131
131
|
title: Execuções
|
data/config/locales/ru.yml
CHANGED
data/config/locales/tr.yml
CHANGED
@@ -111,9 +111,7 @@ tr:
|
|
111
111
|
actions:
|
112
112
|
confirm_destroy: Bu işi silmek istediğinizden emin misiniz?
|
113
113
|
confirm_discard: Bu işi iptal etmek istediğinizden emin misiniz?
|
114
|
-
confirm_force_discard:
|
115
|
-
|
116
|
-
'
|
114
|
+
confirm_force_discard: Bu işi zorla iptal etmek istediğinizden emin misiniz? İş atıldı olarak işaretlenecek ancak devam eden iş durdurulmayacak; ancak başarısızlık durumunda yeniden denenmeyecek.
|
117
115
|
confirm_reschedule: Bu işi yeniden planlamak istediğinizden emin misiniz?
|
118
116
|
confirm_retry: Bu işi tekrar denemek istediğinizden emin misiniz?
|
119
117
|
destroy: İşi Sil
|
@@ -126,6 +124,8 @@ tr:
|
|
126
124
|
discard:
|
127
125
|
notice: İş İptal Edildi
|
128
126
|
executions:
|
127
|
+
application_trace: Application Trace
|
128
|
+
full_trace: Full Trace
|
129
129
|
in_queue: sırada
|
130
130
|
runtime: çalışma süresi
|
131
131
|
title: İşlemler
|
data/config/locales/uk.yml
CHANGED
@@ -135,9 +135,7 @@ uk:
|
|
135
135
|
actions:
|
136
136
|
confirm_destroy: Ви впевнені, що хочете видалити завдання?
|
137
137
|
confirm_discard: Ви впевнені, що хочете відхилити завдання?
|
138
|
-
confirm_force_discard:
|
139
|
-
|
140
|
-
'
|
138
|
+
confirm_force_discard: Ви впевнені, що хочете примусово скасувати цю роботу? Завдання буде позначено як відхилене, але виконуване завдання не буде зупинено – однак його не буде повторено у випадку помилок.
|
141
139
|
confirm_reschedule: Ви впевнені, що хочете перепланувати завдання?
|
142
140
|
confirm_retry: Ви впевнені, що хочете повторити завдання?
|
143
141
|
destroy: Видалити завдання
|
@@ -150,6 +148,8 @@ uk:
|
|
150
148
|
discard:
|
151
149
|
notice: Завдання було відхилено
|
152
150
|
executions:
|
151
|
+
application_trace: Application Trace
|
152
|
+
full_trace: Full Trace
|
153
153
|
in_queue: у черзі
|
154
154
|
runtime: час виконання
|
155
155
|
title: Виконання
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class CreateGoodJobExecutionErrorBacktrace < ActiveRecord::Migration<%= migration_version %>
|
4
|
+
def change
|
5
|
+
reversible do |dir|
|
6
|
+
dir.up do
|
7
|
+
# Ensure this incremental update migration is idempotent
|
8
|
+
# with monolithic install migration.
|
9
|
+
return if connection.column_exists?(:good_job_executions, :error_backtrace)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
add_column :good_job_executions, :error_backtrace, :text, array: true
|
14
|
+
end
|
15
|
+
end
|
@@ -96,7 +96,7 @@ module GoodJob
|
|
96
96
|
|
97
97
|
query = DiscreteExecution.joins(:job)
|
98
98
|
.where(GoodJob::Job.table_name => { concurrency_key: key })
|
99
|
-
.where(DiscreteExecution.arel_table[:created_at].gt(throttle_period.ago))
|
99
|
+
.where(DiscreteExecution.arel_table[:created_at].gt(DiscreteExecution.bind_value('created_at', throttle_period.ago, ActiveRecord::Type::DateTime)))
|
100
100
|
allowed_active_job_ids = query.where(error: nil).or(query.where.not(error: "GoodJob::ActiveJobExtensions::Concurrency::ThrottleExceededError: GoodJob::ActiveJobExtensions::Concurrency::ThrottleExceededError"))
|
101
101
|
.order(created_at: :asc)
|
102
102
|
.limit(throttle_limit)
|
data/lib/good_job/adapter.rb
CHANGED
@@ -42,6 +42,12 @@ module GoodJob
|
|
42
42
|
enqueue_at(active_job, nil)
|
43
43
|
end
|
44
44
|
|
45
|
+
# Defines if enqueueing this job from inside an Active Record transaction
|
46
|
+
# automatically defers the enqueue to after the transaction commit.
|
47
|
+
def enqueue_after_transaction_commit?
|
48
|
+
GoodJob.configuration.enqueue_after_transaction_commit
|
49
|
+
end
|
50
|
+
|
45
51
|
# Enqueues multiple ActiveJob instances at once
|
46
52
|
# @param active_jobs [Array<ActiveJob::Base>] jobs to be enqueued
|
47
53
|
# @return [Integer] number of jobs that were successfully enqueued
|
data/lib/good_job/bulk.rb
CHANGED
@@ -14,7 +14,7 @@ module GoodJob
|
|
14
14
|
|
15
15
|
# Capture jobs to a buffer. Pass either a block, or specific Active Jobs to be buffered.
|
16
16
|
# @param active_jobs [Array<ActiveJob::Base>] Active Jobs to be buffered.
|
17
|
-
# @param queue_adapter Override the jobs
|
17
|
+
# @param queue_adapter Override the jobs implicit queue adapter with an explicit one.
|
18
18
|
# @return [nil, Array<ActiveJob::Base>] The ActiveJob instances that have been buffered; nil if no active buffer
|
19
19
|
def self.capture(active_jobs = nil, queue_adapter: nil, &block)
|
20
20
|
raise(ArgumentError, "Use either the block form or the argument form, not both") if block && active_jobs
|
data/lib/good_job/capsule.rb
CHANGED
@@ -12,7 +12,7 @@ module GoodJob
|
|
12
12
|
cattr_reader :instances, default: Concurrent::Array.new, instance_reader: false
|
13
13
|
|
14
14
|
# @param configuration [GoodJob::Configuration] Configuration to use for this capsule.
|
15
|
-
def initialize(configuration:
|
15
|
+
def initialize(configuration: nil)
|
16
16
|
@configuration = configuration
|
17
17
|
@startable = true
|
18
18
|
@started_at = nil
|
@@ -30,13 +30,13 @@ module GoodJob
|
|
30
30
|
return unless startable?(force: force)
|
31
31
|
|
32
32
|
@shared_executor = GoodJob::SharedExecutor.new
|
33
|
-
@notifier = GoodJob::Notifier.new(enable_listening:
|
34
|
-
@poller = GoodJob::Poller.new(poll_interval:
|
35
|
-
@multi_scheduler = GoodJob::MultiScheduler.from_configuration(
|
33
|
+
@notifier = GoodJob::Notifier.new(enable_listening: configuration.enable_listen_notify, executor: @shared_executor.executor)
|
34
|
+
@poller = GoodJob::Poller.new(poll_interval: configuration.poll_interval)
|
35
|
+
@multi_scheduler = GoodJob::MultiScheduler.from_configuration(configuration, warm_cache_on_initialize: true)
|
36
36
|
@notifier.recipients.push([@multi_scheduler, :create_thread])
|
37
37
|
@poller.recipients.push(-> { @multi_scheduler.create_thread({ fanout: true }) })
|
38
38
|
|
39
|
-
@cron_manager = GoodJob::CronManager.new(
|
39
|
+
@cron_manager = GoodJob::CronManager.new(configuration.cron_entries, start_on_initialize: true, executor: @shared_executor.executor) if configuration.enable_cron?
|
40
40
|
|
41
41
|
@startable = false
|
42
42
|
@started_at = Time.current
|
@@ -51,7 +51,7 @@ module GoodJob
|
|
51
51
|
# * +nil+ will trigger a shutdown but not wait for it to complete.
|
52
52
|
# @return [void]
|
53
53
|
def shutdown(timeout: NONE)
|
54
|
-
timeout =
|
54
|
+
timeout = configuration.shutdown_timeout if timeout == NONE
|
55
55
|
GoodJob._shutdown_all([@shared_executor, @notifier, @poller, @multi_scheduler, @cron_manager].compact, timeout: timeout)
|
56
56
|
@startable = false
|
57
57
|
@started_at = nil
|
@@ -101,6 +101,10 @@ module GoodJob
|
|
101
101
|
|
102
102
|
private
|
103
103
|
|
104
|
+
def configuration
|
105
|
+
@configuration || GoodJob.configuration
|
106
|
+
end
|
107
|
+
|
104
108
|
def startable?(force: false)
|
105
109
|
!@started_at && (@startable || force)
|
106
110
|
end
|
@@ -33,6 +33,8 @@ module GoodJob
|
|
33
33
|
DEFAULT_DASHBOARD_DEFAULT_LOCALE = :en
|
34
34
|
# Default Dashboard Live Poll button enabled
|
35
35
|
DEFAULT_DASHBOARD_LIVE_POLL_ENABLED = true
|
36
|
+
# Default enqueue_after_transaction_commit
|
37
|
+
DEFAULT_ENQUEUE_AFTER_TRANSACTION_COMMIT = false
|
36
38
|
|
37
39
|
def self.validate_execution_mode(execution_mode)
|
38
40
|
raise ArgumentError, "GoodJob execution mode must be one of #{EXECUTION_MODES.join(', ')}. It was '#{execution_mode}' which is not valid." unless execution_mode.in?(EXECUTION_MODES)
|
@@ -389,6 +391,14 @@ module GoodJob
|
|
389
391
|
DEFAULT_DASHBOARD_LIVE_POLL_ENABLED
|
390
392
|
end
|
391
393
|
|
394
|
+
# Whether the Adapter should have Active Job enqueue jobs after the transaction has committed.
|
395
|
+
# @return [Boolean]
|
396
|
+
def enqueue_after_transaction_commit
|
397
|
+
return options[:enqueue_after_transaction_commit] unless options[:enqueue_after_transaction_commit].nil?
|
398
|
+
|
399
|
+
DEFAULT_ENQUEUE_AFTER_TRANSACTION_COMMIT
|
400
|
+
end
|
401
|
+
|
392
402
|
# Whether running in a web server process.
|
393
403
|
# @return [Boolean, nil]
|
394
404
|
def in_webserver?
|
data/lib/good_job/version.rb
CHANGED
data/lib/good_job.rb
CHANGED
@@ -203,7 +203,7 @@ module GoodJob
|
|
203
203
|
deleted_batches_count = 0
|
204
204
|
deleted_discrete_executions_count = 0
|
205
205
|
|
206
|
-
jobs_query = GoodJob::Job.
|
206
|
+
jobs_query = GoodJob::Job.finished_before(timestamp).order(finished_at: :asc).limit(in_batches_of)
|
207
207
|
jobs_query = jobs_query.succeeded unless include_discarded
|
208
208
|
loop do
|
209
209
|
active_job_ids = jobs_query.pluck(:active_job_id)
|
@@ -219,7 +219,7 @@ module GoodJob
|
|
219
219
|
end
|
220
220
|
|
221
221
|
if GoodJob::BatchRecord.migrated?
|
222
|
-
batches_query = GoodJob::BatchRecord.
|
222
|
+
batches_query = GoodJob::BatchRecord.finished_before(timestamp).limit(in_batches_of)
|
223
223
|
batches_query = batches_query.succeeded unless include_discarded
|
224
224
|
loop do
|
225
225
|
deleted = batches_query.delete_all
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: good_job
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 3.
|
4
|
+
version: 3.28.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ben Sheldon
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-
|
11
|
+
date: 2024-04-19 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activejob
|
@@ -94,20 +94,6 @@ dependencies:
|
|
94
94
|
- - ">="
|
95
95
|
- !ruby/object:Gem::Version
|
96
96
|
version: 0.14.1
|
97
|
-
- !ruby/object:Gem::Dependency
|
98
|
-
name: benchmark-ips
|
99
|
-
requirement: !ruby/object:Gem::Requirement
|
100
|
-
requirements:
|
101
|
-
- - ">="
|
102
|
-
- !ruby/object:Gem::Version
|
103
|
-
version: '0'
|
104
|
-
type: :development
|
105
|
-
prerelease: false
|
106
|
-
version_requirements: !ruby/object:Gem::Requirement
|
107
|
-
requirements:
|
108
|
-
- - ">="
|
109
|
-
- !ruby/object:Gem::Version
|
110
|
-
version: '0'
|
111
97
|
- !ruby/object:Gem::Dependency
|
112
98
|
name: capybara
|
113
99
|
requirement: !ruby/object:Gem::Requirement
|
@@ -150,20 +136,6 @@ dependencies:
|
|
150
136
|
- - ">="
|
151
137
|
- !ruby/object:Gem::Version
|
152
138
|
version: '0'
|
153
|
-
- !ruby/object:Gem::Dependency
|
154
|
-
name: pry-rails
|
155
|
-
requirement: !ruby/object:Gem::Requirement
|
156
|
-
requirements:
|
157
|
-
- - ">="
|
158
|
-
- !ruby/object:Gem::Version
|
159
|
-
version: '0'
|
160
|
-
type: :development
|
161
|
-
prerelease: false
|
162
|
-
version_requirements: !ruby/object:Gem::Requirement
|
163
|
-
requirements:
|
164
|
-
- - ">="
|
165
|
-
- !ruby/object:Gem::Version
|
166
|
-
version: '0'
|
167
139
|
- !ruby/object:Gem::Dependency
|
168
140
|
name: puma
|
169
141
|
requirement: !ruby/object:Gem::Requirement
|
@@ -385,6 +357,7 @@ files:
|
|
385
357
|
- lib/generators/good_job/templates/update/migrations/07_recreate_good_job_cron_indexes_with_conditional.rb.erb
|
386
358
|
- lib/generators/good_job/templates/update/migrations/08_create_good_job_labels.rb.erb
|
387
359
|
- lib/generators/good_job/templates/update/migrations/09_create_good_job_labels_index.rb.erb
|
360
|
+
- lib/generators/good_job/templates/update/migrations/10_create_good_job_execution_error_backtrace.rb.erb
|
388
361
|
- lib/generators/good_job/templates/update/migrations/10_remove_good_job_active_id_index.rb.erb
|
389
362
|
- lib/generators/good_job/templates/update/migrations/11_create_index_good_job_jobs_for_candidate_lookup.rb.erb
|
390
363
|
- lib/generators/good_job/update_generator.rb
|