diff --git a/app/controllers/solid_queue_web/failed_jobs_controller.rb b/app/controllers/solid_queue_web/failed_jobs_controller.rb
index 67dc89e..989706f 100644
--- a/app/controllers/solid_queue_web/failed_jobs_controller.rb
+++ b/app/controllers/solid_queue_web/failed_jobs_controller.rb
@@ -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
@@ -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
diff --git a/app/controllers/solid_queue_web/jobs_controller.rb b/app/controllers/solid_queue_web/jobs_controller.rb
index 12395f5..eeca58d 100644
--- a/app/controllers/solid_queue_web/jobs_controller.rb
+++ b/app/controllers/solid_queue_web/jobs_controller.rb
@@ -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
diff --git a/app/models/solid_queue_web/job.rb b/app/models/solid_queue_web/job.rb
index 6281f26..6cc2f57 100644
--- a/app/models/solid_queue_web/job.rb
+++ b/app/models/solid_queue_web/job.rb
@@ -10,4 +10,4 @@ class Job
"failed" => SolidQueue::FailedExecution
}.freeze
end
-end
\ No newline at end of file
+end
diff --git a/app/views/solid_queue_web/failed_jobs/index.html.erb b/app/views/solid_queue_web/failed_jobs/index.html.erb
index 28b9d24..475501d 100644
--- a/app/views/solid_queue_web/failed_jobs/index.html.erb
+++ b/app/views/solid_queue_web/failed_jobs/index.html.erb
@@ -2,14 +2,33 @@
Failed Jobs
<% if @failed_jobs.any? %>
- <%= 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." } %>
<% end %>
+
+
<% if @pagy.last > 1 %>
<%= @pagy.series_nav.html_safe %>
<% end %>
@@ -33,7 +52,10 @@
<% job = execution.job %>
| <%= link_to job.class_name, job_path(job) %> |
- <%= job.queue_name %> |
+
+ <%= link_to job.queue_name, failed_jobs_path(queue: job.queue_name, q: @search),
+ class: "sqd-mono", style: "color: inherit;" %>
+ |
<% if execution.exception_class.present? %>
@@ -56,4 +78,11 @@
<% end %>
-
\ No newline at end of file
+
+
+<% if @queue.present? %>
+
+ Filtering by queue: <%= @queue %> —
+ <%= link_to "Clear filter", failed_jobs_path(q: @search) %>
+
+<% end %>
\ No newline at end of file
diff --git a/app/views/solid_queue_web/jobs/show.html.erb b/app/views/solid_queue_web/jobs/show.html.erb
index 03420d8..3dccc52 100644
--- a/app/views/solid_queue_web/jobs/show.html.erb
+++ b/app/views/solid_queue_web/jobs/show.html.erb
@@ -45,6 +45,11 @@
Concurrency Key
<%= @job.concurrency_key.presence || "—" %>
+ <% if @blocked_execution %>
+ Blocked Until
+ <%= @blocked_execution.expires_at ? @blocked_execution.expires_at.strftime("%Y-%m-%d %H:%M:%S %Z") : "—" %>
+ <% end %>
+
Enqueued At
<%= @job.created_at.strftime("%Y-%m-%d %H:%M:%S %Z") %>
diff --git a/spec/requests/solid_queue_web/failed_jobs_spec.rb b/spec/requests/solid_queue_web/failed_jobs_spec.rb
index a2d6ff0..f4ef3eb 100644
--- a/spec/requests/solid_queue_web/failed_jobs_spec.rb
+++ b/spec/requests/solid_queue_web/failed_jobs_spec.rb
@@ -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
@@ -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
@@ -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
diff --git a/spec/requests/solid_queue_web/jobs_spec.rb b/spec/requests/solid_queue_web/jobs_spec.rb
index ed5a243..b8ccc7f 100644
--- a/spec/requests/solid_queue_web/jobs_spec.rb
+++ b/spec/requests/solid_queue_web/jobs_spec.rb
@@ -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
|