good_job 2.14.4 → 2.15.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 34be72b94928721447a6409e5a63aea86bc371ddcdcfa4c30316ee28dfefe59f
4
- data.tar.gz: 4ca6c70f5456cb79440577c4493f17662930e7e2a259d8077248beb2e7742406
3
+ metadata.gz: b978264b509be9bd83e1e5e67f291b2889deff86c8316f876ab9def8264a7dd0
4
+ data.tar.gz: 9b4172f97c1c7406d3c51ec81f926adaca801baa004aed743a8cac757bb2ce86
5
5
  SHA512:
6
- metadata.gz: 2e0d5ba7b3c0f1adee5491633292424e3e99dd8a27d4f1d3d324f6e1701c68b51042bd11ace9982b3f4c084e1f1b8a84e81d9a2642e02a5a1da4f4187883e69b
7
- data.tar.gz: b22e9d91946eba8363c6c87ceb62d2cbd4e6fd29438eefd17686c42c4ef81c46e1590631326e1e0b4b376d27464ef4bf9bf3fb2573df1fd741fa6a7a0a749810
6
+ metadata.gz: a2696f821699ea080d2b77bb7704192222e6347d1b2be7fcf7a5e049d7b8979ceea3f5236c165fb27bb17fa5f9d8c7d2e5bb863b9c7298de9efb892facb6bca7
7
+ data.tar.gz: 0ab7a37d403e1bec9c0638835c903aff4a1168f33143c7c27acd87c07bf95a61c1475c3e61bea0a7590d167144761cc64b7bc81566746c854c2ae9698aa999fe
data/CHANGELOG.md CHANGED
@@ -1,5 +1,24 @@
1
1
  # Changelog
2
2
 
