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
22 changes: 22 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,27 @@
# Changelog

## [0.3.2] - 2025-06-12

### Added

- Added reject functionality for scheduled jobs with bulk operations support
- New "Reject Selected" button in scheduled jobs view alongside "Execute Selected"
- Added `RejectJobService` for handling job rejection logic
- Added confirmation dialog for reject operations to prevent accidental job cancellation
- Added `POST /reject_jobs` route for bulk rejection operations

### Improved

- Enhanced scheduled jobs UI with dual action buttons (Execute/Reject)
- Improved JavaScript form handling to prevent duplicate job ID submissions
- Added proper error handling and success messaging for reject operations
- Optimized button state management for better user experience

### Fixed

- Fixed duplicate job ID issue in form submissions for bulk operations
- Corrected JavaScript form submission logic to prevent parameter duplication

## [0.3.1] - 2024-03-28

### Improved
Expand Down
6 changes: 1 addition & 5 deletions Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
PATH
remote: .
specs:
solid_queue_monitor (0.2.0)
solid_queue_monitor (0.3.2)
rails (>= 7.0)
solid_queue (>= 0.1.0)

Expand Down Expand Up @@ -255,9 +255,6 @@ GEM
fugit (~> 1.11.0)
railties (>= 7.1)
thor (~> 1.3.1)
sqlite3 (2.6.0)
mini_portile2 (~> 2.8.0)
sqlite3 (2.6.0-arm64-darwin)
stringio (3.1.5)
thor (1.3.2)
timeout (0.4.3)
Expand Down Expand Up @@ -285,7 +282,6 @@ DEPENDENCIES
rubocop-rails
rubocop-rspec
solid_queue_monitor!
sqlite3

BUNDLED WITH
2.6.2
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,12 @@ A lightweight, zero-dependency web interface for monitoring Solid Queue backgrou
- **Dashboard Overview**: Get a quick snapshot of your queue's health with statistics on all job types
- **Ready Jobs**: View jobs that are ready to be executed
- **In Progress Jobs**: Monitor jobs currently being processed by workers
- **Scheduled Jobs**: See upcoming jobs scheduled for future execution
- **Scheduled Jobs**: See upcoming jobs scheduled for future execution with ability to execute immediately or reject permanently
- **Recurring Jobs**: Manage periodic jobs that run on a schedule
- **Failed Jobs**: Track and debug failed jobs, with the ability to retry or discard them
- **Queue Management**: View and filter jobs by queue
- **Advanced Job Filtering**: Filter jobs by class name, queue, status, and job arguments
- **Quick Actions**: Retry or discard failed jobs directly from any view
- **Quick Actions**: Retry or discard failed jobs, execute or reject scheduled jobs directly from any view
- **Performance Optimized**: Designed for high-volume applications with smart pagination
- **Optional Authentication**: Secure your dashboard with HTTP Basic Authentication
- **Responsive Design**: Works on desktop and mobile devices
Expand All @@ -44,7 +44,7 @@ A lightweight, zero-dependency web interface for monitoring Solid Queue backgrou
Add this line to your application's Gemfile:

```ruby
gem 'solid_queue_monitor', '~> 0.3.1'
gem 'solid_queue_monitor', '~> 0.3.2'
```

Then execute:
Expand Down Expand Up @@ -103,9 +103,9 @@ The dashboard provides several views:

- **Overview**: Shows statistics and recent jobs
- **Ready Jobs**: Jobs that are ready to be executed
- **Scheduled Jobs**: Jobs scheduled for future execution
- **Scheduled Jobs**: Jobs scheduled for future execution with execute and reject actions
- **Recurring Jobs**: Jobs that run on a recurring schedule
- **Failed Jobs**: Jobs that have failed with error details
- **Failed Jobs**: Jobs that have failed with error details and retry/discard actions
- **Queues**: Distribution of jobs across different queues

### API-only Applications
Expand All @@ -127,7 +127,7 @@ This makes it easy to find specific jobs when debugging issues in your applicati

- **Production Monitoring**: Keep an eye on your background job processing in production environments
- **Debugging**: Quickly identify and troubleshoot failed jobs
- **Job Management**: Execute scheduled jobs on demand when needed
- **Job Management**: Execute scheduled jobs on demand or reject unwanted jobs permanently
- **Performance Analysis**: Track job distribution and identify bottlenecks
- **DevOps Integration**: Easily integrate with your monitoring stack

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,16 @@ def create
end
redirect_to scheduled_jobs_path
end

