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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- Retry and discard actions on individual failed jobs
- Bulk "Retry All" and "Discard All" actions for failed jobs
- Discard action on individual ready, scheduled, and blocked jobs
- Bulk "Discard All" action for ready, scheduled, and blocked jobs (scoped to current queue filter)
- Roadmap section added to README

### Fixed
Expand Down
36 changes: 36 additions & 0 deletions app/controllers/solid_queue_web/jobs_controller.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
module SolidQueueWeb
class JobsController < ApplicationController
STATUSES = %w[ready scheduled claimed blocked failed].freeze
DISCARDABLE = %w[ready scheduled blocked].freeze

def index
@status = params[:status].presence_in(STATUSES) || "ready"
Expand All @@ -17,5 +18,40 @@ def index
@jobs = @jobs.where(jobs: { queue_name: @queue }) if @queue.present?
@jobs = @jobs.order(created_at: :desc).limit(100)
end

def destroy
execution = execution_model_for!(params[:status]).find(params[:id])
execution.discard
redirect_to jobs_path(status: params[:status], queue: params[:queue]), notice: "Job discarded."
rescue ArgumentError => e
redirect_to jobs_path(status: params[:status], queue: params[:queue]), alert: e.message
rescue => e
redirect_to jobs_path(status: params[:status], queue: params[:queue]), alert: "Could not discard job: #{e.message}"
end

def discard_all
model = execution_model_for!(params[:status])
scope = model.includes(:job)
scope = scope.where(jobs: { queue_name: params[:queue] }) if params[:queue].present?
jobs = scope.map(&:job)
model.discard_all_from_jobs(jobs)
redirect_to jobs_path(status: params[:status], queue: params[:queue]),
notice: "#{jobs.size} #{"job".pluralize(jobs.size)} discarded."
rescue ArgumentError => e
redirect_to jobs_path(status: params[:status], queue: params[:queue]), alert: e.message
rescue => e
redirect_to jobs_path(status: params[:status], queue: params[:queue]), alert: "Could not discard jobs: #{e.message}"
end

private

def execution_model_for!(status)
case status
when "ready" then SolidQueue::ReadyExecution
when "scheduled" then SolidQueue::ScheduledExecution
when "blocked" then SolidQueue::BlockedExecution
else raise ArgumentError, "Cannot discard #{status} jobs from this page."
end
end
end
end
27 changes: 25 additions & 2 deletions app/views/solid_queue_web/jobs/index.html.erb
Original file line number Diff line number Diff line change
@@ -1,4 +1,17 @@
<h1 class="sqd-page-title">Jobs</h1>
<% discardable = SolidQueueWeb::JobsController::DISCARDABLE.include?(@status) %>

<div class="sqd-page-header">
<h1 class="sqd-page-title">Jobs</h1>
<% if discardable && @jobs.any? %>
<div class="sqd-actions">
<%= button_to "Discard All", discard_all_jobs_path,
method: :post,
params: { status: @status, queue: @queue },
class: "sqd-btn sqd-btn--danger",
data: { confirm: "Discard all #{@jobs.size} #{@status} jobs? This cannot be undone." } %>
</div>
<% end %>
</div>

<div class="sqd-filters">
<%= link_to "Ready", jobs_path(status: "ready", queue: @queue), class: @status == "ready" ? "active" : "" %>
Expand All @@ -20,6 +33,7 @@
<th>Priority</th>
<th>Scheduled At</th>
<th>Enqueued At</th>
<% if discardable %><th></th><% end %>
</tr>
</thead>
<tbody>
Expand All @@ -39,6 +53,15 @@
<%= job.scheduled_at ? job.scheduled_at.strftime("%Y-%m-%d %H:%M:%S") : "—" %>
</td>
<td class="sqd-mono"><%= job.created_at.strftime("%Y-%m-%d %H:%M:%S") %></td>
<% if discardable %>
<td class="sqd-row-actions">
<%= button_to "Discard", job_path(execution),
method: :delete,
params: { status: @status, queue: @queue },
class: "sqd-btn sqd-btn--danger sqd-btn--sm",
data: { confirm: "Discard this job?" } %>
</td>
<% end %>
</tr>
<% end %>
</tbody>
Expand All @@ -51,4 +74,4 @@
Filtering by queue: <strong><%= @queue %></strong> &mdash;
<%= link_to "Clear filter", jobs_path(status: @status) %>
</p>
<% end %>
<% end %>
6 changes: 5 additions & 1 deletion config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@
root to: "dashboard#index"

resources :queues, only: [ :index ]
resources :jobs, only: [ :index ]
resources :jobs, only: [ :index, :destroy ] do
collection do
post :discard_all
end
end
resources :failed_jobs, only: [ :index, :destroy ] do
collection do
post :retry_all
Expand Down
71 changes: 71 additions & 0 deletions spec/requests/solid_queue_web/jobs_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
require "rails_helper"

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

let(:ready_execution) { ready_job.ready_execution }

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

it "shows ready jobs by default" do
get "/jobs/jobs"
expect(response.body).to include("TestJob")
end
end

describe "DELETE /jobs/jobs/:id (discard single)" do
it "discards the job and redirects" do
delete "/jobs/jobs/#{ready_execution.id}", params: { status: "ready" }
expect(response).to redirect_to("/jobs/jobs?status=ready")
follow_redirect!
expect(response.body).to include("discarded")
end

it "removes the execution and job" do
expect {
delete "/jobs/jobs/#{ready_execution.id}", params: { status: "ready" }
}.to change(SolidQueue::ReadyExecution, :count).by(-1)
.and change(SolidQueue::Job, :count).by(-1)
end

it "rejects discard for claimed status" do
delete "/jobs/jobs/#{ready_execution.id}", params: { status: "claimed" }
expect(response).to redirect_to("/jobs/jobs?status=claimed")
follow_redirect!
expect(response.body).to include("Cannot discard")
end
end

describe "POST /jobs/jobs/discard_all" do
it "discards all ready jobs and redirects" do
post "/jobs/jobs/discard_all", params: { status: "ready" }
expect(response).to redirect_to("/jobs/jobs?status=ready")
follow_redirect!
expect(response.body).to include("discarded")
end

it "clears all ready executions" do
expect {
post "/jobs/jobs/discard_all", params: { status: "ready" }
}.to change(SolidQueue::ReadyExecution, :count).to(0)
end

it "rejects discard_all for claimed status" do
post "/jobs/jobs/discard_all", params: { status: "claimed" }
expect(response).to redirect_to("/jobs/jobs?status=claimed")
follow_redirect!
expect(response.body).to include("Cannot discard")
end
end
end