solid_queue_web 0.8.0 → 1.0.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/README.md +106 -17
- data/Rakefile +3 -1
- data/app/assets/stylesheets/solid_queue_web/_04_table.css +8 -1
- data/app/assets/stylesheets/solid_queue_web/_05_badges.css +1 -0
- data/app/assets/stylesheets/solid_queue_web/_07_forms.css +17 -0
- data/app/assets/stylesheets/solid_queue_web/_11_throughput.css +30 -1
- data/app/controllers/solid_queue_web/application_controller.rb +19 -1
- data/app/controllers/solid_queue_web/blocked_jobs_controller.rb +11 -0
- data/app/controllers/solid_queue_web/dashboard_controller.rb +2 -38
- data/app/controllers/solid_queue_web/jobs_controller.rb +16 -27
- data/app/controllers/solid_queue_web/metrics_controller.rb +7 -0
- data/app/controllers/solid_queue_web/performance_controller.rb +12 -0
- data/app/controllers/solid_queue_web/queues/jobs_controller.rb +15 -19
- data/app/controllers/solid_queue_web/queues/pauses_controller.rb +21 -0
- data/app/controllers/solid_queue_web/queues_controller.rb +5 -31
- data/app/controllers/solid_queue_web/recurring_tasks/runs_controller.rb +18 -0
- data/app/controllers/solid_queue_web/retry_failed_jobs_controller.rb +23 -2
- data/app/controllers/solid_queue_web/scheduled_jobs_controller.rb +54 -0
- data/app/models/solid_queue_web/job.rb +17 -1
- data/app/services/solid_queue_web/alert_webhook.rb +58 -0
- data/app/services/solid_queue_web/dashboard_stats.rb +47 -0
- data/app/services/solid_queue_web/job_performance_stats.rb +38 -0
- data/app/services/solid_queue_web/metrics_payload.rb +66 -0
- data/app/services/solid_queue_web/queue_stats.rb +52 -0
- data/app/views/layouts/solid_queue_web/application.html.erb +1 -0
- data/app/views/solid_queue_web/dashboard/index.html.erb +68 -24
- data/app/views/solid_queue_web/failed_jobs/index.html.erb +11 -1
- data/app/views/solid_queue_web/history/index.html.erb +1 -1
- data/app/views/solid_queue_web/jobs/index.html.erb +57 -15
- data/app/views/solid_queue_web/performance/index.html.erb +50 -0
- data/app/views/solid_queue_web/queues/index.html.erb +19 -2
- data/app/views/solid_queue_web/queues/jobs/index.html.erb +1 -1
- data/app/views/solid_queue_web/recurring_tasks/index.html.erb +7 -0
- data/app/views/solid_queue_web/scheduled_jobs/update.turbo_stream.erb +9 -0
- data/app/views/solid_queue_web/search/index.html.erb +1 -1
- data/config/routes.rb +16 -10
- data/lib/solid_queue_web/version.rb +1 -1
- data/lib/solid_queue_web.rb +23 -1
- metadata +14 -1
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
module SolidQueueWeb
|
|
2
|
+
class ScheduledJobsController < ApplicationController
|
|
3
|
+
def create
|
|
4
|
+
@period = params[:period].presence_in(PERIOD_DURATIONS.keys)
|
|
5
|
+
job_ids = scheduled_scope.pluck("solid_queue_jobs.id")
|
|
6
|
+
|
|
7
|
+
SolidQueue::ScheduledExecution.where(job_id: job_ids).update_all(scheduled_at: 1.second.ago)
|
|
8
|
+
SolidQueue::Job.where(id: job_ids).update_all(scheduled_at: 1.second.ago)
|
|
9
|
+
|
|
10
|
+
redirect_to jobs_path(status: "scheduled", period: @period),
|
|
11
|
+
notice: "#{job_ids.size} #{"job".pluralize(job_ids.size)} scheduled to run immediately."
|
|
12
|
+
rescue => e
|
|
13
|
+
redirect_to jobs_path(status: "scheduled", period: @period),
|
|
14
|
+
alert: "Could not run jobs: #{e.message}"
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def update
|
|
18
|
+
@execution = SolidQueue::ScheduledExecution.find(params[:id])
|
|
19
|
+
@period = params[:period].presence_in(PERIOD_DURATIONS.keys)
|
|
20
|
+
@run_now = params[:offset] == "now"
|
|
21
|
+
new_time = resolve_new_time(@execution, params[:offset])
|
|
22
|
+
|
|
23
|
+
@execution.update!(scheduled_at: new_time)
|
|
24
|
+
@execution.job.update!(scheduled_at: new_time)
|
|
25
|
+
|
|
26
|
+
respond_to do |format|
|
|
27
|
+
format.turbo_stream
|
|
28
|
+
format.html do
|
|
29
|
+
notice = @run_now ? "Job scheduled to run immediately." : "Job rescheduled by +#{params[:offset]}."
|
|
30
|
+
redirect_to jobs_path(status: "scheduled", period: @period), notice: notice
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
rescue ArgumentError => e
|
|
34
|
+
redirect_to jobs_path(status: "scheduled"), alert: e.message
|
|
35
|
+
rescue => e
|
|
36
|
+
redirect_to jobs_path(status: "scheduled"), alert: "Could not reschedule job: #{e.message}"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def scheduled_scope
|
|
42
|
+
scope = SolidQueue::ScheduledExecution.joins(:job)
|
|
43
|
+
scope = scope.where("solid_queue_jobs.created_at >= ?", PERIOD_DURATIONS[@period].ago) if @period.present?
|
|
44
|
+
scope
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def resolve_new_time(execution, offset)
|
|
48
|
+
return 1.second.ago if offset == "now"
|
|
49
|
+
raise ArgumentError, "Invalid offset." unless PERIOD_DURATIONS.key?(offset)
|
|
50
|
+
|
|
51
|
+
execution.scheduled_at + PERIOD_DURATIONS[offset]
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
module SolidQueueWeb
|
|
2
2
|
class Job
|
|
3
|
-
STATUSES
|
|
3
|
+
STATUSES = %w[ready scheduled claimed blocked failed].freeze
|
|
4
4
|
DISCARDABLE = %w[ready scheduled blocked].freeze
|
|
5
5
|
EXECUTION_MODELS = {
|
|
6
6
|
"ready" => SolidQueue::ReadyExecution,
|
|
@@ -9,5 +9,21 @@ module SolidQueueWeb
|
|
|
9
9
|
"blocked" => SolidQueue::BlockedExecution,
|
|
10
10
|
"failed" => SolidQueue::FailedExecution
|
|
11
11
|
}.freeze
|
|
12
|
+
|
|
13
|
+
def self.execution_model_for!(status)
|
|
14
|
+
raise ArgumentError, "Cannot discard #{status} jobs from this page." unless DISCARDABLE.include?(status)
|
|
15
|
+
|
|
16
|
+
EXECUTION_MODELS[status]
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def self.derive_status(job)
|
|
20
|
+
return "failed" if job.failed_execution.present?
|
|
21
|
+
return "claimed" if job.claimed_execution.present?
|
|
22
|
+
return "blocked" if job.blocked_execution.present?
|
|
23
|
+
return "ready" if job.ready_execution.present?
|
|
24
|
+
return "scheduled" if job.scheduled_execution.present?
|
|
25
|
+
|
|
26
|
+
"finished"
|
|
27
|
+
end
|
|
12
28
|
end
|
|
13
29
|
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
require "net/http"
|
|
2
|
+
require "json"
|
|
3
|
+
require "uri"
|
|
4
|
+
|
|
5
|
+
module SolidQueueWeb
|
|
6
|
+
class AlertWebhook
|
|
7
|
+
MUTEX = Mutex.new
|
|
8
|
+
|
|
9
|
+
class << self
|
|
10
|
+
def call(failure_count:)
|
|
11
|
+
return unless configured?
|
|
12
|
+
return if failure_count < SolidQueueWeb.alert_failure_threshold
|
|
13
|
+
return unless should_fire?
|
|
14
|
+
|
|
15
|
+
Thread.new { post(SolidQueueWeb.alert_webhook_url, failure_count) }
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def reset!
|
|
19
|
+
MUTEX.synchronize { @last_fired_at = nil }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def configured?
|
|
25
|
+
SolidQueueWeb.alert_webhook_url.present? && SolidQueueWeb.alert_failure_threshold.present?
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def should_fire?
|
|
29
|
+
MUTEX.synchronize do
|
|
30
|
+
cooldown = SolidQueueWeb.alert_webhook_cooldown
|
|
31
|
+
return false if @last_fired_at && Time.current - @last_fired_at < cooldown
|
|
32
|
+
|
|
33
|
+
@last_fired_at = Time.current
|
|
34
|
+
true
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def post(url_string, failure_count)
|
|
39
|
+
uri = URI.parse(url_string)
|
|
40
|
+
payload = JSON.generate(
|
|
41
|
+
event: "failure_threshold_exceeded",
|
|
42
|
+
failure_count: failure_count,
|
|
43
|
+
threshold: SolidQueueWeb.alert_failure_threshold,
|
|
44
|
+
fired_at: Time.current.iso8601
|
|
45
|
+
)
|
|
46
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
47
|
+
http.use_ssl = uri.scheme == "https"
|
|
48
|
+
http.open_timeout = 5
|
|
49
|
+
http.read_timeout = 10
|
|
50
|
+
request = Net::HTTP::Post.new(uri.path.presence || "/", "Content-Type" => "application/json")
|
|
51
|
+
request.body = payload
|
|
52
|
+
http.request(request)
|
|
53
|
+
rescue => e
|
|
54
|
+
Rails.logger.error("[SolidQueueWeb] Alert webhook failed: #{e.message}")
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
module SolidQueueWeb
|
|
2
|
+
class DashboardStats
|
|
3
|
+
attr_reader :counts, :throughput, :sparkline, :depth_sparkline, :slow_jobs_count
|
|
4
|
+
|
|
5
|
+
def initialize
|
|
6
|
+
@now = Time.current
|
|
7
|
+
compute
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
private
|
|
11
|
+
|
|
12
|
+
def compute
|
|
13
|
+
@counts = {
|
|
14
|
+
ready: SolidQueue::ReadyExecution.count,
|
|
15
|
+
scheduled: SolidQueue::ScheduledExecution.count,
|
|
16
|
+
claimed: SolidQueue::ClaimedExecution.count,
|
|
17
|
+
failed: SolidQueue::FailedExecution.count,
|
|
18
|
+
blocked: SolidQueue::BlockedExecution.count,
|
|
19
|
+
queues: SolidQueue::Job.select(:queue_name).distinct.count,
|
|
20
|
+
processes: SolidQueue::Process.count,
|
|
21
|
+
recurring: SolidQueue::RecurringTask.count
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
finished_times = SolidQueue::Job.where(finished_at: 24.hours.ago..@now).pluck(:finished_at)
|
|
25
|
+
@throughput = {
|
|
26
|
+
completed_1h: finished_times.count { |t| t >= 1.hour.ago },
|
|
27
|
+
completed_24h: finished_times.size
|
|
28
|
+
}
|
|
29
|
+
@sparkline = 12.times.map do |i|
|
|
30
|
+
from = (12 - i).hours.ago
|
|
31
|
+
to = i == 11 ? @now : (11 - i).hours.ago
|
|
32
|
+
finished_times.count { |t| t >= from && t < to }
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
threshold = SolidQueueWeb.slow_job_threshold
|
|
36
|
+
@slow_jobs_count = threshold ? SolidQueue::ClaimedExecution.where("created_at <= ?", threshold.ago).count : 0
|
|
37
|
+
|
|
38
|
+
job_timestamps = SolidQueue::Job
|
|
39
|
+
.where("created_at >= ? OR finished_at IS NULL", 72.hours.ago)
|
|
40
|
+
.pluck(:created_at, :finished_at)
|
|
41
|
+
@depth_sparkline = 12.times.map do |i|
|
|
42
|
+
t = i == 11 ? @now : (12 - i).hours.ago
|
|
43
|
+
job_timestamps.count { |created, finished| created <= t && (finished.nil? || finished > t) }
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
module SolidQueueWeb
|
|
2
|
+
class JobPerformanceStats
|
|
3
|
+
Row = Struct.new(:class_name, :count, :avg, :p50, :p95, :min, :max, keyword_init: true)
|
|
4
|
+
|
|
5
|
+
def initialize(scope)
|
|
6
|
+
@scope = scope
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def rows
|
|
10
|
+
grouped = @scope.pluck(:class_name, :created_at, :finished_at)
|
|
11
|
+
.group_by(&:first)
|
|
12
|
+
|
|
13
|
+
grouped.map do |class_name, records|
|
|
14
|
+
durations = records.map { |_, created, finished| (finished - created).to_f }.sort
|
|
15
|
+
Row.new(
|
|
16
|
+
class_name: class_name,
|
|
17
|
+
count: durations.size,
|
|
18
|
+
avg: mean(durations),
|
|
19
|
+
p50: percentile(durations, 50),
|
|
20
|
+
p95: percentile(durations, 95),
|
|
21
|
+
min: durations.first,
|
|
22
|
+
max: durations.last
|
|
23
|
+
)
|
|
24
|
+
end.sort_by { |r| -r.p95 }
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def mean(sorted)
|
|
30
|
+
sorted.sum / sorted.size
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def percentile(sorted, pct)
|
|
34
|
+
idx = [(pct / 100.0 * sorted.size).ceil - 1, 0].max
|
|
35
|
+
sorted[idx]
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
module SolidQueueWeb
|
|
2
|
+
class MetricsPayload
|
|
3
|
+
def initialize
|
|
4
|
+
@now = Time.current
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
def to_h
|
|
8
|
+
payload = {
|
|
9
|
+
generated_at: @now.iso8601,
|
|
10
|
+
jobs: job_counts,
|
|
11
|
+
throughput: throughput,
|
|
12
|
+
queues: queue_list,
|
|
13
|
+
processes: process_summary
|
|
14
|
+
}
|
|
15
|
+
slow = slow_jobs_count
|
|
16
|
+
payload[:slow_jobs] = slow unless slow.nil?
|
|
17
|
+
payload
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def job_counts
|
|
23
|
+
{
|
|
24
|
+
ready: SolidQueue::ReadyExecution.count,
|
|
25
|
+
scheduled: SolidQueue::ScheduledExecution.count,
|
|
26
|
+
claimed: SolidQueue::ClaimedExecution.count,
|
|
27
|
+
blocked: SolidQueue::BlockedExecution.count,
|
|
28
|
+
failed: SolidQueue::FailedExecution.count
|
|
29
|
+
}
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def throughput
|
|
33
|
+
finished_times = SolidQueue::Job.where(finished_at: 24.hours.ago..@now).pluck(:finished_at)
|
|
34
|
+
{
|
|
35
|
+
completed_1h: finished_times.count { |t| t >= 1.hour.ago },
|
|
36
|
+
completed_24h: finished_times.size
|
|
37
|
+
}
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def queue_list
|
|
41
|
+
depths = SolidQueue::ReadyExecution
|
|
42
|
+
.joins(:job)
|
|
43
|
+
.group("solid_queue_jobs.queue_name")
|
|
44
|
+
.count
|
|
45
|
+
SolidQueue::Queue.all.sort_by(&:name).map do |q|
|
|
46
|
+
{ name: q.name, depth: depths[q.name] || 0, paused: q.paused? }
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def process_summary
|
|
51
|
+
processes = SolidQueue::Process.all.to_a
|
|
52
|
+
threshold = SolidQueue.process_alive_threshold.ago
|
|
53
|
+
{
|
|
54
|
+
total: processes.size,
|
|
55
|
+
healthy: processes.count { |p| p.last_heartbeat_at >= threshold },
|
|
56
|
+
stale: processes.count { |p| p.last_heartbeat_at < threshold },
|
|
57
|
+
by_kind: processes.group_by(&:kind).transform_values(&:size)
|
|
58
|
+
}
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def slow_jobs_count
|
|
62
|
+
threshold = SolidQueueWeb.slow_job_threshold
|
|
63
|
+
SolidQueue::ClaimedExecution.where("created_at <= ?", threshold.ago).count if threshold
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
module SolidQueueWeb
|
|
2
|
+
class QueueStats
|
|
3
|
+
attr_reader :completed_24h, :failed_24h, :oldest_ready, :failure_sparklines
|
|
4
|
+
|
|
5
|
+
def initialize(queues)
|
|
6
|
+
@queues = queues
|
|
7
|
+
@now = Time.current
|
|
8
|
+
compute
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
def compute
|
|
14
|
+
@completed_24h = SolidQueue::Job
|
|
15
|
+
.where(finished_at: 24.hours.ago..@now)
|
|
16
|
+
.group(:queue_name)
|
|
17
|
+
.count
|
|
18
|
+
|
|
19
|
+
@failed_24h = SolidQueue::FailedExecution
|
|
20
|
+
.joins(:job)
|
|
21
|
+
.where(created_at: 24.hours.ago..@now)
|
|
22
|
+
.group("solid_queue_jobs.queue_name")
|
|
23
|
+
.count
|
|
24
|
+
|
|
25
|
+
@oldest_ready = SolidQueue::ReadyExecution
|
|
26
|
+
.joins(:job)
|
|
27
|
+
.group("solid_queue_jobs.queue_name")
|
|
28
|
+
.minimum("solid_queue_jobs.created_at")
|
|
29
|
+
|
|
30
|
+
failed_raw = SolidQueue::FailedExecution
|
|
31
|
+
.joins(:job)
|
|
32
|
+
.where(created_at: 12.hours.ago..@now)
|
|
33
|
+
.pluck("solid_queue_jobs.queue_name", "solid_queue_failed_executions.created_at")
|
|
34
|
+
done_raw = SolidQueue::Job
|
|
35
|
+
.where(finished_at: 12.hours.ago..@now)
|
|
36
|
+
.pluck(:queue_name, :finished_at)
|
|
37
|
+
|
|
38
|
+
@failure_sparklines = @queues.each_with_object({}) do |queue, h|
|
|
39
|
+
failed_times = failed_raw.filter_map { |q, t| t if q == queue.name }
|
|
40
|
+
done_times = done_raw.filter_map { |q, t| t if q == queue.name }
|
|
41
|
+
h[queue.name] = 12.times.map do |i|
|
|
42
|
+
from = (12 - i).hours.ago
|
|
43
|
+
to = i == 11 ? @now : (11 - i).hours.ago
|
|
44
|
+
f = failed_times.count { |t| t >= from && t < to }
|
|
45
|
+
d = done_times.count { |t| t >= from && t < to }
|
|
46
|
+
total = f + d
|
|
47
|
+
total > 0 ? (f.to_f / total * 100).round : nil
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
<li><%= link_to "Queues", queues_path, class: current_page?(queues_path) ? "active" : "", aria: { current: current_page?(queues_path) ? "page" : nil } %></li>
|
|
22
22
|
<li><%= link_to "Jobs", jobs_path, class: current_page?(jobs_path) ? "active" : "", aria: { current: current_page?(jobs_path) ? "page" : nil } %></li>
|
|
23
23
|
<li><%= link_to "History", history_path, class: current_page?(history_path) ? "active" : "", aria: { current: current_page?(history_path) ? "page" : nil } %></li>
|
|
24
|
+
<li><%= link_to "Performance", performance_path, class: current_page?(performance_path) ? "active" : "", aria: { current: current_page?(performance_path) ? "page" : nil } %></li>
|
|
24
25
|
<li><%= link_to "Failed", failed_jobs_path, class: current_page?(failed_jobs_path) ? "active" : "", aria: { current: current_page?(failed_jobs_path) ? "page" : nil } %></li>
|
|
25
26
|
<li><%= link_to "Recurring", recurring_tasks_path, class: current_page?(recurring_tasks_path) ? "active" : "", aria: { current: current_page?(recurring_tasks_path) ? "page" : nil } %></li>
|
|
26
27
|
<li><%= link_to "Processes", processes_path, class: current_page?(processes_path) ? "active" : "", aria: { current: current_page?(processes_path) ? "page" : nil } %></li>
|
|
@@ -3,61 +3,61 @@
|
|
|
3
3
|
|
|
4
4
|
<div class="sqd-stats">
|
|
5
5
|
<%= link_to jobs_path(status: "ready"), class: "sqd-stat sqd-stat--ready sqd-stat--link" do %>
|
|
6
|
-
<div class="sqd-stat__value"><%= @stats[:ready] %></div>
|
|
6
|
+
<div class="sqd-stat__value"><%= @stats.counts[:ready] %></div>
|
|
7
7
|
<div class="sqd-stat__label">Ready</div>
|
|
8
8
|
<% end %>
|
|
9
9
|
<%= link_to jobs_path(status: "scheduled"), class: "sqd-stat sqd-stat--scheduled sqd-stat--link" do %>
|
|
10
|
-
<div class="sqd-stat__value"><%= @stats[:scheduled] %></div>
|
|
10
|
+
<div class="sqd-stat__value"><%= @stats.counts[:scheduled] %></div>
|
|
11
11
|
<div class="sqd-stat__label">Scheduled</div>
|
|
12
12
|
<% end %>
|
|
13
13
|
<%= link_to jobs_path(status: "claimed"), class: "sqd-stat sqd-stat--claimed sqd-stat--link" do %>
|
|
14
|
-
<div class="sqd-stat__value"><%= @stats[:claimed] %></div>
|
|
14
|
+
<div class="sqd-stat__value"><%= @stats.counts[:claimed] %></div>
|
|
15
15
|
<div class="sqd-stat__label">Running</div>
|
|
16
16
|
<% end %>
|
|
17
17
|
<%= link_to jobs_path(status: "blocked"), class: "sqd-stat sqd-stat--blocked sqd-stat--link" do %>
|
|
18
|
-
<div class="sqd-stat__value"><%= @stats[:blocked] %></div>
|
|
18
|
+
<div class="sqd-stat__value"><%= @stats.counts[:blocked] %></div>
|
|
19
19
|
<div class="sqd-stat__label">Blocked</div>
|
|
20
20
|
<% end %>
|
|
21
21
|
<%= link_to failed_jobs_path, class: "sqd-stat sqd-stat--failed sqd-stat--link" do %>
|
|
22
|
-
<div class="sqd-stat__value"><%= @stats[:failed] %></div>
|
|
22
|
+
<div class="sqd-stat__value"><%= @stats.counts[:failed] %></div>
|
|
23
23
|
<div class="sqd-stat__label">Failed</div>
|
|
24
24
|
<% end %>
|
|
25
25
|
<%= link_to queues_path, class: "sqd-stat sqd-stat--queues sqd-stat--link" do %>
|
|
26
|
-
<div class="sqd-stat__value"><%= @stats[:queues] %></div>
|
|
26
|
+
<div class="sqd-stat__value"><%= @stats.counts[:queues] %></div>
|
|
27
27
|
<div class="sqd-stat__label">Queues</div>
|
|
28
28
|
<% end %>
|
|
29
29
|
<%= link_to recurring_tasks_path, class: "sqd-stat sqd-stat--recurring sqd-stat--link" do %>
|
|
30
|
-
<div class="sqd-stat__value"><%= @stats[:recurring] %></div>
|
|
30
|
+
<div class="sqd-stat__value"><%= @stats.counts[:recurring] %></div>
|
|
31
31
|
<div class="sqd-stat__label">Recurring</div>
|
|
32
32
|
<% end %>
|
|
33
33
|
<%= link_to processes_path, class: "sqd-stat sqd-stat--processes sqd-stat--link" do %>
|
|
34
|
-
<div class="sqd-stat__value"><%= @stats[:processes] %></div>
|
|
34
|
+
<div class="sqd-stat__value"><%= @stats.counts[:processes] %></div>
|
|
35
35
|
<div class="sqd-stat__label">Processes</div>
|
|
36
36
|
<% end %>
|
|
37
37
|
<%= link_to history_path(period: "1h"), class: "sqd-stat sqd-stat--done sqd-stat--link" do %>
|
|
38
|
-
<div class="sqd-stat__value"><%= @throughput[:completed_1h] %></div>
|
|
38
|
+
<div class="sqd-stat__value"><%= @stats.throughput[:completed_1h] %></div>
|
|
39
39
|
<div class="sqd-stat__label">Done (1h)</div>
|
|
40
40
|
<% end %>
|
|
41
41
|
<%= link_to history_path(period: "24h"), class: "sqd-stat sqd-stat--done sqd-stat--link" do %>
|
|
42
|
-
<div class="sqd-stat__value"><%= @throughput[:completed_24h] %></div>
|
|
42
|
+
<div class="sqd-stat__value"><%= @stats.throughput[:completed_24h] %></div>
|
|
43
43
|
<div class="sqd-stat__label">Done (24h)</div>
|
|
44
44
|
<% end %>
|
|
45
45
|
</div>
|
|
46
46
|
|
|
47
|
-
<% max_val = [@sparkline.max, 1].max %>
|
|
47
|
+
<% max_val = [@stats.sparkline.max, 1].max %>
|
|
48
48
|
<div class="sqd-card" style="margin-bottom: 1rem;">
|
|
49
49
|
<div class="sqd-card__header">
|
|
50
50
|
<span class="sqd-card__title">Throughput — Last 12 Hours</span>
|
|
51
51
|
<div class="sqd-throughput__summary">
|
|
52
|
-
<span>1h: <strong><%= @throughput[:completed_1h] %></strong></span>
|
|
53
|
-
<span>24h: <strong><%= @throughput[:completed_24h] %></strong></span>
|
|
52
|
+
<span>1h: <strong><%= @stats.throughput[:completed_1h] %></strong></span>
|
|
53
|
+
<span>24h: <strong><%= @stats.throughput[:completed_24h] %></strong></span>
|
|
54
54
|
</div>
|
|
55
55
|
</div>
|
|
56
|
-
<% if @throughput[:completed_24h] == 0 %>
|
|
56
|
+
<% if @stats.throughput[:completed_24h] == 0 %>
|
|
57
57
|
<div class="sqd-sparkline__empty">No completed jobs in the last 24 hours</div>
|
|
58
58
|
<% else %>
|
|
59
59
|
<div class="sqd-sparkline" aria-label="Jobs completed per hour over the last 12 hours">
|
|
60
|
-
<% @sparkline.each_with_index do |count, i| %>
|
|
60
|
+
<% @stats.sparkline.each_with_index do |count, i| %>
|
|
61
61
|
<% pct = (count.to_f / max_val * 100).round %>
|
|
62
62
|
<% hour_start = (12 - i).hours.ago %>
|
|
63
63
|
<% show_tick = [0, 3, 6, 9, 11].include?(i) %>
|
|
@@ -74,6 +74,36 @@
|
|
|
74
74
|
<% end %>
|
|
75
75
|
</div>
|
|
76
76
|
|
|
77
|
+
<% current_depth = @stats.counts[:ready] + @stats.counts[:scheduled] + @stats.counts[:claimed] + @stats.counts[:blocked] + @stats.counts[:failed] %>
|
|
78
|
+
<% max_depth = [@stats.depth_sparkline.max, 1].max %>
|
|
79
|
+
<div class="sqd-card" style="margin-bottom: 1rem;">
|
|
80
|
+
<div class="sqd-card__header">
|
|
81
|
+
<span class="sqd-card__title">Queue Depth — Last 12 Hours</span>
|
|
82
|
+
<div class="sqd-throughput__summary">
|
|
83
|
+
<span>Now: <strong><%= current_depth %></strong></span>
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
<% if @stats.depth_sparkline.all?(&:zero?) %>
|
|
87
|
+
<div class="sqd-sparkline__empty">No active jobs in the last 12 hours</div>
|
|
88
|
+
<% else %>
|
|
89
|
+
<div class="sqd-sparkline" aria-label="Queue depth over the last 12 hours">
|
|
90
|
+
<% @stats.depth_sparkline.each_with_index do |depth, i| %>
|
|
91
|
+
<% pct = (depth.to_f / max_depth * 100).round %>
|
|
92
|
+
<% t = i == 11 ? Time.current : (12 - i).hours.ago %>
|
|
93
|
+
<% show_tick = [0, 3, 6, 9, 11].include?(i) %>
|
|
94
|
+
<div class="sqd-sparkline__col">
|
|
95
|
+
<div class="sqd-sparkline__bar-wrap">
|
|
96
|
+
<div class="sqd-sparkline__bar sqd-sparkline__bar--depth"
|
|
97
|
+
style="height: <%= [pct, 3].max %>%"
|
|
98
|
+
title="<%= i == 11 ? "now" : t.strftime("%-I%p").downcase %>: <%= depth %> <%= "job".pluralize(depth) %> in queue"></div>
|
|
99
|
+
</div>
|
|
100
|
+
<div class="sqd-sparkline__tick"><%= show_tick ? (i == 11 ? "now" : t.strftime("%-I%p").downcase) : "" %></div>
|
|
101
|
+
</div>
|
|
102
|
+
<% end %>
|
|
103
|
+
</div>
|
|
104
|
+
<% end %>
|
|
105
|
+
</div>
|
|
106
|
+
|
|
77
107
|
<div style="display:grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 1rem;">
|
|
78
108
|
<div class="sqd-card">
|
|
79
109
|
<div class="sqd-card__header">
|
|
@@ -88,37 +118,51 @@
|
|
|
88
118
|
</div>
|
|
89
119
|
</div>
|
|
90
120
|
|
|
91
|
-
<% if @stats[:failed] > 0 %>
|
|
121
|
+
<% if @stats.counts[:failed] > 0 %>
|
|
92
122
|
<div class="sqd-card">
|
|
93
123
|
<div class="sqd-card__header">
|
|
94
124
|
<span class="sqd-card__title">Failed Jobs</span>
|
|
95
125
|
</div>
|
|
96
126
|
<div style="padding: 1rem; display: flex; flex-direction: column; gap: 0.5rem;">
|
|
97
127
|
<p style="color: var(--danger); font-size: 13px;">
|
|
98
|
-
<%= pluralize(@stats[:failed], "failed job") %> need attention.
|
|
128
|
+
<%= pluralize(@stats.counts[:failed], "failed job") %> need attention.
|
|
99
129
|
</p>
|
|
100
|
-
<%= button_to "Retry All Failed",
|
|
130
|
+
<%= button_to "Retry All Failed", retry_all_failed_jobs_path,
|
|
101
131
|
method: :post,
|
|
102
132
|
class: "sqd-btn sqd-btn--primary",
|
|
103
|
-
data: { confirm: "Retry all #{@stats[:failed]} failed #{"job".pluralize(@stats[:failed])}?" } %>
|
|
133
|
+
data: { confirm: "Retry all #{@stats.counts[:failed]} failed #{"job".pluralize(@stats.counts[:failed])}?" } %>
|
|
104
134
|
<%= link_to "Review →", failed_jobs_path, class: "sqd-btn sqd-btn--muted" %>
|
|
105
135
|
</div>
|
|
106
136
|
</div>
|
|
107
137
|
<% end %>
|
|
108
138
|
|
|
109
|
-
<% if @stats
|
|
139
|
+
<% if SolidQueueWeb.slow_job_threshold && @stats.slow_jobs_count > 0 %>
|
|
140
|
+
<div class="sqd-card">
|
|
141
|
+
<div class="sqd-card__header">
|
|
142
|
+
<span class="sqd-card__title">Slow Jobs</span>
|
|
143
|
+
</div>
|
|
144
|
+
<div style="padding: 1rem; display: flex; flex-direction: column; gap: 0.5rem;">
|
|
145
|
+
<p style="color: var(--warning); font-size: 13px;">
|
|
146
|
+
<%= pluralize(@stats.slow_jobs_count, "job") %> running longer than <%= distance_of_time_in_words(SolidQueueWeb.slow_job_threshold) %>.
|
|
147
|
+
</p>
|
|
148
|
+
<%= link_to "Review →", jobs_path(status: "claimed"), class: "sqd-btn sqd-btn--muted" %>
|
|
149
|
+
</div>
|
|
150
|
+
</div>
|
|
151
|
+
<% end %>
|
|
152
|
+
|
|
153
|
+
<% if @stats.counts[:blocked] > 0 %>
|
|
110
154
|
<div class="sqd-card">
|
|
111
155
|
<div class="sqd-card__header">
|
|
112
156
|
<span class="sqd-card__title">Blocked Jobs</span>
|
|
113
157
|
</div>
|
|
114
158
|
<div style="padding: 1rem; display: flex; flex-direction: column; gap: 0.5rem;">
|
|
115
159
|
<p style="color: var(--warning); font-size: 13px;">
|
|
116
|
-
<%= pluralize(@stats[:blocked], "blocked job") %>.
|
|
160
|
+
<%= pluralize(@stats.counts[:blocked], "blocked job") %>.
|
|
117
161
|
</p>
|
|
118
|
-
<%= button_to "Discard All Blocked",
|
|
119
|
-
method: :
|
|
162
|
+
<%= button_to "Discard All Blocked", blocked_jobs_path,
|
|
163
|
+
method: :delete,
|
|
120
164
|
class: "sqd-btn sqd-btn--danger",
|
|
121
|
-
data: { confirm: "Discard all #{@stats[:blocked]} blocked #{"job".pluralize(@stats[:blocked])}? This cannot be undone." } %>
|
|
165
|
+
data: { confirm: "Discard all #{@stats.counts[:blocked]} blocked #{"job".pluralize(@stats.counts[:blocked])}? This cannot be undone." } %>
|
|
122
166
|
<%= link_to "Review →", jobs_path(status: "blocked"), class: "sqd-btn sqd-btn--muted" %>
|
|
123
167
|
</div>
|
|
124
168
|
</div>
|
|
@@ -9,6 +9,16 @@
|
|
|
9
9
|
params: { queue: @queue, q: @search, period: @period },
|
|
10
10
|
class: "sqd-btn sqd-btn--primary",
|
|
11
11
|
data: { confirm: "Retry all #{@failed_jobs.size} failed jobs?" } %>
|
|
12
|
+
<% if @failed_jobs.size > 1 %>
|
|
13
|
+
<% %w[5s 10s 30s 1m].each do |interval| %>
|
|
14
|
+
<%= button_to "+#{interval}", retry_all_failed_jobs_path,
|
|
15
|
+
method: :post,
|
|
16
|
+
params: { stagger: interval, queue: @queue, q: @search, period: @period },
|
|
17
|
+
class: "sqd-btn sqd-btn--muted sqd-btn--sm",
|
|
18
|
+
title: "Retry all, staggered #{interval} apart",
|
|
19
|
+
data: { confirm: "Retry #{@failed_jobs.size} failed jobs staggered #{interval} apart?" } %>
|
|
20
|
+
<% end %>
|
|
21
|
+
<% end %>
|
|
12
22
|
<%= button_to "Discard All", discard_all_failed_jobs_path,
|
|
13
23
|
method: :post,
|
|
14
24
|
params: { queue: @queue, q: @search, period: @period },
|
|
@@ -92,7 +102,7 @@
|
|
|
92
102
|
data-action="change->selection#toggle"
|
|
93
103
|
aria-label="Select job <%= job.class_name %>">
|
|
94
104
|
</td>
|
|
95
|
-
<td><%= link_to job.class_name, job_path(job) %></td>
|
|
105
|
+
<td><%= link_to job.class_name, job_path(job), class: "sqd-table-link" %></td>
|
|
96
106
|
<td>
|
|
97
107
|
<%= link_to job.queue_name, failed_jobs_path(queue: job.queue_name, q: @search, period: @period),
|
|
98
108
|
class: "sqd-mono", style: "color: inherit;" %>
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
<tbody>
|
|
50
50
|
<% @jobs.each do |job| %>
|
|
51
51
|
<tr>
|
|
52
|
-
<td><%= link_to job.class_name, job_path(job) %></td>
|
|
52
|
+
<td><%= link_to job.class_name, job_path(job), class: "sqd-table-link" %></td>
|
|
53
53
|
<td>
|
|
54
54
|
<%= link_to job.queue_name, history_path(queue: job.queue_name, q: @search, period: @period),
|
|
55
55
|
class: "sqd-mono", style: "color: inherit;" %>
|