roundhouse_ui 0.1.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 +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +166 -0
- data/Rakefile +3 -0
- data/app/assets/javascripts/roundhouse_ui/turbo.min.js +35 -0
- data/app/assets/stylesheets/roundhouse_ui/application.css +15 -0
- data/app/controllers/concerns/roundhouse_ui/job_set_browsing.rb +41 -0
- data/app/controllers/roundhouse_ui/application_controller.rb +75 -0
- data/app/controllers/roundhouse_ui/assets_controller.rb +16 -0
- data/app/controllers/roundhouse_ui/audit_controller.rb +7 -0
- data/app/controllers/roundhouse_ui/busy_controller.rb +29 -0
- data/app/controllers/roundhouse_ui/capsules_controller.rb +27 -0
- data/app/controllers/roundhouse_ui/dashboard_controller.rb +26 -0
- data/app/controllers/roundhouse_ui/dead_controller.rb +46 -0
- data/app/controllers/roundhouse_ui/errors_controller.rb +50 -0
- data/app/controllers/roundhouse_ui/jobs_controller.rb +94 -0
- data/app/controllers/roundhouse_ui/metrics_controller.rb +8 -0
- data/app/controllers/roundhouse_ui/queues_controller.rb +40 -0
- data/app/controllers/roundhouse_ui/redis_controller.rb +21 -0
- data/app/controllers/roundhouse_ui/retries_controller.rb +34 -0
- data/app/controllers/roundhouse_ui/scheduled_controller.rb +34 -0
- data/app/controllers/roundhouse_ui/snapshots_controller.rb +26 -0
- data/app/controllers/roundhouse_ui/workers_controller.rb +33 -0
- data/app/helpers/roundhouse_ui/application_helper.rb +4 -0
- data/app/helpers/roundhouse_ui/nav_helper.rb +24 -0
- data/app/helpers/roundhouse_ui/observability_helper.rb +13 -0
- data/app/views/layouts/roundhouse_ui/application.html.erb +365 -0
- data/app/views/roundhouse_ui/audit/index.html.erb +21 -0
- data/app/views/roundhouse_ui/busy/index.html.erb +23 -0
- data/app/views/roundhouse_ui/capsules/index.html.erb +22 -0
- data/app/views/roundhouse_ui/dashboard/show.html.erb +68 -0
- data/app/views/roundhouse_ui/dead/index.html.erb +46 -0
- data/app/views/roundhouse_ui/errors/index.html.erb +28 -0
- data/app/views/roundhouse_ui/jobs/_form.html.erb +24 -0
- data/app/views/roundhouse_ui/jobs/edit.html.erb +2 -0
- data/app/views/roundhouse_ui/jobs/new.html.erb +2 -0
- data/app/views/roundhouse_ui/jobs/show.html.erb +33 -0
- data/app/views/roundhouse_ui/metrics/show.html.erb +49 -0
- data/app/views/roundhouse_ui/queues/index.html.erb +45 -0
- data/app/views/roundhouse_ui/redis/show.html.erb +59 -0
- data/app/views/roundhouse_ui/retries/index.html.erb +33 -0
- data/app/views/roundhouse_ui/scheduled/index.html.erb +29 -0
- data/app/views/roundhouse_ui/shared/_pager.html.erb +15 -0
- data/app/views/roundhouse_ui/snapshots/index.html.erb +25 -0
- data/app/views/roundhouse_ui/workers/index.html.erb +39 -0
- data/config/routes.rb +54 -0
- data/lib/roundhouse_ui/audit.rb +25 -0
- data/lib/roundhouse_ui/cancel_middleware.rb +19 -0
- data/lib/roundhouse_ui/cancellation.rb +37 -0
- data/lib/roundhouse_ui/engine.rb +5 -0
- data/lib/roundhouse_ui/fetch.rb +36 -0
- data/lib/roundhouse_ui/metrics.rb +51 -0
- data/lib/roundhouse_ui/observability.rb +46 -0
- data/lib/roundhouse_ui/pause.rb +59 -0
- data/lib/roundhouse_ui/redaction.rb +33 -0
- data/lib/roundhouse_ui/snapshots.rb +90 -0
- data/lib/roundhouse_ui/version.rb +3 -0
- data/lib/roundhouse_ui.rb +73 -0
- data/lib/tasks/roundhouse_ui_tasks.rake +4 -0
- metadata +131 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
module RoundhouseUi
|
|
2
|
+
# What's executing right now, from Sidekiq::WorkSet — the live in-flight jobs
|
|
3
|
+
# Sidekiq Web calls "Busy". Surfaces long-running (possibly hung) jobs, which
|
|
4
|
+
# the stock UI makes you eyeball.
|
|
5
|
+
class BusyController < ApplicationController
|
|
6
|
+
LONG_RUNNING = 60 # seconds
|
|
7
|
+
|
|
8
|
+
before_action :require_writable!, only: :cancel
|
|
9
|
+
|
|
10
|
+
def index
|
|
11
|
+
@threshold = LONG_RUNNING
|
|
12
|
+
@work = Sidekiq::WorkSet.new.map do |process_id, tid, work|
|
|
13
|
+
{ process: process_id, tid: tid, queue: work.queue, run_at: work.run_at, job: work.job }
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def cancel
|
|
18
|
+
RoundhouseUi::Cancellation.cancel!(params[:jid])
|
|
19
|
+
redirect_to busy_path, notice: "Cancellation requested for #{params[:jid]}."
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def require_writable!
|
|
25
|
+
return unless RoundhouseUi.read_only
|
|
26
|
+
redirect_to busy_path, alert: "Roundhouse is in read-only mode — cancellation is disabled."
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
module RoundhouseUi
|
|
2
|
+
# Sidekiq 7+ capsules — isolated concurrency pools, each with its own queues.
|
|
3
|
+
# Aggregated across the running processes so you can see which capsule serves
|
|
4
|
+
# which queues (and answer "why isn't this queue draining?").
|
|
5
|
+
class CapsulesController < ApplicationController
|
|
6
|
+
def index
|
|
7
|
+
@capsules = aggregate
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
private
|
|
11
|
+
|
|
12
|
+
def aggregate
|
|
13
|
+
capsules = Hash.new { |h, k| h[k] = { name: k, concurrency: 0, processes: 0, queues: {} } }
|
|
14
|
+
|
|
15
|
+
Sidekiq::ProcessSet.new.each do |process|
|
|
16
|
+
(process.capsules || {}).each do |name, data|
|
|
17
|
+
cap = capsules[name]
|
|
18
|
+
cap[:concurrency] += data["concurrency"].to_i
|
|
19
|
+
cap[:processes] += 1
|
|
20
|
+
(data["weights"] || {}).each { |queue, weight| cap[:queues][queue] = weight }
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
capsules.values.sort_by { |c| c[:name] }
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
module RoundhouseUi
|
|
2
|
+
# The dashboard reads straight from Sidekiq's API — no database, no models.
|
|
3
|
+
# Everything here comes out of Redis via Sidekiq::Stats / Sidekiq::Queue.
|
|
4
|
+
class DashboardController < ApplicationController
|
|
5
|
+
def show
|
|
6
|
+
@stats = Sidekiq::Stats.new
|
|
7
|
+
@queues = Sidekiq::Queue.all
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
# Polled by the dashboard for live counts (same approach Sidekiq Web uses —
|
|
11
|
+
# cheap JSON, no WebSocket/build step required).
|
|
12
|
+
def stats
|
|
13
|
+
s = Sidekiq::Stats.new
|
|
14
|
+
render json: {
|
|
15
|
+
processed: s.processed,
|
|
16
|
+
failed: s.failed,
|
|
17
|
+
enqueued: s.enqueued,
|
|
18
|
+
busy: s.workers_size,
|
|
19
|
+
scheduled: s.scheduled_size,
|
|
20
|
+
retries: s.retry_size,
|
|
21
|
+
dead: s.dead_size,
|
|
22
|
+
queues: Sidekiq::Queue.all.size
|
|
23
|
+
}
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
module RoundhouseUi
|
|
2
|
+
class DeadController < ApplicationController
|
|
3
|
+
include JobSetBrowsing
|
|
4
|
+
|
|
5
|
+
before_action :require_writable!, only: %i[requeue destroy bulk]
|
|
6
|
+
|
|
7
|
+
def index
|
|
8
|
+
@query = params[:q].to_s.strip
|
|
9
|
+
@page = [ params[:page].to_i, 1 ].max
|
|
10
|
+
@total = Sidekiq::DeadSet.new.size
|
|
11
|
+
@jobs, @has_next = browse(Sidekiq::DeadSet.new, @query, @page)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def requeue
|
|
15
|
+
entry = Sidekiq::DeadSet.new.find_job(params[:jid])
|
|
16
|
+
entry&.retry
|
|
17
|
+
redirect_to dead_set_path, notice: entry ? "Re-enqueued #{params[:jid]}." : "Job is no longer in the dead set."
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def destroy
|
|
21
|
+
entry = Sidekiq::DeadSet.new.find_job(params[:jid])
|
|
22
|
+
entry&.delete
|
|
23
|
+
redirect_to dead_set_path, notice: entry ? "Deleted #{params[:jid]}." : "Job is no longer in the dead set."
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Act on many at once: retry or delete every selected job in one request.
|
|
27
|
+
def bulk
|
|
28
|
+
set = Sidekiq::DeadSet.new
|
|
29
|
+
count = 0
|
|
30
|
+
Array(params[:jids]).each do |jid|
|
|
31
|
+
entry = set.find_job(jid) or next
|
|
32
|
+
params[:op] == "delete" ? entry.delete : entry.retry
|
|
33
|
+
count += 1
|
|
34
|
+
end
|
|
35
|
+
verb = params[:op] == "delete" ? "Deleted" : "Re-enqueued"
|
|
36
|
+
redirect_to dead_set_path, notice: "#{verb} #{count} job(s)."
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def require_writable!
|
|
42
|
+
return unless RoundhouseUi.read_only
|
|
43
|
+
redirect_to dead_set_path, alert: "Roundhouse is in read-only mode — retry and delete are disabled."
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
module RoundhouseUi
|
|
2
|
+
# Groups failing jobs across the retry + dead sets by a fingerprint of
|
|
3
|
+
# (job class + error class) — so one bad deploy reads as a single issue with
|
|
4
|
+
# a count, not five thousand identical rows. The aggregation Sidekiq Web lacks.
|
|
5
|
+
class ErrorsController < ApplicationController
|
|
6
|
+
SCAN_LIMIT = 1_000 # cap entries scanned per pass; shown honestly in the view
|
|
7
|
+
|
|
8
|
+
def index
|
|
9
|
+
@query = params[:q].to_s.strip
|
|
10
|
+
@scan_limit = SCAN_LIMIT
|
|
11
|
+
@groups, @scanned, @truncated = aggregate
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
def aggregate
|
|
17
|
+
groups = {}
|
|
18
|
+
scanned = 0
|
|
19
|
+
truncated = false
|
|
20
|
+
|
|
21
|
+
{ "retry" => Sidekiq::RetrySet.new, "dead" => Sidekiq::DeadSet.new }.each do |source, set|
|
|
22
|
+
set.each do |entry|
|
|
23
|
+
scanned += 1
|
|
24
|
+
if scanned > SCAN_LIMIT
|
|
25
|
+
truncated = true
|
|
26
|
+
break
|
|
27
|
+
end
|
|
28
|
+
record(groups, source, entry)
|
|
29
|
+
end
|
|
30
|
+
break if truncated
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
list = groups.values.sort_by { |g| -g[:count] }
|
|
34
|
+
list = list.select { |g| "#{g[:klass]} #{g[:error]}".downcase.include?(@query.downcase) } if @query.present?
|
|
35
|
+
[ list, scanned, truncated ]
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def record(groups, source, entry)
|
|
39
|
+
error = entry.item["error_class"] || "UnknownError"
|
|
40
|
+
group = (groups["#{entry.klass}|#{error}"] ||= {
|
|
41
|
+
klass: entry.klass, error: error, count: 0, last_at: nil, queues: [], sources: []
|
|
42
|
+
})
|
|
43
|
+
group[:count] += 1
|
|
44
|
+
group[:queues] |= [ entry.queue ]
|
|
45
|
+
group[:sources] |= [ source ]
|
|
46
|
+
at = entry.at
|
|
47
|
+
group[:last_at] = at if at && (group[:last_at].nil? || at > group[:last_at])
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
module RoundhouseUi
|
|
2
|
+
# Enqueue new jobs and edit/re-enqueue existing ones. Opt-in via
|
|
3
|
+
# RoundhouseUi.allow_job_editing (off by default — a sharp tool).
|
|
4
|
+
#
|
|
5
|
+
# Sidekiq has no in-place edit: a job in a set is keyed by its payload, so an
|
|
6
|
+
# "edit" is delete-the-old + push-the-modified.
|
|
7
|
+
class JobsController < ApplicationController
|
|
8
|
+
SET_BUILDERS = {
|
|
9
|
+
"dead" => -> { Sidekiq::DeadSet.new },
|
|
10
|
+
"retry" => -> { Sidekiq::RetrySet.new },
|
|
11
|
+
"scheduled" => -> { Sidekiq::ScheduledSet.new }
|
|
12
|
+
}.freeze
|
|
13
|
+
REDIRECTS = { "dead" => :dead_set_path, "retry" => :retries_path, "scheduled" => :scheduled_path }.freeze
|
|
14
|
+
|
|
15
|
+
before_action :require_editing_enabled!, except: :show
|
|
16
|
+
|
|
17
|
+
# Read-only inspection — available without allow_job_editing.
|
|
18
|
+
def show
|
|
19
|
+
@set = params[:set]
|
|
20
|
+
@entry = find_entry or return redirect_to(root_path, alert: "Job not found.")
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def new
|
|
24
|
+
@mode = :new
|
|
25
|
+
@job = { "class" => "", "queue" => "default", "args" => "[]" }
|
|
26
|
+
@action_path = jobs_path
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def create
|
|
30
|
+
args = parse_args!(params[:args])
|
|
31
|
+
klass = params[:job_class].to_s.strip
|
|
32
|
+
raise ArgumentError, "Job class is required" if klass.empty?
|
|
33
|
+
|
|
34
|
+
queue = params[:queue].presence || "default"
|
|
35
|
+
Sidekiq::Client.push("class" => klass, "queue" => queue, "args" => args)
|
|
36
|
+
redirect_to queues_path, notice: "Enqueued #{klass} → #{queue}."
|
|
37
|
+
rescue ArgumentError => e
|
|
38
|
+
render_form_error(:new, jobs_path, e)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def edit
|
|
42
|
+
@entry = find_entry or return redirect_to(root_path, alert: "Job not found.")
|
|
43
|
+
@mode = :edit
|
|
44
|
+
@job = {
|
|
45
|
+
"class" => @entry.item["class"],
|
|
46
|
+
"queue" => @entry.queue,
|
|
47
|
+
"args" => JSON.pretty_generate(@entry.item["args"] || [])
|
|
48
|
+
}
|
|
49
|
+
@action_path = job_path(set: params[:set], jid: params[:jid])
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def update
|
|
53
|
+
entry = find_entry or return redirect_to(root_path, alert: "Job not found.")
|
|
54
|
+
args = parse_args!(params[:args])
|
|
55
|
+
klass = params[:job_class].presence || entry.item["class"]
|
|
56
|
+
queue = params[:queue].presence || entry.queue
|
|
57
|
+
|
|
58
|
+
entry.delete
|
|
59
|
+
Sidekiq::Client.push("class" => klass, "queue" => queue, "args" => args)
|
|
60
|
+
redirect_to send(REDIRECTS[params[:set]]), notice: "Edited & re-enqueued #{klass} → #{queue}."
|
|
61
|
+
rescue ArgumentError => e
|
|
62
|
+
@action_path = job_path(set: params[:set], jid: params[:jid])
|
|
63
|
+
render_form_error(:edit, @action_path, e)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
private
|
|
67
|
+
|
|
68
|
+
def find_entry
|
|
69
|
+
builder = SET_BUILDERS[params[:set]] or return nil
|
|
70
|
+
builder.call.find_job(params[:jid])
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def parse_args!(raw)
|
|
74
|
+
parsed = JSON.parse(raw.to_s)
|
|
75
|
+
raise ArgumentError, 'Arguments must be a JSON array, e.g. [123, "abc"]' unless parsed.is_a?(Array)
|
|
76
|
+
parsed
|
|
77
|
+
rescue JSON::ParserError
|
|
78
|
+
raise ArgumentError, "Arguments must be valid JSON"
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def render_form_error(mode, action_path, error)
|
|
82
|
+
flash.now[:alert] = error.message
|
|
83
|
+
@mode = mode
|
|
84
|
+
@action_path = action_path
|
|
85
|
+
@job = { "class" => params[:job_class], "queue" => params[:queue], "args" => params[:args] }
|
|
86
|
+
render mode, status: :unprocessable_entity
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def require_editing_enabled!
|
|
90
|
+
return if RoundhouseUi.allow_job_editing && !RoundhouseUi.read_only
|
|
91
|
+
redirect_to root_path, alert: "Job editing is off — set RoundhouseUi.allow_job_editing = true (and disable read-only)."
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
module RoundhouseUi
|
|
2
|
+
class QueuesController < ApplicationController
|
|
3
|
+
before_action :require_writable!, only: %i[purge pause resume]
|
|
4
|
+
|
|
5
|
+
def index
|
|
6
|
+
@queues = Sidekiq::Queue.all
|
|
7
|
+
@paused = RoundhouseUi::Pause.paused_set
|
|
8
|
+
@fetch_installed = RoundhouseUi::Pause.fetch_installed?
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# Real, OSS-supported destructive action: empties the queue in Redis.
|
|
12
|
+
def purge
|
|
13
|
+
Sidekiq::Queue.new(params[:name]).clear
|
|
14
|
+
redirect_to queues_path, notice: "Purged queue “#{params[:name]}”."
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def pause
|
|
18
|
+
RoundhouseUi::Pause.pause!(params[:name])
|
|
19
|
+
redirect_to queues_path, notice: "Paused “#{params[:name]}”."
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def resume
|
|
23
|
+
RoundhouseUi::Pause.unpause!(params[:name])
|
|
24
|
+
redirect_to queues_path, notice: "Resumed “#{params[:name]}”."
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Non-destructive backup — allowed even in read-only mode.
|
|
28
|
+
def snapshot
|
|
29
|
+
snap = RoundhouseUi::Snapshots.take(params[:name])
|
|
30
|
+
redirect_to queues_path, notice: "Snapshot saved — #{snap[:count]} job(s) from “#{params[:name]}”."
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def require_writable!
|
|
36
|
+
return unless RoundhouseUi.read_only
|
|
37
|
+
redirect_to queues_path, alert: "Roundhouse is in read-only mode — queue actions are disabled."
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
module RoundhouseUi
|
|
2
|
+
# Redis health from INFO — the "is Redis about to silently drop my jobs?" view
|
|
3
|
+
# that Sidekiq Web doesn't have. The headline signal: an eviction policy other
|
|
4
|
+
# than noeviction (with a memory cap) means Sidekiq data can be evicted.
|
|
5
|
+
class RedisController < ApplicationController
|
|
6
|
+
def show
|
|
7
|
+
@info = parse_info(Sidekiq.redis { |conn| conn.call("INFO") })
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
private
|
|
11
|
+
|
|
12
|
+
def parse_info(raw)
|
|
13
|
+
raw.to_s.each_line.each_with_object({}) do |line, info|
|
|
14
|
+
line = line.strip
|
|
15
|
+
next if line.empty? || line.start_with?("#")
|
|
16
|
+
key, value = line.split(":", 2)
|
|
17
|
+
info[key] = value if key && value
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
module RoundhouseUi
|
|
2
|
+
class RetriesController < ApplicationController
|
|
3
|
+
include JobSetBrowsing
|
|
4
|
+
|
|
5
|
+
before_action :require_writable!, only: %i[requeue destroy]
|
|
6
|
+
|
|
7
|
+
def index
|
|
8
|
+
@query = params[:q].to_s.strip
|
|
9
|
+
@page = [ params[:page].to_i, 1 ].max
|
|
10
|
+
@total = Sidekiq::RetrySet.new.size
|
|
11
|
+
@jobs, @has_next = browse(Sidekiq::RetrySet.new, @query, @page)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Retry now — moves the job back to its queue immediately.
|
|
15
|
+
def requeue
|
|
16
|
+
entry = Sidekiq::RetrySet.new.find_job(params[:jid])
|
|
17
|
+
entry&.retry
|
|
18
|
+
redirect_to retries_path, notice: entry ? "Re-enqueued #{params[:jid]}." : "Job is no longer in the retry set."
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def destroy
|
|
22
|
+
entry = Sidekiq::RetrySet.new.find_job(params[:jid])
|
|
23
|
+
entry&.delete
|
|
24
|
+
redirect_to retries_path, notice: entry ? "Deleted #{params[:jid]}." : "Job is no longer in the retry set."
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def require_writable!
|
|
30
|
+
return unless RoundhouseUi.read_only
|
|
31
|
+
redirect_to retries_path, alert: "Roundhouse is in read-only mode — retry and delete are disabled."
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
module RoundhouseUi
|
|
2
|
+
class ScheduledController < ApplicationController
|
|
3
|
+
include JobSetBrowsing
|
|
4
|
+
|
|
5
|
+
before_action :require_writable!, only: %i[enqueue destroy]
|
|
6
|
+
|
|
7
|
+
def index
|
|
8
|
+
@query = params[:q].to_s.strip
|
|
9
|
+
@page = [ params[:page].to_i, 1 ].max
|
|
10
|
+
@total = Sidekiq::ScheduledSet.new.size
|
|
11
|
+
@jobs, @has_next = browse(Sidekiq::ScheduledSet.new, @query, @page)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Enqueue now — pulls the job out of the schedule and onto its queue.
|
|
15
|
+
def enqueue
|
|
16
|
+
entry = Sidekiq::ScheduledSet.new.find_job(params[:jid])
|
|
17
|
+
entry&.add_to_queue
|
|
18
|
+
redirect_to scheduled_path, notice: entry ? "Enqueued #{params[:jid]} now." : "Job is no longer scheduled."
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def destroy
|
|
22
|
+
entry = Sidekiq::ScheduledSet.new.find_job(params[:jid])
|
|
23
|
+
entry&.delete
|
|
24
|
+
redirect_to scheduled_path, notice: entry ? "Deleted #{params[:jid]}." : "Job is no longer scheduled."
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def require_writable!
|
|
30
|
+
return unless RoundhouseUi.read_only
|
|
31
|
+
redirect_to scheduled_path, alert: "Roundhouse is in read-only mode — enqueue and delete are disabled."
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
module RoundhouseUi
|
|
2
|
+
class SnapshotsController < ApplicationController
|
|
3
|
+
before_action :require_writable!, only: %i[restore destroy]
|
|
4
|
+
|
|
5
|
+
def index
|
|
6
|
+
@snapshots = RoundhouseUi::Snapshots.all
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def restore
|
|
10
|
+
count = RoundhouseUi::Snapshots.restore(params[:id])
|
|
11
|
+
redirect_to snapshots_path, notice: "Restored #{count} job(s) to their queue."
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def destroy
|
|
15
|
+
RoundhouseUi::Snapshots.delete(params[:id])
|
|
16
|
+
redirect_to snapshots_path, notice: "Snapshot deleted."
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def require_writable!
|
|
22
|
+
return unless RoundhouseUi.read_only
|
|
23
|
+
redirect_to snapshots_path, alert: "Roundhouse is in read-only mode — restore and delete are disabled."
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
module RoundhouseUi
|
|
2
|
+
# The running Sidekiq process fleet, straight from Sidekiq::ProcessSet.
|
|
3
|
+
# "Quiet" stops a process from pulling new work; "Stop" begins shutdown.
|
|
4
|
+
class WorkersController < ApplicationController
|
|
5
|
+
before_action :require_writable!, only: %i[quiet stop]
|
|
6
|
+
|
|
7
|
+
def index
|
|
8
|
+
@processes = Sidekiq::ProcessSet.new.to_a
|
|
9
|
+
@fetch_active = RoundhouseUi::Pause.fetch_installed?
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def quiet
|
|
13
|
+
find_process(params[:identity])&.quiet!
|
|
14
|
+
redirect_to workers_path, notice: "Sent quiet to #{params[:identity]}."
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def stop
|
|
18
|
+
find_process(params[:identity])&.stop!
|
|
19
|
+
redirect_to workers_path, notice: "Sent stop to #{params[:identity]}."
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def find_process(identity)
|
|
25
|
+
Sidekiq::ProcessSet.new.find { |process| process.identity == identity }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def require_writable!
|
|
29
|
+
return unless RoundhouseUi.read_only
|
|
30
|
+
redirect_to workers_path, alert: "Roundhouse is in read-only mode — quiet and stop are disabled."
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
module RoundhouseUi
|
|
2
|
+
module NavHelper
|
|
3
|
+
# A sidebar nav item with active state and an optional live-updating badge
|
|
4
|
+
# (filled client-side from /stats via the poll in the layout).
|
|
5
|
+
def nav_link(label, path, icon:, badge: nil, badge_class: nil)
|
|
6
|
+
here = request.path == path || (path != root_path && request.path.start_with?(path))
|
|
7
|
+
link_to path, class: "rh-nav#{' is-active' if here}" do
|
|
8
|
+
safe_join([
|
|
9
|
+
content_tag(:span, icon, class: "rh-ico"),
|
|
10
|
+
content_tag(:span, label, class: "rh-lbl"),
|
|
11
|
+
badge ? content_tag(:span, "", class: "rh-badge #{badge_class}", data: { nav: badge }) : "".html_safe
|
|
12
|
+
])
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Health label for a queue from its latency (oldest-job age, in seconds).
|
|
17
|
+
def queue_state(latency, paused: false)
|
|
18
|
+
return [ "Paused", "rh-st-paused" ] if paused
|
|
19
|
+
return [ "Stuck", "rh-st-crit" ] if latency > 600
|
|
20
|
+
return [ "At risk", "rh-st-warn" ] if latency > 60
|
|
21
|
+
[ "Healthy", "rh-st-ok" ]
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
module RoundhouseUi
|
|
2
|
+
module ObservabilityHelper
|
|
3
|
+
# Renders a deep-link to the configured observability tool for a job, or
|
|
4
|
+
# nothing when no adapter is configured (the default).
|
|
5
|
+
def trace_link(klass:, jid:, queue: nil)
|
|
6
|
+
adapter = RoundhouseUi.observability
|
|
7
|
+
url = adapter.job_url(klass: klass, jid: jid, queue: queue)
|
|
8
|
+
return unless url
|
|
9
|
+
|
|
10
|
+
link_to "↗ #{adapter.label}", url, target: "_blank", rel: "noopener", class: "rh-trace"
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|