job_board 0.4.0 → 0.5.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 +8 -0
- data/app/controllers/job_board/application_controller.rb +2 -0
- data/app/controllers/job_board/assets_controller.rb +3 -1
- data/app/controllers/job_board/jobs_controller.rb +5 -3
- data/app/controllers/job_board/metrics_controller.rb +2 -0
- data/app/controllers/job_board/processes_controller.rb +2 -0
- data/app/controllers/job_board/queues/pauses_controller.rb +2 -0
- data/app/controllers/job_board/queues_controller.rb +5 -1
- data/app/controllers/job_board/recurring_tasks/runs_controller.rb +5 -2
- data/app/controllers/job_board/recurring_tasks_controller.rb +2 -0
- data/app/helpers/job_board/application_helper.rb +4 -2
- data/app/models/job_board/cron_description.rb +12 -7
- data/app/models/job_board/job_presenter.rb +2 -0
- data/app/models/job_board/job_row.rb +2 -0
- data/app/models/job_board/jobs_query.rb +7 -2
- data/app/models/job_board/latency_sla.rb +4 -2
- data/app/models/job_board/page.rb +4 -2
- data/app/models/job_board/process_tree.rb +4 -2
- data/app/models/job_board/queue_list.rb +4 -2
- data/app/models/job_board/throughput.rb +3 -1
- data/app/views/job_board/queues/_table.html.erb +9 -0
- data/config/routes.rb +5 -3
- data/lib/job_board/assets/application.css +1 -0
- data/lib/job_board/configuration.rb +2 -0
- data/lib/job_board/engine.rb +2 -0
- data/lib/job_board/version.rb +3 -1
- data/lib/job_board.rb +2 -0
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 18b3b4aa806ccbda04e2bd2100e4561eefec8b1ec665975b3d34d22d9ababadf
|
|
4
|
+
data.tar.gz: 31ba104259fe986f5c2eceee2f335d153246d7d21f6c9baf22764420091fbfa2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 746ddd882343564e9cec4dfe8a621b84e8410dc5aaac35adbce9a6adc8223fdddbf3a392ed6a9486fa5208268829a6daa470304dea9a27ec06aec300d2d9f8a3
|
|
7
|
+
data.tar.gz: 13bae2092fa89294028c00c03bbd528fbab80c2cef3156bfb1d6a09177dc1629b5898b58db93df75091508bef54e0d1adecd7a64e2b83b3aa7861bdbcbea35ef
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.5.0
|
|
4
|
+
|
|
5
|
+
- Queues (home) page: new **Running** column showing how many jobs are currently
|
|
6
|
+
claimed by a worker and in progress for each queue. Positive counts link to that
|
|
7
|
+
queue's in-progress jobs; the count refreshes live with the page's auto-refresh.
|
|
8
|
+
|
|
9
|
+

