From e30d19142f9c08867c06fa1b93d41c3769dc89d9 Mon Sep 17 00:00:00 2001 From: Chuck Smith Date: Thu, 21 May 2026 15:34:50 -0400 Subject: [PATCH 1/3] feat: retry failed job with modified arguments Adds an editable textarea to the Arguments card on the job detail page for failed jobs. Submitting patches the job record then retries in one step; invalid JSON redirects back with an alert without touching the failed execution. Co-Authored-By: Claude Sonnet 4.6 --- .../stylesheets/solid_queue_web/_07_forms.css | 24 +++++++ .../failed_jobs/arguments_controller.rb | 15 +++++ app/views/solid_queue_web/jobs/show.html.erb | 14 +++- config/routes.rb | 1 + .../failed_job_arguments_spec.rb | 67 +++++++++++++++++++ 5 files changed, 120 insertions(+), 1 deletion(-) create mode 100644 app/controllers/solid_queue_web/failed_jobs/arguments_controller.rb create mode 100644 spec/requests/solid_queue_web/failed_job_arguments_spec.rb diff --git a/app/assets/stylesheets/solid_queue_web/_07_forms.css b/app/assets/stylesheets/solid_queue_web/_07_forms.css index 5f4dabc..6ab5e18 100644 --- a/app/assets/stylesheets/solid_queue_web/_07_forms.css +++ b/app/assets/stylesheets/solid_queue_web/_07_forms.css @@ -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; } \ No newline at end of file diff --git a/app/controllers/solid_queue_web/failed_jobs/arguments_controller.rb b/app/controllers/solid_queue_web/failed_jobs/arguments_controller.rb new file mode 100644 index 0000000..1384185 --- /dev/null +++ b/app/controllers/solid_queue_web/failed_jobs/arguments_controller.rb @@ -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 diff --git a/app/views/solid_queue_web/jobs/show.html.erb b/app/views/solid_queue_web/jobs/show.html.erb index 6a8acfa..31d0df6 100644 --- a/app/views/solid_queue_web/jobs/show.html.erb +++ b/app/views/solid_queue_web/jobs/show.html.erb @@ -63,7 +63,19 @@

Arguments

-
<%= JSON.pretty_generate(@job.arguments) rescue @job.arguments.inspect %>
+ <% 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 %> +
<%= JSON.pretty_generate(@job.arguments) rescue @job.arguments.inspect %>
+ <% end %>
diff --git a/config/routes.rb b/config/routes.rb index 86cfd55..3e10857 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -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 diff --git a/spec/requests/solid_queue_web/failed_job_arguments_spec.rb b/spec/requests/solid_queue_web/failed_job_arguments_spec.rb new file mode 100644 index 0000000..31b7128 --- /dev/null +++ b/spec/requests/solid_queue_web/failed_job_arguments_spec.rb @@ -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 From ffeb86209dccf14fef13ee797ce63d79d71b2325 Mon Sep 17 00:00:00 2001 From: Chuck Smith Date: Thu, 21 May 2026 15:34:57 -0400 Subject: [PATCH 2/3] docs: add CHANGELOG entry for retry with modified arguments Co-Authored-By: Claude Sonnet 4.6 --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0027fb9..e61eafb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 From e3694c28fe71bff92e86de4602b2eadd9709730c Mon Sep 17 00:00:00 2001 From: Chuck Smith Date: Thu, 21 May 2026 16:21:38 -0400 Subject: [PATCH 3/3] docs: update README and ROADMAP for retry with modified arguments Co-Authored-By: Claude Sonnet 4.6 --- README.md | 2 +- ROADMAP.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 5a844e4..5cf32ca 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/ROADMAP.md b/ROADMAP.md index 1d8e39a..54b3c6a 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -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 }`). |