Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Retry and discard actions on individual failed jobs
- Bulk "Retry All" and "Discard All" actions for failed jobs
- Roadmap section added to README

### Fixed

- Failed jobs view now renders error class and message correctly (seed data format and missing CSS class)
Expand Down
31 changes: 30 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

[![Gem Version](https://badge.fury.io/rb/solid_queue_web.svg)](https://rubygems.org/gems/solid_queue_web)

A read-only Rails engine that mounts a monitoring dashboard for [Solid Queue](https://github.com/rails/solid_queue). View queues, inspect jobs by status, and browse failed executions — all without leaving your app.
A Rails engine that mounts a monitoring dashboard for [Solid Queue](https://github.com/rails/solid_queue). View queues, inspect jobs by status, browse failed executions, and take action — all without leaving your app.

## Features

Expand Down Expand Up @@ -56,6 +56,35 @@ HTTP Basic authentication is used as a fallback when the block returns falsy.
- Rails >= 8.1.3
- solid_queue >= 1.0

## Roadmap

The following features are planned. Contributions welcome.

### Actions on failed jobs
- [ ] Retry a single failed job
- [ ] Discard (delete) a single failed job
- [ ] Bulk retry all failed jobs
- [ ] Bulk discard all failed jobs

### Actions on ready / scheduled jobs
- [ ] Discard (cancel) a single ready or scheduled job
- [ ] Bulk discard all jobs in a queue

### Job detail page
- [ ] Full job details: arguments, priority, attempts, scheduled time
- [ ] Full error backtrace for failed jobs

### Pagination
- [ ] Paginate jobs, failed jobs, and queues lists

### Queue management
- [ ] Pause / resume a queue (block new executions)

### Process visibility
- [ ] Processes page showing workers, dispatchers, and last heartbeat

---

## Contributing

Bug reports and pull requests are welcome on [GitHub](https://github.com/eclectic-coding/solid_queue_web).
Expand Down
15 changes: 15 additions & 0 deletions app/assets/stylesheets/solid_queue_web/application.css
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,21 @@ body {
.sqd-page-title {
font-size: 20px;
font-weight: 600;
margin-bottom: 0;
}

.sqd-page-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 1.5rem;
}

.sqd-actions {
display: flex;
gap: 0.5rem;
}

/* Flash notices */
.sqd-flash {
padding: 0.75rem 1rem;
Expand Down Expand Up @@ -218,6 +230,9 @@ tbody tr:hover { background: var(--bg); }
.sqd-btn--primary { background: var(--primary); color: #fff; border-color: var(--primary); }
.sqd-btn--danger { background: var(--danger); color: #fff; border-color: var(--danger); }
.sqd-btn--muted { background: var(--surface); color: var(--text); border-color: var(--border); }
.sqd-btn--sm { padding: 0.2rem 0.55rem; font-size: 11px; }

.sqd-row-actions { white-space: nowrap; text-align: right; width: 1%; }

/* Filters */
.sqd-filters {
Expand Down
29 changes: 29 additions & 0 deletions app/controllers/solid_queue_web/failed_jobs_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,34 @@ def index
.order(created_at: :desc)
.limit(100)
end

def retry
execution = SolidQueue::FailedExecution.find(params[:id])
execution.retry
redirect_to failed_jobs_path, notice: "Job queued for retry."
rescue => e
redirect_to failed_jobs_path, alert: "Could not retry job: #{e.message}"
end

def destroy
execution = SolidQueue::FailedExecution.find(params[:id])
execution.discard
redirect_to failed_jobs_path, notice: "Job discarded."
rescue => e
redirect_to failed_jobs_path, alert: "Could not discard job: #{e.message}"
end

def retry_all
executions = SolidQueue::FailedExecution.includes(:job).to_a
jobs = executions.map(&:job)
SolidQueue::FailedExecution.retry_all(jobs)
redirect_to failed_jobs_path, notice: "#{jobs.size} #{"job".pluralize(jobs.size)} queued for retry."
end

def discard_all
count = SolidQueue::FailedExecution.count
SolidQueue::FailedExecution.discard_all_in_batches
redirect_to failed_jobs_path, notice: "#{count} #{"job".pluralize(count)} discarded."
end
end
end
20 changes: 19 additions & 1 deletion app/views/solid_queue_web/failed_jobs/index.html.erb
Original file line number Diff line number Diff line change
@@ -1,4 +1,14 @@
<h1 class="sqd-page-title">Failed Jobs</h1>
<div class="sqd-page-header">
<h1 class="sqd-page-title">Failed Jobs</h1>
<% if @failed_jobs.any? %>
<div class="sqd-actions">
<%= button_to "Retry All", retry_all_failed_jobs_path, method: :post, class: "sqd-btn sqd-btn--primary",
data: { confirm: "Retry all #{@failed_jobs.size} failed jobs?" } %>
<%= button_to "Discard All", discard_all_failed_jobs_path, method: :post, class: "sqd-btn sqd-btn--danger",
data: { confirm: "Discard all #{@failed_jobs.size} failed jobs? This cannot be undone." } %>
</div>
<% end %>
</div>

<div class="sqd-card">
<% if @failed_jobs.empty? %>
Expand All @@ -11,6 +21,7 @@
<th>Queue</th>
<th>Error</th>
<th>Failed At</th>
<th></th>
</tr>
</thead>
<tbody>
Expand All @@ -29,6 +40,13 @@
<% end %>
</td>
<td class="sqd-mono"><%= execution.created_at.strftime("%Y-%m-%d %H:%M:%S") %></td>
<td class="sqd-row-actions">
<%= button_to "Retry", retry_failed_job_path(execution), method: :post,
class: "sqd-btn sqd-btn--primary sqd-btn--sm" %>
<%= button_to "Discard", failed_job_path(execution), method: :delete,
class: "sqd-btn sqd-btn--danger sqd-btn--sm",
data: { confirm: "Discard this job?" } %>
</td>
</tr>
<% end %>
</tbody>
Expand Down
10 changes: 9 additions & 1 deletion config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,13 @@

resources :queues, only: [ :index ]
resources :jobs, only: [ :index ]
resources :failed_jobs, only: [ :index ]
resources :failed_jobs, only: [ :index, :destroy ] do
collection do
post :retry_all
post :discard_all
end
member do
post :retry
end
end
end
2 changes: 2 additions & 0 deletions spec/rails_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

require "rspec/rails"

ActionController::Base.allow_forgery_protection = false

# Load solid_queue schema into the test database
ActiveRecord::Schema.verbose = false
load File.expand_path("dummy/db/schema.rb", __dir__)
Expand Down
93 changes: 93 additions & 0 deletions spec/requests/solid_queue_web/failed_jobs_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
require "rails_helper"

RSpec.describe "FailedJobs", type: :request do
let(:job) do
SolidQueue::Job.create!(
queue_name: "default",
class_name: "TestJob",
arguments: {},
active_job_id: SecureRandom.uuid
)
end

let!(:execution) do
job.ready_execution&.destroy
SolidQueue::FailedExecution.create!(
job: job,
error: { exception_class: "RuntimeError", message: "boom", backtrace: [] }
)
end

describe "GET /jobs/failed_jobs" do
it "returns HTTP success" do
get "/jobs/failed_jobs"
expect(response).to have_http_status(:ok)
end

it "displays failed job class name" do
get "/jobs/failed_jobs"
expect(response.body).to include("TestJob")
end
end

describe "POST /jobs/failed_jobs/:id/retry" do
it "retries the job and redirects" do
post "/jobs/failed_jobs/#{execution.id}/retry"
expect(response).to redirect_to("/jobs/failed_jobs")
follow_redirect!
expect(response.body).to include("queued for retry")
end

it "removes the failed execution" do
expect {
post "/jobs/failed_jobs/#{execution.id}/retry"
}.to change(SolidQueue::FailedExecution, :count).by(-1)
end
end

describe "DELETE /jobs/failed_jobs/:id" do
it "discards the job and redirects" do
delete "/jobs/failed_jobs/#{execution.id}"
expect(response).to redirect_to("/jobs/failed_jobs")
follow_redirect!
expect(response.body).to include("discarded")
end

it "removes the failed execution and job" do
expect {
delete "/jobs/failed_jobs/#{execution.id}"
}.to change(SolidQueue::FailedExecution, :count).by(-1)
.and change(SolidQueue::Job, :count).by(-1)
end
end

describe "POST /jobs/failed_jobs/retry_all" do
it "retries all failed jobs and redirects" do
post "/jobs/failed_jobs/retry_all"
expect(response).to redirect_to("/jobs/failed_jobs")
follow_redirect!
expect(response.body).to include("queued for retry")
end

it "clears all failed executions" do
expect {
post "/jobs/failed_jobs/retry_all"
}.to change(SolidQueue::FailedExecution, :count).to(0)
end
end

describe "POST /jobs/failed_jobs/discard_all" do
it "discards all failed jobs and redirects" do
post "/jobs/failed_jobs/discard_all"
expect(response).to redirect_to("/jobs/failed_jobs")
follow_redirect!
expect(response.body).to include("discarded")
end

it "clears all failed executions and jobs" do
expect {
post "/jobs/failed_jobs/discard_all"
}.to change(SolidQueue::FailedExecution, :count).to(0)
end
end
end