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
33 changes: 24 additions & 9 deletions app/controllers/solid_queue_web/failed_jobs_controller.rb
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
module SolidQueueWeb
class FailedJobsController < ApplicationController
before_action :set_filter_params, only: [ :index, :retry_all, :discard_all ]

def index
@pagy, @failed_jobs = pagy(
SolidQueue::FailedExecution.includes(:job).order(created_at: :desc)
)
@pagy, @failed_jobs = pagy(filtered_scope.order(created_at: :desc))
end

def retry
Expand All @@ -23,16 +23,31 @@ def destroy
end

def retry_all
executions = SolidQueue::FailedExecution.includes(:job).to_a
jobs = executions.map(&:job)
jobs = filtered_scope.map(&:job)
SolidQueue::FailedExecution.retry_all(jobs)
redirect_to failed_jobs_path, notice: "#{jobs.size} #{"job".pluralize(jobs.size)} queued for retry."
redirect_to failed_jobs_path(queue: @queue, q: @search),
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."
jobs = filtered_scope.map(&:job)
SolidQueue::FailedExecution.discard_all_from_jobs(jobs)
redirect_to failed_jobs_path(queue: @queue, q: @search),
notice: "#{jobs.size} #{"job".pluralize(jobs.size)} discarded."
end

private

def set_filter_params
@queue = params[:queue].presence
@search = params[:q].presence
end

def filtered_scope
scope = SolidQueue::FailedExecution.includes(:job)
scope = scope.references(:job).where(solid_queue_jobs: { queue_name: @queue }) if @queue.present?
scope = scope.references(:job).where("solid_queue_jobs.class_name LIKE ?", "%#{@search}%") if @search.present?
scope
end
end
end
1 change: 1 addition & 0 deletions app/controllers/solid_queue_web/jobs_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ def show
.includes(:ready_execution, :scheduled_execution, :claimed_execution, :blocked_execution, :failed_execution)
.find(params[:id])
@failed_execution = @job.failed_execution
@blocked_execution = @job.blocked_execution
@execution_status = derive_status(@job)
end

Expand Down
2 changes: 1 addition & 1 deletion app/models/solid_queue_web/job.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,4 @@ class Job
"failed" => SolidQueue::FailedExecution
}.freeze
end
end
end
37 changes: 33 additions & 4 deletions app/views/solid_queue_web/failed_jobs/index.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,33 @@
<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",
<%= button_to "Retry All", retry_all_failed_jobs_path,
method: :post,
params: { queue: @queue, q: @search },
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",
<%= button_to "Discard All", discard_all_failed_jobs_path,
method: :post,
params: { queue: @queue, q: @search },
class: "sqd-btn sqd-btn--danger",
data: { confirm: "Discard all #{@failed_jobs.size} failed jobs? This cannot be undone." } %>
</div>
<% end %>
</div>

<form class="sqd-search" action="<%= failed_jobs_path %>" method="get" data-controller="search">
<% if @queue.present? %>
<input type="hidden" name="queue" value="<%= @queue %>">
<% end %>
<input class="sqd-search__input" type="search" name="q" value="<%= @search %>"
placeholder="Filter by job class…" autocomplete="off" aria-label="Filter by job class"
data-action="input->search#filter">
<button type="submit" class="sqd-btn sqd-btn--muted">Search</button>
<% if @search.present? %>
<%= link_to "Clear", failed_jobs_path(queue: @queue), class: "sqd-btn sqd-btn--muted" %>
<% end %>
</form>

<% if @pagy.last > 1 %>
<%= @pagy.series_nav.html_safe %>
<% end %>
Expand All @@ -33,7 +52,10 @@
<% job = execution.job %>
<tr>
<td><%= link_to job.class_name, job_path(job) %></td>
<td class="sqd-mono"><%= job.queue_name %></td>
<td>
<%= link_to job.queue_name, failed_jobs_path(queue: job.queue_name, q: @search),
class: "sqd-mono", style: "color: inherit;" %>
</td>
<td>
<% if execution.exception_class.present? %>
<div class="sqd-error-msg sqd-truncate" title="<%= execution.exception_class %>: <%= execution.message %>">
Expand All @@ -56,4 +78,11 @@
</tbody>
</table>
<% end %>
</div>
</div>

<% if @queue.present? %>
<p style="margin-top: 0.75rem; font-size: 13px; color: var(--muted);">
Filtering by queue: <strong><%= @queue %></strong> &mdash;
<%= link_to "Clear filter", failed_jobs_path(q: @search) %>
</p>
<% end %>
5 changes: 5 additions & 0 deletions app/views/solid_queue_web/jobs/show.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,11 @@
<dt>Concurrency Key</dt>
<dd class="sqd-mono"><%= @job.concurrency_key.presence || "—" %></dd>

<% if @blocked_execution %>
<dt>Blocked Until</dt>
<dd class="sqd-mono"><%= @blocked_execution.expires_at ? @blocked_execution.expires_at.strftime("%Y-%m-%d %H:%M:%S %Z") : "—" %></dd>
<% end %>

