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
34 changes: 23 additions & 11 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ on:
- main

jobs:
test:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
Expand All @@ -18,17 +18,29 @@ jobs:
uses: ruby/setup-ruby@v1
with:
ruby-version: "3.2"
bundler-cache: false

- name: Install dependencies
run: |
gem install bundler -v '2.4.22'
bundle config set --local path 'vendor/bundle'
bundle config set --local frozen 'false'
bundle install
bundler-cache: true

- name: Run RuboCop
run: bundle exec rubocop --parallel

# - name: Run RSpec
# run: bundle exec rspec
test:
runs-on: ubuntu-latest
strategy:
matrix:
ruby-version: ["3.2", "3.3"]
steps:
- uses: actions/checkout@v4

- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: ${{ matrix.ruby-version }}
bundler-cache: true

- name: Install sqlite3 from source
run: |
gem uninstall sqlite3 --all --ignore-dependencies || true
gem install sqlite3 --platform=ruby

- name: Run RSpec
run: bundle exec rspec
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
/pkg/
/spec/reports/
/tmp/

/log/

.DS_Store
.rspec_status
Expand Down
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@
- Informative tooltip on hover explaining the feature
- User preference persistence via localStorage (survives page reloads)
- Responsive design for auto-refresh controls on mobile devices
- Pause/Resume queue functionality for incident response
- Pause button to stop processing jobs on specific queues
- Resume button to restart processing on paused queues
- Visual status indicator showing Active/Paused state
- Confirmation dialog before pausing to prevent accidents
- Paused queues highlighted with amber background

## [0.3.2] - 2025-06-12

Expand Down
2 changes: 2 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@ gemspec

group :development, :test do
gem 'factory_bot_rails'
gem 'rails-controller-testing'
gem 'rspec-rails'
gem 'rubocop'
gem 'rubocop-rails'
gem 'rubocop-rspec'
gem 'sqlite3'
end
9 changes: 9 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,10 @@ GEM
activesupport (= 8.0.2)
bundler (>= 1.15.0)
railties (= 8.0.2)
rails-controller-testing (1.0.5)
actionpack (>= 5.0.1.rc1)
actionview (>= 5.0.1.rc1)
activesupport (>= 5.0.1.rc1)
rails-dom-testing (2.2.0)
activesupport (>= 5.0.0)
minitest
Expand Down Expand Up @@ -255,6 +259,9 @@ GEM
fugit (~> 1.11.0)
railties (>= 7.1)
thor (~> 1.3.1)
sqlite3 (2.9.0)
mini_portile2 (~> 2.8.0)
sqlite3 (2.9.0-arm64-darwin)
stringio (3.1.5)
thor (1.3.2)
timeout (0.4.3)
Expand All @@ -277,11 +284,13 @@ PLATFORMS

DEPENDENCIES
factory_bot_rails
rails-controller-testing
rspec-rails
rubocop
rubocop-rails
rubocop-rspec
solid_queue_monitor!
sqlite3

BUNDLED WITH
2.6.2
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ A lightweight, zero-dependency web interface for monitoring Solid Queue backgrou
- **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
- **Queue Management**: View and filter jobs by queue with pause/resume controls
- **Pause/Resume Queues**: Temporarily stop processing jobs on specific queues for incident response
- **Advanced Job Filtering**: Filter jobs by class name, queue, status, and job arguments
- **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
Expand Down
13 changes: 11 additions & 2 deletions app/controllers/solid_queue_monitor/application_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,17 @@ class ApplicationController < ActionController::Base
skip_before_action :verify_authenticity_token

def set_flash_message(message, type)
session[:flash_message] = message
session[:flash_type] = type
# Store in instance variable for access in views
@flash_message = message
@flash_type = type

# Try to use Rails flash if available
begin
flash[:notice] = message if type == :success
flash[:alert] = message if type == :error
rescue StandardError
# Flash not available (e.g., no session middleware)
end
end