def reject_all
result = SolidQueueMonitor::RejectJobService.new.reject_many(params[:job_ids])

if result[:success]
set_flash_message(result[:message], 'success')
else
set_flash_message(result[:message], 'error')
end
redirect_to scheduled_jobs_path
end
end
end
47 changes: 38 additions & 9 deletions app/presenters/solid_queue_monitor/scheduled_jobs_presenter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -46,31 +46,33 @@ def generate_filter_form

<div class="bulk-actions-bar">
<button type="button" class="action-button execute-button" id="execute-selected-top" disabled>Execute Selected</button>
<button type="button" class="action-button discard-button" id="reject-selected-top" disabled>Reject Selected</button>
</div>
HTML
end

def generate_table_with_actions
<<-HTML
<form id="scheduled-jobs-form" action="#{execute_jobs_path}" method="POST">
<form id="scheduled-jobs-form" method="POST">
#{generate_table}
</form>
<script>
document.addEventListener('DOMContentLoaded', function() {
const selectAllCheckbox = document.querySelector('th input[type="checkbox"]');
const jobCheckboxes = document.getElementsByName('job_ids[]');
const executeButton = document.getElementById('execute-selected-top');
const rejectButton = document.getElementById('reject-selected-top');
const form = document.getElementById('scheduled-jobs-form');
#{' '}
selectAllCheckbox.addEventListener('change', function() {
jobCheckboxes.forEach(checkbox => checkbox.checked = this.checked);
updateExecuteButton();
updateButtonStates();
});

jobCheckboxes.forEach(checkbox => {
checkbox.addEventListener('change', function() {
selectAllCheckbox.checked = Array.from(jobCheckboxes).every(cb => cb.checked);
updateExecuteButton();
updateButtonStates();
});
});
#{' '}
Expand All @@ -79,6 +81,31 @@ def generate_table_with_actions
const selectedIds = Array.from(document.querySelectorAll('input[name="job_ids[]"]:checked')).map(cb => cb.value);
if (selectedIds.length === 0) return;
#{' '}
submitForm('#{execute_jobs_path}', selectedIds);
});
#{' '}
// Add event listener for the reject button
rejectButton.addEventListener('click', function() {
const selectedIds = Array.from(document.querySelectorAll('input[name="job_ids[]"]:checked')).map(cb => cb.value);
if (selectedIds.length === 0) return;
#{' '}
if (confirm('Are you sure you want to reject the selected jobs? This action cannot be undone.')) {
submitForm('#{reject_jobs_path}', selectedIds);
}
});
#{' '}
function submitForm(actionUrl, selectedIds) {
// Uncheck all checkboxes to prevent duplicate submission
document.querySelectorAll('input[name="job_ids[]"]').forEach(checkbox => {
checkbox.checked = false;
});

// Clear any existing hidden inputs
document.querySelectorAll('input[type="hidden"][name="job_ids[]"]').forEach(input => input.remove());

// Set form action
form.action = actionUrl;

// Add selected IDs as hidden inputs
selectedIds.forEach(id => {
const input = document.createElement('input');
Expand All @@ -87,18 +114,20 @@ def generate_table_with_actions
input.value = id;
form.appendChild(input);
});
#{' '}

// Submit the form
form.submit();
});
}
#{' '}
function updateExecuteButton() {
function updateButtonStates() {
const checkboxes = document.getElementsByName('job_ids[]');
const checked = Array.from(checkboxes).some(cb => cb.checked);
executeButton.disabled = !checked;
rejectButton.disabled = !checked;
}
#{' '}
// Initialize button state
updateExecuteButton();
// Initialize button states
updateButtonStates();
});
</script>
HTML
Expand Down Expand Up @@ -130,7 +159,7 @@ def generate_row(execution)
<<-HTML
<tr>
<td>
<input type="checkbox" name="job_ids[]" value="#{execution.id}" onchange="updateExecuteButton()">
<input type="checkbox" name="job_ids[]" value="#{execution.id}">
</td>
<td>#{execution.job.class_name}</td>
<td>#{execution.queue_name}</td>
Expand Down
49 changes: 49 additions & 0 deletions app/services/solid_queue_monitor/reject_job_service.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# frozen_string_literal: true

