sidekiq_queue_manager 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.github/workflows/ci.yml +41 -0
- data/INSTALLATION.md +191 -0
- data/README.md +376 -0
- data/app/assets/javascripts/sidekiq_queue_manager/application.js +1836 -0
- data/app/assets/stylesheets/sidekiq_queue_manager/application.css +1018 -0
- data/app/assets/stylesheets/sidekiq_queue_manager/modals.css +838 -0
- data/app/controllers/sidekiq_queue_manager/application_controller.rb +190 -0
- data/app/controllers/sidekiq_queue_manager/assets_controller.rb +87 -0
- data/app/controllers/sidekiq_queue_manager/dashboard_controller.rb +373 -0
- data/app/services/sidekiq_queue_manager/queue_service.rb +475 -0
- data/app/views/layouts/sidekiq_queue_manager/application.html.erb +132 -0
- data/app/views/sidekiq_queue_manager/dashboard/index.html.erb +208 -0
- data/config/routes.rb +48 -0
- data/lib/sidekiq_queue_manager/configuration.rb +157 -0
- data/lib/sidekiq_queue_manager/engine.rb +151 -0
- data/lib/sidekiq_queue_manager/logging_middleware.rb +29 -0
- data/lib/sidekiq_queue_manager/version.rb +12 -0
- data/lib/sidekiq_queue_manager.rb +122 -0
- metadata +227 -0
@@ -0,0 +1,190 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SidekiqQueueManager
|
4
|
+
# Base application controller for the SidekiqQueueManager engine
|
5
|
+
#
|
6
|
+
# Provides common functionality, security measures, and configuration
|
7
|
+
# for all controllers within the gem.
|
8
|
+
#
|
9
|
+
# Inherits from the main application's ApplicationController to ensure
|
10
|
+
# access to authentication methods and other application-specific functionality.
|
11
|
+
#
|
12
|
+
class ApplicationController < (defined?(::ApplicationController) ? ::ApplicationController : ActionController::Base)
|
13
|
+
# Engine-specific layout
|
14
|
+
layout 'sidekiq_queue_manager/application'
|
15
|
+
|
16
|
+
# Authentication and security using Ruby's declarative approach
|
17
|
+
before_action :authenticate_access
|
18
|
+
before_action :set_security_headers
|
19
|
+
|
20
|
+
protected
|
21
|
+
|
22
|
+
# Handle authentication - explicit configuration following professional standards
|
23
|
+
def authenticate_access
|
24
|
+
return custom_authentication if custom_authentication_configured?
|
25
|
+
|
26
|
+
basic_authentication if basic_authentication_enabled?
|
27
|
+
|
28
|
+
# If both disabled, access is allowed (explicit choice for development)
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
# Ruby's case-based authentication routing
|
34
|
+
def custom_authentication
|
35
|
+
return unless respond_to?(authentication_method, true)
|
36
|
+
|
37
|
+
send(authentication_method)
|
38
|
+
end
|
39
|
+
|
40
|
+
def basic_authentication
|
41
|
+
validate_basic_auth_configuration!
|
42
|
+
perform_basic_authentication
|
43
|
+
end
|
44
|
+
|
45
|
+
# Predicate methods for cleaner conditionals (Ruby convention)
|
46
|
+
def custom_authentication_configured?
|
47
|
+
SidekiqQueueManager.configuration.custom_authentication?
|
48
|
+
end
|
49
|
+
|
50
|
+
def basic_authentication_enabled?
|
51
|
+
SidekiqQueueManager.basic_auth_enabled?
|
52
|
+
end
|
53
|
+
|
54
|
+
def authentication_method
|
55
|
+
SidekiqQueueManager.configuration.authentication_method
|
56
|
+
end
|
57
|
+
|
58
|
+
# Configuration validation with descriptive error handling
|
59
|
+
def validate_basic_auth_configuration!
|
60
|
+
return if basic_auth_password.present?
|
61
|
+
|
62
|
+
render_authentication_configuration_error
|
63
|
+
end
|
64
|
+
|
65
|
+
def basic_auth_password
|
66
|
+
SidekiqQueueManager.configuration.basic_auth_password
|
67
|
+
end
|
68
|
+
|
69
|
+
def basic_auth_username
|
70
|
+
SidekiqQueueManager.configuration.basic_auth_username
|
71
|
+
end
|
72
|
+
|
73
|
+
# Ruby's multiline string with here-doc for readable error messages
|
74
|
+
def render_authentication_configuration_error
|
75
|
+
Rails.logger.error '[SidekiqQueueManager] basic_auth_password not configured - authentication will fail'
|
76
|
+
|
77
|
+
error_message = <<~ERROR
|
78
|
+
Sidekiq Queue Manager: Authentication Not Configured
|
79
|
+
|
80
|
+
Basic authentication is enabled but no password is set.
|
81
|
+
|
82
|
+
To fix this, add the following to config/initializers/sidekiq_queue_manager.rb:
|
83
|
+
|
84
|
+
SidekiqQueueManager.configure do |config|
|
85
|
+
config.basic_auth_password = 'your-secure-password-here'
|
86
|
+
end
|
87
|
+
|
88
|
+
Or disable authentication (NOT recommended for production):
|
89
|
+
|
90
|
+
SidekiqQueueManager.configure do |config|
|
91
|
+
config.basic_auth_enabled = false
|
92
|
+
end
|
93
|
+
ERROR
|
94
|
+
|
95
|
+
render plain: error_message, status: :internal_server_error
|
96
|
+
end
|
97
|
+
|
98
|
+
# HTTP Basic Authentication implementation with Ruby's secure comparison
|
99
|
+
def perform_basic_authentication
|
100
|
+
authenticate_or_request_with_http_basic('Sidekiq Queue Manager') do |username, password|
|
101
|
+
# Use secure comparison to prevent timing attacks
|
102
|
+
credentials_valid?(username, password)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def credentials_valid?(username, password)
|
107
|
+
username_match = ActiveSupport::SecurityUtils.secure_compare(username, basic_auth_username)
|
108
|
+
password_match = ActiveSupport::SecurityUtils.secure_compare(password, basic_auth_password)
|
109
|
+
|
110
|
+
username_match && password_match
|
111
|
+
end
|
112
|
+
|
113
|
+
# Set security headers for all responses using Ruby's functional approach
|
114
|
+
def set_security_headers
|
115
|
+
apply_standard_security_headers
|
116
|
+
apply_csp_header if SidekiqQueueManager.configuration.enable_csp?
|
117
|
+
end
|
118
|
+
|
119
|
+
def apply_standard_security_headers
|
120
|
+
response.headers.merge!(
|
121
|
+
'X-Frame-Options' => 'SAMEORIGIN',
|
122
|
+
'X-Content-Type-Options' => 'nosniff',
|
123
|
+
'X-XSS-Protection' => '1; mode=block',
|
124
|
+
'Referrer-Policy' => 'strict-origin-when-cross-origin'
|
125
|
+
)
|
126
|
+
end
|
127
|
+
|
128
|
+
def apply_csp_header
|
129
|
+
response.headers['Content-Security-Policy'] = content_security_policy
|
130
|
+
end
|
131
|
+
|
132
|
+
# Build Content Security Policy header using Ruby's array joining
|
133
|
+
def content_security_policy
|
134
|
+
[
|
135
|
+
"default-src 'self'",
|
136
|
+
"script-src 'self' 'unsafe-inline'",
|
137
|
+
"style-src 'self' 'unsafe-inline'",
|
138
|
+
"img-src 'self' data:",
|
139
|
+
"font-src 'self'",
|
140
|
+
"connect-src 'self'",
|
141
|
+
"frame-ancestors 'none'"
|
142
|
+
].join('; ')
|
143
|
+
end
|
144
|
+
|
145
|
+
# Helper to determine asset serving strategy using Ruby's expressive error handling
|
146
|
+
def asset_serving_info
|
147
|
+
@asset_serving_info ||= detect_asset_serving_strategy
|
148
|
+
end
|
149
|
+
helper_method :asset_serving_info
|
150
|
+
|
151
|
+
def detect_asset_serving_strategy
|
152
|
+
asset_pipeline_strategy
|
153
|
+
rescue StandardError
|
154
|
+
direct_serving_strategy
|
155
|
+
end
|
156
|
+
|
157
|
+
def asset_pipeline_strategy
|
158
|
+
{
|
159
|
+
css_path: asset_path('sidekiq_queue_manager/application.css'),
|
160
|
+
js_path: asset_path('sidekiq_queue_manager/application.js'),
|
161
|
+
modals_css_path: asset_path('sidekiq_queue_manager/modals.css'),
|
162
|
+
use_asset_pipeline: true
|
163
|
+
}
|
164
|
+
end
|
165
|
+
|
166
|
+
def direct_serving_strategy
|
167
|
+
mount_path = sidekiq_dashboard.root_path.chomp('/')
|
168
|
+
|
169
|
+
{
|
170
|
+
css_path: "#{mount_path}/assets/sidekiq_queue_manager/application.css",
|
171
|
+
js_path: "#{mount_path}/assets/sidekiq_queue_manager/application.js",
|
172
|
+
modals_css_path: "#{mount_path}/assets/sidekiq_queue_manager/modals.css",
|
173
|
+
use_asset_pipeline: false
|
174
|
+
}
|
175
|
+
end
|
176
|
+
|
177
|
+
# Common helper for accessing main application methods
|
178
|
+
def main_app
|
179
|
+
@main_app ||= Rails.application.routes.url_helpers
|
180
|
+
end
|
181
|
+
|
182
|
+
# Logging helpers with gem prefix using Ruby's method delegation
|
183
|
+
%w[info error].each do |level|
|
184
|
+
define_method "log_#{level}" do |message|
|
185
|
+
Rails.logger.public_send(level, "[SidekiqQueueManager] #{message}")
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
@@ -0,0 +1,87 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SidekiqQueueManager
|
4
|
+
# Controller to serve assets when the asset pipeline is not available
|
5
|
+
# This ensures CSS and JavaScript work in API-only Rails apps
|
6
|
+
class AssetsController < ApplicationController
|
7
|
+
# Skip CSRF protection for asset requests
|
8
|
+
skip_before_action :verify_authenticity_token
|
9
|
+
|
10
|
+
# Serve CSS file
|
11
|
+
def css
|
12
|
+
css_content = load_asset_file('application.css')
|
13
|
+
|
14
|
+
respond_to do |format|
|
15
|
+
format.css do
|
16
|
+
render plain: css_content, content_type: 'text/css'
|
17
|
+
end
|
18
|
+
format.all do
|
19
|
+
render plain: css_content, content_type: 'text/css'
|
20
|
+
end
|
21
|
+
end
|
22
|
+
rescue StandardError => e
|
23
|
+
Rails.logger.error "[SidekiqQueueManager] Error serving CSS: #{e.message}"
|
24
|
+
render plain: "/* CSS loading error: #{e.message} */", content_type: 'text/css'
|
25
|
+
end
|
26
|
+
|
27
|
+
# Serve JavaScript file
|
28
|
+
def js
|
29
|
+
js_content = load_asset_file('application.js')
|
30
|
+
|
31
|
+
respond_to do |format|
|
32
|
+
format.js do
|
33
|
+
render plain: js_content, content_type: 'application/javascript'
|
34
|
+
end
|
35
|
+
format.all do
|
36
|
+
render plain: js_content, content_type: 'application/javascript'
|
37
|
+
end
|
38
|
+
end
|
39
|
+
rescue StandardError => e
|
40
|
+
Rails.logger.error "[SidekiqQueueManager] Error serving JS: #{e.message}"
|
41
|
+
render plain: "/* JavaScript loading error: #{e.message} */", content_type: 'application/javascript'
|
42
|
+
end
|
43
|
+
|
44
|
+
# Serve modals CSS file
|
45
|
+
def modals_css
|
46
|
+
css_content = load_asset_file('modals.css')
|
47
|
+
|
48
|
+
respond_to do |format|
|
49
|
+
format.css do
|
50
|
+
render plain: css_content, content_type: 'text/css'
|
51
|
+
end
|
52
|
+
format.all do
|
53
|
+
render plain: css_content, content_type: 'text/css'
|
54
|
+
end
|
55
|
+
end
|
56
|
+
rescue StandardError => e
|
57
|
+
Rails.logger.error "[SidekiqQueueManager] Error serving modals CSS: #{e.message}"
|
58
|
+
render plain: "/* Modals CSS loading error: #{e.message} */", content_type: 'text/css'
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
# Load asset file from the gem's asset directory
|
64
|
+
def load_asset_file(filename)
|
65
|
+
asset_path = case filename
|
66
|
+
when 'application.css'
|
67
|
+
File.join(gem_assets_path, 'stylesheets', 'sidekiq_queue_manager', 'application.css')
|
68
|
+
when 'modals.css'
|
69
|
+
File.join(gem_assets_path, 'stylesheets', 'sidekiq_queue_manager', 'modals.css')
|
70
|
+
when 'application.js'
|
71
|
+
File.join(gem_assets_path, 'javascripts', 'sidekiq_queue_manager', 'application.js')
|
72
|
+
else
|
73
|
+
raise "Unknown asset: #{filename}"
|
74
|
+
end
|
75
|
+
|
76
|
+
raise "Asset file not found: #{asset_path}" unless File.exist?(asset_path)
|
77
|
+
|
78
|
+
File.read(asset_path)
|
79
|
+
end
|
80
|
+
|
81
|
+
# Get the path to the gem's assets directory
|
82
|
+
def gem_assets_path
|
83
|
+
@gem_assets_path ||= File.expand_path('../../assets', __dir__)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
@@ -0,0 +1,373 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SidekiqQueueManager
|
4
|
+
# Main dashboard controller providing the web interface for queue management
|
5
|
+
#
|
6
|
+
# Handles both JSON API responses and HTML views for the queue manager interface.
|
7
|
+
# Includes comprehensive error handling, authentication, and real-time updates.
|
8
|
+
#
|
9
|
+
class DashboardController < ApplicationController
|
10
|
+
include ActionController::Live
|
11
|
+
|
12
|
+
# Prevent CSRF token issues with JSON API requests
|
13
|
+
protect_from_forgery with: :null_session, if: -> { request.format.json? }
|
14
|
+
|
15
|
+
# Apply authentication if configured
|
16
|
+
before_action :authenticate_user!, if: -> { authentication_required? }
|
17
|
+
before_action :set_queue_name, only: %i[
|
18
|
+
queue_status pause_queue resume_queue jobs delete_job
|
19
|
+
set_limit remove_limit set_process_limit remove_process_limit
|
20
|
+
block unblock clear
|
21
|
+
]
|
22
|
+
|
23
|
+
# Comprehensive error handling for all controller actions
|
24
|
+
rescue_from StandardError, with: :handle_api_error
|
25
|
+
rescue_from SidekiqQueueManager::ServiceError, with: :handle_service_error
|
26
|
+
rescue_from ActionController::ParameterMissing, with: :handle_parameter_error
|
27
|
+
|
28
|
+
# ========================================
|
29
|
+
# Main Dashboard Actions
|
30
|
+
# ========================================
|
31
|
+
|
32
|
+
# Main dashboard interface - serves both HTML and JSON
|
33
|
+
# GET /
|
34
|
+
def index
|
35
|
+
respond_to do |format|
|
36
|
+
format.html { render_dashboard_html }
|
37
|
+
format.json { render_dashboard_json }
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# Real-time metrics endpoint for AJAX updates
|
42
|
+
# GET /metrics
|
43
|
+
def metrics
|
44
|
+
set_no_cache_headers
|
45
|
+
render json: api_response(data: QueueService.queue_metrics)
|
46
|
+
end
|
47
|
+
|
48
|
+
# Server-Sent Events stream for real-time updates
|
49
|
+
# GET /live
|
50
|
+
def live_stream
|
51
|
+
response.headers['Content-Type'] = 'text/event-stream'
|
52
|
+
response.headers['Cache-Control'] = 'no-cache'
|
53
|
+
response.headers['X-Accel-Buffering'] = 'no'
|
54
|
+
|
55
|
+
begin
|
56
|
+
# Send initial data
|
57
|
+
send_sse_event('metrics', QueueService.queue_metrics)
|
58
|
+
|
59
|
+
# Stream updates every refresh interval
|
60
|
+
loop do
|
61
|
+
sleep(refresh_interval_seconds)
|
62
|
+
|
63
|
+
metrics = QueueService.queue_metrics
|
64
|
+
send_sse_event('metrics', metrics)
|
65
|
+
end
|
66
|
+
rescue ActionController::Live::ClientDisconnected
|
67
|
+
logger.info('Client disconnected from live stream')
|
68
|
+
rescue StandardError => e
|
69
|
+
logger.error("Live stream error: #{e.message}")
|
70
|
+
send_sse_event('error', { message: 'Stream error occurred' })
|
71
|
+
ensure
|
72
|
+
response.stream.close
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
# ========================================
|
77
|
+
# Queue Control Actions (JSON API)
|
78
|
+
# ========================================
|
79
|
+
|
80
|
+
# Pause a specific queue
|
81
|
+
# POST /queues/:name/pause
|
82
|
+
def pause_queue
|
83
|
+
result = QueueService.pause_queue(@queue_name)
|
84
|
+
render json: api_response(
|
85
|
+
success: result[:success],
|
86
|
+
message: result[:message],
|
87
|
+
data: result
|
88
|
+
), status: result[:success] ? :ok : :unprocessable_entity
|
89
|
+
end
|
90
|
+
|
91
|
+
# Resume a specific queue
|
92
|
+
# POST /queues/:name/resume
|
93
|
+
def resume_queue
|
94
|
+
result = QueueService.resume_queue(@queue_name)
|
95
|
+
render json: api_response(
|
96
|
+
success: result[:success],
|
97
|
+
message: result[:message],
|
98
|
+
data: result
|
99
|
+
), status: result[:success] ? :ok : :unprocessable_entity
|
100
|
+
end
|
101
|
+
|
102
|
+
# Get status for a specific queue
|
103
|
+
# GET /queues/:name/status
|
104
|
+
def queue_status
|
105
|
+
result = QueueService.queue_status(@queue_name)
|
106
|
+
render json: api_response(data: result)
|
107
|
+
end
|
108
|
+
|
109
|
+
# Bulk pause all non-critical queues
|
110
|
+
# POST /queues/pause_all
|
111
|
+
def pause_all
|
112
|
+
result = QueueService.pause_all_queues
|
113
|
+
render json: api_response(
|
114
|
+
success: result[:success],
|
115
|
+
message: result[:message],
|
116
|
+
data: result
|
117
|
+
), status: result[:success] ? :ok : :unprocessable_entity
|
118
|
+
end
|
119
|
+
|
120
|
+
# Bulk resume all queues
|
121
|
+
# POST /queues/resume_all
|
122
|
+
def resume_all
|
123
|
+
result = QueueService.resume_all_queues
|
124
|
+
render json: api_response(
|
125
|
+
success: result[:success],
|
126
|
+
message: result[:message],
|
127
|
+
data: result
|
128
|
+
), status: result[:success] ? :ok : :unprocessable_entity
|
129
|
+
end
|
130
|
+
|
131
|
+
# Global summary statistics
|
132
|
+
# GET /queues/summary
|
133
|
+
def summary
|
134
|
+
set_no_cache_headers
|
135
|
+
|
136
|
+
metrics = QueueService.queue_metrics
|
137
|
+
summary_data = {
|
138
|
+
total_queues: metrics[:queues].size,
|
139
|
+
total_enqueued: metrics[:global_stats][:enqueued],
|
140
|
+
total_busy: metrics[:global_stats][:busy],
|
141
|
+
paused_queues: metrics[:queues].count { |_, queue| queue[:paused] },
|
142
|
+
critical_queues: metrics[:queues].count { |_, queue| queue[:critical] }
|
143
|
+
}
|
144
|
+
|
145
|
+
render json: api_response(data: summary_data)
|
146
|
+
end
|
147
|
+
|
148
|
+
# ========================================
|
149
|
+
# Advanced Queue Operations
|
150
|
+
# ========================================
|
151
|
+
|
152
|
+
# View jobs in a specific queue with pagination
|
153
|
+
# GET /queues/:name/jobs
|
154
|
+
def jobs
|
155
|
+
page = params[:page] || 1
|
156
|
+
per_page = params[:per_page] || 10
|
157
|
+
|
158
|
+
result = QueueService.view_queue_jobs(@queue_name, page: page, per_page: per_page)
|
159
|
+
render json: api_response(
|
160
|
+
success: result[:success],
|
161
|
+
message: result[:message],
|
162
|
+
data: result
|
163
|
+
), status: result[:success] ? :ok : :unprocessable_entity
|
164
|
+
end
|
165
|
+
|
166
|
+
# Delete a specific job from a queue
|
167
|
+
# DELETE /queues/:name/delete_job
|
168
|
+
def delete_job
|
169
|
+
job_id = params.require(:job_id)
|
170
|
+
|
171
|
+
result = QueueService.delete_job(@queue_name, job_id)
|
172
|
+
render json: api_response(
|
173
|
+
success: result[:success],
|
174
|
+
message: result[:message]
|
175
|
+
), status: result[:success] ? :ok : :unprocessable_entity
|
176
|
+
end
|
177
|
+
|
178
|
+
# Set queue limit
|
179
|
+
# POST /queues/:name/set_limit
|
180
|
+
def set_limit
|
181
|
+
limit = params.require(:limit).to_i
|
182
|
+
|
183
|
+
result = QueueService.set_queue_limit(@queue_name, limit)
|
184
|
+
render json: api_response(
|
185
|
+
success: result[:success],
|
186
|
+
message: result[:message]
|
187
|
+
), status: result[:success] ? :ok : :unprocessable_entity
|
188
|
+
end
|
189
|
+
|
190
|
+
# Remove queue limit
|
191
|
+
# DELETE /queues/:name/remove_limit
|
192
|
+
def remove_limit
|
193
|
+
result = QueueService.remove_queue_limit(@queue_name)
|
194
|
+
render json: api_response(
|
195
|
+
success: result[:success],
|
196
|
+
message: result[:message]
|
197
|
+
), status: result[:success] ? :ok : :unprocessable_entity
|
198
|
+
end
|
199
|
+
|
200
|
+
# Set process limit
|
201
|
+
# POST /queues/:name/set_process_limit
|
202
|
+
def set_process_limit
|
203
|
+
limit = params.require(:limit).to_i
|
204
|
+
|
205
|
+
result = QueueService.set_process_limit(@queue_name, limit)
|
206
|
+
render json: api_response(
|
207
|
+
success: result[:success],
|
208
|
+
message: result[:message]
|
209
|
+
), status: result[:success] ? :ok : :unprocessable_entity
|
210
|
+
end
|
211
|
+
|
212
|
+
# Remove process limit
|
213
|
+
# DELETE /queues/:name/remove_process_limit
|
214
|
+
def remove_process_limit
|
215
|
+
result = QueueService.remove_process_limit(@queue_name)
|
216
|
+
render json: api_response(
|
217
|
+
success: result[:success],
|
218
|
+
message: result[:message]
|
219
|
+
), status: result[:success] ? :ok : :unprocessable_entity
|
220
|
+
end
|
221
|
+
|
222
|
+
# Block a queue
|
223
|
+
# POST /queues/:name/block
|
224
|
+
def block
|
225
|
+
result = QueueService.block_queue(@queue_name)
|
226
|
+
render json: api_response(
|
227
|
+
success: result[:success],
|
228
|
+
message: result[:message]
|
229
|
+
), status: result[:success] ? :ok : :unprocessable_entity
|
230
|
+
end
|
231
|
+
|
232
|
+
# Unblock a queue
|
233
|
+
# POST /queues/:name/unblock
|
234
|
+
def unblock
|
235
|
+
result = QueueService.unblock_queue(@queue_name)
|
236
|
+
render json: api_response(
|
237
|
+
success: result[:success],
|
238
|
+
message: result[:message]
|
239
|
+
), status: result[:success] ? :ok : :unprocessable_entity
|
240
|
+
end
|
241
|
+
|
242
|
+
# Clear all jobs from a queue
|
243
|
+
# POST /queues/:name/clear
|
244
|
+
def clear
|
245
|
+
result = QueueService.clear_queue(@queue_name)
|
246
|
+
render json: api_response(
|
247
|
+
success: result[:success],
|
248
|
+
message: result[:message],
|
249
|
+
data: result[:data]
|
250
|
+
), status: result[:success] ? :ok : :unprocessable_entity
|
251
|
+
end
|
252
|
+
|
253
|
+
private
|
254
|
+
|
255
|
+
# ========================================
|
256
|
+
# Helper Methods
|
257
|
+
# ========================================
|
258
|
+
|
259
|
+
def render_dashboard_html
|
260
|
+
@initial_metrics = QueueService.queue_metrics
|
261
|
+
@configuration = SidekiqQueueManager.configuration.to_h
|
262
|
+
render template: 'sidekiq_queue_manager/dashboard/index'
|
263
|
+
end
|
264
|
+
|
265
|
+
def render_dashboard_json
|
266
|
+
set_no_cache_headers
|
267
|
+
render json: api_response(data: QueueService.queue_metrics)
|
268
|
+
end
|
269
|
+
|
270
|
+
def set_queue_name
|
271
|
+
@queue_name = params[:name] || params[:queue_name]
|
272
|
+
|
273
|
+
if @queue_name.blank?
|
274
|
+
render json: api_error_response('Queue name is required'), status: :bad_request
|
275
|
+
return
|
276
|
+
end
|
277
|
+
|
278
|
+
# Validate queue name exists
|
279
|
+
return if available_queue_names.include?(@queue_name)
|
280
|
+
|
281
|
+
render json: api_error_response("Invalid queue name: #{@queue_name}"), status: :not_found
|
282
|
+
nil
|
283
|
+
end
|
284
|
+
|
285
|
+
def available_queue_names
|
286
|
+
@available_queue_names ||= SidekiqQueueManager::QueueService.send(:available_queues)
|
287
|
+
end
|
288
|
+
|
289
|
+
def authentication_required?
|
290
|
+
SidekiqQueueManager.configuration.authentication_method.present?
|
291
|
+
end
|
292
|
+
|
293
|
+
def authenticate_user!
|
294
|
+
method_name = SidekiqQueueManager.configuration.authentication_method
|
295
|
+
send(method_name) if respond_to?(method_name, true)
|
296
|
+
end
|
297
|
+
|
298
|
+
def refresh_interval_seconds
|
299
|
+
SidekiqQueueManager.configuration.refresh_interval / 1000.0
|
300
|
+
end
|
301
|
+
|
302
|
+
# ========================================
|
303
|
+
# Server-Sent Events Helpers
|
304
|
+
# ========================================
|
305
|
+
|
306
|
+
def send_sse_event(event_type, data)
|
307
|
+
response.stream.write("event: #{event_type}\n")
|
308
|
+
response.stream.write("data: #{data.to_json}\n\n")
|
309
|
+
rescue StandardError => e
|
310
|
+
logger.error("Failed to send SSE event: #{e.message}")
|
311
|
+
end
|
312
|
+
|
313
|
+
# ========================================
|
314
|
+
# Response Formatters
|
315
|
+
# ========================================
|
316
|
+
|
317
|
+
def api_response(success: true, message: nil, data: nil, **extra)
|
318
|
+
response_hash = {
|
319
|
+
success: success,
|
320
|
+
timestamp: Time.current.iso8601
|
321
|
+
}
|
322
|
+
|
323
|
+
response_hash[:message] = message if message.present?
|
324
|
+
response_hash[:data] = data if data.present?
|
325
|
+
response_hash.merge!(extra) if extra.any?
|
326
|
+
|
327
|
+
response_hash
|
328
|
+
end
|
329
|
+
|
330
|
+
def api_error_response(message, **extra)
|
331
|
+
api_response(success: false, message: message, **extra)
|
332
|
+
end
|
333
|
+
|
334
|
+
# ========================================
|
335
|
+
# HTTP Headers
|
336
|
+
# ========================================
|
337
|
+
|
338
|
+
def set_no_cache_headers
|
339
|
+
response.headers['Cache-Control'] = 'no-cache, no-store, must-revalidate'
|
340
|
+
response.headers['Pragma'] = 'no-cache'
|
341
|
+
response.headers['Expires'] = '0'
|
342
|
+
end
|
343
|
+
|
344
|
+
# ========================================
|
345
|
+
# Error Handlers
|
346
|
+
# ========================================
|
347
|
+
|
348
|
+
def handle_api_error(exception)
|
349
|
+
logger.error("API Error: #{exception.class} - #{exception.message}")
|
350
|
+
logger.error(exception.backtrace.join("\n")) if Rails.env.development?
|
351
|
+
|
352
|
+
error_message = if Rails.env.production?
|
353
|
+
'An unexpected error occurred'
|
354
|
+
else
|
355
|
+
"#{exception.class}: #{exception.message}"
|
356
|
+
end
|
357
|
+
|
358
|
+
render json: api_error_response(error_message), status: :internal_server_error
|
359
|
+
end
|
360
|
+
|
361
|
+
def handle_service_error(exception)
|
362
|
+
logger.error("Service Error: #{exception.message}")
|
363
|
+
render json: api_error_response(exception.message), status: :unprocessable_entity
|
364
|
+
end
|
365
|
+
|
366
|
+
def handle_parameter_error(exception)
|
367
|
+
logger.warn("Parameter Error: #{exception.message}")
|
368
|
+
render json: api_error_response("Missing required parameter: #{exception.param}"),
|
369
|
+
status: :bad_request
|
370
|
+
end
|
371
|
+
end
|
372
|
+
end
|
373
|
+
|