good_job 2.14.4 → 2.15.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +47 -0
- data/README.md +35 -7
- data/engine/app/assets/good_job/style.css +5 -0
- data/engine/app/controllers/good_job/jobs_controller.rb +10 -3
- data/engine/app/helpers/good_job/application_helper.rb +43 -13
- data/engine/app/views/good_job/cron_entries/index.html.erb +1 -1
- data/engine/app/views/good_job/jobs/_executions.erb +46 -0
- data/engine/app/views/good_job/jobs/_table.erb +13 -1
- data/engine/app/views/good_job/jobs/show.html.erb +56 -2
- data/engine/app/views/good_job/shared/_filter.erb +1 -1
- data/engine/app/views/good_job/shared/icons/_check.html.erb +4 -3
- data/engine/app/views/good_job/shared/icons/_clock.html.erb +5 -0
- data/engine/app/views/good_job/shared/icons/_dash_circle.html.erb +5 -0
- data/engine/app/views/good_job/shared/icons/_exclamation.html.erb +4 -3
- data/engine/config/locales/en.yml +13 -0
- data/engine/config/locales/es.yml +13 -0
- data/engine/config/locales/nl.yml +13 -0
- data/engine/config/locales/ru.yml +13 -0
- data/engine/config/routes.rb +1 -3
- data/lib/good_job/active_job_job.rb +16 -32
- data/lib/good_job/cli.rb +5 -5
- data/lib/good_job/configuration.rb +10 -0
- data/lib/good_job/execution.rb +61 -2
- data/lib/good_job/job_performer.rb +1 -1
- data/lib/good_job/log_subscriber.rb +2 -2
- data/lib/good_job/version.rb +1 -1
- data/lib/good_job.rb +15 -10
- metadata +5 -4
- data/engine/app/controllers/good_job/executions_controller.rb +0 -10
- data/engine/app/views/good_job/executions/_table.erb +0 -62
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: bb6d6387b67811dec6d23e26267a4fe356c42ddc3feb14cb046f57c61c2b5802
|
4
|
+
data.tar.gz: 7ce4090bfeb05c67913f0d6f99fb2517a4370b669fdbfb648a17bfdca3d92efc
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b2efda0438bff75e52ab9e390ee3bf33bdbb64b13a4937ed3e58170100b0afd0a0ead5a3b1b9d902548c4356eba1b9f21296bd94f4f84ecd7884ef2b28fa0a09
|
7
|
+
data.tar.gz: d66ebb51d7c83b0b8c77d1300707a5c82df9bdafd1bc91231093ce331d59ff788c9c59539e10f3996664b572dec3b1dab019d01d2855bf497e3b4efd86929616
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,52 @@
|
|
1
1
|
# Changelog
|
2
2
|
|
3
|
+
## [v2.15.2](https://github.com/bensheldon/good_job/tree/v2.15.2) (2022-06-17)
|
4
|
+
|
5
|
+
[Full Changelog](https://github.com/bensheldon/good_job/compare/v2.15.1...v2.15.2)
|
6
|
+
|
7
|
+
**Closed issues:**
|
8
|
+
|
9
|
+
- ActiveRecord::StatementInvalid PG::ProgramLimitExceeded: ERROR: index row size 3296 exceeds btree version 4 maximum 2704 for index [\#612](https://github.com/bensheldon/good_job/issues/612)
|
10
|
+
|
11
|
+
**Merged pull requests:**
|
12
|
+
|
13
|
+
- Zeitwerk ignore `lib/active_job` [\#617](https://github.com/bensheldon/good_job/pull/617) ([bensheldon](https://github.com/bensheldon))
|
14
|
+
|
15
|
+
## [v2.15.1](https://github.com/bensheldon/good_job/tree/v2.15.1) (2022-05-24)
|
16
|
+
|
17
|
+
[Full Changelog](https://github.com/bensheldon/good_job/compare/v2.15.0...v2.15.1)
|
18
|
+
|
19
|
+
**Closed issues:**
|
20
|
+
|
21
|
+
- dashboard/engine – i18n: Wrong translation scope [\#605](https://github.com/bensheldon/good_job/issues/605)
|
22
|
+
- Concurrency not properly putting jobs in the queue [\#603](https://github.com/bensheldon/good_job/issues/603)
|
23
|
+
- Some dashboard actions have a routing error [\#602](https://github.com/bensheldon/good_job/issues/602)
|
24
|
+
|
25
|
+
**Merged pull requests:**
|
26
|
+
|
27
|
+
- Fix/i18n status scopes [\#607](https://github.com/bensheldon/good_job/pull/607) ([Jay-Schneider](https://github.com/Jay-Schneider))
|
28
|
+
- Make "Live Polling" ToC entry clickable [\#606](https://github.com/bensheldon/good_job/pull/606) ([aried3r](https://github.com/aried3r))
|
29
|
+
- Update readme explaining Concurrency implementation and how to integrate Dashboard with API-only Rails apps [\#604](https://github.com/bensheldon/good_job/pull/604) ([bensheldon](https://github.com/bensheldon))
|
30
|
+
|
31
|
+
## [v2.15.0](https://github.com/bensheldon/good_job/tree/v2.15.0) (2022-05-18)
|
32
|
+
|
33
|
+
[Full Changelog](https://github.com/bensheldon/good_job/compare/v2.14.4...v2.15.0)
|
34
|
+
|
35
|
+
**Implemented enhancements:**
|
36
|
+
|
37
|
+
- Remove ability to destroy individual Executions from Dashboard; rename "Toggle" to "Inspect" everywhere [\#601](https://github.com/bensheldon/good_job/pull/601) ([bensheldon](https://github.com/bensheldon))
|
38
|
+
- Adds the ability to delete jobs on the dashboard; add `cleanup_discarded_jobs` option to retain discarded jobs during cleanup [\#597](https://github.com/bensheldon/good_job/pull/597) ([TAGraves](https://github.com/TAGraves))
|
39
|
+
- Dashboard: show more details about jobs [\#575](https://github.com/bensheldon/good_job/pull/575) ([bkeepers](https://github.com/bkeepers))
|
40
|
+
|
41
|
+
**Closed issues:**
|
42
|
+
|
43
|
+
- Show status on jobs\#show page [\#547](https://github.com/bensheldon/good_job/issues/547)
|
44
|
+
|
45
|
+
**Merged pull requests:**
|
46
|
+
|
47
|
+
- Disable ActiveRecord Connection Reaper in test [\#600](https://github.com/bensheldon/good_job/pull/600) ([bensheldon](https://github.com/bensheldon))
|
48
|
+
- Update README dashboard screenshot [\#599](https://github.com/bensheldon/good_job/pull/599) ([aried3r](https://github.com/aried3r))
|
49
|
+
|
3
50
|
## [v2.14.4](https://github.com/bensheldon/good_job/tree/v2.14.4) (2022-05-15)
|
4
51
|
|
5
52
|
[Full Changelog](https://github.com/bensheldon/good_job/compare/v2.14.3...v2.14.4)
|
data/README.md
CHANGED
@@ -39,7 +39,10 @@ For more of the story of GoodJob, read the [introductory blog post](https://isla
|
|
39
39
|
- [Configuration options](#configuration-options)
|
40
40
|
- [Global options](#global-options)
|
41
41
|
- [Dashboard](#dashboard)
|
42
|
+
- [API-only Rails applications](#api-only-rails-applications)
|
43
|
+
- [Live Polling](#live-polling)
|
42
44
|
- [ActiveJob concurrency](#activejob-concurrency)
|
45
|
+
- [How concurrency controls work](#how-concurrency-controls-work)
|
43
46
|
- [Cron-style repeating/recurring jobs](#cron-style-repeatingrecurring-jobs)
|
44
47
|
- [Updating](#updating)
|
45
48
|
- [Upgrading minor versions](#upgrading-minor-versions)
|
@@ -185,7 +188,7 @@ separate isolated execution pools with semicolons and threads with colons.
|
|
185
188
|
|
186
189
|
#### `good_job cleanup_preserved_jobs`
|
187
190
|
|
188
|
-
`good_job cleanup_preserved_jobs`
|
191
|
+
`good_job cleanup_preserved_jobs` destroys preserved job records. See `GoodJob.preserve_job_records` for when this command is useful.
|
189
192
|
|
190
193
|
```bash
|
191
194
|
$ bundle exec good_job help cleanup_preserved_jobs
|
@@ -194,11 +197,11 @@ Usage:
|
|
194
197
|
good_job cleanup_preserved_jobs
|
195
198
|
|
196
199
|
Options:
|
197
|
-
[--before-seconds-ago=SECONDS] #
|
200
|
+
[--before-seconds-ago=SECONDS] # Destroy records finished more than this many seconds ago (env var: GOOD_JOB_CLEANUP_PRESERVED_JOBS_BEFORE_SECONDS_AGO, default: 86400)
|
198
201
|
|
199
|
-
|
202
|
+
Destroys preserved job records.
|
200
203
|
|
201
|
-
By default, GoodJob
|
204
|
+
By default, GoodJob destroys job records when the job is performed and this
|
202
205
|
command is not necessary.
|
203
206
|
|
204
207
|
However, when `GoodJob.preserve_job_records = true`, the jobs will be
|
@@ -206,7 +209,7 @@ preserved in the database. This is useful when wanting to analyze or
|
|
206
209
|
inspect job performance.
|
207
210
|
|
208
211
|
If you are preserving job records this way, use this command regularly
|
209
|
-
to
|
212
|
+
to destroy old records and preserve space in your database.
|
210
213
|
```
|
211
214
|
|
212
215
|
### Configuration options
|
@@ -269,6 +272,7 @@ Available configuration options are:
|
|
269
272
|
- `shutdown_timeout` (float) number of seconds to wait for jobs to finish when shutting down before stopping the thread. Defaults to forever: `-1`. You can also set this with the environment variable `GOOD_JOB_SHUTDOWN_TIMEOUT`.
|
270
273
|
- `enable_cron` (boolean) whether to run cron process. Defaults to `false`. You can also set this with the environment variable `GOOD_JOB_ENABLE_CRON`.
|
271
274
|
- `cron` (hash) cron configuration. Defaults to `{}`. You can also set this as a JSON string with the environment variable `GOOD_JOB_CRON`
|
275
|
+
- `cleanup_discarded_jobs` (boolean) whether to destroy discarded jobs when cleaning up preserved jobs using the `$ good_job cleanup_preserved_jobs` CLI command or calling `GoodJob.cleanup_preserved_jobs`. Defaults to `true`. Can also be set with the environment variable `GOOD_JOB_CLEANUP_DISCARDED_JOBS`. _This configuration is only used when {GoodJob.preserve_job_records} is `true`._
|
272
276
|
- `cleanup_preserved_jobs_before_seconds_ago` (integer) number of seconds to preserve jobs when using the `$ good_job cleanup_preserved_jobs` CLI command or calling `GoodJob.cleanup_preserved_jobs`. Defaults to `86400` (1 day). Can also be set with the environment variable `GOOD_JOB_CLEANUP_PRESERVED_JOBS_BEFORE_SECONDS_AGO`. _This configuration is only used when {GoodJob.preserve_job_records} is `true`._
|
273
277
|
- `cleanup_interval_jobs` (integer) Number of jobs a Scheduler will execute before cleaning up preserved jobs. Defaults to `nil`. Can also be set with the environment variable `GOOD_JOB_CLEANUP_INTERVAL_JOBS`.
|
274
278
|
- `cleanup_interval_seconds` (integer) Number of seconds a Scheduler will wait before cleaning up preserved jobs. Defaults to `nil`. Can also be set with the environment variable `GOOD_JOB_CLEANUP_INTERVAL_SECONDS`.
|
@@ -367,6 +371,23 @@ GoodJob includes a Dashboard as a mountable `Rails::Engine`.
|
|
367
371
|
end
|
368
372
|
```
|
369
373
|
|
374
|
+
#### API-only Rails applications
|
375
|
+
|
376
|
+
API-only Rails applications may not have all of the required Rack middleware for the GoodJob Dashboard to function. To re-add the middlware:
|
377
|
+
|
378
|
+
```ruby
|
379
|
+
# config/application.rb
|
380
|
+
module MyApp
|
381
|
+
class Application < Rails::Application
|
382
|
+
#...
|
383
|
+
config.middleware.use Rack::MethodOverride
|
384
|
+
config.middleware.use ActionDispatch::Flash
|
385
|
+
config.middleware.use ActionDispatch::Cookies
|
386
|
+
config.middleware.use ActionDispatch::Session::CookieStore
|
387
|
+
end
|
388
|
+
end
|
389
|
+
```
|
390
|
+
|
370
391
|
#### Live Polling
|
371
392
|
|
372
393
|
The Dashboard can be set to automatically refresh by checking "Live Poll" in the Dashboard header, or by setting `?poll=10` with the interval in seconds (default 30 seconds).
|
@@ -419,6 +440,13 @@ job = MyJob.perform_later("Alice")
|
|
419
440
|
job.good_job_concurrency_key #=> "Unique-Alice"
|
420
441
|
```
|
421
442
|
|
443
|
+
#### How concurrency controls work
|
444
|
+
|
445
|
+
GoodJob's concurrency control strategy for `perform_limit` is "optimistic retry with an incremental backoff". The [code is readable](https://github.com/bensheldon/good_job/blob/main/lib/good_job/active_job_extensions/concurrency.rb).
|
446
|
+
|
447
|
+
- "Optimistic" meaning that the implementation's performance trade-off assumes that collisions are atypical (e.g. two users enqueue the same job at the same time) rather than regular (e.g. the system enqueues thousands of colliding jobs at the same time).
|
448
|
+
- "Retry with an incremental backoff" means that when `perform_limit` is exceeded, the job will raise a `GoodJob::ActiveJobExtensions::Concurrency::ConcurrencyExceededError` which is caught by a `retry_on` handler which re-schedules the job to execute in the near future with an incremental backoff.
|
449
|
+
|
422
450
|
### Cron-style repeating/recurring jobs
|
423
451
|
|
424
452
|
GoodJob can enqueue jobs on a recurring basis that can be used as a replacement for cron.
|
@@ -826,7 +854,7 @@ If your application is already using an ActiveJob backend, you will need to inst
|
|
826
854
|
|
827
855
|
GoodJob is fully instrumented with [`ActiveSupport::Notifications`](https://edgeguides.rubyonrails.org/active_support_instrumentation.html#introduction-to-instrumentation).
|
828
856
|
|
829
|
-
By default, GoodJob will
|
857
|
+
By default, GoodJob will destroy job records after they are run, regardless of whether they succeed or not (raising a kind of `StandardError`), unless they are interrupted (raising a kind of `Exception`).
|
830
858
|
|
831
859
|
To preserve job records for later inspection, set an initializer:
|
832
860
|
|
@@ -835,7 +863,7 @@ To preserve job records for later inspection, set an initializer:
|
|
835
863
|
GoodJob.preserve_job_records = true
|
836
864
|
```
|
837
865
|
|
838
|
-
It is also necessary to
|
866
|
+
It is also necessary to destroy these preserved jobs from the database after a certain time period:
|
839
867
|
|
840
868
|
- For example, in a Rake task:
|
841
869
|
|
@@ -7,6 +7,7 @@ module GoodJob
|
|
7
7
|
discard: "discarded",
|
8
8
|
reschedule: "rescheduled",
|
9
9
|
retry: "retried",
|
10
|
+
destroy: "destroyed",
|
10
11
|
}.freeze
|
11
12
|
|
12
13
|
rescue_from GoodJob::ActiveJobJob::AdapterNotGoodJobError,
|
@@ -36,6 +37,8 @@ module GoodJob
|
|
36
37
|
job.reschedule_job
|
37
38
|
when :retry
|
38
39
|
job.retry_job
|
40
|
+
when :destroy
|
41
|
+
job.destroy_job
|
39
42
|
end
|
40
43
|
|
41
44
|
job
|
@@ -53,9 +56,7 @@ module GoodJob
|
|
53
56
|
end
|
54
57
|
|
55
58
|
def show
|
56
|
-
@
|
57
|
-
.order(Arel.sql("COALESCE(scheduled_at, created_at) DESC"))
|
58
|
-
redirect_to jobs_path, alert: "Executions for Active Job #{params[:id]} not found" if @executions.empty?
|
59
|
+
@job = ActiveJobJob.find(params[:id])
|
59
60
|
end
|
60
61
|
|
61
62
|
def discard
|
@@ -76,6 +77,12 @@ module GoodJob
|
|
76
77
|
redirect_back(fallback_location: jobs_path, notice: "Job has been retried")
|
77
78
|
end
|
78
79
|
|
80
|
+
def destroy
|
81
|
+
@job = ActiveJobJob.find(params[:id])
|
82
|
+
@job.destroy_job
|
83
|
+
redirect_back(fallback_location: jobs_path, notice: "Job has been destroyed")
|
84
|
+
end
|
85
|
+
|
79
86
|
private
|
80
87
|
|
81
88
|
def redirect_on_error(exception)
|
@@ -1,24 +1,54 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
module GoodJob
|
3
3
|
module ApplicationHelper
|
4
|
-
def
|
5
|
-
|
4
|
+
def format_duration(sec)
|
5
|
+
return unless sec
|
6
|
+
|
7
|
+
if sec < 1
|
8
|
+
t 'duration.milliseconds', ms: (sec * 1000).floor
|
9
|
+
elsif sec < 10
|
10
|
+
t 'duration.less_than_10_seconds', sec: sec.floor
|
11
|
+
elsif sec < 60
|
12
|
+
t 'duration.seconds', sec: sec.floor
|
13
|
+
elsif sec < 3600
|
14
|
+
t 'duration.minutes', min: (sec / 60).floor, sec: (sec % 60).floor
|
15
|
+
else
|
16
|
+
t 'duration.hours', hour: (sec / 3600).floor, min: ((sec % 3600) / 60).floor
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def relative_time(timestamp, **args)
|
21
|
+
text = timestamp.future? ? "in #{time_ago_in_words(timestamp, **args)}" : "#{time_ago_in_words(timestamp, **args)} ago"
|
6
22
|
tag.time(text, datetime: timestamp, title: timestamp)
|
7
23
|
end
|
8
24
|
|
25
|
+
STATUS_ICONS = {
|
26
|
+
discarded: "exclamation",
|
27
|
+
finished: "check",
|
28
|
+
queued: "dash_circle",
|
29
|
+
retried: "arrow_clockwise",
|
30
|
+
running: "play",
|
31
|
+
scheduled: "clock",
|
32
|
+
}.freeze
|
33
|
+
|
34
|
+
STATUS_COLOR = {
|
35
|
+
discarded: "danger",
|
36
|
+
finished: "success",
|
37
|
+
queued: "warning",
|
38
|
+
retried: "secondary",
|
39
|
+
running: "primary",
|
40
|
+
scheduled: "secondary",
|
41
|
+
}.freeze
|
42
|
+
|
9
43
|
def status_badge(status)
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
when :queued, :scheduled, :retried
|
14
|
-
"badge rounded-pill bg-secondary"
|
15
|
-
when :running
|
16
|
-
"badge rounded-pill bg-primary"
|
17
|
-
when :discarded
|
18
|
-
"badge rounded-pill bg-danger"
|
19
|
-
end
|
44
|
+
content_tag :span, status_icon(status, class: "text-white") + t(status, scope: 'good_job.status'),
|
45
|
+
class: "badge rounded-pill bg-#{STATUS_COLOR.fetch(status)} d-inline-flex gap-2 ps-1 pe-3 align-items-center"
|
46
|
+
end
|
20
47
|
|
21
|
-
|
48
|
+
def status_icon(status, **options)
|
49
|
+
options[:class] ||= "text-#{STATUS_COLOR.fetch(status)}"
|
50
|
+
icon = render_icon STATUS_ICONS.fetch(status)
|
51
|
+
content_tag :span, icon, **options
|
22
52
|
end
|
23
53
|
|
24
54
|
def render_icon(name)
|
@@ -27,7 +27,7 @@
|
|
27
27
|
<td class="font-monospace"><%= cron_entry.key %></td>
|
28
28
|
<td class="font-monospace"><%= cron_entry.schedule %></td>
|
29
29
|
<td>
|
30
|
-
<%= tag.button("
|
30
|
+
<%= tag.button("Inspect", type: "button", class: "btn btn-sm btn-outline-primary", role: "button",
|
31
31
|
data: { bs_toggle: "collapse", bs_target: "##{dom_id(cron_entry, 'properties')}" },
|
32
32
|
aria: { expanded: false, controls: dom_id(cron_entry, 'properties') }) %>
|
33
33
|
<%= tag.pre(JSON.pretty_generate(cron_entry.display_properties), id: dom_id(cron_entry, 'properties'), class: "collapse cron-entry-properties") %>
|
@@ -0,0 +1,46 @@
|
|
1
|
+
<h5>Executions</h5>
|
2
|
+
<div class="card mb-4" data-live-poll-region="executions-table">
|
3
|
+
<div class="list-group list-group-flush">
|
4
|
+
<% executions.each do |execution| %>
|
5
|
+
<%= tag.div id: dom_id(execution), class: "list-group-item py-3" do %>
|
6
|
+
<div class="d-md-flex">
|
7
|
+
<div class="flex-fill">
|
8
|
+
<div class="small text-muted">
|
9
|
+
#<%= execution.number %>:
|
10
|
+
<%= tag.code link_to(execution.id, "##{dom_id(execution)}", class: "text-muted text-decoration-none") %>
|
11
|
+
</div>
|
12
|
+
<div class="d-flex gap-2 align-items-center text-muted mt-1">
|
13
|
+
<%= status_badge execution.status %>
|
14
|
+
<%= relative_time execution.last_status_at, include_seconds: true %>
|
15
|
+
|
16
|
+
<% if execution.runtime_latency %>
|
17
|
+
• <div><%= format_duration execution.runtime_latency %> runtime</div>
|
18
|
+
<% end %>
|
19
|
+
<% if execution.queue_latency %>
|
20
|
+
• <div><%= format_duration execution.queue_latency %> in queue</div>
|
21
|
+
<% end %>
|
22
|
+
</div>
|
23
|
+
</div>
|
24
|
+
<div>
|
25
|
+
<div class="mt-4 d-flex gap-2">
|
26
|
+
<%= tag.button type: "button", class: "btn btn-sm btn-outline-primary", role: "button",
|
27
|
+
data: { bs_toggle: "collapse", bs_target: "##{dom_id(execution, 'params')}" },
|
28
|
+
aria: { expanded: false, controls: dom_id(execution, "params") } do %>
|
29
|
+
Inspect
|
30
|
+
<% end %>
|
31
|
+
</div>
|
32
|
+
</div>
|
33
|
+
</div>
|
34
|
+
<% if execution.error %>
|
35
|
+
<div class="mt-3">
|
36
|
+
<strong class="small">Error:</strong>
|
37
|
+
<pre class="text-wrap text-break m-0"><%= execution.error %></pre>
|
38
|
+
</div>
|
39
|
+
<% end %>
|
40
|
+
<div>
|
41
|
+
<%= tag.pre JSON.pretty_generate(execution.serialized_params), id: dom_id(execution, "params"), class: "collapse bg-light card card-body p-3 my-3" %>
|
42
|
+
</div>
|
43
|
+
<% end %>
|
44
|
+
<% end %>
|
45
|
+
</div>
|
46
|
+
</div>
|
@@ -34,6 +34,10 @@
|
|
34
34
|
<%= form.button type: 'submit', name: 'mass_action', value: 'retry', class: 'btn btn-sm btn-outline-primary', title: "Retry all", data: { confirm: "Confirm retry all", disable: true } do %>
|
35
35
|
<%= render_icon "arrow_clockwise" %> All
|
36
36
|
<% end %>
|
37
|
+
|
38
|
+
<%= form.button type: 'submit', name: 'mass_action', value: 'destroy', class: 'btn btn-sm btn-outline-primary', title: "Destroy all", data: { confirm: "Confirm destroy all", disable: true } do %>
|
39
|
+
<%= render_icon "trash" %> All
|
40
|
+
<% end %>
|
37
41
|
</div>
|
38
42
|
</th>
|
39
43
|
</tr>
|
@@ -63,7 +67,7 @@
|
|
63
67
|
<td><%= job.executions_count %></td>
|
64
68
|
<td class="text-break"><%= truncate(job.recent_error, length: 1_000) %></td>
|
65
69
|
<td>
|
66
|
-
<%= tag.button "
|
70
|
+
<%= tag.button "Inspect", type: "button", class: "btn btn-sm btn-outline-primary", role: "button",
|
67
71
|
data: { bs_toggle: "collapse", bs_target: "##{dom_id(job, 'params')}" },
|
68
72
|
aria: { expanded: false, controls: dom_id(job, "params") }
|
69
73
|
%>
|
@@ -94,6 +98,14 @@
|
|
94
98
|
<% else %>
|
95
99
|
<button class="btn btn-sm btn-outline-secondary" disabled><%= render_icon "arrow_clockwise" %></button>
|
96
100
|
<% end %>
|
101
|
+
|
102
|
+
<% if job.status.in? [:discarded, :finished] %>
|
103
|
+
<%= link_to job_path(job.id), method: :delete, class: "btn btn-sm btn-outline-primary", title: "Destroy job", data: { confirm: "Confirm destroy", disable: true } do %>
|
104
|
+
<%= render_icon "trash" %>
|
105
|
+
<% end %>
|
106
|
+
<% else %>
|
107
|
+
<button class="btn btn-sm btn-outline-secondary" disabled><%= render_icon "trash" %></button>
|
108
|
+
<% end %>
|
97
109
|
</div>
|
98
110
|
</td>
|
99
111
|
</tr>
|
@@ -1,3 +1,57 @@
|
|
1
|
-
<
|
1
|
+
<div class="break-out bg-light border-bottom py-2 mb-3">
|
2
|
+
<div class="container-fluid pt-2">
|
3
|
+
<div class="d-flex align-items-center">
|
4
|
+
<div class="flex-fill">
|
5
|
+
<nav aria-label="breadcrumb">
|
6
|
+
<ol class="breadcrumb small mb-0">
|
7
|
+
<li class="breadcrumb-item"><%= link_to "Jobs", jobs_path %></li>
|
8
|
+
<li class="breadcrumb-item active" aria-current="page">ActiveJob ID: <%= tag.code @job.id %></li>
|
9
|
+
</ol>
|
10
|
+
</nav>
|
11
|
+
<h2 class="mb-1"><%= tag.code @job.job_class %></h2>
|
12
|
+
<div class="text-muted small d-flex gap-2">
|
13
|
+
<div>Queue: <%= tag.strong @job.queue_name %></div>
|
14
|
+
•
|
15
|
+
<div>Priority: <%= tag.strong @job.priority %></div>
|
16
|
+
</div>
|
17
|
+
</div>
|
18
|
+
<div>
|
19
|
+
<% job_reschedulable = @job.status.in? [:scheduled, :retried, :queued] %>
|
20
|
+
<%= button_to reschedule_job_path(@job.id), method: :put,
|
21
|
+
class: "btn btn-sm #{job_reschedulable ? 'btn-outline-primary' : 'btn-outline-secondary'}",
|
22
|
+
form_class: "d-inline-block",
|
23
|
+
disabled: !job_reschedulable,
|
24
|
+
aria: { label: "Reschedule job" },
|
25
|
+
title: "Reschedule job",
|
26
|
+
data: { confirm: "Confirm reschedule" } do %>
|
27
|
+
<%= render_icon "skip_forward" %>
|
28
|
+
Reschedule
|
29
|
+
<% end %>
|
2
30
|
|
3
|
-
|
31
|
+
<% job_discardable = @job.status.in? [:scheduled, :retried, :queued] %>
|
32
|
+
<%= button_to discard_job_path(@job.id), method: :put, class: "btn btn-sm #{job_discardable ? 'btn-outline-primary' : 'btn-outline-secondary'}", form_class: "d-inline-block", disabled: !job_discardable, aria: { label: "Discard job" }, title: "Discard job", data: { confirm: "Confirm discard" } do %>
|
33
|
+
<%= render_icon "stop" %>
|
34
|
+
Discard
|
35
|
+
<% end %>
|
36
|
+
|
37
|
+
<%= button_to retry_job_path(@job.id), method: :put, class: "btn btn-sm #{@job.status == :discarded ? 'btn-outline-primary' : 'btn-outline-secondary'}", form_class: "d-inline-block", disabled: @job.status != :discarded, aria: { label: "Retry job" }, title: "Retry job", data: { confirm: "Confirm retry" } do %>
|
38
|
+
<%= render_icon "arrow_clockwise" %>
|
39
|
+
Retry
|
40
|
+
<% end %>
|
41
|
+
|
42
|
+
<% job_destroyable = @job.status.in? [:discarded, :finished] %>
|
43
|
+
<%= button_to job_path(@job.id), method: :delete, class: "btn btn-sm #{job_destroyable ? 'btn-outline-primary' : 'btn-outline-secondary'}", form_class: "d-inline-block", disabled: !job_destroyable, aria: { label: "Destroy job" }, title: "Destroy job", data: { confirm: "Confirm destroy" } do %>
|
44
|
+
<%= render_icon "trash" %>
|
45
|
+
Destroy
|
46
|
+
<% end %>
|
47
|
+
</div>
|
48
|
+
</div>
|
49
|
+
</div>
|
50
|
+
</div>
|
51
|
+
|
52
|
+
<div class="my-4">
|
53
|
+
<h5>Arguments</h5>
|
54
|
+
<%= tag.pre @job.serialized_params["arguments"].map(&:inspect).join(', ') %>
|
55
|
+
</div>
|
56
|
+
|
57
|
+
<%= render 'executions', executions: @job.executions.reverse %>
|
@@ -10,7 +10,7 @@
|
|
10
10
|
<% filter.states.each do |name, count| %>
|
11
11
|
<li class="nav-item">
|
12
12
|
<%= link_to url_for({state: name}), class: "nav-link #{"active" if params[:state] == name}" do %>
|
13
|
-
<%= name.
|
13
|
+
<%= t(name, scope: 'good_job.status') %>
|
14
14
|
<span class="badge bg-primary rounded-pill <%= "bg-secondary" if count == 0 %>"><%= count %></span>
|
15
15
|
<% end %>
|
16
16
|
</li>
|
@@ -1,4 +1,5 @@
|
|
1
|
-
<!-- https://icons.getbootstrap.com/icons/check-circle
|
2
|
-
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-check-circle
|
3
|
-
<path d="
|
1
|
+
<!-- https://icons.getbootstrap.com/icons/check-circle/ -->
|
2
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-check-circle" viewBox="0 0 16 16">
|
3
|
+
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z" />
|
4
|
+
<path d="M10.97 4.97a.235.235 0 0 0-.02.022L7.477 9.417 5.384 7.323a.75.75 0 0 0-1.06 1.06L6.97 11.03a.75.75 0 0 0 1.079-.02l3.992-4.99a.75.75 0 0 0-1.071-1.05z" />
|
4
5
|
</svg>
|
@@ -0,0 +1,5 @@
|
|
1
|
+
<!-- https://icons.getbootstrap.com/icons/clock/ -->
|
2
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-clock" viewBox="0 0 16 16">
|
3
|
+
<path d="M8 3.5a.5.5 0 0 0-1 0V9a.5.5 0 0 0 .252.434l3.5 2a.5.5 0 0 0 .496-.868L8 8.71V3.5z" />
|
4
|
+
<path d="M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16zm7-8A7 7 0 1 1 1 8a7 7 0 0 1 14 0z" />
|
5
|
+
</svg>
|
@@ -0,0 +1,5 @@
|
|
1
|
+
<!-- https://icons.getbootstrap.com/icons/dash-circle/ -->
|
2
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-dash-circle" viewBox="0 0 16 16">
|
3
|
+
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z" />
|
4
|
+
<path d="M4 8a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1h-7A.5.5 0 0 1 4 8z" />
|
5
|
+
</svg>
|
@@ -1,4 +1,5 @@
|
|
1
|
-
<!-- https://icons.getbootstrap.com/icons/exclamation-
|
2
|
-
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-exclamation-
|
3
|
-
<path d="M8
|
1
|
+
<!-- https://icons.getbootstrap.com/icons/exclamation-circle/ -->
|
2
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-exclamation-circle" viewBox="0 0 16 16">
|
3
|
+
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z" />
|
4
|
+
<path d="M7.002 11a1 1 0 1 1 2 0 1 1 0 0 1-2 0zM7.1 4.995a.905.905 0 1 1 1.8 0l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 4.995z" />
|
4
5
|
</svg>
|
@@ -39,6 +39,12 @@ en:
|
|
39
39
|
x_years:
|
40
40
|
one: 1 year
|
41
41
|
other: "%{count} years"
|
42
|
+
duration:
|
43
|
+
hours: "%{hour}h %{min}m"
|
44
|
+
less_than_10_seconds: "%{sec}s"
|
45
|
+
milliseconds: "%{ms}ms"
|
46
|
+
minutes: "%{min}m %{sec}s"
|
47
|
+
seconds: "%{sec}s"
|
42
48
|
good_job:
|
43
49
|
shared:
|
44
50
|
footer:
|
@@ -50,3 +56,10 @@ en:
|
|
50
56
|
live_poll: Live Poll
|
51
57
|
name: "GoodJob 👍"
|
52
58
|
processes: Processes
|
59
|
+
status:
|
60
|
+
discarded: Discarded
|
61
|
+
finished: Finished
|
62
|
+
queued: Queued
|
63
|
+
retried: Retried
|
64
|
+
running: Running
|
65
|
+
scheduled: Scheduled
|
@@ -39,6 +39,12 @@ es:
|
|
39
39
|
x_years:
|
40
40
|
one: 1 año
|
41
41
|
other: "%{count} años"
|
42
|
+
duration:
|
43
|
+
hours: "%{hour}h %{min}m"
|
44
|
+
less_than_10_seconds: "%{sec}s"
|
45
|
+
milliseconds: "%{ms}ms"
|
46
|
+
minutes: "%{min}m %{sec}s"
|
47
|
+
seconds: "%{sec}s"
|
42
48
|
good_job:
|
43
49
|
shared:
|
44
50
|
footer:
|
@@ -50,3 +56,10 @@ es:
|
|
50
56
|
live_poll: En vivo
|
51
57
|
name: "GoodJob 👍"
|
52
58
|
processes: Procesos
|
59
|
+
status:
|
60
|
+
discarded: Descartado
|
61
|
+
finished: Acabado
|
62
|
+
queued: Puesto en cola
|
63
|
+
retried: reintentado
|
64
|
+
running: Corriendo
|
65
|
+
scheduled: Programado
|
@@ -39,6 +39,12 @@ nl:
|
|
39
39
|
x_years:
|
40
40
|
one: 1 jaar
|
41
41
|
other: "%{count} jaren"
|
42
|
+
duration:
|
43
|
+
hours: "%{hour}h %{min}m"
|
44
|
+
less_than_10_seconds: "%{sec}s"
|
45
|
+
milliseconds: "%{ms}ms"
|
46
|
+
minutes: "%{min}m %{sec}s"
|
47
|
+
seconds: "%{sec}s"
|
42
48
|
good_job:
|
43
49
|
shared:
|
44
50
|
footer:
|
@@ -50,3 +56,10 @@ nl:
|
|
50
56
|
live_poll: Live Poll
|
51
57
|
name: "GoodJob 👍"
|
52
58
|
processes: Processen
|
59
|
+
status:
|
60
|
+
discarded: weggegooid
|
61
|
+
finished: Afgewerkt
|
62
|
+
queued: In de wachtrij
|
63
|
+
retried: Opnieuw geprobeerd
|
64
|
+
running: Rennen
|
65
|
+
scheduled: Gepland
|
@@ -63,6 +63,12 @@ ru:
|
|
63
63
|
many: "%{count} лет"
|
64
64
|
one: 1 год
|
65
65
|
other: "%{count} года"
|
66
|
+
duration:
|
67
|
+
hours: "%{hour}h %{min}m"
|
68
|
+
less_than_10_seconds: "%{sec}s"
|
69
|
+
milliseconds: "%{ms}мс"
|
70
|
+
minutes: "%{min}м %{sec}с"
|
71
|
+
seconds: "%{sec}s"
|
66
72
|
good_job:
|
67
73
|
shared:
|
68
74
|
footer:
|
@@ -74,3 +80,10 @@ ru:
|
|
74
80
|
live_poll: Живой Опрос
|
75
81
|
name: "GoodJob 👍"
|
76
82
|
processes: Процессы
|
83
|
+
status:
|
84
|
+
discarded: Отброшено
|
85
|
+
finished: Законченный
|
86
|
+
queued: В очереди
|
87
|
+
retried: Повторная попытка
|
88
|
+
running: Бег
|
89
|
+
scheduled: по расписанию
|
data/engine/config/routes.rb
CHANGED
@@ -2,9 +2,7 @@
|
|
2
2
|
GoodJob::Engine.routes.draw do
|
3
3
|
root to: redirect(path: 'jobs')
|
4
4
|
|
5
|
-
resources :
|
6
|
-
|
7
|
-
resources :jobs, only: %i[index show] do
|
5
|
+
resources :jobs, only: %i[index show destroy] do
|
8
6
|
collection do
|
9
7
|
get :mass_update, to: redirect(path: 'jobs')
|
10
8
|
put :mass_update
|
@@ -54,9 +54,11 @@ module GoodJob
|
|
54
54
|
# Advisory locked and executing
|
55
55
|
scope :running, -> { where(finished_at: nil).joins_advisory_locks.where.not(pg_locks: { locktype: nil }) }
|
56
56
|
# Completed executing successfully
|
57
|
-
scope :finished, -> { where.not(finished_at: nil)
|
57
|
+
scope :finished, -> { not_discarded.where.not(finished_at: nil) }
|
58
58
|
# Errored but will not be retried
|
59
59
|
scope :discarded, -> { where.not(finished_at: nil).where.not(error: nil) }
|
60
|
+
# Not errored
|
61
|
+
scope :not_discarded, -> { where(error: nil) }
|
60
62
|
|
61
63
|
# The job's ActiveJob UUID
|
62
64
|
# @return [String]
|
@@ -71,38 +73,8 @@ module GoodJob
|
|
71
73
|
end
|
72
74
|
|
73
75
|
# The status of the Job, based on the state of its most recent execution.
|
74
|
-
# There are 3 buckets of non-overlapping statuses:
|
75
|
-
# 1. The job will be executed
|
76
|
-
# - queued: The job will execute immediately when an execution thread becomes available.
|
77
|
-
# - scheduled: The job is scheduled to execute in the future.
|
78
|
-
# - retried: The job previously errored on execution and will be re-executed in the future.
|
79
|
-
# 2. The job is being executed
|
80
|
-
# - running: the job is actively being executed by an execution thread
|
81
|
-
# 3. The job will not execute
|
82
|
-
# - finished: The job executed successfully
|
83
|
-
# - discarded: The job previously errored on execution and will not be re-executed in the future.
|
84
|
-
#
|
85
76
|
# @return [Symbol]
|
86
|
-
|
87
|
-
execution = head_execution
|
88
|
-
if execution.finished_at.present?
|
89
|
-
if execution.error.present?
|
90
|
-
:discarded
|
91
|
-
else
|
92
|
-
:finished
|
93
|
-
end
|
94
|
-
elsif (execution.scheduled_at || execution.created_at) > DateTime.current
|
95
|
-
if execution.serialized_params.fetch('executions', 0) > 1
|
96
|
-
:retried
|
97
|
-
else
|
98
|
-
:scheduled
|
99
|
-
end
|
100
|
-
elsif running?
|
101
|
-
:running
|
102
|
-
else
|
103
|
-
:queued
|
104
|
-
end
|
105
|
-
end
|
77
|
+
delegate :status, :last_status_at, to: :head_execution
|
106
78
|
|
107
79
|
# This job's most recent {Execution}
|
108
80
|
# @param reload [Booelan] whether to reload executions
|
@@ -225,6 +197,18 @@ module GoodJob
|
|
225
197
|
end
|
226
198
|
end
|
227
199
|
|
200
|
+
# Destroy all of a discarded or finished job's executions from the database so that it will no longer appear on the dashboard.
|
201
|
+
# @return [void]
|
202
|
+
def destroy_job
|
203
|
+
with_advisory_lock do
|
204
|
+
execution = head_execution(reload: true)
|
205
|
+
|
206
|
+
raise ActionForStateMismatchError if execution.finished_at.blank?
|
207
|
+
|
208
|
+
destroy
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
228
212
|
# Utility method to determine which execution record is used to represent this job
|
229
213
|
# @return [String]
|
230
214
|
def _execution_id
|
data/lib/good_job/cli.rb
CHANGED
@@ -120,11 +120,11 @@ module GoodJob
|
|
120
120
|
default_task :start
|
121
121
|
|
122
122
|
# @!macro thor.desc
|
123
|
-
desc :cleanup_preserved_jobs, "
|
123
|
+
desc :cleanup_preserved_jobs, "Destroys preserved job records."
|
124
124
|
long_desc <<~DESCRIPTION
|
125
|
-
|
125
|
+
Destroys preserved job records.
|
126
126
|
|
127
|
-
By default, GoodJob
|
127
|
+
By default, GoodJob destroys job records when the job is performed and this
|
128
128
|
command is not necessary.
|
129
129
|
|
130
130
|
However, when `GoodJob.preserve_job_records = true`, the jobs will be
|
@@ -132,13 +132,13 @@ module GoodJob
|
|
132
132
|
inspect job performance.
|
133
133
|
|
134
134
|
If you are preserving job records this way, use this command regularly
|
135
|
-
to
|
135
|
+
to destroy old records and preserve space in your database.
|
136
136
|
|
137
137
|
DESCRIPTION
|
138
138
|
method_option :before_seconds_ago,
|
139
139
|
type: :numeric,
|
140
140
|
banner: 'SECONDS',
|
141
|
-
desc: "
|
141
|
+
desc: "Destroy records finished more than this many seconds ago (env var: GOOD_JOB_CLEANUP_PRESERVED_JOBS_BEFORE_SECONDS_AGO, default: 86400)"
|
142
142
|
|
143
143
|
def cleanup_preserved_jobs
|
144
144
|
set_up_application!
|
@@ -171,6 +171,16 @@ module GoodJob
|
|
171
171
|
cron.map { |cron_key, params| GoodJob::CronEntry.new(params.merge(key: cron_key)) }
|
172
172
|
end
|
173
173
|
|
174
|
+
# Whether to destroy discarded jobs when cleaning up preserved jobs.
|
175
|
+
# This configuration is only used when {GoodJob.preserve_job_records} is +true+.
|
176
|
+
# @return [Boolean]
|
177
|
+
def cleanup_discarded_jobs?
|
178
|
+
return rails_config[:cleanup_discarded_jobs] unless rails_config[:cleanup_discarded_jobs].nil?
|
179
|
+
return ActiveModel::Type::Boolean.new.cast(env['GOOD_JOB_CLEANUP_DISCARDED_JOBS']) unless env['GOOD_JOB_CLEANUP_DISCARDED_JOBS'].nil?
|
180
|
+
|
181
|
+
true
|
182
|
+
end
|
183
|
+
|
174
184
|
# Number of seconds to preserve jobs when using the +good_job cleanup_preserved_jobs+ CLI command.
|
175
185
|
# This configuration is only used when {GoodJob.preserve_job_records} is +true+.
|
176
186
|
# @return [Integer]
|
data/lib/good_job/execution.rb
CHANGED
@@ -88,7 +88,7 @@ module GoodJob
|
|
88
88
|
# @return [ActiveRecord::Relation]
|
89
89
|
scope :priority_ordered, -> { order('priority DESC NULLS LAST') }
|
90
90
|
|
91
|
-
# Order jobs by scheduled
|
91
|
+
# Order jobs by scheduled or created (oldest first).
|
92
92
|
# @!method schedule_ordered
|
93
93
|
# @!scope class
|
94
94
|
# @return [ActiveRecord::Relation]
|
@@ -96,7 +96,7 @@ module GoodJob
|
|
96
96
|
|
97
97
|
# Get Jobs were completed before the given timestamp. If no timestamp is
|
98
98
|
# provided, get all jobs that have been completed. By default, GoodJob
|
99
|
-
#
|
99
|
+
# destroys jobs after they are completed and this will find no jobs.
|
100
100
|
# However, if you have changed {GoodJob.preserve_job_records}, this may
|
101
101
|
# find completed Jobs.
|
102
102
|
# @!method finished(timestamp = nil)
|
@@ -272,6 +272,65 @@ module GoodJob
|
|
272
272
|
end
|
273
273
|
end
|
274
274
|
|
275
|
+
# There are 3 buckets of non-overlapping statuses:
|
276
|
+
# 1. The job will be executed
|
277
|
+
# - queued: The job will execute immediately when an execution thread becomes available.
|
278
|
+
# - scheduled: The job is scheduled to execute in the future.
|
279
|
+
# - retried: The job previously errored on execution and will be re-executed in the future.
|
280
|
+
# 2. The job is being executed
|
281
|
+
# - running: the job is actively being executed by an execution thread
|
282
|
+
# 3. The job will not execute
|
283
|
+
# - finished: The job executed successfully
|
284
|
+
# - discarded: The job previously errored on execution and will not be re-executed in the future.
|
285
|
+
#
|
286
|
+
# @return [Symbol]
|
287
|
+
def status
|
288
|
+
if finished_at.present?
|
289
|
+
if error.present?
|
290
|
+
:discarded
|
291
|
+
else
|
292
|
+
:finished
|
293
|
+
end
|
294
|
+
elsif (scheduled_at || created_at) > DateTime.current
|
295
|
+
if serialized_params.fetch('executions', 0) > 1
|
296
|
+
:retried
|
297
|
+
else
|
298
|
+
:scheduled
|
299
|
+
end
|
300
|
+
elsif running?
|
301
|
+
:running
|
302
|
+
else
|
303
|
+
:queued
|
304
|
+
end
|
305
|
+
end
|
306
|
+
|
307
|
+
def running?
|
308
|
+
performed_at? && !finished_at?
|
309
|
+
end
|
310
|
+
|
311
|
+
def number
|
312
|
+
serialized_params.fetch('executions', 0) + 1
|
313
|
+
end
|
314
|
+
|
315
|
+
# The last relevant timestamp for this execution
|
316
|
+
def last_status_at
|
317
|
+
finished_at || performed_at || scheduled_at || created_at
|
318
|
+
end
|
319
|
+
|
320
|
+
# Time between when this job was expected to run and when it started running
|
321
|
+
def queue_latency
|
322
|
+
now = Time.zone.now
|
323
|
+
expected_start = scheduled_at || created_at
|
324
|
+
actual_start = performed_at || now
|
325
|
+
|
326
|
+
actual_start - expected_start unless expected_start >= now
|
327
|
+
end
|
328
|
+
|
329
|
+
# Time between when this job started and finished
|
330
|
+
def runtime_latency
|
331
|
+
(finished_at || Time.zone.now) - performed_at if performed_at
|
332
|
+
end
|
333
|
+
|
275
334
|
private
|
276
335
|
|
277
336
|
def active_job_data
|
@@ -140,10 +140,10 @@ module GoodJob
|
|
140
140
|
# @!macro notification_responder
|
141
141
|
def cleanup_preserved_jobs(event)
|
142
142
|
timestamp = event.payload[:timestamp]
|
143
|
-
|
143
|
+
destroyed_records_count = event.payload[:destroyed_records_count]
|
144
144
|
|
145
145
|
info do
|
146
|
-
"GoodJob
|
146
|
+
"GoodJob destroyed #{destroyed_records_count} preserved #{'job'.pluralize(destroyed_records_count)} finished before #{timestamp}."
|
147
147
|
end
|
148
148
|
end
|
149
149
|
|
data/lib/good_job/version.rb
CHANGED
data/lib/good_job.rb
CHANGED
@@ -8,7 +8,9 @@ Zeitwerk::Loader.for_gem.tap do |loader|
|
|
8
8
|
loader.inflector.inflect({
|
9
9
|
"cli" => "CLI",
|
10
10
|
})
|
11
|
-
loader.ignore(
|
11
|
+
loader.ignore("#{__dir__}/generators")
|
12
|
+
loader.ignore("#{__dir__}/active_job")
|
13
|
+
loader.push_dir("#{__dir__}/active_job/queue_adapters", namespace: ActiveJob::QueueAdapters)
|
12
14
|
loader.setup
|
13
15
|
end
|
14
16
|
|
@@ -43,7 +45,7 @@ module GoodJob
|
|
43
45
|
# @!attribute [rw] preserve_job_records
|
44
46
|
# @!scope class
|
45
47
|
# Whether to preserve job records in the database after they have finished (default: +false+).
|
46
|
-
# By default, GoodJob
|
48
|
+
# By default, GoodJob destroys job records after the job is completed successfully.
|
47
49
|
# If you want to preserve jobs for latter inspection, set this to +true+.
|
48
50
|
# If you want to preserve only jobs that finished with error for latter inspection, set this to +:on_unhandled_error+.
|
49
51
|
# If +true+, you will need to clean out jobs using the +good_job cleanup_preserved_jobs+ CLI command or
|
@@ -126,25 +128,28 @@ module GoodJob
|
|
126
128
|
end
|
127
129
|
end
|
128
130
|
|
129
|
-
#
|
130
|
-
# By default, GoodJob
|
131
|
+
# Destroys preserved job records.
|
132
|
+
# By default, GoodJob destroys job records when the job is performed and this
|
131
133
|
# method is not necessary. However, when `GoodJob.preserve_job_records = true`,
|
132
134
|
# the jobs will be preserved in the database. This is useful when wanting to
|
133
135
|
# analyze or inspect job performance.
|
134
136
|
# If you are preserving job records this way, use this method regularly to
|
135
|
-
#
|
136
|
-
# @params older_than [nil,Numeric,ActiveSupport::Duration] Jobs older than this will be
|
137
|
-
# @return [Integer] Number of jobs that were
|
137
|
+
# destroy old records and preserve space in your database.
|
138
|
+
# @params older_than [nil,Numeric,ActiveSupport::Duration] Jobs older than this will be destroyed (default: +86400+).
|
139
|
+
# @return [Integer] Number of jobs that were destroyed.
|
138
140
|
def self.cleanup_preserved_jobs(older_than: nil)
|
139
|
-
|
141
|
+
configuration = GoodJob::Configuration.new({})
|
142
|
+
older_than ||= configuration.cleanup_preserved_jobs_before_seconds_ago
|
140
143
|
timestamp = Time.current - older_than
|
144
|
+
include_discarded = configuration.cleanup_discarded_jobs?
|
141
145
|
|
142
146
|
ActiveSupport::Notifications.instrument("cleanup_preserved_jobs.good_job", { older_than: older_than, timestamp: timestamp }) do |payload|
|
143
147
|
old_jobs = GoodJob::ActiveJobJob.where('finished_at <= ?', timestamp)
|
148
|
+
old_jobs = old_jobs.not_discarded unless include_discarded
|
144
149
|
old_jobs_count = old_jobs.count
|
145
150
|
|
146
|
-
GoodJob::Execution.where(job: old_jobs).
|
147
|
-
payload[:
|
151
|
+
GoodJob::Execution.where(job: old_jobs).destroy_all
|
152
|
+
payload[:destroyed_records_count] = old_jobs_count
|
148
153
|
end
|
149
154
|
end
|
150
155
|
|
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: 2.
|
4
|
+
version: 2.15.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ben Sheldon
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2022-
|
11
|
+
date: 2022-06-17 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activejob
|
@@ -377,7 +377,6 @@ files:
|
|
377
377
|
- engine/app/controllers/good_job/application_controller.rb
|
378
378
|
- engine/app/controllers/good_job/assets_controller.rb
|
379
379
|
- engine/app/controllers/good_job/cron_entries_controller.rb
|
380
|
-
- engine/app/controllers/good_job/executions_controller.rb
|
381
380
|
- engine/app/controllers/good_job/jobs_controller.rb
|
382
381
|
- engine/app/controllers/good_job/processes_controller.rb
|
383
382
|
- engine/app/filters/good_job/base_filter.rb
|
@@ -385,7 +384,7 @@ files:
|
|
385
384
|
- engine/app/helpers/good_job/application_helper.rb
|
386
385
|
- engine/app/views/good_job/cron_entries/index.html.erb
|
387
386
|
- engine/app/views/good_job/cron_entries/show.html.erb
|
388
|
-
- engine/app/views/good_job/
|
387
|
+
- engine/app/views/good_job/jobs/_executions.erb
|
389
388
|
- engine/app/views/good_job/jobs/_table.erb
|
390
389
|
- engine/app/views/good_job/jobs/index.html.erb
|
391
390
|
- engine/app/views/good_job/jobs/show.html.erb
|
@@ -397,6 +396,8 @@ files:
|
|
397
396
|
- engine/app/views/good_job/shared/_navbar.erb
|
398
397
|
- engine/app/views/good_job/shared/icons/_arrow_clockwise.html.erb
|
399
398
|
- engine/app/views/good_job/shared/icons/_check.html.erb
|
399
|
+
- engine/app/views/good_job/shared/icons/_clock.html.erb
|
400
|
+
- engine/app/views/good_job/shared/icons/_dash_circle.html.erb
|
400
401
|
- engine/app/views/good_job/shared/icons/_exclamation.html.erb
|
401
402
|
- engine/app/views/good_job/shared/icons/_play.html.erb
|
402
403
|
- engine/app/views/good_job/shared/icons/_skip_forward.html.erb
|
@@ -1,10 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
module GoodJob
|
3
|
-
class ExecutionsController < GoodJob::ApplicationController
|
4
|
-
def destroy
|
5
|
-
deleted_count = GoodJob::Execution.where(id: params[:id]).delete_all
|
6
|
-
message = deleted_count.positive? ? { notice: "Job execution deleted" } : { alert: "Job execution not deleted" }
|
7
|
-
redirect_back fallback_location: jobs_path, **message
|
8
|
-
end
|
9
|
-
end
|
10
|
-
end
|
@@ -1,62 +0,0 @@
|
|
1
|
-
<div class="my-3" data-live-poll-region="executions-table">
|
2
|
-
<div class="table-responsive">
|
3
|
-
<table class="table table-hover table-sm mb-0" id="executions_index_table">
|
4
|
-
<thead>
|
5
|
-
<tr>
|
6
|
-
<th>ActiveJob ID</th>
|
7
|
-
<th>Execution ID</th>
|
8
|
-
<th>Job Class</th>
|
9
|
-
<th>Queue</th>
|
10
|
-
<th>Scheduled At</th>
|
11
|
-
<th>Error</th>
|
12
|
-
<th>
|
13
|
-
ActiveJob Params
|
14
|
-
<%= tag.button "Toggle", type: "button", class: "btn btn-sm btn-outline-primary", role: "button",
|
15
|
-
data: { bs_toggle: "collapse", bs_target: ".job-params" },
|
16
|
-
aria: { expanded: false, controls: executions.map { |execution| "##{dom_id(execution, "params")}" }.join(" ") }
|
17
|
-
%>
|
18
|
-
</th>
|
19
|
-
<th>Actions</th>
|
20
|
-
</tr>
|
21
|
-
</thead>
|
22
|
-
<tbody>
|
23
|
-
<% if executions.present? %>
|
24
|
-
<% executions.each do |execution| %>
|
25
|
-
<tr id="<%= dom_id(execution) %>">
|
26
|
-
<td>
|
27
|
-
<%= link_to job_path(execution.serialized_params['job_id']) do %>
|
28
|
-
<code><%= execution.active_job_id %></code>
|
29
|
-
<% end %>
|
30
|
-
</td>
|
31
|
-
<td>
|
32
|
-
<%= link_to job_path(execution.active_job_id, anchor: dom_id(execution)) do %>
|
33
|
-
<code><%= execution.id %></code>
|
34
|
-
<% end %>
|
35
|
-
</td>
|
36
|
-
<td><%= execution.serialized_params['job_class'] %></td>
|
37
|
-
<td><%= execution.queue_name %></td>
|
38
|
-
<td><%= relative_time(execution.scheduled_at || execution.created_at) %></td>
|
39
|
-
<td class="text-break"><%= truncate(execution.error, length: 1_000) %></td>
|
40
|
-
<td>
|
41
|
-
<%= tag.button "Preview", type: "button", class: "btn btn-sm btn-outline-primary", role: "button",
|
42
|
-
data: { bs_toggle: "collapse", bs_target: "##{dom_id(execution, 'params')}" },
|
43
|
-
aria: { expanded: false, controls: dom_id(execution, "params") }
|
44
|
-
%>
|
45
|
-
<%= tag.pre JSON.pretty_generate(execution.serialized_params), id: dom_id(execution, "params"), class: "collapse job-params" %>
|
46
|
-
</td>
|
47
|
-
<td>
|
48
|
-
<%= button_to execution_path(execution.id), method: :delete, class: "btn btn-sm btn-outline-danger", title: "Delete execution", data: { confirm: "Confirm delete" } do %>
|
49
|
-
<%= render_icon "trash" %>
|
50
|
-
<% end %>
|
51
|
-
</td>
|
52
|
-
</tr>
|
53
|
-
<% end %>
|
54
|
-
<% else %>
|
55
|
-
<tr>
|
56
|
-
<td colspan="8" class="py-2 text-center text-muted">No executions found.</td>
|
57
|
-
</tr>
|
58
|
-
<% end %>
|
59
|
-
</tbody>
|
60
|
-
</table>
|
61
|
-
</div>
|
62
|
-
</div>
|