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 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 }`). | 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