3
+ ## [v2.15.0](https://github.com/bensheldon/good_job/tree/v2.15.0) (2022-05-18)
4
+
5
+ [Full Changelog](https://github.com/bensheldon/good_job/compare/v2.14.4...v2.15.0)
6
+
7
+ **Implemented enhancements:**
8
+
9
+ - 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))
10
+ - Dashboard: show more details about jobs [\#575](https://github.com/bensheldon/good_job/pull/575) ([bkeepers](https://github.com/bkeepers))
11
+
12
+ **Closed issues:**
13
+
14
+ - Show status on jobs\#show page [\#547](https://github.com/bensheldon/good_job/issues/547)
15
+
16
+ **Merged pull requests:**
17
+
18
+ - 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))
19
+ - Disable ActiveRecord Connection Reaper in test [\#600](https://github.com/bensheldon/good_job/pull/600) ([bensheldon](https://github.com/bensheldon))
20
+ - Update README dashboard screenshot [\#599](https://github.com/bensheldon/good_job/pull/599) ([aried3r](https://github.com/aried3r))
21
+
3
22
  ## [v2.14.4](https://github.com/bensheldon/good_job/tree/v2.14.4) (2022-05-15)
4
23
 
5
24
  [Full Changelog](https://github.com/bensheldon/good_job/compare/v2.14.3...v2.14.4)
data/README.md CHANGED
@@ -185,7 +185,7 @@ separate isolated execution pools with semicolons and threads with colons.
185
185
 
186
186
  #### `good_job cleanup_preserved_jobs`
187
187
 
188
- `good_job cleanup_preserved_jobs` deletes preserved job records. See `GoodJob.preserve_job_records` for when this command is useful.
188
+ `good_job cleanup_preserved_jobs` destroys preserved job records. See `GoodJob.preserve_job_records` for when this command is useful.
189
189
 
190
190
  ```bash
191
191
  $ bundle exec good_job help cleanup_preserved_jobs
@@ -194,11 +194,11 @@ Usage:
194
194
  good_job cleanup_preserved_jobs
195
195
 
196
196
  Options:
197
- [--before-seconds-ago=SECONDS] # Delete records finished more than this many seconds ago (env var: GOOD_JOB_CLEANUP_PRESERVED_JOBS_BEFORE_SECONDS_AGO, default: 86400)
197
+ [--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
198
 
199
- Deletes preserved job records.
199
+ Destroys preserved job records.
200
200
 
201
- By default, GoodJob deletes job records when the job is performed and this
201
+ By default, GoodJob destroys job records when the job is performed and this
202
202
  command is not necessary.
203
203
 
204
204
  However, when `GoodJob.preserve_job_records = true`, the jobs will be
@@ -206,7 +206,7 @@ preserved in the database. This is useful when wanting to analyze or
206
206
  inspect job performance.
207
207
 
208
208
  If you are preserving job records this way, use this command regularly
209
- to delete old records and preserve space in your database.
209
+ to destroy old records and preserve space in your database.
210
210
  ```
211
211
 
212
212
  ### Configuration options
@@ -269,6 +269,7 @@ Available configuration options are:
269
269
  - `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
270
  - `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
271
  - `cron` (hash) cron configuration. Defaults to `{}`. You can also set this as a JSON string with the environment variable `GOOD_JOB_CRON`
272
+ - `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
273
  - `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
274
  - `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
275
  - `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`.
@@ -826,7 +827,7 @@ If your application is already using an ActiveJob backend, you will need to inst
826
827
 
827
828
  GoodJob is fully instrumented with [`ActiveSupport::Notifications`](https://edgeguides.rubyonrails.org/active_support_instrumentation.html#introduction-to-instrumentation).
828
829
 
829
- By default, GoodJob will delete 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
+ 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
831
 
831
832
  To preserve job records for later inspection, set an initializer:
832
833
 
@@ -835,7 +836,7 @@ To preserve job records for later inspection, set an initializer:
835
836
  GoodJob.preserve_job_records = true
836
837
  ```
837
838
 
838
- It is also necessary to delete these preserved jobs from the database after a certain time period:
839
+ It is also necessary to destroy these preserved jobs from the database after a certain time period:
839
840
 
840
841
  - For example, in a Rake task:
841
842
 
@@ -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
- @executions = GoodJob::Execution.active_job_id(params[:id])
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 relative_time(timestamp)
5
- text = timestamp.future? ? "in #{time_ago_in_words(timestamp)}" : "#{time_ago_in_words(timestamp)} ago"
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
- classes = case status
11
- when :finished
12
- "badge rounded-pill bg-success"
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: '.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
- content_tag :span, status.to_s, class: classes
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("Preview", type: "button", class: "btn btn-sm btn-outline-primary", role: "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 "Preview", type: "button", class: "btn btn-sm btn-outline-primary", role: "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
- <h1 class="mb-3">ActiveJob ID: <code><%= @executions.first.id %></code></h1>
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
- <%= render 'good_job/executions/table', executions: @executions %>
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.titleize %>
13
+ <%= t(name, scope: '.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-fill/ -->
2
- <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-check-circle-fill <%= local_assigns[:class] %>" viewBox="0 0 16 16">
3
- <path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zm-3.97-3.03a.75.75 0 0 0-1.08.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-.01-1.05z" />
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-triangle-fill/ -->
2
- <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-exclamation-triangle-fill <%= local_assigns[:class] %>" viewBox="0 0 16 16">
3
- <path d="M8.982 1.566a1.13 1.13 0 0 0-1.96 0L.165 13.233c-.457.778.091 1.767.98 1.767h13.713c.889 0 1.438-.99.98-1.767L8.982 1.566zM8 5c.535 0 .954.462.9.995l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 5.995A.905.905 0 0 1 8 5zm.002 6a1 1 0 1 1 0 2 1 1 0 0 1 0-2z" />
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: по расписанию
@@ -2,9 +2,7 @@
2
2
  GoodJob::Engine.routes.draw do
3
3
  root to: redirect(path: 'jobs')
4
4
 
5
- resources :executions, only: %i[destroy]
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).where(error: 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
- def status
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, "Deletes preserved job records."
123
+ desc :cleanup_preserved_jobs, "Destroys preserved job records."
124
124
  long_desc <<~DESCRIPTION
125
- Deletes preserved job records.
125
+ Destroys preserved job records.
126
126
 
127
- By default, GoodJob deletes job records when the job is performed and this
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 delete old records and preserve space in your database.
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: "Delete records finished more than this many seconds ago (env var: GOOD_JOB_CLEANUP_PRESERVED_JOBS_BEFORE_SECONDS_AGO, default: 86400)"
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]
@@ -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 (unscheduled or soonest first).
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
- # deletes jobs after they are completed and this will find no jobs.
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
@@ -57,7 +57,7 @@ module GoodJob
57
57
  job_query.next_scheduled_at(after: after, limit: limit, now_limit: now_limit)
58
58
  end
59
59
 
60
- # Delete expired preserved jobs
60
+ # Destroy expired preserved jobs
61
61
  # @return [void]
62
62
  def cleanup
63
63
  GoodJob.cleanup_preserved_jobs
@@ -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
- deleted_records_count = event.payload[:deleted_records_count]
143
+ destroyed_records_count = event.payload[:destroyed_records_count]
144
144
 
145
145
  info do
146
- "GoodJob deleted #{deleted_records_count} preserved #{'job'.pluralize(deleted_records_count)} finished before #{timestamp}."
146
+ "GoodJob destroyed #{destroyed_records_count} preserved #{'job'.pluralize(destroyed_records_count)} finished before #{timestamp}."
147
147
  end
148
148
  end
149
149
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
  module GoodJob
3
3
  # GoodJob gem version.
4
- VERSION = '2.14.4'
4
+ VERSION = '2.15.0'
5
5
  end
data/lib/good_job.rb CHANGED
@@ -43,7 +43,7 @@ module GoodJob
43
43
  # @!attribute [rw] preserve_job_records
44
44
  # @!scope class
45
45
  # Whether to preserve job records in the database after they have finished (default: +false+).
46
- # By default, GoodJob deletes job records after the job is completed successfully.
46
+ # By default, GoodJob destroys job records after the job is completed successfully.
47
47
  # If you want to preserve jobs for latter inspection, set this to +true+.
48
48
  # If you want to preserve only jobs that finished with error for latter inspection, set this to +:on_unhandled_error+.
49
49
  # If +true+, you will need to clean out jobs using the +good_job cleanup_preserved_jobs+ CLI command or
@@ -126,25 +126,28 @@ module GoodJob
126
126
  end
127
127
  end
128
128
 
129
- # Deletes preserved job records.
130
- # By default, GoodJob deletes job records when the job is performed and this
129
+ # Destroys preserved job records.
130
+ # By default, GoodJob destroys job records when the job is performed and this
131
131
  # method is not necessary. However, when `GoodJob.preserve_job_records = true`,
132
132
  # the jobs will be preserved in the database. This is useful when wanting to
133
133
  # analyze or inspect job performance.
134
134
  # If you are preserving job records this way, use this method regularly to
135
- # delete old records and preserve space in your database.
136
- # @params older_than [nil,Numeric,ActiveSupport::Duration] Jobs older than this will be deleted (default: +86400+).
137
- # @return [Integer] Number of jobs that were deleted.
135
+ # destroy old records and preserve space in your database.
136
+ # @params older_than [nil,Numeric,ActiveSupport::Duration] Jobs older than this will be destroyed (default: +86400+).
137
+ # @return [Integer] Number of jobs that were destroyed.
138
138
  def self.cleanup_preserved_jobs(older_than: nil)
139
- older_than ||= GoodJob::Configuration.new({}).cleanup_preserved_jobs_before_seconds_ago
139
+ configuration = GoodJob::Configuration.new({})
140
+ older_than ||= configuration.cleanup_preserved_jobs_before_seconds_ago
140
141
  timestamp = Time.current - older_than
142
+ include_discarded = configuration.cleanup_discarded_jobs?
141
143
 
142
144
  ActiveSupport::Notifications.instrument("cleanup_preserved_jobs.good_job", { older_than: older_than, timestamp: timestamp }) do |payload|
143
145
  old_jobs = GoodJob::ActiveJobJob.where('finished_at <= ?', timestamp)
146
+ old_jobs = old_jobs.not_discarded unless include_discarded
144
147
  old_jobs_count = old_jobs.count
145
148
 
146
- GoodJob::Execution.where(job: old_jobs).delete_all
147
- payload[:deleted_records_count] = old_jobs_count
149
+ GoodJob::Execution.where(job: old_jobs).destroy_all
150
+ payload[:destroyed_records_count] = old_jobs_count
148
151
  end
149
152
  end
150
153
 
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.14.4
4
+ version: 2.15.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: 2022-05-15 00:00:00.000000000 Z
11
+ date: 2022-05-18 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/executions/_table.erb
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&nbsp;
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>