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

## [Unreleased]

### Added

- Retry failed job with modified arguments — the Arguments card on the job detail page becomes an editable textarea for failed jobs; editing the JSON and clicking "Retry with these arguments" updates the job record and retries in one step; invalid JSON redirects back with an error message without touching the failed execution

## [1.0.0] - 2026-05-21

### Added
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ SolidQueueWeb surfaces all of this in a browser UI available at any route you ch
- **Jobs** — filterable by status (ready, scheduled, claimed, blocked, failed), queue, and priority; search by job class name with dynamic auto-submit; time-based period filter (1 h / 24 h / 7 d); discard individual or all jobs; Turbo Frame navigation so only the table updates on filter or search; auto-refreshes every 10 seconds
- **Scheduled job management** — reschedule a scheduled job to run immediately ("Run Now") or push its `scheduled_at` forward by 1 h, 24 h, or 7 d; Turbo Stream responses update the row in place; "Run All Now" bulk action promotes every scheduled job in the current filtered view in a single operation
- **Failed jobs** — list of failed executions with error details; search by class name; filter by queue; time-based period filter; retry or discard individually or in bulk; bulk retry with configurable stagger (+5s / +10s / +30s / +1m) to avoid thundering herd on recovery
- **Job detail** — full arguments, timestamps, blocked-until date, and error backtrace; action buttons based on job status
- **Job detail** — full arguments, timestamps, blocked-until date, and error backtrace; action buttons based on job status; failed jobs show an editable arguments textarea so you can correct a bad payload and retry in one step without redeploying
- **Queue management** — pause and resume individual queues; queue-scoped job list with status filter, search, and discard
- **Recurring tasks** — all configured recurring tasks with cron schedule, next run time, last run time, and static/dynamic classification; "Run Now" button enqueues a task immediately without waiting for its next scheduled run
- **Processes** — workers, dispatchers, and supervisors with heartbeat health status; auto-refreshes every 10 seconds
Expand Down
2 changes: 1 addition & 1 deletion ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ Pull requests for any of these are welcome. See [Contributing](README.md#contrib

| Feature | Notes |
|---|---|
| **Retry failed job with modified arguments** | Form on the job detail page — edit the arguments JSON before retrying. Fixes bad payloads without redeploying. |
| ~~**Retry failed job with modified arguments**~~ | ✓ Shipped — editable textarea on the job detail page; submitting updates the job record and retries in one step. |
| **Multiple webhook targets** | Change `alert_webhook_url` to accept an array. Fan out to Slack + PagerDuty simultaneously. |
| **Queue depth alert** | Fire a webhook when a queue's ready count exceeds a per-queue threshold (e.g. `alert_queue_thresholds: { critical: 50 }`). |

Expand Down
24 changes: 24 additions & 0 deletions app/assets/stylesheets/solid_queue_web/_07_forms.css
Original file line number Diff line number Diff line change
Expand Up @@ -117,4 +117,28 @@
background: var(--muted);
border-color: var(--muted);
color: #fff;
}

.sqd-textarea {
width: 100%;
padding: 0.5rem 0.75rem;
border: 1px solid var(--border);
border-radius: 5px;
font-size: 13px;
background: var(--surface);
color: var(--text);
line-height: 1.6;
resize: vertical;
box-sizing: border-box;
display: block;
}

.sqd-textarea:focus {
outline: 2px solid var(--primary);
outline-offset: -1px;
border-color: var(--primary);
}

.sqd-args-form__submit {
margin-top: 0.75rem;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
module SolidQueueWeb
class FailedJobs::ArgumentsController < ApplicationController
def update
execution = SolidQueue::FailedExecution.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: "Job arguments updated and queued for retry."
rescue JSON::ParserError
redirect_to job_path(execution.job), alert: "Invalid JSON: could not parse arguments."
rescue => e
redirect_to failed_jobs_path, alert: "Could not update job: #{e.message}"
end
end
end
14 changes: 13 additions & 1 deletion app/views/solid_queue_web/jobs/show.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,19 @@

<div class="sqd-card sqd-detail-section">
<h2 class="sqd-section-title">Arguments</h2>
<pre class="sqd-pre"><%= JSON.pretty_generate(@job.arguments) rescue @job.arguments.inspect %></pre>
<% if @execution_status == "failed" && @job.failed_execution %>
<% args_json = begin; JSON.pretty_generate(@job.arguments); rescue; @job.arguments.inspect; end %>
<%= form_with url: failed_job_arguments_path(@job.failed_execution), method: :patch do |f| %>
<%= f.text_area :arguments,
value: args_json,
class: "sqd-textarea sqd-mono",
rows: [args_json.lines.count + 1, 6].max,
"aria-label": "Job arguments JSON" %>
<%= f.submit "Retry with these arguments", class: "sqd-btn sqd-btn--primary sqd-args-form__submit" %>
<% end %>
<% else %>
<pre class="sqd-pre"><%= JSON.pretty_generate(@job.arguments) rescue @job.arguments.inspect %></pre>
<% end %>
</div>
</div>

Expand Down
1 change: 1 addition & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
resource :failed_job_selection, path: "failed_jobs/selection", only: [:create, :destroy],
controller: "failed_jobs/selections"
resources :failed_jobs, only: [:index, :destroy] do
resource :arguments, only: [:update], controller: "failed_jobs/arguments"
collection do
post :retry_all, to: "retry_failed_jobs#create"
post :discard_all, action: :destroy
Expand Down
67 changes: 67 additions & 0 deletions spec/requests/solid_queue_web/failed_job_arguments_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
require "rails_helper"

RSpec.describe "FailedJobs::Arguments", type: :request do
let(:job) do
SolidQueue::Job.create!(
queue_name: "default",
class_name: "TestJob",
arguments: { user_id: 42 },
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 "PATCH /jobs/failed_jobs/:failed_job_id/arguments" do
it "updates the job arguments and retries" do
patch "/jobs/failed_jobs/#{execution.id}/arguments",
params: { arguments: { user_id: 999 }.to_json }
expect(response).to redirect_to("/jobs/failed_jobs")
follow_redirect!
expect(response.body).to include("queued for retry")
end

it "removes the failed execution on success" do
expect {
patch "/jobs/failed_jobs/#{execution.id}/arguments",
params: { arguments: { user_id: 999 }.to_json }
}.to change(SolidQueue::FailedExecution, :count).by(-1)
end

it "persists the new arguments on the job" do
patch "/jobs/failed_jobs/#{execution.id}/arguments",
params: { arguments: { user_id: 999 }.to_json }
expect(job.reload.arguments).to include("user_id" => 999)
end

it "redirects to the job detail page on invalid JSON" do
patch "/jobs/failed_jobs/#{execution.id}/arguments",
params: { arguments: "not valid json{{" }
expect(response).to redirect_to("/jobs/list/#{job.id}")
follow_redirect!
expect(response.body).to include("Invalid JSON")
end

it "leaves the failed execution in place on invalid JSON" do
expect {
patch "/jobs/failed_jobs/#{execution.id}/arguments",
params: { arguments: "not valid json{{" }
}.not_to change(SolidQueue::FailedExecution, :count)
end

it "handles unexpected errors gracefully" do
allow_any_instance_of(SolidQueue::FailedExecution).to receive(:retry).and_raise(RuntimeError, "db error")
patch "/jobs/failed_jobs/#{execution.id}/arguments",
params: { arguments: { user_id: 1 }.to_json }
expect(response).to redirect_to("/jobs/failed_jobs")
follow_redirect!
expect(response.body).to include("Could not update job")
end
end
end