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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,7 @@ The dashboard provides several views:
- **Overview**: Shows statistics, recent jobs, and job activity chart
- **Ready Jobs**: Jobs that are ready to be executed
- **Scheduled Jobs**: Jobs scheduled for future execution with execute and reject actions
- **Recurring Jobs**: Jobs that run on a recurring schedule
- **Recurring Jobs**: Jobs that run on a recurring schedule, with a Run now action that enqueues one task immediately without changing its schedule
- **Failed Jobs**: Jobs that have failed with error details and retry/discard actions
- **Queues**: Distribution of jobs across different queues with pause/resume controls
- **Workers**: Real-time monitoring of all Solid Queue processes with health status
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,11 @@ def index
@sort = sort_params
@action_path = recurring_jobs_path
end

def enqueue
result = SolidQueueMonitor::EnqueueRecurringTaskService.call(params[:id])
set_flash_message(result[:message], result[:success] ? 'success' : 'error')
redirect_to recurring_jobs_path
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# frozen_string_literal: true

module SolidQueueMonitor
class EnqueueRecurringTaskService
class << self
def call(id)
task = SolidQueue::RecurringTask.find(id)
enqueued_job = task.enqueue(at: Time.current)

if enqueued?(enqueued_job)
{ success: true, message: "Recurring task '#{task.key}' has been queued to run now" }
else
{ success: false, message: "Could not enqueue recurring task '#{task.key}'. It may already be queued for this time" }
end
rescue ActiveRecord::RecordNotFound
{ success: false, message: 'Recurring task not found' }
rescue StandardError => e
{ success: false, message: "Failed to enqueue recurring task: #{e.message}" }
end

private

def enqueued?(enqueued_job)
enqueued_job.respond_to?(:successfully_enqueued?) && enqueued_job.successfully_enqueued?
end
end
end
end
9 changes: 9 additions & 0 deletions app/views/solid_queue_monitor/recurring_jobs/_row.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,13 @@
<td><%= queue_link(job.queue_name) %></td>
<td><%= job.priority || 'Default' %></td>
<td><%= format_datetime(job.updated_at) %></td>
<td class="actions-cell">
<form method="post"
action="<%= enqueue_recurring_job_path(job) %>"
class="inline-form"
data-confirm="Enqueue <%= job.key %> to run now? Its schedule will not change.">
<%= csrf_token_field_if_enabled %>
<button type="submit" class="action-button execute-button">Run now</button>
</form>
</td>
</tr>
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
{ sort_key: nil, label: 'Schedule' },
{ sort_key: :queue_name, label: 'Queue' },
{ sort_key: :priority, label: 'Priority' },
{ sort_key: nil, label: 'Last Updated' }
{ sort_key: nil, label: 'Last Updated' },
{ sort_key: nil, label: 'Actions' }
] %>

<%= render 'solid_queue_monitor/shared/jobs_table',
Expand Down
4 changes: 3 additions & 1 deletion config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@

resources :ready_jobs, only: [:index]
resources :scheduled_jobs, only: [:index]
resources :recurring_jobs, only: [:index]
resources :recurring_jobs, only: [:index] do
post :enqueue, on: :member
end
resources :failed_jobs, only: [:index]
resources :in_progress_jobs, only: [:index]
resources :queues, only: [:index]
Expand Down
54 changes: 52 additions & 2 deletions spec/requests/solid_queue_monitor/recurring_jobs_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,18 @@

RSpec.describe 'Recurring Jobs' do
describe 'GET /recurring_jobs' do
let!(:recurring_task1) do
let(:recurring_task1) do
create(:solid_queue_recurring_task, key: 'daily_cleanup', class_name: 'DailyCleanupJob')
end
let!(:recurring_task2) do
let(:recurring_task2) do
create(:solid_queue_recurring_task, key: 'hourly_metrics', class_name: 'HourlyMetricsJob')
end

before do
recurring_task1
recurring_task2
end

it 'returns a successful response' do
get '/recurring_jobs'

Expand All @@ -28,6 +33,7 @@

expect(response.body).to include('daily_cleanup')
expect(response.body).to include('DailyCleanupJob')
expect(response.body).to include('Run now')
end

context 'with no recurring tasks' do
Expand All @@ -51,4 +57,48 @@
end
end
end

describe 'POST /recurring_jobs/:id/enqueue' do
include ActiveSupport::Testing::TimeHelpers

let(:recurring_task) do
create(:solid_queue_recurring_task, key: 'daily_cleanup', class_name: 'SolidQueueMonitorEnqueueProbeJob')
end