<dt>Enqueued At</dt>
<dd class="sqd-mono"><%= @job.created_at.strftime("%Y-%m-%d %H:%M:%S %Z") %></dd>

Expand Down
110 changes: 110 additions & 0 deletions spec/requests/solid_queue_web/failed_jobs_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,72 @@
get "/jobs/failed_jobs"
expect(response.body).to include("TestJob")
end

it "renders a search form" do
get "/jobs/failed_jobs"
expect(response.body).to include("Filter by job class")
end
end

describe "GET /jobs/failed_jobs?q= (class name search)" do
let!(:other_job) do
j = SolidQueue::Job.create!(
queue_name: "default",
class_name: "MailerJob",
arguments: {},
active_job_id: SecureRandom.uuid
)
j.ready_execution&.destroy
SolidQueue::FailedExecution.create!(
job: j,
error: { exception_class: "RuntimeError", message: "oops", backtrace: [] }
)
j
end

it "returns only matching jobs" do
get "/jobs/failed_jobs", params: { q: "Test" }
expect(response.body).to include("TestJob")
expect(response.body).not_to include("MailerJob")
end

it "renders a clear link when search is active" do
get "/jobs/failed_jobs", params: { q: "Test" }
expect(response.body).to include("Clear")
end
end

describe "GET /jobs/failed_jobs?queue= (queue filter)" do
let!(:other_job) do
j = SolidQueue::Job.create!(
queue_name: "mailers",
class_name: "MailerJob",
arguments: {},
active_job_id: SecureRandom.uuid
)
j.ready_execution&.destroy
SolidQueue::FailedExecution.create!(
job: j,
error: { exception_class: "RuntimeError", message: "oops", backtrace: [] }
)
j
end

it "returns only jobs in the specified queue" do
get "/jobs/failed_jobs", params: { queue: "default" }
expect(response.body).to include("TestJob")
expect(response.body).not_to include("MailerJob")
end

it "shows queue filter indicator" do
get "/jobs/failed_jobs", params: { queue: "default" }
expect(response.body).to include("Filtering by queue")
end

it "queue names in the table link to the queue filter" do
get "/jobs/failed_jobs"
expect(response.body).to include("queue=default")
end
end

describe "POST /jobs/failed_jobs/:id/retry" do
Expand Down Expand Up @@ -90,6 +156,28 @@
post "/jobs/failed_jobs/retry_all"
}.to change(SolidQueue::FailedExecution, :count).to(0)
end

it "scopes retry to the specified queue" do
other_job = SolidQueue::Job.create!(
queue_name: "mailers",
class_name: "MailerJob",
arguments: {},
active_job_id: SecureRandom.uuid
)
other_job.ready_execution&.destroy
SolidQueue::FailedExecution.create!(
job: other_job,
error: { exception_class: "RuntimeError", message: "oops", backtrace: [] }
)

post "/jobs/failed_jobs/retry_all", params: { queue: "default" }
expect(SolidQueue::FailedExecution.joins(:job).where(solid_queue_jobs: { queue_name: "mailers" }).count).to eq(1)
end

it "preserves queue param in redirect" do
post "/jobs/failed_jobs/retry_all", params: { queue: "default" }
expect(response).to redirect_to("/jobs/failed_jobs?queue=default")
end
end

describe "POST /jobs/failed_jobs/discard_all" do
Expand All @@ -105,5 +193,27 @@
post "/jobs/failed_jobs/discard_all"
}.to change(SolidQueue::FailedExecution, :count).to(0)
end

it "scopes discard to the specified queue" do
other_job = SolidQueue::Job.create!(
queue_name: "mailers",
class_name: "MailerJob",
arguments: {},
active_job_id: SecureRandom.uuid
)
other_job.ready_execution&.destroy
SolidQueue::FailedExecution.create!(
job: other_job,
error: { exception_class: "RuntimeError", message: "oops", backtrace: [] }
)

post "/jobs/failed_jobs/discard_all", params: { queue: "default" }
expect(SolidQueue::FailedExecution.joins(:job).where(solid_queue_jobs: { queue_name: "mailers" }).count).to eq(1)
end

it "preserves queue param in redirect" do
post "/jobs/failed_jobs/discard_all", params: { queue: "default" }
expect(response).to redirect_to("/jobs/failed_jobs?queue=default")
end
end
end
14 changes: 14 additions & 0 deletions spec/requests/solid_queue_web/jobs_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,20 @@
get "/jobs/list/#{ready_job.id}"
expect(response).to have_http_status(:ok)
end

it "shows blocked until date for blocked jobs" do
ready_job.ready_execution&.destroy
ready_job.update!(concurrency_key: "TestJob/1")
allow_any_instance_of(SolidQueue::BlockedExecution).to receive(:set_expires_at)
SolidQueue::BlockedExecution.create!(
job: ready_job,
queue_name: ready_job.queue_name,
priority: ready_job.priority,
expires_at: 1.hour.from_now
)
get "/jobs/list/#{ready_job.id}"
expect(response.body).to include("Blocked Until")
end
end

describe "GET /jobs/list" do
Expand Down
Loading