private
Expand Down
22 changes: 15 additions & 7 deletions app/controllers/solid_queue_monitor/base_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,21 @@ def paginate(relation)
end

def render_page(title, content)
# Get flash message from session
message = session[:flash_message]
message_type = session[:flash_type]

# Clear the flash message from session after using it
session.delete(:flash_message)
session.delete(:flash_type)
# Get flash message from instance variable (set by set_flash_message) or session
message = @flash_message
message_type = @flash_type

# Try to get from session as fallback, but don't fail if session unavailable
begin
message ||= session[:flash_message]
message_type ||= session[:flash_type]

# Clear the flash message from session after using it
session.delete(:flash_message) if message
session.delete(:flash_type) if message_type
rescue StandardError
# Session not available (e.g., no session middleware in tests)
end

html = SolidQueueMonitor::HtmlGenerator.new(
title: title,
Expand Down
19 changes: 18 additions & 1 deletion app/controllers/solid_queue_monitor/queues_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,25 @@ def index
@queues = SolidQueue::Job.group(:queue_name)
.select('queue_name, COUNT(*) as job_count')
.order('job_count DESC')
@paused_queues = QueuePauseService.paused_queues

render_page('Queues', SolidQueueMonitor::QueuesPresenter.new(@queues).render)
render_page('Queues', SolidQueueMonitor::QueuesPresenter.new(@queues, @paused_queues).render)
end

def pause
queue_name = params[:queue_name]
result = QueuePauseService.new(queue_name).pause

set_flash_message(result[:message], result[:success] ? 'success' : 'error')
redirect_to queues_path
end

def resume
queue_name = params[:queue_name]
result = QueuePauseService.new(queue_name).resume

set_flash_message(result[:message], result[:success] ? 'success' : 'error')
redirect_to queues_path
end
end
end
51 changes: 45 additions & 6 deletions app/presenters/solid_queue_monitor/queues_presenter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@

module SolidQueueMonitor
class QueuesPresenter < BasePresenter
def initialize(records)
def initialize(records, paused_queues = [])
@records = records
@paused_queues = paused_queues
end

def render
Expand All @@ -19,10 +20,12 @@ def generate_table
<thead>
<tr>
<th>Queue Name</th>
<th>Status</th>
<th>Total Jobs</th>
<th>Ready Jobs</th>
<th>Scheduled Jobs</th>
<th>Failed Jobs</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
Expand All @@ -34,17 +37,53 @@ def generate_table
end

def generate_row(queue)
queue_name = queue.queue_name || 'default'
paused = @paused_queues.include?(queue_name)

<<-HTML
<tr>
<td>#{queue.queue_name || 'default'}</td>
<tr class="#{paused ? 'queue-paused' : ''}">
<td>#{queue_name}</td>
<td>#{status_badge(paused)}</td>
<td>#{queue.job_count}</td>
<td>#{ready_jobs_count(queue.queue_name)}</td>
<td>#{scheduled_jobs_count(queue.queue_name)}</td>
<td>#{failed_jobs_count(queue.queue_name)}</td>
<td>#{ready_jobs_count(queue_name)}</td>
<td>#{scheduled_jobs_count(queue_name)}</td>
<td>#{failed_jobs_count(queue_name)}</td>
<td class="actions-cell">#{action_button(queue_name, paused)}</td>
</tr>
HTML
end

def status_badge(paused)
if paused
'<span class="status-badge status-paused">Paused</span>'
else
'<span class="status-badge status-active">Active</span>'
end
end

def action_button(queue_name, paused)
if paused
<<-HTML
<form action="#{resume_queue_path}" method="post" class="inline-form">
<input type="hidden" name="queue_name" value="#{queue_name}">
<button type="submit" class="action-button resume-button" title="Resume queue processing">
Resume
</button>
</form>
HTML
else
<<-HTML
<form action="#{pause_queue_path}" method="post" class="inline-form"
onsubmit="return confirm('Are you sure you want to pause the #{queue_name} queue? Workers will stop processing jobs from this queue.');">
<input type="hidden" name="queue_name" value="#{queue_name}">
<button type="submit" class="action-button pause-button" title="Pause queue processing">
Pause
</button>
</form>
HTML
end
end

def ready_jobs_count(queue_name)
SolidQueue::ReadyExecution.where(queue_name: queue_name).count
end
Expand Down
34 changes: 34 additions & 0 deletions app/services/solid_queue_monitor/queue_pause_service.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# frozen_string_literal: true

module SolidQueueMonitor
class QueuePauseService
delegate :paused?, to: :@queue

def initialize(queue_name)
@queue_name = queue_name
@queue = SolidQueue::Queue.new(queue_name)
end

def pause
return { success: false, message: "Queue '#{@queue_name}' is already paused" } if paused?

@queue.pause
{ success: true, message: "Queue '#{@queue_name}' has been paused" }
rescue StandardError => e
{ success: false, message: "Failed to pause queue: #{e.message}" }
end

def resume
return { success: false, message: "Queue '#{@queue_name}' is not paused" } unless paused?

@queue.resume
{ success: true, message: "Queue '#{@queue_name}' has been resumed" }
rescue StandardError => e
{ success: false, message: "Failed to resume queue: #{e.message}" }
end

def self.paused_queues
SolidQueue::Pause.pluck(:queue_name)
end
end
end
24 changes: 24 additions & 0 deletions app/services/solid_queue_monitor/stylesheet_generator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,30 @@ def generate
.solid_queue_monitor .status-failed { background: #fee2e2; color: #991b1b; }
.solid_queue_monitor .status-scheduled { background: #dbeafe; color: #1e40af; }
.solid_queue_monitor .status-pending { background: #f3f4f6; color: #374151; }
.solid_queue_monitor .status-active { background: #d1fae5; color: #065f46; }
.solid_queue_monitor .status-paused { background: #fef3c7; color: #92400e; }

.solid_queue_monitor .queue-paused {
background-color: #fffbeb;
}

.solid_queue_monitor .pause-button {
background: #f59e0b;
color: white;
}

.solid_queue_monitor .pause-button:hover {
background: #d97706;
}

.solid_queue_monitor .resume-button {
background: #10b981;
color: white;
}

.solid_queue_monitor .resume-button:hover {
background: #059669;
}

.solid_queue_monitor .execute-btn {
background: var(--primary-color);
Expand Down
3 changes: 3 additions & 0 deletions config/database.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
test:
adapter: sqlite3
database: ":memory:"
8 changes: 7 additions & 1 deletion config/routes.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
# frozen_string_literal: true

# Guard against multiple loads of routes file in test environment
SolidQueueMonitor::Engine.routes.draw do
root to: 'overview#index', as: :root
return if SolidQueueMonitor::Engine.routes.routes.any? { |r| r.name == 'root' }

root to: 'overview#index'

resources :ready_jobs, only: [:index]
resources :scheduled_jobs, only: [:index]
Expand All @@ -17,4 +20,7 @@
post 'discard_failed_job/:id', to: 'failed_jobs#discard', as: :discard_failed_job
post 'retry_failed_jobs', to: 'failed_jobs#retry_all', as: :retry_failed_jobs
post 'discard_failed_jobs', to: 'failed_jobs#discard_all', as: :discard_failed_jobs

post 'pause_queue', to: 'queues#pause', as: :pause_queue
post 'resume_queue', to: 'queues#resume', as: :resume_queue
end
5 changes: 5 additions & 0 deletions lib/solid_queue_monitor/engine.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ class Engine < ::Rails::Engine
# Optional: Add eager loading for production
config.eager_load_paths << root.join('app', 'services')

# Ensure session middleware is available
initializer 'solid_queue_monitor.middleware' do |app|
app.config.session_store :cookie_store, key: '_solid_queue_monitor_session' unless app.config.session_store
end

initializer 'solid_queue_monitor.assets' do |app|
# Optional: Add assets if needed
end
Expand Down
Empty file removed log/test.log
Empty file.
Loading