Skip to content

Commit 0fa2db6

Browse files
Merge pull request #45 from eclectic-coding/feat/recurring-task-run-now
feat: recurring task Run Now
2 parents 79fa235 + d19201a commit 0fa2db6

7 files changed

Lines changed: 108 additions & 3 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Added
1111

12+
- Recurring task "Run Now" — a "Run Now" button on the Recurring Tasks page triggers `task.enqueue(at: Time.current)` to enqueue the job immediately without waiting for its next scheduled run; SolidQueue's `RecurringExecution` deduplication prevents double-enqueuing
1213
- Read replica support — when `connects_to` is set to `{ reading: <role>, writing: <role> }`, the engine automatically routes GET requests to the reading role and mutating requests (POST/DELETE/PATCH) to the writing role via `ActiveRecord::Base.connected_to(role:)`; passing any other hash (e.g. `{ role: :writing }`, `{ shard: :name }`) falls through to `connected_to` directly; defaults to `nil` so single-database setups are unaffected
1314
- Webhook alert config — `alert_webhook_url` and `alert_failure_threshold` settings POST a JSON payload (`event`, `failure_count`, `threshold`, `fired_at`) to any URL when the failed job count meets or exceeds the threshold; fires asynchronously in a background thread so dashboard requests are never blocked; a configurable `alert_webhook_cooldown` (default 3600 s) prevents repeated alerts while the count stays elevated; HTTP errors are logged and swallowed
1415
- Bulk retry with delay — "+5s", "+10s", "+30s", and "+1m" stagger buttons on the Failed Jobs page retry all matched jobs with a configurable interval between each; the first job runs immediately, subsequent jobs are scheduled at incremental offsets; uses per-execution `retry` so `scheduled_at` is respected by SolidQueue's dispatcher; buttons only appear when more than one job is present

README.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ SolidQueueWeb surfaces all of this in a browser UI available at any route you ch
4040
- **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
4141
- **Job detail** — full arguments, timestamps, blocked-until date, and error backtrace; action buttons based on job status
4242
- **Queue management** — pause and resume individual queues; queue-scoped job list with status filter, search, and discard
43-
- **Recurring tasks** — all configured recurring tasks with cron schedule, next run time, last run time, and static/dynamic classification
43+
- **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
4444
- **Processes** — workers, dispatchers, and supervisors with heartbeat health status; auto-refreshes every 10 seconds
4545
- **Global search** — search across all job statuses at once by class name substring; results grouped by status with match count and direct links to filtered views; native datalist autocomplete pre-populated from all known job classes; auto-submits on selection
4646
- **Targeted bulk actions** — checkboxes on the jobs and failed jobs lists for selecting individual rows; selection bar shows count and action buttons ("Discard Selected" for jobs, "Retry Selected" / "Discard Selected" for failed jobs); select-all checkbox in the table header
@@ -165,7 +165,6 @@ Planned features, roughly ordered by priority:
165165

166166
**Operations**
167167
- Admin audit log — record who retried or discarded which jobs and when (requires host-app user identity)
168-
- Recurring task "Run Now" — manually trigger a recurring task immediately without waiting for its next scheduled run
169168
- Failed job retry with modified arguments — edit the arguments JSON from the job detail page before retrying; useful for correcting bad payloads without redeploying
170169
- Bulk scheduled job actions — "Run All Now" button on the Scheduled tab, mirroring the "Retry All" pattern on the Failed Jobs page
171170

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
module SolidQueueWeb
2+
class RecurringTasks::RunsController < ApplicationController
3+
def create
4+
task = SolidQueue::RecurringTask.find_by!(key: params[:recurring_task_key])
5+
result = task.enqueue(at: Time.current)
6+
7+
if result
8+
redirect_to recurring_tasks_path, notice: "\"#{task.key}\" queued for immediate execution."
9+
else
10+
redirect_to recurring_tasks_path, alert: "Could not enqueue \"#{task.key}\" — it may have just run."
11+
end
12+
rescue ActiveRecord::RecordNotFound
13+
redirect_to recurring_tasks_path, alert: "Recurring task not found."
14+
rescue => e
15+
redirect_to recurring_tasks_path, alert: "Could not run task: #{e.message}"
16+
end
17+
end
18+
end

app/views/solid_queue_web/recurring_tasks/index.html.erb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
<th scope="col">Next Run</th>
1515
<th scope="col">Last Run</th>
1616
<th scope="col">Type</th>
17+
<th scope="col"><span class="sqd-sr-only">Actions</span></th>
1718
</tr>
1819
</thead>
1920
<tbody>
@@ -60,6 +61,12 @@
6061
<span class="sqd-badge sqd-badge--dynamic">Dynamic</span>
6162
<% end %>
6263
</td>
64+
<td class="sqd-row-actions">
65+
<%= button_to "Run Now", recurring_task_run_path(task.key),
66+
method: :post,
67+
class: "sqd-btn sqd-btn--primary sqd-btn--sm",
68+
data: { confirm: "Run \"#{task.key}\" immediately?" } %>
69+
</td>
6370
</tr>
6471
<% end %>
6572
</tbody>

config/routes.rb

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55
get "search", to: "search#index", as: :search
66
get "history", to: "history#index", as: :history
77

