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
14 changes: 14 additions & 0 deletions app/assets/stylesheets/solid_stack_web/_09_detail.css
Original file line number Diff line number Diff line change
Expand Up @@ -68,4 +68,18 @@
word-break: break-word;
max-height: 400px;
overflow-y: auto;
}

.sqw-code-input {
font-family: ui-monospace, "SFMono-Regular", Menlo, monospace;
font-size: 12px;
background: var(--bg);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 0.75rem;
width: 100%;
resize: vertical;
color: var(--text);
line-height: 1.5;
box-sizing: border-box;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
module SolidStackWeb
module FailedJobs
class ArgumentsController < ApplicationController
def update
@execution = SolidQueue::FailedExecution.includes(:job).find(params[:failed_job_id])
new_arguments = JSON.parse(params[:arguments])
@execution.job.update!(arguments: new_arguments)
@execution.retry
redirect_to failed_jobs_path, notice: "Arguments updated and job queued for retry."
rescue JSON::ParserError
redirect_to failed_job_path(@execution), alert: "Invalid JSON — arguments were not saved."
rescue => e
redirect_to failed_jobs_path, alert: "Could not update job: #{e.message}"
end
end
end
end
7 changes: 7 additions & 0 deletions app/controllers/solid_stack_web/failed_jobs_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,13 @@ def index
end
end

def show
@execution = ::SolidQueue::FailedExecution.includes(:job).find(params[:id])
@arguments = JSON.pretty_generate(@execution.job.arguments) if @execution.job.arguments.present?
rescue JSON::GeneratorError
@arguments = @execution.job.arguments.to_s
end

def destroy
@execution = ::SolidQueue::FailedExecution.find(params[:id])
@execution.job.destroy!
Expand Down
2 changes: 1 addition & 1 deletion app/views/solid_stack_web/failed_jobs/index.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
aria-label="Select <%= execution.job.class_name %>"
data-selection-target="checkbox"
data-action="change->selection#toggle"></td>
<td class="sqw-monospace"><%= execution.job.class_name %></td>
<td class="sqw-monospace"><%= link_to execution.job.class_name, failed_job_path(execution) %></td>
<td><span class="sqw-badge sqw-badge--queue"><%= execution.job.queue_name %></span></td>
<td class="sqw-muted sqw-truncate" title="<%= execution.exception_class %>: <%= execution.message %>"><%= execution.exception_class %></td>
<td class="sqw-muted"><%= execution.created_at.strftime("%b %d %H:%M") %></td>
Expand Down
58 changes: 58 additions & 0 deletions app/views/solid_stack_web/failed_jobs/show.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<div class="sqw-page-header sqw-page-header--split">
<div>
<div class="sqw-breadcrumb">
<%= link_to "Failed Jobs", failed_jobs_path %> &rsaquo; Detail
</div>
<h1 class="sqw-page-title sqw-monospace"><%= @execution.job.class_name %></h1>
</div>

<div class="sqw-detail-actions">
<%= button_to "Retry", retry_failed_job_path(@execution),
method: :post, class: "sqw-btn sqw-btn--sm" %>
<%= button_to "Discard", failed_job_path(@execution),
method: :delete, class: "sqw-btn sqw-btn--danger",
data: { turbo_confirm: "Discard this job?" } %>
</div>
</div>

<div class="sqw-detail-grid">
<div class="sqw-detail-card sqw-detail-section">
<h2 class="sqw-section-title">Details</h2>
<dl class="sqw-dl">
<dt>Queue</dt>
<dd class="sqw-monospace"><%= @execution.job.queue_name %></dd>

<dt>Priority</dt>
<dd><%= @execution.job.priority %></dd>

<dt>Active Job ID</dt>
<dd class="sqw-monospace sqw-truncate" title="<%= @execution.job.active_job_id %>"><%= @execution.job.active_job_id.presence || "—" %></dd>

<dt>Failed At</dt>
<dd class="sqw-monospace"><%= @execution.created_at.strftime("%Y-%m-%d %H:%M:%S UTC") %></dd>

<dt>Error</dt>
<dd class="sqw-monospace"><%= @execution.exception_class %></dd>

<dt>Message</dt>
<dd class="sqw-muted"><%= @execution.message %></dd>
</dl>
</div>

<div class="sqw-detail-card sqw-detail-section">
<h2 class="sqw-section-title">Backtrace</h2>
<pre class="sqw-code-block"><%= Array(@execution.backtrace).first(10).join("\n").presence || "—" %></pre>
</div>
</div>

<div class="sqw-detail-card sqw-detail-section" style="margin-top: 1.5rem;">
<h2 class="sqw-section-title">Arguments</h2>
<%= form_with url: failed_job_arguments_path(@execution), method: :patch do |f| %>
<%= f.text_area :arguments, value: @arguments, rows: 12,
class: "sqw-code-input", aria: { label: "Job arguments JSON" },
spellcheck: false %>
<div style="margin-top: 0.75rem;">
<%= f.submit "Update &amp; Retry".html_safe, class: "sqw-btn sqw-btn--sm" %>
</div>
<% end %>
</div>
3 changes: 2 additions & 1 deletion config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@
end
end

resources :failed_jobs, only: [:index, :destroy] do
resources :failed_jobs, only: [:index, :show, :destroy] do
member { post :retry }
resource :arguments, only: [:update], controller: "failed_jobs/arguments"
end

resources :queues, only: [:index] do
Expand Down
22 changes: 22 additions & 0 deletions spec/requests/solid_stack_web/failed_job_selections_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,17 @@ def create_failed(class_name: "FailingJob", queue_name: "default")
end

describe "POST /failed_jobs/selection (bulk retry)" do
it "redirects with alert when retry raises" do
execution = create_failed
allow_any_instance_of(SolidQueue::FailedExecution).to receive(:retry).and_raise(RuntimeError, "db error")

post "#{engine_root}/failed_jobs/selection",
params: { job_ids: [execution.id] }

expect(response).to redirect_to("#{engine_root}/failed_jobs")
expect(flash[:alert]).to eq("Could not retry jobs: db error")
end

it "retries only the selected jobs" do
exec_a = create_failed(class_name: "JobA")
exec_b = create_failed(class_name: "JobB")
Expand Down Expand Up @@ -59,6 +70,17 @@ def create_failed(class_name: "FailingJob", queue_name: "default")
end

describe "DELETE /failed_jobs/selection (bulk discard)" do
it "redirects with alert when discard raises" do
execution = create_failed
allow(SolidQueue::Job).to receive(:where).and_raise(RuntimeError, "db error")

delete "#{engine_root}/failed_jobs/selection",
params: { job_ids: [execution.id] }

expect(response).to redirect_to("#{engine_root}/failed_jobs")
expect(flash[:alert]).to eq("Could not discard jobs: db error")
end

it "discards only the selected jobs" do
exec_a = create_failed(class_name: "JobA")
exec_b = create_failed(class_name: "JobB")
Expand Down
80 changes: 80 additions & 0 deletions spec/requests/solid_stack_web/failed_jobs_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,86 @@ def create_failed(class_name: "FailingJob", queue_name: "default")
end
end

describe "GET /failed_jobs/:id" do
it "returns 200 and shows the job class" do
execution = create_failed(class_name: "BrokenJob")
get "#{engine_root}/failed_jobs/#{execution.id}"
expect(response).to have_http_status(:ok)
expect(response.body).to include("BrokenJob")
end

it "shows the error class and message" do
execution = create_failed
get "#{engine_root}/failed_jobs/#{execution.id}"
expect(response.body).to include("RuntimeError")
expect(response.body).to include("something went wrong")
end

it "shows a breadcrumb back to failed jobs" do
execution = create_failed
get "#{engine_root}/failed_jobs/#{execution.id}"
expect(response.body).to include("sqw-breadcrumb")
expect(response.body).to include("Failed Jobs")
end

it "renders the argument editor form" do
execution = create_failed
get "#{engine_root}/failed_jobs/#{execution.id}"
expect(response.body).to include("sqw-code-input")
expect(response.body).to include("Update")
end

it "shows Retry and Discard buttons" do
execution = create_failed
get "#{engine_root}/failed_jobs/#{execution.id}"
expect(response.body).to include("Retry")
expect(response.body).to include("Discard")
end

it "falls back to raw arguments string when JSON generation fails" do
execution = create_failed
allow(JSON).to receive(:pretty_generate).and_raise(JSON::GeneratorError, "NaN")

get "#{engine_root}/failed_jobs/#{execution.id}"

expect(response).to have_http_status(:ok)
end
end

describe "PATCH /failed_jobs/:id/arguments" do
it "updates arguments and retries the job" do
execution = create_failed
new_args = { "executions" => 0, "exception_executions" => {}, "user_id" => 99 }.to_json

patch "#{engine_root}/failed_jobs/#{execution.id}/arguments",
params: { arguments: new_args }

expect(SolidQueue::FailedExecution.exists?(execution.id)).to be false
expect(response).to redirect_to("#{engine_root}/failed_jobs")
end

it "redirects back with alert on invalid JSON" do
execution = create_failed

patch "#{engine_root}/failed_jobs/#{execution.id}/arguments",
params: { arguments: "not valid json {{" }

expect(SolidQueue::FailedExecution.exists?(execution.id)).to be true
expect(response).to redirect_to("#{engine_root}/failed_jobs/#{execution.id}")
end

it "redirects with alert when update raises" do
execution = create_failed
allow_any_instance_of(SolidQueue::Job).to receive(:update!).and_raise(RuntimeError, "db error")

patch "#{engine_root}/failed_jobs/#{execution.id}/arguments",
params: { arguments: { "executions" => 0, "exception_executions" => {} }.to_json }

expect(response).to redirect_to("#{engine_root}/failed_jobs")
expect(flash[:alert]).to eq("Could not update job: db error")
end
end

describe "POST /failed_jobs/:id/retry" do
it "re-enqueues the job and redirects" do
execution = create_failed
Expand Down
6 changes: 6 additions & 0 deletions spec/requests/solid_stack_web/jobs_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,12 @@ def create_ready(class_name: "MyJob", queue_name: "default", priority: 0)

expect(response).to redirect_to("#{engine_root}/jobs?status=claimed")
end

it "sets an alert when status is not discardable" do
delete "#{engine_root}/jobs/selection", params: { status: "claimed" }

expect(flash[:alert]).to eq("Cannot discard claimed jobs.")
end
end

describe "POST /jobs/discard_all" do
Expand Down