job_board 0.1.0 → 0.2.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 +9 -0
- data/README.md +27 -0
- data/app/controllers/job_board/queues_controller.rb +1 -1
- data/app/models/job_board/queue_list.rb +35 -0
- data/app/views/job_board/queues/_table.html.erb +44 -0
- data/app/views/job_board/queues/index.html.erb +16 -43
- data/lib/job_board/assets/application.css +13 -3
- data/lib/job_board/assets/application.js +14 -1
- data/lib/job_board/configuration.rb +7 -0
- data/lib/job_board/version.rb +1 -1
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c0a2428d3806231d3051a64d1ed7f693e51341cbbc12a4a2fe8240f1f1c56d73
|
|
4
|
+
data.tar.gz: 59cb3f9a17d3ae3cc0b9ac69adb11ec5c1c27838391d559c2c6f9a23e2a65306
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: fc936decbae3e1e9457ac3a6d6643f67907e4b0314ee3368e22516040779381f549af59c7b60fffa28f026b1edb88feddfa62cdd32772b9e4571eb413ad1e46f
|
|
7
|
+
data.tar.gz: c7f69762ffd1695bb6fc3dd64d59a8248f3d445255cc8f2276d9c7ddfb3a0b2008898c8557988fe4c76455389fbe213e752b36bdf1d4ccbdf56393e5c74ef4b0
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.0
|
|
4
|
+
|
|
5
|
+
- Queues page: new **Last enqueued** column showing when each queue last received a job.
|
|
6
|
+
- Queues with nothing enqueued in the last 30 days collapse into a one-click
|
|
7
|
+
"Inactive queues" section. Configurable via `config.queue_activity_window`
|
|
8
|
+
(`nil` shows everything in one table); paused queues always stay in the main table.
|
|
9
|
+
- The auto-refresh poller now preserves the open/closed state of collapsible sections
|
|
10
|
+
across refreshes.
|
|
11
|
+
|
|
3
12
|
## 0.1.0
|
|
4
13
|
|
|
5
14
|
Initial release.
|
data/README.md
CHANGED
|
@@ -69,6 +69,9 @@ JobBoard.configure do |config|
|
|
|
69
69
|
config.stale_process_threshold = nil # heartbeat age before a process is flagged stale;
|
|
70
70
|
# nil uses SolidQueue.process_alive_threshold (5 min)
|
|
71
71
|
config.http_basic_auth = nil # see Authentication above
|
|
72
|
+
config.queue_activity_window = 30.days # collapse queues with nothing enqueued in
|
|
73
|
+
# that window into "Inactive queues";
|
|
74
|
+
# nil shows everything in one table
|
|
72
75
|
|
|
73
76
|
# Latency (seconds) above which a queue is highlighted as breaching:
|
|
74
77
|
config.latency_warning_threshold = 60 # global default
|
|
@@ -94,6 +97,30 @@ Explicit `latency_warning_thresholds` entries still win over the naming conventi
|
|
|
94
97
|
The queues page also sorts by SLA: queues with a detectable latency target come first
|
|
95
98
|
(strictest at the top), and everything else follows alphabetically.
|
|
96
99
|
|
|
100
|
+
### Hiding inactive queues
|
|
101
|
+
|
|
102
|
+
Because Solid Queue derives the queue list from the jobs table, a queue keeps showing
|
|
103
|
+
up as long as *any* job row with its name exists — including queues you retired long
|
|
104
|
+
ago. Each queue's **Last enqueued** column shows when it last received a job, and
|
|
105
|
+
queues with nothing enqueued in the last **30 days** (configurable) collapse into a
|
|
106
|
+
one-click **Inactive queues** section at the bottom of the page — out of the way but
|
|
107
|
+
never invisible, since a quiet queue can be a symptom rather than noise. Paused
|
|
108
|
+
queues always stay in the main table regardless of idleness, because a pause is
|
|
109
|
+
deliberate state someone needs to see.
|
|
110
|
+
|
|
111
|
+
```ruby
|
|
112
|
+
JobBoard.configure do |config|
|
|
113
|
+
config.queue_activity_window = 7.days # tighter window
|
|
114
|
+
# or nil to always show every queue in one table
|
|
115
|
+
end
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Also worth knowing: if the lingering rows are preserved finished jobs, Solid Queue's
|
|
119
|
+
dispatcher normally clears them after `SolidQueue.clear_finished_jobs_after` (default
|
|
120
|
+
1 day) — stale names often mean that cleanup isn't running, or that old failed jobs
|
|
121
|
+
are still waiting to be retried or discarded. Dealing with those makes retired queues
|
|
122
|
+
disappear entirely.
|
|
123
|
+
|
|
97
124
|
## Notes
|
|
98
125
|
|
|
99
126
|
- **Separate queue database**: works automatically. Job Board reads and writes exclusively
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
module JobBoard
|
|
2
2
|
class QueuesController < ApplicationController
|
|
3
3
|
def index
|
|
4
|
-
@
|
|
4
|
+
@queue_list = QueueList.build
|
|
5
5
|
@failed_counts = SolidQueue::FailedExecution.joins(:job)
|
|
6
6
|
.group("solid_queue_jobs.queue_name").count
|
|
7
7
|
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
module JobBoard
|
|
2
|
+
# Partitions queues into active and inactive based on when each queue last
|
|
3
|
+
# had a job enqueued. A queue is inactive when its newest job is older than
|
|
4
|
+
# the activity window; paused queues always count as active, since a pause
|
|
5
|
+
# is deliberate state someone needs to see. Without a window, every queue
|
|
6
|
+
# is active.
|
|
7
|
+
class QueueList
|
|
8
|
+
attr_reader :active, :inactive, :last_enqueued_at, :window
|
|
9
|
+
|
|
10
|
+
def self.build(window: JobBoard.config.queue_activity_window)
|
|
11
|
+
new(LatencySla.sort(SolidQueue::Queue.all),
|
|
12
|
+
last_enqueued_at: SolidQueue::Job.group(:queue_name).maximum(:created_at),
|
|
13
|
+
window: window)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def initialize(queues, last_enqueued_at:, window: nil)
|
|
17
|
+
@last_enqueued_at = last_enqueued_at
|
|
18
|
+
@window = window
|
|
19
|
+
@active, @inactive = partition(queues)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def partition(queues)
|
|
25
|
+
return [queues, []] unless window
|
|
26
|
+
|
|
27
|
+
cutoff = Time.current - window
|
|
28
|
+
paused = SolidQueue::Pause.where(queue_name: queues.map(&:name)).pluck(:queue_name).to_set
|
|
29
|
+
queues.partition do |queue|
|
|
30
|
+
newest = last_enqueued_at[queue.name]
|
|
31
|
+
paused.include?(queue.name) || (newest && newest >= cutoff)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
<table class="data-table">
|
|
2
|
+
<thead>
|
|
3
|
+
<tr>
|
|
4
|
+
<th>Queue</th>
|
|
5
|
+
<th class="num">Ready jobs</th>
|
|
6
|
+
<th class="num">Failed</th>
|
|
7
|
+
<th class="num">Latency</th>
|
|
8
|
+
<th>Last enqueued</th>
|
|
9
|
+
<th>Status</th>
|
|
10
|
+
<th class="actions"></th>
|
|
11
|
+
</tr>
|
|
12
|
+
</thead>
|
|
13
|
+
<tbody>
|
|
14
|
+
<% queues.each do |queue| %>
|
|
15
|
+
<tr>
|
|
16
|
+
<td><%= link_to queue.name, jobs_path(status: "ready", queue_name: queue.name) %></td>
|
|
17
|
+
<td class="num"><%= queue.size %></td>
|
|
18
|
+
<td class="num">
|
|
19
|
+
<% failed = @failed_counts[queue.name] || 0 %>
|
|
20
|
+
<% if failed.positive? %>
|
|
21
|
+
<%= link_to failed, jobs_path(status: "failed", queue_name: queue.name), class: "failed-count" %>
|
|
22
|
+
<% else %>
|
|
23
|
+
<span class="muted">0</span>
|
|
24
|
+
<% end %>
|
|
25
|
+
</td>
|
|
26
|
+
<td class="num">
|
|
27
|
+
<span class="latency <%= "latency--high" if queue.latency > latency_threshold_for(queue.name) %>" title="<%= queue.latency %> seconds">
|
|
28
|
+
<%= duration(queue.latency) %>
|
|
29
|
+
</span>
|
|
30
|
+
</td>
|
|
31
|
+
<td><%= time_ago(@queue_list.last_enqueued_at[queue.name]) %></td>
|
|
32
|
+
<td><%= queue.paused? ? status_badge(:paused) : status_badge(:active) %></td>
|
|
33
|
+
<td class="actions">
|
|
34
|
+
<% if queue.paused? %>
|
|
35
|
+
<%= button_to "Resume", queue_pause_path(queue.name), method: :delete, class: "btn" %>
|
|
36
|
+
<% else %>
|
|
37
|
+
<%= button_to "Pause", queue_pause_path(queue.name), method: :post, class: "btn",
|
|
38
|
+
form: { data: { confirm: "Pause queue \"#{queue.name}\"? Workers will stop picking up its jobs." } } %>
|
|
39
|
+
<% end %>
|
|
40
|
+
</td>
|
|
41
|
+
</tr>
|
|
42
|
+
<% end %>
|
|
43
|
+
</tbody>
|
|
44
|
+
</table>
|
|
@@ -1,50 +1,23 @@
|
|
|
1
1
|
<h1>Queues</h1>
|
|
2
2
|
|
|
3
3
|
<div data-poll-region="queues">
|
|
4
|
-
<% if @
|
|
4
|
+
<% if @queue_list.active.empty? && @queue_list.inactive.empty? %>
|
|
5
5
|
<p class="empty-state">No queues yet — queues appear once jobs are enqueued.</p>
|
|
6
6
|
<% else %>
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
<td class="num"><%= queue.size %></td>
|
|
23
|
-
<td class="num">
|
|
24
|
-
<% failed = @failed_counts[queue.name] || 0 %>
|
|
25
|
-
<% if failed.positive? %>
|
|
26
|
-
<%= link_to failed, jobs_path(status: "failed", queue_name: queue.name), class: "failed-count" %>
|
|
27
|
-
<% else %>
|
|
28
|
-
<span class="muted">0</span>
|
|
29
|
-
<% end %>
|
|
30
|
-
</td>
|
|
31
|
-
<td class="num">
|
|
32
|
-
<span class="latency <%= "latency--high" if queue.latency > latency_threshold_for(queue.name) %>" title="<%= queue.latency %> seconds">
|
|
33
|
-
<%= duration(queue.latency) %>
|
|
34
|
-
</span>
|
|
35
|
-
</td>
|
|
36
|
-
<td><%= queue.paused? ? status_badge(:paused) : status_badge(:active) %></td>
|
|
37
|
-
<td class="actions">
|
|
38
|
-
<% if queue.paused? %>
|
|
39
|
-
<%= button_to "Resume", queue_pause_path(queue.name), method: :delete, class: "btn" %>
|
|
40
|
-
<% else %>
|
|
41
|
-
<%= button_to "Pause", queue_pause_path(queue.name), method: :post, class: "btn",
|
|
42
|
-
form: { data: { confirm: "Pause queue \"#{queue.name}\"? Workers will stop picking up its jobs." } } %>
|
|
43
|
-
<% end %>
|
|
44
|
-
</td>
|
|
45
|
-
</tr>
|
|
46
|
-
<% end %>
|
|
47
|
-
</tbody>
|
|
48
|
-
</table>
|
|
7
|
+
<% if @queue_list.active.any? %>
|
|
8
|
+
<%= render "table", queues: @queue_list.active %>
|
|
9
|
+
<% else %>
|
|
10
|
+
<p class="empty-state">No queues have had jobs enqueued in the last <%= duration(@queue_list.window.to_i) %>.</p>
|
|
11
|
+
<% end %>
|
|
12
|
+
|
|
13
|
+
<% if @queue_list.inactive.any? %>
|
|
14
|
+
<details class="inactive-queues" data-persist="inactive-queues">
|
|
15
|
+
<summary>
|
|
16
|
+
Inactive queues (<%= @queue_list.inactive.size %>)
|
|
17
|
+
<span class="muted">— nothing enqueued in the last <%= duration(@queue_list.window.to_i) %></span>
|
|
18
|
+
</summary>
|
|
19
|
+
<%= render "table", queues: @queue_list.inactive %>
|
|
20
|
+
</details>
|
|
21
|
+
<% end %>
|
|
49
22
|
<% end %>
|
|
50
23
|
</div>
|
|
@@ -52,9 +52,7 @@ body {
|
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
main {
|
|
55
|
-
|
|
56
|
-
margin: 0 auto;
|
|
57
|
-
padding: 1.5rem 1rem 4rem;
|
|
55
|
+
padding: 1.5rem 1.5rem 4rem;
|
|
58
56
|
}
|
|
59
57
|
|
|
60
58
|
h1 { font-size: 1.4rem; margin: 0.5rem 0 1rem; }
|
|
@@ -176,6 +174,18 @@ code {
|
|
|
176
174
|
color: var(--text-muted);
|
|
177
175
|
}
|
|
178
176
|
|
|
177
|
+
/* Queues page: collapsed section for idle queues */
|
|
178
|
+
.inactive-queues { margin-top: 1.25rem; }
|
|
179
|
+
.inactive-queues > summary {
|
|
180
|
+
cursor: pointer;
|
|
181
|
+
color: var(--text-muted);
|
|
182
|
+
font-size: 0.85rem;
|
|
183
|
+
padding: 0.35rem 0.25rem;
|
|
184
|
+
user-select: none;
|
|
185
|
+
}
|
|
186
|
+
.inactive-queues > summary:hover { color: var(--text); }
|
|
187
|
+
.inactive-queues[open] > summary { margin-bottom: 0.5rem; }
|
|
188
|
+
|
|
179
189
|
.latency { font-weight: 600; }
|
|
180
190
|
.latency--high { color: var(--danger); }
|
|
181
191
|
.failed-count { color: var(--danger); font-weight: 600; }
|
|
@@ -31,7 +31,20 @@
|
|
|
31
31
|
document.querySelectorAll("[data-poll-region]").forEach(function (region) {
|
|
32
32
|
var key = region.getAttribute("data-poll-region");
|
|
33
33
|
var replacement = fresh.querySelector('[data-poll-region="' + key + '"]');
|
|
34
|
-
if (replacement)
|
|
34
|
+
if (!replacement) return;
|
|
35
|
+
|
|
36
|
+
// Remember which <details data-persist> are open so the swap
|
|
37
|
+
// doesn't collapse them.
|
|
38
|
+
var open = {};
|
|
39
|
+
region.querySelectorAll("details[data-persist]").forEach(function (details) {
|
|
40
|
+
open[details.dataset.persist] = details.open;
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
region.innerHTML = replacement.innerHTML;
|
|
44
|
+
|
|
45
|
+
region.querySelectorAll("details[data-persist]").forEach(function (details) {
|
|
46
|
+
if (details.dataset.persist in open) details.open = open[details.dataset.persist];
|
|
47
|
+
});
|
|
35
48
|
});
|
|
36
49
|
})
|
|
37
50
|
.catch(function () { /* transient network errors are fine; try again next tick */ });
|
|
@@ -13,6 +13,12 @@ module JobBoard
|
|
|
13
13
|
# nil falls back to SolidQueue.process_alive_threshold.
|
|
14
14
|
attr_accessor :stale_process_threshold
|
|
15
15
|
|
|
16
|
+
# Queues with no job enqueued inside this window (seconds or an
|
|
17
|
+
# ActiveSupport::Duration) are collapsed into an "Inactive queues"
|
|
18
|
+
# section on the queues page. Paused queues always stay visible.
|
|
19
|
+
# nil shows every queue in one table.
|
|
20
|
+
attr_accessor :queue_activity_window
|
|
21
|
+
|
|
16
22
|
# Seconds of latency after which a queue is highlighted as breaching.
|
|
17
23
|
# Per-queue override: latency_warning_thresholds["queue_name"] = seconds.
|
|
18
24
|
# Queues named in the within_* convention (e.g. "within_5_minutes") get
|
|
@@ -25,6 +31,7 @@ module JobBoard
|
|
|
25
31
|
@poll_interval = 5
|
|
26
32
|
@per_page = 25
|
|
27
33
|
@stale_process_threshold = nil
|
|
34
|
+
@queue_activity_window = 30.days
|
|
28
35
|
@latency_warning_threshold = 60
|
|
29
36
|
@latency_warning_thresholds = {}
|
|
30
37
|
end
|
data/lib/job_board/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: job_board
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Mike
|
|
@@ -62,12 +62,14 @@ files:
|
|
|
62
62
|
- app/models/job_board/latency_sla.rb
|
|
63
63
|
- app/models/job_board/page.rb
|
|
64
64
|
- app/models/job_board/process_tree.rb
|
|
65
|
+
- app/models/job_board/queue_list.rb
|
|
65
66
|
- app/views/job_board/jobs/_filters.html.erb
|
|
66
67
|
- app/views/job_board/jobs/_job_row.html.erb
|
|
67
68
|
- app/views/job_board/jobs/index.html.erb
|
|
68
69
|
- app/views/job_board/jobs/show.html.erb
|
|
69
70
|
- app/views/job_board/processes/_process.html.erb
|
|
70
71
|
- app/views/job_board/processes/index.html.erb
|
|
72
|
+
- app/views/job_board/queues/_table.html.erb
|
|
71
73
|
- app/views/job_board/queues/index.html.erb
|
|
72
74
|
- app/views/job_board/recurring_tasks/index.html.erb
|
|
73
75
|
- app/views/job_board/shared/_nav.html.erb
|