module SolidQueueMonitor
class RejectJobService
def call(id)
execution = SolidQueue::ScheduledExecution.find(id)
reject_job(execution)
end

def reject_many(ids)
return { success: false, message: 'No jobs selected' } if ids.blank?

success_count = 0
failed_count = 0

ids.each do |id|
execution = SolidQueue::ScheduledExecution.find_by(id: id)
if execution
reject_job(execution)
success_count += 1
else
failed_count += 1
end
rescue StandardError
failed_count += 1
end

if success_count.positive? && failed_count.zero?
{ success: true, message: 'All selected jobs have been rejected' }
elsif success_count.positive? && failed_count.positive?
{ success: true, message: "#{success_count} jobs rejected, #{failed_count} failed" }
else
{ success: false, message: 'Failed to reject jobs' }
end
end

private

def reject_job(execution)
ActiveRecord::Base.transaction do
# Mark the associated job as finished to indicate it was rejected
execution.job.update!(finished_at: Time.current)

# Remove the scheduled execution
execution.destroy
end
end
end
end
1 change: 1 addition & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
resources :queues, only: [:index]

post 'execute_jobs', to: 'scheduled_jobs#create', as: :execute_jobs
post 'reject_jobs', to: 'scheduled_jobs#reject_all', as: :reject_jobs

post 'retry_failed_job/:id', to: 'failed_jobs#retry', as: :retry_failed_job
post 'discard_failed_job/:id', to: 'failed_jobs#discard', as: :discard_failed_job
Expand Down
2 changes: 1 addition & 1 deletion lib/solid_queue_monitor/version.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# frozen_string_literal: true

module SolidQueueMonitor
VERSION = '0.3.1'
VERSION = '0.3.2'
end
79 changes: 79 additions & 0 deletions spec/services/solid_queue_monitor/reject_job_service_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe SolidQueueMonitor::RejectJobService do
describe '#reject_many' do
subject { described_class.new }

let!(:scheduled_execution1) { create(:solid_queue_scheduled_execution) }
let!(:scheduled_execution2) { create(:solid_queue_scheduled_execution) }

it 'rejects scheduled jobs and marks them as finished' do
expect do
result = subject.reject_many([scheduled_execution1.id, scheduled_execution2.id])
expect(result[:success]).to be true
end.to change(SolidQueue::ScheduledExecution, :count).by(-2)
end

it 'marks associated jobs as finished when rejecting' do
subject.reject_many([scheduled_execution1.id])

job = scheduled_execution1.job.reload
expect(job.finished_at).to be_present
end

it 'returns success message when all jobs are rejected successfully' do
result = subject.reject_many([scheduled_execution1.id, scheduled_execution2.id])

expect(result[:success]).to be true
expect(result[:message]).to eq('All selected jobs have been rejected')
end

it 'handles non-existent job IDs gracefully' do
result = subject.reject_many([999_999])

expect(result[:success]).to be false
expect(result[:message]).to eq('Failed to reject jobs')
end

it 'handles empty job IDs array gracefully' do
result = subject.reject_many([])

expect(result[:success]).to be false
expect(result[:message]).to eq('No jobs selected')
end

it 'handles mix of valid and invalid job IDs' do
result = subject.reject_many([scheduled_execution1.id, 999_999])

expect(result[:success]).to be true
expect(result[:message]).to include('1 jobs rejected, 1 failed')
end

it 'removes scheduled execution from database' do
subject.reject_many([scheduled_execution1.id])

expect(SolidQueue::ScheduledExecution.find_by(id: scheduled_execution1.id)).to be_nil
end
end

describe '#call' do
subject { described_class.new }

let!(:scheduled_execution) { create(:solid_queue_scheduled_execution) }

it 'rejects a single scheduled job' do
expect do
subject.call(scheduled_execution.id)
end.to change(SolidQueue::ScheduledExecution, :count).by(-1)
end

it 'marks the job as finished' do
subject.call(scheduled_execution.id)

job = scheduled_execution.job.reload
expect(job.finished_at).to be_present
end
end
end