diff --git a/CHANGELOG.md b/CHANGELOG.md index 05ba8bc..6188fd6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Failure rate sparkline per queue — a mini 12-bar chart in the Queues table shows the percentage of jobs that failed (vs. completed) in each of the last 12 hours; bars are red, sized proportionally to the failure rate (0–100 %), and include a tooltip with the hour label and exact percentage; empty hours render as a faint border-colored bar; queues with no activity in the last 12 hours show "—" - Queue depth trend — a "Queue Depth — Last 12 Hours" card on the dashboard shows estimated queue depth at 12 hourly snapshots; depth at time T is the count of jobs created before T that had not yet finished by T; bars are purple (distinct from the blue throughput and red failure rate charts); the card header shows current depth; empty state shown when no active jobs exist in the window +- Slow job detection — a configurable `slow_job_threshold` setting (default nil = disabled) flags claimed jobs that have been running longer than the threshold; when set, the Running tab gains a "Running For" column showing each job's elapsed time, slow jobs are highlighted with an orange row background and a "slow" badge, and a "Slow Jobs" warning card appears on the dashboard linking to the Running tab ## [0.8.0] - 2026-05-20 diff --git a/README.md b/README.md index 2a8c285..21a35b0 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,7 @@ SolidQueueWeb surfaces all of this in a browser UI available at any route you ch - **Dark mode** — ☽/☀ toggle in the header; preference persists to `localStorage` and defaults to the OS `prefers-color-scheme` on first visit; zero extra dependencies — implemented via CSS custom properties and a small Stimulus controller - **Dashboard quick actions** — "Retry All Failed" and "Discard All Blocked" cards appear on the dashboard only when the respective count is non-zero; one-click bulk operations with confirm dialogs, keeping the dashboard clean when everything is healthy - **CSV export** — "Export CSV" button on the jobs, failed jobs, and history pages downloads all records matching the current filters; columns are tailored per view +- **Slow job detection** — when `slow_job_threshold` is configured, claimed jobs running longer than the threshold are flagged with an orange row, a "slow" badge, and a "Running For" duration column on the Running tab; a "Slow Jobs" warning card appears on the dashboard with a link to the Running tab ## Screenshots @@ -96,6 +97,7 @@ SolidQueueWeb.configure do |config| config.dashboard_refresh_interval = 10_000 # dashboard auto-refresh in ms (default: 5_000) config.default_refresh_interval = 30_000 # jobs/processes/history auto-refresh in ms (default: 10_000) config.search_results_limit = 10 # max results per status in global search (default: 25) + config.slow_job_threshold = 5.minutes # flag claimed jobs running longer than this (default: nil = disabled) end SolidQueueWeb.authenticate do @@ -111,9 +113,6 @@ No authentication is enforced by default. When the `authenticate` block returns Planned features, roughly ordered by priority: -**Observability** -- Slow job detection — flag jobs exceeding a configurable duration threshold - **Operations** - Scheduled job management — reschedule a job to run immediately, or push its `scheduled_at` forward - Bulk retry with delay — retry all failed jobs with a configurable stagger to avoid thundering herd diff --git a/app/assets/stylesheets/solid_queue_web/_04_table.css b/app/assets/stylesheets/solid_queue_web/_04_table.css index 8ba8488..f54c226 100644 --- a/app/assets/stylesheets/solid_queue_web/_04_table.css +++ b/app/assets/stylesheets/solid_queue_web/_04_table.css @@ -44,9 +44,16 @@ td { tr:last-child td { border-bottom: none; } tbody tr:hover { background: var(--bg); } +.sqd-table-link, +.sqd-table-link:hover, +.sqd-table-link:visited { text-decoration: none; color: var(--primary); } .sqd-empty { text-align: center; padding: 3rem 1rem; color: var(--muted); -} \ No newline at end of file +} + +.sqd-row--slow { background: rgba(253, 126, 20, 0.07); } +.sqd-row--slow:hover { background: rgba(253, 126, 20, 0.13); } +.sqd-slow-duration { color: var(--warning); font-weight: 600; } \ No newline at end of file diff --git a/app/assets/stylesheets/solid_queue_web/_05_badges.css b/app/assets/stylesheets/solid_queue_web/_05_badges.css index 1c5e3fd..dbb8bc4 100644 --- a/app/assets/stylesheets/solid_queue_web/_05_badges.css +++ b/app/assets/stylesheets/solid_queue_web/_05_badges.css @@ -21,6 +21,7 @@ .sqd-badge--supervisor { background: #e0d7f5; color: #4a2c8a; } .sqd-badge--worker { background: #d1e7dd; color: #0f5132; } .sqd-badge--dispatcher { background: #cff4fc; color: #055160; } +.sqd-badge--slow { background: #ffe8cc; color: #7c3d00; } .sqd-process-meta { font-size: 12px; color: var(--muted); } .sqd-process-meta span + span::before { content: " · "; } diff --git a/app/services/solid_queue_web/dashboard_stats.rb b/app/services/solid_queue_web/dashboard_stats.rb index 1621792..7c84e43 100644 --- a/app/services/solid_queue_web/dashboard_stats.rb +++ b/app/services/solid_queue_web/dashboard_stats.rb @@ -1,6 +1,6 @@ module SolidQueueWeb class DashboardStats - attr_reader :counts, :throughput, :sparkline, :depth_sparkline + attr_reader :counts, :throughput, :sparkline, :depth_sparkline, :slow_jobs_count def initialize @now = Time.current @@ -32,6 +32,9 @@ def compute finished_times.count { |t| t >= from && t < to } end + threshold = SolidQueueWeb.slow_job_threshold + @slow_jobs_count = threshold ? SolidQueue::ClaimedExecution.where("created_at <= ?", threshold.ago).count : 0 + job_timestamps = SolidQueue::Job .where("created_at >= ? OR finished_at IS NULL", 72.hours.ago) .pluck(:created_at, :finished_at) diff --git a/app/views/solid_queue_web/dashboard/index.html.erb b/app/views/solid_queue_web/dashboard/index.html.erb index 3b63cbc..616d37c 100644 --- a/app/views/solid_queue_web/dashboard/index.html.erb +++ b/app/views/solid_queue_web/dashboard/index.html.erb @@ -136,6 +136,20 @@ <% end %> + <% if SolidQueueWeb.slow_job_threshold && @stats.slow_jobs_count > 0 %> +
+ <%= pluralize(@stats.slow_jobs_count, "job") %> running longer than <%= distance_of_time_in_words(SolidQueueWeb.slow_job_threshold) %>. +
+ <%= link_to "Review →", jobs_path(status: "claimed"), class: "sqd-btn sqd-btn--muted" %> +| Priority | Scheduled At | Enqueued At | + <% if @status == "claimed" %> +Running For | + <% end %>
|---|---|---|---|
| <%= @status %> - <%= link_to job.class_name, job_path(job), style: "margin-left: 0.5rem;", data: { turbo_frame: "_top" } %> + <% if slow %> + slow + <% end %> + <%= link_to job.class_name, job_path(job), class: "sqd-table-link", style: "margin-left: 0.5rem;", data: { turbo_frame: "_top" } %> | <%= link_to job.queue_name, queue_jobs_path(queue_name: job.queue_name, status: @status), @@ -143,6 +151,11 @@ <%= job.scheduled_at ? job.scheduled_at.strftime("%Y-%m-%d %H:%M:%S") : "—" %> | <%= job.created_at.strftime("%Y-%m-%d %H:%M:%S") %> | + <% if @status == "claimed" %> +"> + <%= time_ago_in_words(execution.created_at) %> + | + <% end %>
| <%= @status %> - <%= link_to job.class_name, job_path(job), style: "margin-left: 0.5rem;", data: { turbo_frame: "_top" } %> + <%= link_to job.class_name, job_path(job), class: "sqd-table-link", style: "margin-left: 0.5rem;", data: { turbo_frame: "_top" } %> | <%= job.priority %> | diff --git a/app/views/solid_queue_web/search/index.html.erb b/app/views/solid_queue_web/search/index.html.erb index 2dfe8fb..617e722 100644 --- a/app/views/solid_queue_web/search/index.html.erb +++ b/app/views/solid_queue_web/search/index.html.erb @@ -50,7 +50,7 @@ <% data[:executions].each do |execution| %> <% job = execution.job %> | |
| <%= link_to job.class_name, job_path(job) %> | +<%= link_to job.class_name, job_path(job), class: "sqd-table-link" %> | <%= job.queue_name %> | <%= job.created_at.strftime("%Y-%m-%d %H:%M:%S") %> |