Skip to content

Commit 31fce34

Browse files
authored
feat: added reject option for schedule jobs (#11)
* added reject option for schedule jobs * rubocop fixes * updated readme file * fixed rubocop
1 parent 2ed8c23 commit 31fce34

9 files changed

Lines changed: 208 additions & 21 deletions

File tree

CHANGELOG.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,27 @@
11
# Changelog
22

3+
## [0.3.2] - 2025-06-12
4+
5+
### Added
6+
7+
- Added reject functionality for scheduled jobs with bulk operations support
8+
- New "Reject Selected" button in scheduled jobs view alongside "Execute Selected"
9+
- Added `RejectJobService` for handling job rejection logic
10+
- Added confirmation dialog for reject operations to prevent accidental job cancellation
11+
- Added `POST /reject_jobs` route for bulk rejection operations
12+
13+
### Improved
14+
15+
- Enhanced scheduled jobs UI with dual action buttons (Execute/Reject)
16+
- Improved JavaScript form handling to prevent duplicate job ID submissions
17+
- Added proper error handling and success messaging for reject operations
18+
- Optimized button state management for better user experience
19+
20+
### Fixed
21+
22+
- Fixed duplicate job ID issue in form submissions for bulk operations
23+
- Corrected JavaScript form submission logic to prevent parameter duplication
24+
325
## [0.3.1] - 2024-03-28
426

527
### Improved

Gemfile.lock

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
PATH
22
remote: .
33
specs:
4-
solid_queue_monitor (0.2.0)
4+
solid_queue_monitor (0.3.2)
55
rails (>= 7.0)
66
solid_queue (>= 0.1.0)
77

@@ -255,9 +255,6 @@ GEM
255255
fugit (~> 1.11.0)
256256
railties (>= 7.1)
257257
thor (~> 1.3.1)
258-
sqlite3 (2.6.0)
259-
mini_portile2 (~> 2.8.0)
260-
sqlite3 (2.6.0-arm64-darwin)
261258
stringio (3.1.5)
262259
thor (1.3.2)
263260
timeout (0.4.3)
@@ -285,7 +282,6 @@ DEPENDENCIES
285282
rubocop-rails
286283
rubocop-rspec
287284
solid_queue_monitor!
288-
sqlite3
289285

290286
BUNDLED WITH
291287
2.6.2

README.md

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

4646
```ruby
47-
gem 'solid_queue_monitor', '~> 0.3.1'
47+
gem 'solid_queue_monitor', '~> 0.3.2'
4848
```
4949

5050
Then execute:
@@ -103,9 +103,9 @@ The dashboard provides several views:
103103

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

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

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

app/controllers/solid_queue_monitor/scheduled_jobs_controller.rb

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,5 +21,16 @@ def create
2121
end
2222
redirect_to scheduled_jobs_path
2323
end
24+
25+
def reject_all
26+
result = SolidQueueMonitor::RejectJobService.new.reject_many(params[:job_ids])
27+
28+
if result[:success]
29+
set_flash_message(result[:message], 'success')
30+
else
31+
set_flash_message(result[:message], 'error')
32+
end
33+
redirect_to scheduled_jobs_path
34+
end
2435
end
2536
end

app/presenters/solid_queue_monitor/scheduled_jobs_presenter.rb

Lines changed: 38 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -46,31 +46,33 @@ def generate_filter_form
4646
4747
<div class="bulk-actions-bar">
4848
<button type="button" class="action-button execute-button" id="execute-selected-top" disabled>Execute Selected</button>
49+
<button type="button" class="action-button discard-button" id="reject-selected-top" disabled>Reject Selected</button>
4950
</div>
5051
HTML
5152
end
5253

5354
def generate_table_with_actions
5455
<<-HTML
55-
<form id="scheduled-jobs-form" action="#{execute_jobs_path}" method="POST">
56+
<form id="scheduled-jobs-form" method="POST">
5657
#{generate_table}
5758
</form>
5859
<script>
5960
document.addEventListener('DOMContentLoaded', function() {
6061
const selectAllCheckbox = document.querySelector('th input[type="checkbox"]');
6162
const jobCheckboxes = document.getElementsByName('job_ids[]');
6263
const executeButton = document.getElementById('execute-selected-top');
64+
const rejectButton = document.getElementById('reject-selected-top');
6365
const form = document.getElementById('scheduled-jobs-form');
6466
#{' '}
6567
selectAllCheckbox.addEventListener('change', function() {
6668
jobCheckboxes.forEach(checkbox => checkbox.checked = this.checked);
67-
updateExecuteButton();
69+
updateButtonStates();
6870
});
6971
7072
jobCheckboxes.forEach(checkbox => {
7173
checkbox.addEventListener('change', function() {
7274
selectAllCheckbox.checked = Array.from(jobCheckboxes).every(cb => cb.checked);
73-
updateExecuteButton();
75+
updateButtonStates();
7476
});
7577
});
7678
#{' '}
@@ -79,6 +81,31 @@ def generate_table_with_actions
7981
const selectedIds = Array.from(document.querySelectorAll('input[name="job_ids[]"]:checked')).map(cb => cb.value);
8082
if (selectedIds.length === 0) return;
8183
#{' '}
84+
submitForm('#{execute_jobs_path}', selectedIds);
85+
});
86+
#{' '}
87+
// Add event listener for the reject button
88+
rejectButton.addEventListener('click', function() {
89+
const selectedIds = Array.from(document.querySelectorAll('input[name="job_ids[]"]:checked')).map(cb => cb.value);
90+
if (selectedIds.length === 0) return;
91+
#{' '}
92+
if (confirm('Are you sure you want to reject the selected jobs? This action cannot be undone.')) {
93+
submitForm('#{reject_jobs_path}', selectedIds);
94+
}
95+
});
96+
#{' '}
97+
function submitForm(actionUrl, selectedIds) {
98+
// Uncheck all checkboxes to prevent duplicate submission
99+
document.querySelectorAll('input[name="job_ids[]"]').forEach(checkbox => {
100+
checkbox.checked = false;
101+
});
102+
103+
// Clear any existing hidden inputs
104+
document.querySelectorAll('input[type="hidden"][name="job_ids[]"]').forEach(input => input.remove());
105+
106+
// Set form action
107+
form.action = actionUrl;
108+
82109
// Add selected IDs as hidden inputs
83110
selectedIds.forEach(id => {
84111
const input = document.createElement('input');
@@ -87,18 +114,20 @@ def generate_table_with_actions
87114
input.value = id;
88115
form.appendChild(input);
89116
});
90-
#{' '}
117+
118+
// Submit the form
91119
form.submit();
92-
});
120+
}
93121
#{' '}
94-
function updateExecuteButton() {
122+
function updateButtonStates() {
95123
const checkboxes = document.getElementsByName('job_ids[]');
96124
const checked = Array.from(checkboxes).some(cb => cb.checked);
97125
executeButton.disabled = !checked;
126+
rejectButton.disabled = !checked;
98127
}
99128
#{' '}
100-
// Initialize button state
101-
updateExecuteButton();
129+
// Initialize button states
130+
updateButtonStates();
102131
});
103132
</script>
104133
HTML
@@ -130,7 +159,7 @@ def generate_row(execution)
130159
<<-HTML
131160
<tr>
132161
<td>
133-
<input type="checkbox" name="job_ids[]" value="#{execution.id}" onchange="updateExecuteButton()">
162+
<input type="checkbox" name="job_ids[]" value="#{execution.id}">
134163
</td>
135164
<td>#{execution.job.class_name}</td>
136165
<td>#{execution.queue_name}</td>
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# frozen_string_literal: true
2+
3+
module SolidQueueMonitor
4+
class RejectJobService
5+
def call(id)
6+
execution = SolidQueue::ScheduledExecution.find(id)
7+
reject_job(execution)
8+
end
9+
10+
def reject_many(ids)
11+
return { success: false, message: 'No jobs selected' } if ids.blank?
12+
13+
success_count = 0
14+
failed_count = 0
15+
16+
ids.each do |id|
17+
execution = SolidQueue::ScheduledExecution.find_by(id: id)
18+
if execution
19+
reject_job(execution)
20+
success_count += 1
21+
else
22+
failed_count += 1
23+
end
24+
rescue StandardError
25+
failed_count += 1
26+
end
27+
28+
if success_count.positive? && failed_count.zero?
29+
{ success: true, message: 'All selected jobs have been rejected' }
30+
elsif success_count.positive? && failed_count.positive?
31+
{ success: true, message: "#{success_count} jobs rejected, #{failed_count} failed" }
32+
else
33+
{ success: false, message: 'Failed to reject jobs' }
34+
end
35+
end
36+
37+
private
38+
39+
def reject_job(execution)
40+
ActiveRecord::Base.transaction do
41+
# Mark the associated job as finished to indicate it was rejected
42+
execution.job.update!(finished_at: Time.current)
43+
44+
# Remove the scheduled execution
45+
execution.destroy
46+
end
47+
end
48+
end
49+
end