8-
resources :recurring_tasks, only: [:index]
8+
resources :recurring_tasks, only: [:index], param: :key do
9+
resource :run, only: [:create], controller: "recurring_tasks/runs"
10+
end
911
resources :processes, only: [:index]
1012
resources :queues, only: [:index], param: :name do
1113
resource :pause, only: [:create, :destroy], controller: "queues/pauses"

spec/dummy/db/seeds.rb

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
solid_queue_blocked_executions
2121
solid_queue_ready_executions
2222
solid_queue_recurring_executions
23+
solid_queue_recurring_tasks
2324
solid_queue_jobs
2425
solid_queue_processes
2526
solid_queue_semaphores
@@ -208,6 +209,19 @@
208209
end
209210
end
210211

212+
puts "Seeding recurring tasks..."
213+
recurring_tasks_data = [
214+
{ key: "nightly-cleanup", schedule: "0 2 * * *", command: "CleanupJob.perform_later", queue_name: "default", description: "Nightly database cleanup", static: true },
215+
{ key: "hourly-data-sync", schedule: "0 * * * *", command: "DataSyncJob.perform_later", queue_name: "default", description: "Sync data from external APIs every hour", static: true },
216+
{ key: "weekly-report", schedule: "0 8 * * 1", command: "ReportGeneratorJob.perform_later", queue_name: "low_priority", description: "Generate weekly summary report on Monday morning", static: true },
217+
{ key: "send-digest-email", schedule: "0 9 * * *", command: "UserMailerJob.perform_later", queue_name: "mailers", description: "Daily digest email to all users", static: true },
218+
{ key: "export-invoices", schedule: "30 1 1 * *", command: "InvoiceGeneratorJob.perform_later", queue_name: "critical", description: "Monthly invoice export on the 1st", static: true },
219+
{ key: "purge-old-images", schedule: "0 3 * * 0", command: "ImageProcessingJob.perform_later", queue_name: "low_priority", description: "Weekly purge of unattached image uploads", static: true },
220+
{ key: "dynamic-notification", schedule: "*/15 * * * *", command: "NotificationJob.perform_later", queue_name: "mailers", description: "Push notifications every 15 minutes", static: false }
221+
]
222+
223+
recurring_tasks_data.each { |attrs| SolidQueue::RecurringTask.create!(attrs) }
224+
211225
puts "Done! Created:"
212226
puts " #{SolidQueue::ReadyExecution.count} ready jobs"
213227
puts " #{SolidQueue::ScheduledExecution.count} scheduled jobs"
@@ -216,3 +230,4 @@
216230
puts " #{SolidQueue::FailedExecution.count} failed jobs"
217231
puts " #{SolidQueue::Process.count} processes"
218232
puts " #{SolidQueue::Job.where.not(finished_at: nil).count} finished jobs"
233+
puts " #{SolidQueue::RecurringTask.count} recurring tasks"
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
require "rails_helper"
2+
3+
RSpec.describe "RecurringTasks::Runs", type: :request do
4+
let!(:task) do
5+
SolidQueue::RecurringTask.create!(
6+
key: "cleanup-task",
7+
schedule: "0 2 * * *",
8+
command: "CleanupJob.perform_later"
9+
)
10+
end
11+
12+
before do
13+
allow_any_instance_of(SolidQueue::RecurringTask).to receive(:enqueue)
14+
.with(at: instance_of(ActiveSupport::TimeWithZone))
15+
.and_return(double("job", job_id: SecureRandom.uuid))
16+
end
17+
18+
describe "POST /jobs/recurring_tasks/:key/run" do
19+
it "redirects to the recurring tasks page" do
20+
post "/jobs/recurring_tasks/cleanup-task/run"
21+
expect(response).to redirect_to("/jobs/recurring_tasks")
22+
end
23+
24+
it "shows a success notice" do
25+
post "/jobs/recurring_tasks/cleanup-task/run"
26+
follow_redirect!
27+
expect(response.body).to include("queued for immediate execution")
28+
end
29+
30+
it "includes the task key in the notice" do
31+
post "/jobs/recurring_tasks/cleanup-task/run"
32+
follow_redirect!
33+
expect(response.body).to include("cleanup-task")
34+
end
35+
36+
it "renders a Run Now button on the index page" do
37+
get "/jobs/recurring_tasks"
38+
expect(response.body).to include("Run Now")
39+
end
40+
41+
it "returns 404 redirect for an unknown key" do
42+
post "/jobs/recurring_tasks/nonexistent-task/run"
43+
expect(response).to redirect_to("/jobs/recurring_tasks")
44+
follow_redirect!
45+
expect(response.body).to include("Recurring task not found")
46+
end
47+
48+
it "shows an alert when enqueue returns false" do
49+
allow_any_instance_of(SolidQueue::RecurringTask).to receive(:enqueue).and_return(false)
50+
post "/jobs/recurring_tasks/cleanup-task/run"
51+
follow_redirect!
52+
expect(response.body).to include("Could not enqueue")
53+
end
54+
55+
it "handles unexpected errors gracefully" do
56+
allow_any_instance_of(SolidQueue::RecurringTask).to receive(:enqueue).and_raise(RuntimeError, "boom")
57+
post "/jobs/recurring_tasks/cleanup-task/run"
58+
expect(response).to redirect_to("/jobs/recurring_tasks")
59+
follow_redirect!
60+
expect(response.body).to include("Could not run task")
61+
end
62+
end
63+
end

0 commit comments

Comments
 (0)