context 'when the task exists' do
before { recurring_task }

it 'enqueues the task immediately and redirects' do
expect do
post enqueue_recurring_job_path(recurring_task)
end.to change(SolidQueue::Job, :count).by(1)
.and change(SolidQueue::ReadyExecution, :count).by(1)

expect(response).to redirect_to('/recurring_jobs')

job = SolidQueue::Job.last
expect(job.class_name).to eq('SolidQueueMonitorEnqueueProbeJob')
expect(job.queue_name).to eq(recurring_task.queue_name)
expect(recurring_task.reload.schedule).to eq('0 * * * *')
end

it 'does not enqueue a second copy for the same timestamp' do
travel_to Time.zone.local(2026, 10, 7, 12, 0, 0) do
post enqueue_recurring_job_path(recurring_task)

expect do
post enqueue_recurring_job_path(recurring_task)
end.not_to change(SolidQueue::Job, :count)
end

expect(response).to redirect_to('/recurring_jobs')
end
end

it 'redirects with an error when the task does not exist' do
post enqueue_recurring_job_path(0)

expect(response).to redirect_to('/recurring_jobs')
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe SolidQueueMonitor::EnqueueRecurringTaskService do
describe '.call' do
let(:recurring_task) do
create(:solid_queue_recurring_task,
key: 'hourly_metrics',
class_name: 'SolidQueueMonitorEnqueueProbeJob',
queue_name: 'metrics',
priority: 3)
end

context 'when the task exists' do
before { recurring_task }

it 'enqueues the recurring task into the ready queue' do
result = nil

expect do
result = described_class.call(recurring_task.id)
end.to change(SolidQueue::ReadyExecution, :count).by(1)

expect(result[:success]).to be(true)
expect(result[:message]).to include('hourly_metrics')

ready_execution = SolidQueue::ReadyExecution.last
expect(ready_execution.queue_name).to eq('metrics')
expect(ready_execution.priority).to eq(3)
expect(ready_execution.job.class_name).to eq('SolidQueueMonitorEnqueueProbeJob')
end

it 'returns an error when enqueue fails' do
task = SolidQueue::RecurringTask.find(recurring_task.id)
allow(SolidQueue::RecurringTask).to receive(:find).and_return(task)
allow(task).to receive(:enqueue).and_raise(StandardError, 'missing job class')

result = described_class.call(recurring_task.id)

expect(result[:success]).to be(false)
expect(result[:message]).to include('missing job class')
end
end

it 'returns an error when the task does not exist' do
result = described_class.call(0)

expect(result[:success]).to be(false)
expect(result[:message]).to eq('Recurring task not found')
end
end
end
10 changes: 10 additions & 0 deletions spec/spec_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,16 @@
t.datetime :created_at, null: false
end

create_table :solid_queue_recurring_executions, force: true do |t|
t.integer :job_id, null: false
t.string :task_key, null: false
t.datetime :run_at, null: false
t.datetime :created_at, null: false
end

add_index :solid_queue_recurring_executions, :job_id, unique: true
add_index :solid_queue_recurring_executions, %i[task_key run_at], unique: true

create_table :solid_queue_recurring_tasks, force: true do |t|
t.string :key, null: false
t.string :schedule, null: false
Expand Down
23 changes: 23 additions & 0 deletions spec/support/solid_queue_stubs.rb
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,31 @@ class Pause < ApplicationRecord
self.table_name = 'solid_queue_pauses'
end

class RecurringExecution < ApplicationRecord
self.table_name = 'solid_queue_recurring_executions'
end

class RecurringTask < ApplicationRecord
self.table_name = 'solid_queue_recurring_tasks'

# Mirrors SolidQueue::RecurringTask#enqueue for the test double.
# The real model records one execution per task and timestamp, then
# places the job on the ready queue immediately.
def enqueue(at:)
return false if RecurringExecution.exists?(task_key: key, run_at: at)

job = Job.create!(
queue_name: queue_name.presence || 'default',
class_name: class_name.presence || 'SolidQueue::RecurringJob',
arguments: arguments,
priority: priority || 0,
active_job_id: SecureRandom.uuid
)
ReadyExecution.create!(job: job, queue_name: job.queue_name, priority: job.priority)
RecurringExecution.create!(job_id: job.id, task_key: key, run_at: at)

Struct.new(:successfully_enqueued?).new(true)
end
end

class Process < ApplicationRecord
Expand Down
Loading