config/routes.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
resources :queues, only: [:index]
1212

1313
post 'execute_jobs', to: 'scheduled_jobs#create', as: :execute_jobs
14+
post 'reject_jobs', to: 'scheduled_jobs#reject_all', as: :reject_jobs
1415

1516
post 'retry_failed_job/:id', to: 'failed_jobs#retry', as: :retry_failed_job
1617
post 'discard_failed_job/:id', to: 'failed_jobs#discard', as: :discard_failed_job

lib/solid_queue_monitor/version.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# frozen_string_literal: true
22

33
module SolidQueueMonitor
4-
VERSION = '0.3.1'
4+
VERSION = '0.3.2'
55
end
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
# frozen_string_literal: true
2+
3+
require 'spec_helper'
4+
5+
RSpec.describe SolidQueueMonitor::RejectJobService do
6+
describe '#reject_many' do
7+
subject { described_class.new }
8+
9+
let!(:scheduled_execution1) { create(:solid_queue_scheduled_execution) }
10+
let!(:scheduled_execution2) { create(:solid_queue_scheduled_execution) }
11+
12+
it 'rejects scheduled jobs and marks them as finished' do
13+
expect do
14+
result = subject.reject_many([scheduled_execution1.id, scheduled_execution2.id])
15+
expect(result[:success]).to be true
16+
end.to change(SolidQueue::ScheduledExecution, :count).by(-2)
17+
end
18+
19+
it 'marks associated jobs as finished when rejecting' do
20+
subject.reject_many([scheduled_execution1.id])
21+
22+
job = scheduled_execution1.job.reload
23+
expect(job.finished_at).to be_present
24+
end
25+
26+
it 'returns success message when all jobs are rejected successfully' do
27+
result = subject.reject_many([scheduled_execution1.id, scheduled_execution2.id])
28+
29+
expect(result[:success]).to be true
30+
expect(result[:message]).to eq('All selected jobs have been rejected')
31+
end
32+
33+
it 'handles non-existent job IDs gracefully' do
34+
result = subject.reject_many([999_999])
35+
36+
expect(result[:success]).to be false
37+
expect(result[:message]).to eq('Failed to reject jobs')
38+
end
39+
40+
it 'handles empty job IDs array gracefully' do
41+
result = subject.reject_many([])
42+
43+
expect(result[:success]).to be false
44+
expect(result[:message]).to eq('No jobs selected')
45+
end
46+
47+
it 'handles mix of valid and invalid job IDs' do
48+
result = subject.reject_many([scheduled_execution1.id, 999_999])
49+
50+
expect(result[:success]).to be true
51+
expect(result[:message]).to include('1 jobs rejected, 1 failed')
52+
end
53+
54+
it 'removes scheduled execution from database' do
55+
subject.reject_many([scheduled_execution1.id])
56+
57+
expect(SolidQueue::ScheduledExecution.find_by(id: scheduled_execution1.id)).to be_nil
58+
end
59+
end
60+
61+
describe '#call' do
62+
subject { described_class.new }
63+
64+
let!(:scheduled_execution) { create(:solid_queue_scheduled_execution) }
65+
66+
it 'rejects a single scheduled job' do
67+
expect do
68+
subject.call(scheduled_execution.id)
69+
end.to change(SolidQueue::ScheduledExecution, :count).by(-1)
70+
end
71+
72+
it 'marks the job as finished' do
73+
subject.call(scheduled_execution.id)
74+
75+
job = scheduled_execution.job.reload
76+
expect(job.finished_at).to be_present
77+
end
78+
end
79+
end

0 commit comments

Comments
 (0)