|
|
10
|
+
|
|
3
11
|
## 0.4.0
|
|
4
12
|
|
|
5
13
|
- Queues (home) page: a real-time **Throughput** line chart at the top, plotting jobs
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module JobBoard
|
|
2
4
|
class AssetsController < ApplicationController
|
|
3
5
|
skip_forgery_protection
|
|
@@ -15,7 +17,7 @@ module JobBoard
|
|
|
15
17
|
|
|
16
18
|
expires_in 1.year, public: true
|
|
17
19
|
send_file JobBoard::Engine.root.join("lib/job_board/assets", name),
|
|
18
|
-
|
|
20
|
+
type: content_type, disposition: :inline
|
|
19
21
|
end
|
|
20
22
|
end
|
|
21
23
|
end
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module JobBoard
|
|
2
4
|
class JobsController < ApplicationController
|
|
3
|
-
before_action :set_query, only: [
|
|
5
|
+
before_action :set_query, only: %i[index retry_all discard_all]
|
|
4
6
|
|
|
5
7
|
def index
|
|
6
8
|
@page = @query.page(before: params[:before], limit: JobBoard.config.per_page)
|
|
@@ -72,13 +74,13 @@ module JobBoard
|
|
|
72
74
|
.find(params[:id])
|
|
73
75
|
end
|
|
74
76
|
|
|
75
|
-
def failed_jobs_in_batches(&
|
|
77
|
+
def failed_jobs_in_batches(&)
|
|
76
78
|
query = JobsQuery.new(
|
|
77
79
|
status: "failed",
|
|
78
80
|
queue_name: params[:queue_name].presence,
|
|
79
81
|
class_name: params[:class_name].presence
|
|
80
82
|
)
|
|
81
|
-
query.failed_jobs_relation.find_in_batches(batch_size: 500, &
|
|
83
|
+
query.failed_jobs_relation.find_in_batches(batch_size: 500, &)
|
|
82
84
|
end
|
|
83
85
|
|
|
84
86
|
def failed_jobs_path
|
|
@@ -1,9 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module JobBoard
|
|
2
4
|
class QueuesController < ApplicationController
|
|
3
5
|
def index
|
|
4
6
|
@queue_list = QueueList.build
|
|
5
7
|
@failed_counts = SolidQueue::FailedExecution.joins(:job)
|
|
6
|
-
|
|
8
|
+
.group("solid_queue_jobs.queue_name").count
|
|
9
|
+
@running_counts = SolidQueue::ClaimedExecution.joins(:job)
|
|
10
|
+
.group("solid_queue_jobs.queue_name").count
|
|
7
11
|
end
|
|
8
12
|
end
|
|
9
13
|
end
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module JobBoard
|
|
2
4
|
module RecurringTasks
|
|
3
5
|
class RunsController < ApplicationController
|
|
@@ -9,8 +11,9 @@ module JobBoard
|
|
|
9
11
|
elsif task.enqueue(at: Time.current)
|
|
10
12
|
redirect_to recurring_tasks_path, notice: "Enqueued \"#{task.key}\"."
|
|
11
13
|
else
|
|
12
|
-
|
|
13
|
-
|
|
14
|
+
alert = "\"#{task.key}\" was not enqueued — it already ran at " \
|
|
15
|
+
"this exact time or the enqueue failed."
|
|
16
|
+
redirect_to recurring_tasks_path, alert: alert
|
|
14
17
|
end
|
|
15
18
|
end
|
|
16
19
|
end
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module JobBoard
|
|
2
4
|
module ApplicationHelper
|
|
3
5
|
def status_badge(status)
|
|
@@ -9,14 +11,14 @@ module JobBoard
|
|
|
9
11
|
|
|
10
12
|
time = Time.zone.parse(time) if time.is_a?(String)
|
|
11
13
|
tag.time("#{duration((Time.current - time).abs)} #{time.future? ? "from now" : "ago"}",
|
|
12
|
-
|
|
14
|
+
title: time.iso8601, datetime: time.iso8601)
|
|
13
15
|
end
|
|
14
16
|
|
|
15
17
|
def duration(seconds)
|
|
16
18
|
seconds = seconds.to_i
|
|
17
19
|
return "0s" if seconds <= 0
|
|
18
20
|
|
|
19
|
-
parts = { "d" =>
|
|
21
|
+
parts = { "d" => 86_400, "h" => 3600, "m" => 60, "s" => 1 }.filter_map do |unit, size|
|
|
20
22
|
value, seconds = seconds.divmod(size)
|
|
21
23
|
"#{value}#{unit}" if value.positive?
|
|
22
24
|
end
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require "fugit"
|
|
2
4
|
|
|
3
5
|
module JobBoard
|
|
@@ -44,6 +46,7 @@ module JobBoard
|
|
|
44
46
|
if (step = seconds_step)
|
|
45
47
|
# Sub-minute schedules only describe cleanly when nothing else is constrained.
|
|
46
48
|
return nil unless cron.minutes.nil? && cron.hours.nil?
|
|
49
|
+
|
|
47
50
|
"Every #{step} seconds"
|
|
48
51
|
elsif cron.seconds && cron.seconds != [0]
|
|
49
52
|
nil
|
|
@@ -60,7 +63,7 @@ module JobBoard
|
|
|
60
63
|
else
|
|
61
64
|
times = cron.hours.product(cron.minutes || [0]).sort
|
|
62
65
|
if times.size <= 4
|
|
63
|
-
"At #{join_and(times.map { |h, m| format("
|
|
66
|
+
"At #{join_and(times.map { |h, m| format("%<h>02d:%<m>02d", h: h, m: m) })}"
|
|
64
67
|
else
|
|
65
68
|
"At #{numbers("minute", cron.minutes)} past #{numbers("hour", cron.hours)}"
|
|
66
69
|
end
|
|
@@ -79,17 +82,19 @@ module JobBoard
|
|
|
79
82
|
return nil if cron.weekdays.nil?
|
|
80
83
|
|
|
81
84
|
plain = cron.weekdays.select { |_, nth| nth.nil? }.map { |day,| day % 7 }.sort.uniq
|
|
82
|
-
nth = cron.weekdays.
|
|
85
|
+
nth = cron.weekdays.filter_map do |day, n|
|
|
86
|
+
next if n.nil?
|
|
87
|
+
|
|
83
88
|
"the #{n == -1 ? "last" : n.ordinalize} #{DAYS[day % 7]} of the month"
|
|
84
89
|
end
|
|
85
90
|
|
|
86
91
|
phrases = []
|
|
87
92
|
if plain.any?
|
|
88
93
|
phrases << if plain.size > 2 && consecutive?(plain)
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
94
|
+
"#{DAYS[plain.first]} through #{DAYS[plain.last]}"
|
|
95
|
+
else
|
|
96
|
+
join_and(plain.map { |day| DAYS[day] })
|
|
97
|
+
end
|
|
93
98
|
end
|
|
94
99
|
"on #{join_and(phrases + nth)}"
|
|
95
100
|
end
|
|
@@ -99,7 +104,7 @@ module JobBoard
|
|
|
99
104
|
|
|
100
105
|
# "0 0 1 1 *" reads better as "on January 1" than "on day 1 of the month in January".
|
|
101
106
|
if cron.monthdays.size == 1 && cron.monthdays.first.positive? &&
|
|
102
|
-
|
|
107
|
+
cron.months&.size == 1 && cron.weekdays.nil?
|
|
103
108
|
@month_consumed = true
|
|
104
109
|
return "on #{MONTHS[cron.months.first - 1]} #{cron.monthdays.first}"
|
|
105
110
|
end
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module JobBoard
|
|
2
4
|
# Status-scoped job finder. Each status is driven by its execution table so a
|
|
3
5
|
# row's status is known without calling Job#status (which needs 5 preloads).
|
|
@@ -71,8 +73,11 @@ module JobBoard
|
|
|
71
73
|
|
|
72
74
|
def filtered(scope, own_queue_column:)
|
|
73
75
|
if queue_name
|
|
74
|
-
scope = own_queue_column
|
|
75
|
-
|
|
76
|
+
scope = if own_queue_column
|
|
77
|
+
scope.where(queue_name: queue_name)
|
|
78
|
+
else
|
|
79
|
+
scope.joins(:job).where(solid_queue_jobs: { queue_name: queue_name })
|
|
80
|
+
end
|
|
76
81
|
end
|
|
77
82
|
scope = scope.joins(:job).where(solid_queue_jobs: { class_name: class_name }) if class_name
|
|
78
83
|
scope
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module JobBoard
|
|
2
4
|
# Detects a queue's latency SLA from explicit configuration or from the
|
|
3
5
|
# within_* naming convention (e.g. "within_5_minutes" => 300).
|
|
4
6
|
module LatencySla
|
|
5
|
-
UNITS = { "second" => 1, "minute" => 60, "hour" => 3600, "day" =>
|
|
7
|
+
UNITS = { "second" => 1, "minute" => 60, "hour" => 3600, "day" => 86_400 }.freeze
|
|
6
8
|
|
|
7
9
|
# Seconds, or nil when the queue has no detectable SLA.
|
|
8
10
|
def self.detect(queue_name)
|
|
@@ -24,7 +26,7 @@ module JobBoard
|
|
|
24
26
|
|
|
25
27
|
def self.from_name(queue_name)
|
|
26
28
|
match = /\Awithin_(\d+)_(second|minute|hour|day)s?\z/i.match(queue_name.to_s)
|
|
27
|
-
match && match[1].to_i * UNITS.fetch(match[2].downcase)
|
|
29
|
+
match && (match[1].to_i * UNITS.fetch(match[2].downcase))
|
|
28
30
|
end
|
|
29
31
|
private_class_method :from_name
|
|
30
32
|
end
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module JobBoard
|
|
2
4
|
# Keyset pagination over a relation ordered by id DESC.
|
|
3
5
|
# Fetches limit + 1 records to detect whether an older page exists.
|
|
@@ -24,8 +26,8 @@ module JobBoard
|
|
|
24
26
|
records.last&.id
|
|
25
27
|
end
|
|
26
28
|
|
|
27
|
-
def each(&
|
|
28
|
-
rows.each(&
|
|
29
|
+
def each(&)
|
|
30
|
+
rows.each(&)
|
|
29
31
|
end
|
|
30
32
|
|
|
31
33
|
def empty?
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module JobBoard
|
|
2
4
|
# Supervisor -> supervisee tree of SolidQueue processes with staleness flags,
|
|
3
5
|
# claimed-job counts, and the in-progress jobs each worker holds.
|
|
@@ -12,11 +14,11 @@ module JobBoard
|
|
|
12
14
|
|
|
13
15
|
def initialize(stale_threshold:)
|
|
14
16
|
processes = SolidQueue::Process.order(:id).to_a
|
|
15
|
-
ids = processes.
|
|
17
|
+
ids = processes.to_set(&:id)
|
|
16
18
|
counts = SolidQueue::ClaimedExecution.group(:process_id).count
|
|
17
19
|
@claimed_by_process =
|
|
18
20
|
SolidQueue::ClaimedExecution.where(process_id: processes.map(&:id))
|
|
19
|
-
|
|
21
|
+
.includes(:job).group_by(&:process_id)
|
|
20
22
|
|
|
21
23
|
cutoff = stale_threshold.ago
|
|
22
24
|
children = processes.group_by(&:supervisor_id)
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module JobBoard
|
|
2
4
|
# Partitions queues into active and inactive based on when each queue last
|
|
3
5
|
# had a job enqueued. A queue is inactive when its newest job is older than
|
|
@@ -9,8 +11,8 @@ module JobBoard
|
|
|
9
11
|
|
|
10
12
|
def self.build(window: JobBoard.config.queue_activity_window)
|
|
11
13
|
new(LatencySla.sort(SolidQueue::Queue.all),
|
|
12
|
-
|
|
13
|
-
|
|
14
|
+
last_enqueued_at: SolidQueue::Job.group(:queue_name).maximum(:created_at),
|
|
15
|
+
window: window)
|
|
14
16
|
end
|
|
15
17
|
|
|
16
18
|
def initialize(queues, last_enqueued_at:, window: nil)
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
<tr>
|
|
4
4
|
<th>Queue</th>
|
|
5
5
|
<th class="num">Ready jobs</th>
|
|
6
|
+
<th class="num">Running</th>
|
|
6
7
|
<th class="num">Failed</th>
|
|
7
8
|
<th class="num">Latency</th>
|
|
8
9
|
<th>Last enqueued</th>
|
|
@@ -15,6 +16,14 @@
|
|
|
15
16
|
<tr>
|
|
16
17
|
<td><%= link_to queue.name, jobs_path(status: "ready", queue_name: queue.name) %></td>
|
|
17
18
|
<td class="num"><%= queue.size %></td>
|
|
19
|
+
<td class="num">
|
|
20
|
+
<% running = @running_counts[queue.name] || 0 %>
|
|
21
|
+
<% if running.positive? %>
|
|
22
|
+
<%= link_to running, jobs_path(status: "in_progress", queue_name: queue.name), class: "running-count" %>
|
|
23
|
+
<% else %>
|
|
24
|
+
<span class="muted">0</span>
|
|
25
|
+
<% end %>
|
|
26
|
+
</td>
|
|
18
27
|
<td class="num">
|
|
19
28
|
<% failed = @failed_counts[queue.name] || 0 %>
|
|
20
29
|
<% if failed.positive? %>
|
data/config/routes.rb
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
JobBoard::Engine.routes.draw do
|
|
2
4
|
root to: "queues#index"
|
|
3
5
|
|
|
4
6
|
get "queues", to: "queues#index", as: :queues
|
|
5
|
-
scope "queues/:queue_name", constraints: { queue_name:
|
|
7
|
+
scope "queues/:queue_name", constraints: { queue_name: %r{[^/]+} }, format: false do
|
|
6
8
|
post "pause", to: "queues/pauses#create", as: :queue_pause
|
|
7
9
|
delete "pause", to: "queues/pauses#destroy"
|
|
8
10
|
end
|
|
9
11
|
|
|
10
|
-
resources :jobs, only: [
|
|
12
|
+
resources :jobs, only: %i[index show] do
|
|
11
13
|
member do
|
|
12
14
|
post :retry
|
|
13
15
|
delete :discard
|
|
@@ -20,7 +22,7 @@ JobBoard::Engine.routes.draw do
|
|
|
20
22
|
|
|
21
23
|
resources :processes, only: :index
|
|
22
24
|
resources :recurring_tasks, only: :index
|
|
23
|
-
scope "recurring_tasks/:key", constraints: { key:
|
|
25
|
+
scope "recurring_tasks/:key", constraints: { key: %r{[^/]+} }, format: false do
|
|
24
26
|
post "run", to: "recurring_tasks/runs#create", as: :recurring_task_run
|
|
25
27
|
end
|
|
26
28
|
|
data/lib/job_board/engine.rb
CHANGED
data/lib/job_board/version.rb
CHANGED
data/lib/job_board.rb
CHANGED