good_job 2.9.5 → 2.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +40 -0
- data/README.md +7 -3
- data/engine/app/assets/scripts.js +133 -1
- data/engine/app/filters/good_job/executions_filter.rb +1 -1
- data/engine/app/helpers/good_job/application_helper.rb +15 -0
- data/engine/app/views/good_job/cron_entries/index.html.erb +11 -1
- data/engine/app/views/good_job/executions/_table.erb +36 -30
- data/engine/app/views/good_job/executions/index.html.erb +4 -6
- data/engine/app/views/good_job/jobs/_table.erb +41 -37
- data/engine/app/views/good_job/jobs/index.html.erb +8 -5
- data/engine/app/views/good_job/processes/index.html.erb +42 -36
- data/engine/app/views/good_job/shared/_chart.erb +1 -23
- data/engine/app/views/good_job/shared/_filter.erb +47 -54
- data/engine/app/views/layouts/good_job/base.html.erb +21 -1
- data/lib/good_job/active_job_extensions/concurrency.rb +1 -1
- data/lib/good_job/version.rb +1 -1
- metadata +6 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4722bcae4ba953937f1e78dd730c61898ec0aa25e4c8d5fedc6163e218de4381
|
|
4
|
+
data.tar.gz: fe659df13800eadf8f8d45a2d31b4b4dd466d0f9517addefdfd5169c351c7c76
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3fff2b35292aebd982e6a11fac22d9a16b2468a19bf4e50c354a3f6806831fbcb2ab07d5cbe74c4ccd6bbeeb178abeeee700da0dbfdb0281f79b173b22a2ce9a
|
|
7
|
+
data.tar.gz: ea88184a6b3b3be56604b90af426c4c491f735dc5951854710bfeda55a1c4b930b76190845cb90b512d4e9e2f9a1f3b746478f232d10529c8a6971e448ab1419
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,45 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [v2.11.0](https://github.com/bensheldon/good_job/tree/v2.11.0) (2022-02-27)
|
|
4
|
+
|
|
5
|
+
[Full Changelog](https://github.com/bensheldon/good_job/compare/v2.10.0...v2.11.0)
|
|
6
|
+
|
|
7
|
+
**Closed issues:**
|
|
8
|
+
|
|
9
|
+
- How do I ensure that a the same job can't run twice? \(unique job / avoid duplicates\) [\#531](https://github.com/bensheldon/good_job/issues/531)
|
|
10
|
+
- Bulk reschedule and discard jobs via dashboard [\#527](https://github.com/bensheldon/good_job/issues/527)
|
|
11
|
+
- "Live Poll" dashboard [\#526](https://github.com/bensheldon/good_job/issues/526)
|
|
12
|
+
|
|
13
|
+
**Merged pull requests:**
|
|
14
|
+
|
|
15
|
+
- Add support for live polling the dashboard [\#528](https://github.com/bensheldon/good_job/pull/528) ([danielwestendorf](https://github.com/danielwestendorf))
|
|
16
|
+
|
|
17
|
+
## [v2.10.0](https://github.com/bensheldon/good_job/tree/v2.10.0) (2022-02-18)
|
|
18
|
+
|
|
19
|
+
[Full Changelog](https://github.com/bensheldon/good_job/compare/v2.9.6...v2.10.0)
|
|
20
|
+
|
|
21
|
+
**Closed issues:**
|
|
22
|
+
|
|
23
|
+
- Cron jobs not getting run [\#519](https://github.com/bensheldon/good_job/issues/519)
|
|
24
|
+
- Slow queries with many finished entries and concurrency control [\#514](https://github.com/bensheldon/good_job/issues/514)
|
|
25
|
+
- Make default retry behaviour safer [\#505](https://github.com/bensheldon/good_job/issues/505)
|
|
26
|
+
|
|
27
|
+
**Merged pull requests:**
|
|
28
|
+
|
|
29
|
+
- Fix Benchmark job throughput script [\#522](https://github.com/bensheldon/good_job/pull/522) ([douglara](https://github.com/douglara))
|
|
30
|
+
- Update development Gemfile.lock [\#521](https://github.com/bensheldon/good_job/pull/521) ([bensheldon](https://github.com/bensheldon))
|
|
31
|
+
- Ensure Rails 6.0 is tested against Ruby 3.0; use Ruby 3.0 in demo environment [\#520](https://github.com/bensheldon/good_job/pull/520) ([bensheldon](https://github.com/bensheldon))
|
|
32
|
+
- Dashboard: update search filters and some small UI updates [\#518](https://github.com/bensheldon/good_job/pull/518) ([multiplegeorges](https://github.com/multiplegeorges))
|
|
33
|
+
- Document safer setting for retry\_on\_unhandled\_error [\#517](https://github.com/bensheldon/good_job/pull/517) ([tamaloa](https://github.com/tamaloa))
|
|
34
|
+
|
|
35
|
+
## [v2.9.6](https://github.com/bensheldon/good_job/tree/v2.9.6) (2022-02-07)
|
|
36
|
+
|
|
37
|
+
[Full Changelog](https://github.com/bensheldon/good_job/compare/v2.9.5...v2.9.6)
|
|
38
|
+
|
|
39
|
+
**Merged pull requests:**
|
|
40
|
+
|
|
41
|
+
- Limit query for allowed concurrent jobs to unfinished [\#515](https://github.com/bensheldon/good_job/pull/515) ([til](https://github.com/til))
|
|
42
|
+
|
|
3
43
|
## [v2.9.5](https://github.com/bensheldon/good_job/tree/v2.9.5) (2022-02-07)
|
|
4
44
|
|
|
5
45
|
[Full Changelog](https://github.com/bensheldon/good_job/compare/v2.9.4...v2.9.5)
|
data/README.md
CHANGED
|
@@ -276,7 +276,7 @@ Available configuration options are:
|
|
|
276
276
|
- `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`.
|
|
277
277
|
- `logger` ([Rails Logger](https://api.rubyonrails.org/classes/ActiveSupport/Logger.html)) lets you set a custom logger for GoodJob. It should be an instance of a Rails `Logger` (Default: `Rails.logger`).
|
|
278
278
|
- `preserve_job_records` (boolean) keeps job records in your database even after jobs are completed. (Default: `false`)
|
|
279
|
-
- `retry_on_unhandled_error` (boolean) causes jobs to be re-queued and retried if they raise an instance of `StandardError`. Instances of `Exception`, like SIGINT, will *always* be retried, regardless of this attribute’s value. (Default: `true`)
|
|
279
|
+
- `retry_on_unhandled_error` (boolean) causes jobs to be re-queued and retried if they raise an instance of `StandardError`. Be advised this may lead to jobs being repeated infinitely ([see below for more on retries](#retries)). Instances of `Exception`, like SIGINT, will *always* be retried, regardless of this attribute’s value. (Default: `true`)
|
|
280
280
|
- `on_thread_error` (proc, lambda, or callable) will be called when an Exception. It can be useful for logging errors to bug tracking services, like Sentry or Airbrake. Example:
|
|
281
281
|
|
|
282
282
|
```ruby
|
|
@@ -307,7 +307,7 @@ Good Job’s general behavior can also be configured via attributes directly on
|
|
|
307
307
|
- **`GoodJob.active_record_parent_class`** (string) The ActiveRecord parent class inherited by GoodJob's ActiveRecord model `GoodJob::Job` (defaults to `"ActiveRecord::Base"`). Configure this when using [multiple databases with ActiveRecord](https://guides.rubyonrails.org/active_record_multiple_databases.html) or when other custom configuration is necessary for the ActiveRecord model to connect to the Postgres database. _The value must be a String to avoid premature initialization of ActiveRecord._
|
|
308
308
|
- **`GoodJob.logger`** ([Rails Logger](https://api.rubyonrails.org/classes/ActiveSupport/Logger.html)) lets you set a custom logger for GoodJob. It should be an instance of a Rails `Logger`.
|
|
309
309
|
- **`GoodJob.preserve_job_records`** (boolean) keeps job records in your database even after jobs are completed. (Default: `false`)
|
|
310
|
-
- **`GoodJob.retry_on_unhandled_error`** (boolean) causes jobs to be re-queued and retried if they raise an instance of `StandardError`. Instances of `Exception`, like SIGINT, will *always* be retried, regardless of this attribute’s value. (Default: `true`)
|
|
310
|
+
- **`GoodJob.retry_on_unhandled_error`** (boolean) causes jobs to be re-queued and retried if they raise an instance of `StandardError`. Be advised this may lead to jobs being repeated infinitely ([see below for more on retries](#retries)). Instances of `Exception`, like SIGINT, will *always* be retried, regardless of this attribute’s value. (Default: `true`)
|
|
311
311
|
- **`GoodJob.on_thread_error`** (proc, lambda, or callable) will be called when an Exception. It can be useful for logging errors to bug tracking services, like Sentry or Airbrake.
|
|
312
312
|
|
|
313
313
|
You’ll generally want to configure these in `config/initializers/good_job.rb`, like so:
|
|
@@ -369,6 +369,10 @@ GoodJob includes a Dashboard as a mountable `Rails::Engine`.
|
|
|
369
369
|
end
|
|
370
370
|
```
|
|
371
371
|
|
|
372
|
+
#### Live Polling
|
|
373
|
+
|
|
374
|
+
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).
|
|
375
|
+
|
|
372
376
|
### ActiveJob concurrency
|
|
373
377
|
|
|
374
378
|
GoodJob can extend ActiveJob to provide limits on concurrently running jobs, either at time of _enqueue_ or at _perform_. Limiting concurrency can help prevent duplicate, double or unecessary jobs from being enqueued, or race conditions when performing, for example when interacting with 3rd-party APIs.
|
|
@@ -529,7 +533,7 @@ class ApplicationJob < ActiveJob::Base
|
|
|
529
533
|
end
|
|
530
534
|
```
|
|
531
535
|
|
|
532
|
-
When using `retry_on` with _a limited number of retries_, the final exception will not be rescued and will raise to GoodJob. GoodJob can be configured to discard un-handled exceptions instead of retrying them:
|
|
536
|
+
When using `retry_on` with _a limited number of retries_, the final exception will not be rescued and will raise to GoodJob. GoodJob can be configured to discard un-handled exceptions instead of retrying them. Be aware that if NOT setting `retry_on_unhandled_error` to `false` good_job will by default retry the failing job and may do this infinitely without pause thereby at least causing high load. In most cases `retry_on_unhandled_error` should be set as following:
|
|
533
537
|
|
|
534
538
|
```ruby
|
|
535
539
|
# config/initializers/good_job.rb
|
|
@@ -1 +1,133 @@
|
|
|
1
|
-
|
|
1
|
+
/*jshint esversion: 6, strict: false */
|
|
2
|
+
const GOOD_JOB_DEFAULT_POLL_INTERVAL_SECONDS = 30;
|
|
3
|
+
const GOOD_JOB_MINIMUM_POLL_INTERVAL = 1000;
|
|
4
|
+
|
|
5
|
+
const GoodJob = {
|
|
6
|
+
// Register functions to execute when the DOM is ready
|
|
7
|
+
ready: (callback) => {
|
|
8
|
+
if (document.readyState !== "loading") {
|
|
9
|
+
callback();
|
|
10
|
+
} else {
|
|
11
|
+
document.addEventListener("DOMContentLoaded", callback);
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
|
|
15
|
+
init: () => {
|
|
16
|
+
GoodJob.updateSettings();
|
|
17
|
+
GoodJob.addListeners();
|
|
18
|
+
GoodJob.pollUpdates();
|
|
19
|
+
GoodJob.renderCharts(true);
|
|
20
|
+
},
|
|
21
|
+
|
|
22
|
+
addListeners: () => {
|
|
23
|
+
const gjActionEls = document.querySelectorAll('[data-gj-action]');
|
|
24
|
+
|
|
25
|
+
for (let i = 0; i < gjActionEls.length; i++) {
|
|
26
|
+
const el = gjActionEls[i];
|
|
27
|
+
const [eventName, func] = el.dataset.gjAction.split('#');
|
|
28
|
+
|
|
29
|
+
el.addEventListener(eventName, GoodJob[func]);
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
updateSettings: () => {
|
|
34
|
+
const queryString = window.location.search;
|
|
35
|
+
const urlParams = new URLSearchParams(queryString);
|
|
36
|
+
|
|
37
|
+
// live poll interval and enablement
|
|
38
|
+
if (urlParams.has('poll')) {
|
|
39
|
+
const parsedInterval = (parseInt(urlParams.get('poll')) || GOOD_JOB_DEFAULT_POLL_INTERVAL_SECONDS) * 1000;
|
|
40
|
+
|
|
41
|
+
GoodJob.pollEnabled = true;
|
|
42
|
+
GoodJob.pollInterval = Math.max(parsedInterval, GOOD_JOB_MINIMUM_POLL_INTERVAL);
|
|
43
|
+
GoodJob.setStorage('pollInterval', GoodJob.pollInterval);
|
|
44
|
+
} else {
|
|
45
|
+
GoodJob.pollEnabled = GoodJob.getStorage('pollEnabled') || false;
|
|
46
|
+
GoodJob.pollInterval = GoodJob.getStorage('pollInterval') || GOOD_JOB_DEFAULT_POLL_INTERVAL_SECONDS;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
document.getElementById('toggle-poll').checked = GoodJob.pollEnabled;
|
|
50
|
+
},
|
|
51
|
+
|
|
52
|
+
togglePoll: (ev) => {
|
|
53
|
+
GoodJob.pollEnabled = ev.currentTarget.checked;
|
|
54
|
+
GoodJob.setStorage('pollEnabled', GoodJob.pollEnabled);
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
pollUpdates: () => {
|
|
58
|
+
setTimeout(() => {
|
|
59
|
+
if (GoodJob.pollEnabled === true) {
|
|
60
|
+
fetch(window.location.href)
|
|
61
|
+
.then(resp => resp.text())
|
|
62
|
+
.then(GoodJob.updateContent)
|
|
63
|
+
.finally(GoodJob.pollUpdates);
|
|
64
|
+
} else {
|
|
65
|
+
GoodJob.pollUpdates();
|
|
66
|
+
}
|
|
67
|
+
}, GoodJob.pollInterval);
|
|
68
|
+
},
|
|
69
|
+
|
|
70
|
+
updateContent: (newContent) => {
|
|
71
|
+
const domParser = new DOMParser();
|
|
72
|
+
const parsedDOM = domParser.parseFromString(newContent, "text/html");
|
|
73
|
+
|
|
74
|
+
const newElements = parsedDOM.querySelectorAll('[data-gj-poll-replace]');
|
|
75
|
+
|
|
76
|
+
for (let i = 0; i < newElements.length; i++) {
|
|
77
|
+
const newEl = newElements[i];
|
|
78
|
+
const oldEl = document.getElementById(newEl.id);
|
|
79
|
+
|
|
80
|
+
if (oldEl) {
|
|
81
|
+
oldEl.replaceWith(newEl);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
GoodJob.renderCharts(false);
|
|
86
|
+
},
|
|
87
|
+
|
|
88
|
+
renderCharts: (animate) => {
|
|
89
|
+
const charts = document.querySelectorAll('.chart');
|
|
90
|
+
|
|
91
|
+
for (let i = 0; i < charts.length; i++) {
|
|
92
|
+
const chartEl = charts[i];
|
|
93
|
+
const chartData = JSON.parse(chartEl.dataset.json);
|
|
94
|
+
|
|
95
|
+
const ctx = chartEl.getContext('2d');
|
|
96
|
+
const chart = new Chart(ctx, {
|
|
97
|
+
type: 'line',
|
|
98
|
+
data: {
|
|
99
|
+
labels: chartData.labels,
|
|
100
|
+
datasets: chartData.datasets
|
|
101
|
+
},
|
|
102
|
+
options: {
|
|
103
|
+
animation: animate,
|
|
104
|
+
responsive: true,
|
|
105
|
+
maintainAspectRatio: false,
|
|
106
|
+
scales: {
|
|
107
|
+
y: {
|
|
108
|
+
beginAtZero: true
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
|
|
116
|
+
getStorage: (key) => {
|
|
117
|
+
const value = localStorage.getItem('good_job-' + key);
|
|
118
|
+
|
|
119
|
+
if (value === 'true') {
|
|
120
|
+
return true;
|
|
121
|
+
} else if (value === 'false') {
|
|
122
|
+
return false;
|
|
123
|
+
} else {
|
|
124
|
+
return value;
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
|
|
128
|
+
setStorage: (key, value) => {
|
|
129
|
+
localStorage.setItem('good_job-' + key, value);
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
GoodJob.ready(GoodJob.init);
|
|
@@ -5,5 +5,20 @@ module GoodJob
|
|
|
5
5
|
text = timestamp.future? ? "in #{time_ago_in_words(timestamp)}" : "#{time_ago_in_words(timestamp)} ago"
|
|
6
6
|
tag.time(text, datetime: timestamp, title: timestamp)
|
|
7
7
|
end
|
|
8
|
+
|
|
9
|
+
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
|
|
20
|
+
|
|
21
|
+
content_tag :span, status.to_s, class: classes
|
|
22
|
+
end
|
|
8
23
|
end
|
|
9
24
|
end
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
<div class="my-3 flex">
|
|
2
|
+
<h2>Cron Schedules</h2>
|
|
3
|
+
</div>
|
|
4
|
+
|
|
1
5
|
<% if @cron_entries.present? %>
|
|
2
6
|
<div class="card my-3">
|
|
3
7
|
<div class="table-responsive">
|
|
@@ -47,5 +51,11 @@
|
|
|
47
51
|
</div>
|
|
48
52
|
</div>
|
|
49
53
|
<% else %>
|
|
50
|
-
<
|
|
54
|
+
<div class="card my-3">
|
|
55
|
+
<div class="card-body">
|
|
56
|
+
<p class="card-text">
|
|
57
|
+
<em>No cron schedules found.</em>
|
|
58
|
+
</p>
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
51
61
|
<% end %>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
<div class="card my-3">
|
|
1
|
+
<div class="card my-3" data-gj-poll-replace id="executions-table">
|
|
2
2
|
<div class="table-responsive">
|
|
3
|
-
<table class="table card-table table-bordered table-hover table-sm mb-0">
|
|
3
|
+
<table class="table card-table table-bordered table-hover table-sm mb-0" id="executions_index_table">
|
|
4
4
|
<thead>
|
|
5
5
|
<tr>
|
|
6
6
|
<th>ActiveJob ID</th>
|
|
@@ -20,34 +20,40 @@
|
|
|
20
20
|
</tr>
|
|
21
21
|
</thead>
|
|
22
22
|
<tbody>
|
|
23
|
-
<% executions.
|
|
24
|
-
|
|
25
|
-
<
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
<%=
|
|
49
|
-
|
|
50
|
-
|
|
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 "good_job/shared/icons/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>
|
|
51
57
|
</tr>
|
|
52
58
|
<% end %>
|
|
53
59
|
</tbody>
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
<div class="card my-3 p-6">
|
|
1
|
+
<div class="card my-3 p-6" data-gj-poll-replace id="executions-chart">
|
|
2
2
|
<%= render 'good_job/shared/chart', chart_data: GoodJob::ScheduledByQueueChart.new(@filter).data %>
|
|
3
3
|
</div>
|
|
4
4
|
|
|
5
5
|
<%= render 'good_job/shared/filter', filter: @filter %>
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
<%= render 'good_job/executions/table', executions: @filter.records %>
|
|
7
|
+
<%= render 'good_job/executions/table', executions: @filter.records %>
|
|
9
8
|
|
|
10
|
-
|
|
9
|
+
<% if @filter.records.present? %>
|
|
10
|
+
<nav aria-label="Job pagination" class="mt-3" data-gj-poll-replace id="executions-pagination">
|
|
11
11
|
<ul class="pagination">
|
|
12
12
|
<li class="page-item">
|
|
13
13
|
<%= link_to({ after_scheduled_at: (@filter.last.scheduled_at || @filter.last.created_at), after_id: @filter.last.id }, class: "page-link") do %>
|
|
@@ -16,6 +16,4 @@
|
|
|
16
16
|
</li>
|
|
17
17
|
</ul>
|
|
18
18
|
</nav>
|
|
19
|
-
<% else %>
|
|
20
|
-
<em>No executions present.</em>
|
|
21
19
|
<% end %>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<div class="card my-3">
|
|
1
|
+
<div class="card my-3" data-gj-poll-replace id="jobs-table">
|
|
2
2
|
<div class="table-responsive">
|
|
3
3
|
<table class="table card-table table-bordered table-hover table-sm mb-0">
|
|
4
4
|
<thead>
|
|
@@ -21,45 +21,49 @@
|
|
|
21
21
|
</tr>
|
|
22
22
|
</thead>
|
|
23
23
|
<tbody>
|
|
24
|
-
<% jobs.
|
|
25
|
-
|
|
26
|
-
<
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
</td>
|
|
31
|
-
<td>
|
|
32
|
-
<span class="badge bg-secondary"><%= job.status %></span>
|
|
33
|
-
</td>
|
|
34
|
-
<td><%= job.job_class %></td>
|
|
35
|
-
<td><%= job.queue_name %></td>
|
|
36
|
-
<td><%= relative_time(job.scheduled_at || job.created_at) %></td>
|
|
37
|
-
<td><%= job.executions_count %></td>
|
|
38
|
-
<td class="text-break"><%= truncate(job.recent_error, length: 1_000) %></td>
|
|
39
|
-
<td>
|
|
40
|
-
<%= tag.button "Preview", type: "button", class: "btn btn-sm btn-outline-primary", role: "button",
|
|
41
|
-
data: { bs_toggle: "collapse", bs_target: "##{dom_id(job, 'params')}" },
|
|
42
|
-
aria: { expanded: false, controls: dom_id(job, "params") }
|
|
43
|
-
%>
|
|
44
|
-
<%= tag.pre JSON.pretty_generate(job.serialized_params), id: dom_id(job, "params"), class: "collapse job-params" %>
|
|
45
|
-
</td>
|
|
46
|
-
<td>
|
|
47
|
-
<div class="text-nowrap">
|
|
48
|
-
<% job_reschedulable = job.status.in? [:scheduled, :retried, :queued] %>
|
|
49
|
-
<%= button_to reschedule_job_path(job.id), method: :put, class: "btn btn-sm #{job_reschedulable ? 'btn-outline-primary' : 'btn-outline-secondary'}", form_class: "d-inline-block", disabled: !job_reschedulable, aria: { label: "Reschedule job" }, title: "Reschedule job", data: { confirm: "Confirm reschedule" } do %>
|
|
50
|
-
<%= render "good_job/shared/icons/skip_forward" %>
|
|
24
|
+
<% if jobs.present? %>
|
|
25
|
+
<% jobs.each do |job| %>
|
|
26
|
+
<tr class="<%= dom_class(job) %>" id="<%= dom_id(job) %>">
|
|
27
|
+
<td>
|
|
28
|
+
<%= link_to job_path(job.id) do %>
|
|
29
|
+
<code><%= job.id %></code>
|
|
51
30
|
<% end %>
|
|
31
|
+
</td>
|
|
32
|
+
<td><%= status_badge(job.status) %></td>
|
|
33
|
+
<td><%= job.job_class %></td>
|
|
34
|
+
<td><%= job.queue_name %></td>
|
|
35
|
+
<td><%= relative_time(job.scheduled_at || job.created_at) %></td>
|
|
36
|
+
<td><%= job.executions_count %></td>
|
|
37
|
+
<td class="text-break"><%= truncate(job.recent_error, length: 1_000) %></td>
|
|
38
|
+
<td>
|
|
39
|
+
<%= tag.button "Preview", type: "button", class: "btn btn-sm btn-outline-primary", role: "button",
|
|
40
|
+
data: { bs_toggle: "collapse", bs_target: "##{dom_id(job, 'params')}" },
|
|
41
|
+
aria: { expanded: false, controls: dom_id(job, "params") }
|
|
42
|
+
%>
|
|
43
|
+
<%= tag.pre JSON.pretty_generate(job.serialized_params), id: dom_id(job, "params"), class: "collapse job-params" %>
|
|
44
|
+
</td>
|
|
45
|
+
<td>
|
|
46
|
+
<div class="text-nowrap">
|
|
47
|
+
<% job_reschedulable = job.status.in? [:scheduled, :retried, :queued] %>
|
|
48
|
+
<%= button_to reschedule_job_path(job.id), method: :put, class: "btn btn-sm #{job_reschedulable ? 'btn-outline-primary' : 'btn-outline-secondary'}", form_class: "d-inline-block", disabled: !job_reschedulable, aria: { label: "Reschedule job" }, title: "Reschedule job", data: { confirm: "Confirm reschedule" } do %>
|
|
49
|
+
<%= render "good_job/shared/icons/skip_forward" %>
|
|
50
|
+
<% end %>
|
|
52
51
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
52
|
+
<% job_discardable = job.status.in? [:scheduled, :retried, :queued] %>
|
|
53
|
+
<%= 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 %>
|
|
54
|
+
<%= render "good_job/shared/icons/stop" %>
|
|
55
|
+
<% end %>
|
|
57
56
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
57
|
+
<%= 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 %>
|
|
58
|
+
<%= render "good_job/shared/icons/arrow_clockwise" %>
|
|
59
|
+
<% end %>
|
|
60
|
+
</div>
|
|
61
|
+
</td>
|
|
62
|
+
</tr>
|
|
63
|
+
<% end %>
|
|
64
|
+
<% else %>
|
|
65
|
+
<tr>
|
|
66
|
+
<td colspan="8" class="py-2 text-center text-muted">No jobs found.</td>
|
|
63
67
|
</tr>
|
|
64
68
|
<% end %>
|
|
65
69
|
</tbody>
|
|
@@ -1,12 +1,17 @@
|
|
|
1
|
-
<div class="
|
|
1
|
+
<div class="my-3 flex">
|
|
2
|
+
<h2>All Jobs</h2>
|
|
3
|
+
</div>
|
|
4
|
+
|
|
5
|
+
<div class="card my-3 p-6" data-gj-poll-replace id="jobs-chart">
|
|
2
6
|
<%= render 'good_job/shared/chart', chart_data: GoodJob::ScheduledByQueueChart.new(@filter).data %>
|
|
3
7
|
</div>
|
|
4
8
|
|
|
5
9
|
<%= render 'good_job/shared/filter', filter: @filter %>
|
|
6
10
|
|
|
11
|
+
<%= render 'good_job/jobs/table', jobs: @filter.records %>
|
|
12
|
+
|
|
7
13
|
<% if @filter.records.present? %>
|
|
8
|
-
|
|
9
|
-
<nav aria-label="Job pagination" class="mt-3">
|
|
14
|
+
<nav aria-label="Job pagination" class="mt-3" data-gj-poll-replace id="jobs-pagination">
|
|
10
15
|
<ul class="pagination">
|
|
11
16
|
<li class="page-item">
|
|
12
17
|
<%= link_to(@filter.to_params(after_scheduled_at: (@filter.last.scheduled_at || @filter.last.created_at), after_id: @filter.last.id), class: "page-link") do %>
|
|
@@ -15,6 +20,4 @@
|
|
|
15
20
|
</li>
|
|
16
21
|
</ul>
|
|
17
22
|
</nav>
|
|
18
|
-
<% else %>
|
|
19
|
-
<em>No jobs present.</em>
|
|
20
23
|
<% end %>
|
|
@@ -1,40 +1,46 @@
|
|
|
1
|
-
|
|
2
|
-
<
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
<div class="my-3 flex">
|
|
2
|
+
<h2>Processes</h2>
|
|
3
|
+
</div>
|
|
4
|
+
|
|
5
|
+
<div data-gj-poll-replace id="processes">
|
|
6
|
+
<% if !GoodJob::Process.migrated? %>
|
|
7
|
+
<div class="card my-3">
|
|
8
|
+
<div class="card-body">
|
|
9
|
+
<p class="card-text">
|
|
10
|
+
<em>Feature unavailable because of pending database migration.</em>
|
|
11
|
+
</p>
|
|
12
|
+
</div>
|
|
7
13
|
</div>
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
<th>State</th>
|
|
18
|
-
</tr>
|
|
19
|
-
</thead>
|
|
20
|
-
<tbody>
|
|
21
|
-
<% @processes.each do |process| %>
|
|
22
|
-
<tr class="<%= dom_class(process) %>" id="<%= dom_id(process) %>">
|
|
23
|
-
<td><%= process.id %></td>
|
|
24
|
-
<td><%= relative_time(process.created_at) %></td>
|
|
25
|
-
<td><%= tag.pre JSON.pretty_generate(process.state) %></td>
|
|
14
|
+
<% elsif @processes.present? %>
|
|
15
|
+
<div class="card my-3">
|
|
16
|
+
<div class="table-responsive">
|
|
17
|
+
<table class="table card-table table-bordered table-hover table-sm mb-0">
|
|
18
|
+
<thead>
|
|
19
|
+
<tr>
|
|
20
|
+
<th>Process UUID</th>
|
|
21
|
+
<th>Created At</th></th>
|
|
22
|
+
<th>State</th>
|
|
26
23
|
</tr>
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
24
|
+
</thead>
|
|
25
|
+
<tbody>
|
|
26
|
+
<% @processes.each do |process| %>
|
|
27
|
+
<tr class="<%= dom_class(process) %>" id="<%= dom_id(process) %>">
|
|
28
|
+
<td><%= process.id %></td>
|
|
29
|
+
<td><%= relative_time(process.created_at) %></td>
|
|
30
|
+
<td><%= tag.pre JSON.pretty_generate(process.state) %></td>
|
|
31
|
+
</tr>
|
|
32
|
+
<% end %>
|
|
33
|
+
</tbody>
|
|
34
|
+
</table>
|
|
35
|
+
</div>
|
|
30
36
|
</div>
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
</
|
|
37
|
+
<% else %>
|
|
38
|
+
<div class="card my-3">
|
|
39
|
+
<div class="card-body">
|
|
40
|
+
<p class="card-text">
|
|
41
|
+
<em>No GoodJob processes found.</em>
|
|
42
|
+
</p>
|
|
43
|
+
</div>
|
|
38
44
|
</div>
|
|
39
|
-
|
|
40
|
-
|
|
45
|
+
<% end %>
|
|
46
|
+
</div>
|
|
@@ -1,25 +1,3 @@
|
|
|
1
1
|
<div class="chart-wrapper">
|
|
2
|
-
<canvas
|
|
2
|
+
<canvas class="chart" data-json="<%= chart_data.to_json %>"></canvas>
|
|
3
3
|
</div>
|
|
4
|
-
|
|
5
|
-
<%= javascript_tag nonce: true do %>
|
|
6
|
-
const chartData = <%== chart_data.to_json %>;
|
|
7
|
-
|
|
8
|
-
const ctx = document.getElementById('chart').getContext('2d');
|
|
9
|
-
const chart = new Chart(ctx, {
|
|
10
|
-
type: 'line',
|
|
11
|
-
data: {
|
|
12
|
-
labels: chartData.labels,
|
|
13
|
-
datasets: chartData.datasets
|
|
14
|
-
},
|
|
15
|
-
options: {
|
|
16
|
-
responsive: true,
|
|
17
|
-
maintainAspectRatio: false,
|
|
18
|
-
scales: {
|
|
19
|
-
y: {
|
|
20
|
-
beginAtZero: true
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
});
|
|
25
|
-
<% end %>
|
|
@@ -1,66 +1,59 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
<
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
<% else %>
|
|
12
|
-
<%= link_to(filter.to_params(job_class: name), class: 'btn btn-sm btn-outline-secondary', role: "button") do %>
|
|
13
|
-
<%= name %> (<%= count %>)
|
|
14
|
-
<% end %>
|
|
1
|
+
<%= form_with(url: "", method: :get, local: true, id: "filter_form") do |form| %>
|
|
2
|
+
<%= hidden_field_tag :poll, value: params[:poll] %>
|
|
3
|
+
<div class="d-flex flex-row w-100">
|
|
4
|
+
<div class="me-2">
|
|
5
|
+
<label for="job_class_filter">Job class</label>
|
|
6
|
+
<select name="job_class" id="job_class_filter" class="form-select">
|
|
7
|
+
<option value="" <%= "selected='selected'" if params[:job_class].blank? %>>All jobs</option>
|
|
8
|
+
|
|
9
|
+
<% filter.job_classes.each do |name, count| %>
|
|
10
|
+
<option value="<%= name.to_param %>" <%= "selected='selected'" if params[:job_class] == name %>><%= name %> (<%= count %>)</option>
|
|
15
11
|
<% end %>
|
|
16
|
-
|
|
12
|
+
</select>
|
|
17
13
|
</div>
|
|
18
14
|
|
|
19
|
-
<div class=
|
|
20
|
-
<
|
|
21
|
-
<
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
<% end %>
|
|
27
|
-
<% else %>
|
|
28
|
-
<%= link_to(filter.to_params(state: name), class: 'btn btn-sm btn-outline-secondary', role: "button") do %>
|
|
29
|
-
<%= name %> (<%= count %>)
|
|
30
|
-
<% end %>
|
|
15
|
+
<div class="me-2">
|
|
16
|
+
<label for="job_state_filter">State</label>
|
|
17
|
+
<select name="state" id="job_state_filter" class="form-select">
|
|
18
|
+
<option value="" <%= "selected='selected'" if params[:state].blank? %>>All states</option>
|
|
19
|
+
|
|
20
|
+
<% filter.states.each do |name, count| %>
|
|
21
|
+
<option value="<%= name.to_param %>" <%= "selected='selected'" if params[:state] == name %>><%= name %> (<%= count %>)</option>
|
|
31
22
|
<% end %>
|
|
32
|
-
|
|
23
|
+
</select>
|
|
33
24
|
</div>
|
|
34
25
|
|
|
35
|
-
<div class=
|
|
36
|
-
<
|
|
37
|
-
<
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
<% end %>
|
|
43
|
-
<% else %>
|
|
44
|
-
<%= link_to(filter.to_params(queue_name: name), class: 'btn btn-sm btn-outline-secondary', role: "button") do %>
|
|
45
|
-
<%= name %> (<%= count %>)
|
|
46
|
-
<% end %>
|
|
26
|
+
<div class="me-2">
|
|
27
|
+
<label for="job_queue_filter">Queue</label>
|
|
28
|
+
<select name="queue_name" id="job_queue_filter" class="form-select">
|
|
29
|
+
<option value="" <%= "selected='selected'" if params[:queue_name].blank? %>>All queues</option>
|
|
30
|
+
|
|
31
|
+
<% filter.queues.each do |name, count| %>
|
|
32
|
+
<option value="<%= name.to_param %>" <%= "selected='selected'" if params[:queue_name] == name %>><%= name %> (<%= count %>)</option>
|
|
47
33
|
<% end %>
|
|
48
|
-
|
|
34
|
+
</select>
|
|
49
35
|
</div>
|
|
50
36
|
|
|
51
|
-
<div class="
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
<% end %>
|
|
37
|
+
<div class="me-2 flex-fill d-flex flex-col align-items-end">
|
|
38
|
+
<label class="visually-hidden" for="query" aria-label="Search by class, job id, job params, and error text.">Search by class, job id, job params, and error text.</label>
|
|
39
|
+
<%= search_field_tag "query", params[:query], class: "form-control", placeholder: "Search by class, job id, job params, and error text." %>
|
|
40
|
+
</div>
|
|
56
41
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
</div>
|
|
63
|
-
<% end %>
|
|
42
|
+
<div class="d-flex flex-col align-items-end">
|
|
43
|
+
<div>
|
|
44
|
+
<%= form.submit "Search", name: nil, class: "btn btn-primary" %>
|
|
45
|
+
<%= link_to "Clear all", filter.to_params(job_class: nil, state: nil, queue_name: nil, query: nil), class: "btn btn-secondary" %>
|
|
46
|
+
</div>
|
|
64
47
|
</div>
|
|
65
48
|
</div>
|
|
66
|
-
|
|
49
|
+
<% end %>
|
|
50
|
+
|
|
51
|
+
<%= javascript_tag nonce: true do %>
|
|
52
|
+
document.addEventListener("DOMContentLoaded", () => {
|
|
53
|
+
document.querySelectorAll("#job_class_filter, #job_state_filter, #job_queue_filter").forEach((filter) => {
|
|
54
|
+
filter.addEventListener("change", () => {
|
|
55
|
+
document.querySelector("#filter_form").submit();
|
|
56
|
+
});
|
|
57
|
+
})
|
|
58
|
+
})
|
|
59
|
+
<% end %>
|
|
@@ -45,7 +45,10 @@
|
|
|
45
45
|
</div>
|
|
46
46
|
</li>
|
|
47
47
|
</ul>
|
|
48
|
-
<div
|
|
48
|
+
<div>
|
|
49
|
+
<input type="checkbox" id="toggle-poll" name="toggle-poll" data-gj-action='change#togglePoll' <%= 'checked' if params[:poll].present? %>>
|
|
50
|
+
<label for="toggle-poll">Live Poll</label>
|
|
51
|
+
</div>
|
|
49
52
|
</div>
|
|
50
53
|
</div>
|
|
51
54
|
</nav>
|
|
@@ -70,7 +73,24 @@
|
|
|
70
73
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
|
71
74
|
</div>
|
|
72
75
|
<% end %>
|
|
76
|
+
|
|
73
77
|
<%= yield %>
|
|
74
78
|
</div>
|
|
79
|
+
|
|
80
|
+
<footer class="footer mt-auto py-3 bg-light fixed-bottom" id="footer" data-gj-poll-replace>
|
|
81
|
+
<div class="container-fluid">
|
|
82
|
+
<div class="row">
|
|
83
|
+
<div class="col-6">
|
|
84
|
+
<span class="text-muted">
|
|
85
|
+
Last updated: <time id="page-updated-at" datetime="<%= Time.current.utc.iso8601 %>"><%= Time.current %></time>
|
|
86
|
+
</span>
|
|
87
|
+
</div>
|
|
88
|
+
|
|
89
|
+
<div class="col-6 text-end">
|
|
90
|
+
Remember, you're doing a Good Job too!
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
</footer>
|
|
75
95
|
</body>
|
|
76
96
|
</html>
|
|
@@ -62,7 +62,7 @@ module GoodJob
|
|
|
62
62
|
next if key.blank?
|
|
63
63
|
|
|
64
64
|
GoodJob::Execution.advisory_lock_key(key, function: "pg_advisory_lock") do
|
|
65
|
-
allowed_active_job_ids = GoodJob::Execution.where(concurrency_key: key).advisory_locked.order(Arel.sql("COALESCE(performed_at, scheduled_at, created_at) ASC")).limit(perform_limit).pluck(:active_job_id)
|
|
65
|
+
allowed_active_job_ids = GoodJob::Execution.unfinished.where(concurrency_key: key).advisory_locked.order(Arel.sql("COALESCE(performed_at, scheduled_at, created_at) ASC")).limit(perform_limit).pluck(:active_job_id)
|
|
66
66
|
# The current job has already been locked and will appear in the previous query
|
|
67
67
|
raise GoodJob::ActiveJobExtensions::Concurrency::ConcurrencyExceededError unless allowed_active_job_ids.include? job.job_id
|
|
68
68
|
end
|
data/lib/good_job/version.rb
CHANGED
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.11.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ben Sheldon
|
|
8
|
-
autorequire:
|
|
8
|
+
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2022-02-
|
|
11
|
+
date: 2022-02-28 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: activejob
|
|
@@ -448,7 +448,7 @@ metadata:
|
|
|
448
448
|
homepage_uri: https://github.com/bensheldon/good_job
|
|
449
449
|
source_code_uri: https://github.com/bensheldon/good_job
|
|
450
450
|
rubygems_mfa_required: 'true'
|
|
451
|
-
post_install_message:
|
|
451
|
+
post_install_message:
|
|
452
452
|
rdoc_options:
|
|
453
453
|
- "--title"
|
|
454
454
|
- GoodJob - a multithreaded, Postgres-based ActiveJob backend for Ruby on Rails
|
|
@@ -471,8 +471,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
471
471
|
- !ruby/object:Gem::Version
|
|
472
472
|
version: '0'
|
|
473
473
|
requirements: []
|
|
474
|
-
rubygems_version: 3.
|
|
475
|
-
signing_key:
|
|
474
|
+
rubygems_version: 3.3.7
|
|
475
|
+
signing_key:
|
|
476
476
|
specification_version: 4
|
|
477
477
|
summary: A multithreaded, Postgres-based ActiveJob backend for Ruby on Rails
|
|
478
478
|
test_files: []
|