solid_queue_monitor 0.1.2 → 0.2.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 +4 -4
- data/README.md +4 -4
- data/app/controllers/solid_queue_monitor/base_controller.rb +194 -0
- data/app/controllers/solid_queue_monitor/failed_jobs_controller.rb +62 -0
- data/app/controllers/solid_queue_monitor/in_progress_jobs_controller.rb +27 -0
- data/app/controllers/solid_queue_monitor/overview_controller.rb +25 -0
- data/app/controllers/solid_queue_monitor/queues_controller.rb +11 -0
- data/app/controllers/solid_queue_monitor/ready_jobs_controller.rb +14 -0
- data/app/controllers/solid_queue_monitor/recurring_jobs_controller.rb +14 -0
- data/app/controllers/solid_queue_monitor/scheduled_jobs_controller.rb +24 -0
- data/app/presenters/solid_queue_monitor/base_presenter.rb +6 -2
- data/app/presenters/solid_queue_monitor/failed_jobs_presenter.rb +0 -4
- data/app/presenters/solid_queue_monitor/in_progress_jobs_presenter.rb +71 -0
- data/app/presenters/solid_queue_monitor/jobs_presenter.rb +36 -2
- data/app/presenters/solid_queue_monitor/stats_presenter.rb +2 -2
- data/app/services/solid_queue_monitor/failed_job_service.rb +0 -6
- data/app/services/solid_queue_monitor/html_generator.rb +2 -1
- data/app/services/solid_queue_monitor/stats_calculator.rb +1 -0
- data/config/routes.rb +12 -12
- data/lib/solid_queue_monitor/version.rb +2 -2
- metadata +11 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 264dd6a6542855e5209702c11fbb46c1c267b5c8d48d530887030f924bdb929f
|
4
|
+
data.tar.gz: 4d5a33c4152f29d0c3ba4c9fd6c59582e7af9694e85469a78cde9a9d173a4433
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 704927588ba7329d988a7533ab70e6b444254abc64a15684fbeccce47221c929c39701abd8fbb02dbebf444c0c3a9521cbcc84260d06d3c6fdafca6e01ad2be5
|
7
|
+
data.tar.gz: 8b0a608d0ae89c54719dd5286e9de77cb4145d1f1c3dc7c3f8c1b46a521ae1b2e0ad37d35e7c2796a187122fc38f401b423a46109cc81328cff6805274338357
|
data/README.md
CHANGED
@@ -32,16 +32,16 @@ A lightweight, zero-dependency web interface for monitoring Solid Queue backgrou
|
|
32
32
|
|
33
33
|

|
34
34
|
|
35
|
-
###
|
35
|
+
### Failed Jobs
|
36
36
|
|
37
|
-

|
38
38
|
|
39
39
|
## Installation
|
40
40
|
|
41
41
|
Add this line to your application's Gemfile:
|
42
42
|
|
43
43
|
```ruby
|
44
|
-
gem 'solid_queue_monitor', '~> 0.
|
44
|
+
gem 'solid_queue_monitor', '~> 0.2.0'
|
45
45
|
```
|
46
46
|
|
47
47
|
Then execute:
|
@@ -151,4 +151,4 @@ Everyone interacting in the SolidQueueMonitor project's codebases, issue tracker
|
|
151
151
|
|
152
152
|
- [Solid Queue](https://github.com/rails/solid_queue) - The official Rails background job framework
|
153
153
|
- [Rails](https://github.com/rails/rails) - The web application framework
|
154
|
-
- [ActiveJob](https://github.com/rails/rails/tree/main/activejob) - Rails job
|
154
|
+
- [ActiveJob](https://github.com/rails/rails/tree/main/activejob) - Rails job frameworkk
|
@@ -0,0 +1,194 @@
|
|
1
|
+
module SolidQueueMonitor
|
2
|
+
class BaseController < ActionController::Base
|
3
|
+
include ActionController::HttpAuthentication::Basic::ControllerMethods
|
4
|
+
include ActionController::Flash
|
5
|
+
|
6
|
+
before_action :authenticate, if: -> { SolidQueueMonitor::AuthenticationService.authentication_required? }
|
7
|
+
layout false
|
8
|
+
skip_before_action :verify_authenticity_token
|
9
|
+
|
10
|
+
# Define a helper method for setting flash messages
|
11
|
+
def set_flash_message(message, type)
|
12
|
+
session[:flash_message] = message
|
13
|
+
session[:flash_type] = type
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def authenticate
|
19
|
+
authenticate_or_request_with_http_basic do |username, password|
|
20
|
+
SolidQueueMonitor::AuthenticationService.authenticate(username, password)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def paginate(relation)
|
25
|
+
PaginationService.new(relation, current_page, per_page).paginate
|
26
|
+
end
|
27
|
+
|
28
|
+
def render_page(title, content)
|
29
|
+
# Get flash message from session
|
30
|
+
message = session[:flash_message]
|
31
|
+
message_type = session[:flash_type]
|
32
|
+
|
33
|
+
# Clear the flash message from session after using it
|
34
|
+
session.delete(:flash_message)
|
35
|
+
session.delete(:flash_type)
|
36
|
+
|
37
|
+
html = SolidQueueMonitor::HtmlGenerator.new(
|
38
|
+
title: title,
|
39
|
+
content: content,
|
40
|
+
message: message,
|
41
|
+
message_type: message_type
|
42
|
+
).generate
|
43
|
+
|
44
|
+
render html: html.html_safe
|
45
|
+
end
|
46
|
+
|
47
|
+
def current_page
|
48
|
+
(params[:page] || 1).to_i
|
49
|
+
end
|
50
|
+
|
51
|
+
def per_page
|
52
|
+
SolidQueueMonitor.jobs_per_page
|
53
|
+
end
|
54
|
+
|
55
|
+
# Preload job statuses to avoid N+1 queries
|
56
|
+
def preload_job_statuses(jobs)
|
57
|
+
return if jobs.empty?
|
58
|
+
|
59
|
+
# Get all job IDs
|
60
|
+
job_ids = jobs.map(&:id)
|
61
|
+
|
62
|
+
# Find all failed jobs in a single query
|
63
|
+
failed_job_ids = SolidQueue::FailedExecution.where(job_id: job_ids).pluck(:job_id)
|
64
|
+
|
65
|
+
# Find all scheduled jobs in a single query
|
66
|
+
scheduled_job_ids = SolidQueue::ScheduledExecution.where(job_id: job_ids).pluck(:job_id)
|
67
|
+
|
68
|
+
# Attach the status information to each job
|
69
|
+
jobs.each do |job|
|
70
|
+
job.instance_variable_set(:@failed, failed_job_ids.include?(job.id))
|
71
|
+
job.instance_variable_set(:@scheduled, scheduled_job_ids.include?(job.id))
|
72
|
+
end
|
73
|
+
|
74
|
+
# Define the method to check if a job is failed
|
75
|
+
SolidQueue::Job.class_eval do
|
76
|
+
def failed?
|
77
|
+
if instance_variable_defined?(:@failed)
|
78
|
+
@failed
|
79
|
+
else
|
80
|
+
SolidQueue::FailedExecution.exists?(job_id: id)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def scheduled?
|
85
|
+
if instance_variable_defined?(:@scheduled)
|
86
|
+
@scheduled
|
87
|
+
else
|
88
|
+
SolidQueue::ScheduledExecution.exists?(job_id: id)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def filter_jobs(relation)
|
95
|
+
relation = relation.where("class_name LIKE ?", "%#{params[:class_name]}%") if params[:class_name].present?
|
96
|
+
relation = relation.where("queue_name LIKE ?", "%#{params[:queue_name]}%") if params[:queue_name].present?
|
97
|
+
|
98
|
+
if params[:status].present?
|
99
|
+
case params[:status]
|
100
|
+
when 'completed'
|
101
|
+
relation = relation.where.not(finished_at: nil)
|
102
|
+
when 'failed'
|
103
|
+
failed_job_ids = SolidQueue::FailedExecution.pluck(:job_id)
|
104
|
+
relation = relation.where(id: failed_job_ids)
|
105
|
+
when 'scheduled'
|
106
|
+
scheduled_job_ids = SolidQueue::ScheduledExecution.pluck(:job_id)
|
107
|
+
relation = relation.where(id: scheduled_job_ids)
|
108
|
+
when 'pending'
|
109
|
+
# Pending jobs are those that are not completed, failed, or scheduled
|
110
|
+
failed_job_ids = SolidQueue::FailedExecution.pluck(:job_id)
|
111
|
+
scheduled_job_ids = SolidQueue::ScheduledExecution.pluck(:job_id)
|
112
|
+
relation = relation.where(finished_at: nil)
|
113
|
+
.where.not(id: failed_job_ids + scheduled_job_ids)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
relation
|
118
|
+
end
|
119
|
+
|
120
|
+
def filter_ready_jobs(relation)
|
121
|
+
return relation unless params[:class_name].present? || params[:queue_name].present?
|
122
|
+
|
123
|
+
if params[:class_name].present?
|
124
|
+
job_ids = SolidQueue::Job.where("class_name LIKE ?", "%#{params[:class_name]}%").pluck(:id)
|
125
|
+
relation = relation.where(job_id: job_ids)
|
126
|
+
end
|
127
|
+
|
128
|
+
if params[:queue_name].present?
|
129
|
+
relation = relation.where("queue_name LIKE ?", "%#{params[:queue_name]}%")
|
130
|
+
end
|
131
|
+
|
132
|
+
relation
|
133
|
+
end
|
134
|
+
|
135
|
+
def filter_scheduled_jobs(relation)
|
136
|
+
return relation unless params[:class_name].present? || params[:queue_name].present?
|
137
|
+
|
138
|
+
if params[:class_name].present?
|
139
|
+
job_ids = SolidQueue::Job.where("class_name LIKE ?", "%#{params[:class_name]}%").pluck(:id)
|
140
|
+
relation = relation.where(job_id: job_ids)
|
141
|
+
end
|
142
|
+
|
143
|
+
if params[:queue_name].present?
|
144
|
+
relation = relation.where("queue_name LIKE ?", "%#{params[:queue_name]}%")
|
145
|
+
end
|
146
|
+
|
147
|
+
relation
|
148
|
+
end
|
149
|
+
|
150
|
+
def filter_recurring_jobs(relation)
|
151
|
+
return relation unless params[:class_name].present? || params[:queue_name].present?
|
152
|
+
|
153
|
+
if params[:class_name].present?
|
154
|
+
relation = relation.where("class_name LIKE ?", "%#{params[:class_name]}%")
|
155
|
+
end
|
156
|
+
|
157
|
+
if params[:queue_name].present?
|
158
|
+
relation = relation.where("queue_name LIKE ?", "%#{params[:queue_name]}%")
|
159
|
+
end
|
160
|
+
|
161
|
+
relation
|
162
|
+
end
|
163
|
+
|
164
|
+
def filter_failed_jobs(relation)
|
165
|
+
return relation unless params[:class_name].present? || params[:queue_name].present?
|
166
|
+
|
167
|
+
if params[:class_name].present?
|
168
|
+
job_ids = SolidQueue::Job.where("class_name LIKE ?", "%#{params[:class_name]}%").pluck(:id)
|
169
|
+
relation = relation.where(job_id: job_ids)
|
170
|
+
end
|
171
|
+
|
172
|
+
if params[:queue_name].present?
|
173
|
+
# Check if FailedExecution has queue_name column
|
174
|
+
if relation.column_names.include?('queue_name')
|
175
|
+
relation = relation.where("queue_name LIKE ?", "%#{params[:queue_name]}%")
|
176
|
+
else
|
177
|
+
# If not, filter by job's queue_name
|
178
|
+
job_ids = SolidQueue::Job.where("queue_name LIKE ?", "%#{params[:queue_name]}%").pluck(:id)
|
179
|
+
relation = relation.where(job_id: job_ids)
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
relation
|
184
|
+
end
|
185
|
+
|
186
|
+
def filter_params
|
187
|
+
{
|
188
|
+
class_name: params[:class_name],
|
189
|
+
queue_name: params[:queue_name],
|
190
|
+
status: params[:status]
|
191
|
+
}
|
192
|
+
end
|
193
|
+
end
|
194
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
module SolidQueueMonitor
|
2
|
+
class FailedJobsController < BaseController
|
3
|
+
def index
|
4
|
+
base_query = SolidQueue::FailedExecution.includes(:job).order(created_at: :desc)
|
5
|
+
@failed_jobs = paginate(filter_failed_jobs(base_query))
|
6
|
+
|
7
|
+
render_page('Failed Jobs', SolidQueueMonitor::FailedJobsPresenter.new(@failed_jobs[:records],
|
8
|
+
current_page: @failed_jobs[:current_page],
|
9
|
+
total_pages: @failed_jobs[:total_pages],
|
10
|
+
filters: filter_params
|
11
|
+
).render)
|
12
|
+
end
|
13
|
+
|
14
|
+
def retry
|
15
|
+
id = params[:id]
|
16
|
+
service = SolidQueueMonitor::FailedJobService.new
|
17
|
+
|
18
|
+
if service.retry_job(id)
|
19
|
+
set_flash_message("Job #{id} has been queued for retry.", 'success')
|
20
|
+
else
|
21
|
+
set_flash_message("Failed to retry job #{id}.", 'error')
|
22
|
+
end
|
23
|
+
|
24
|
+
redirect_to params[:redirect_to].present? ? params[:redirect_to] : failed_jobs_path
|
25
|
+
end
|
26
|
+
|
27
|
+
def discard
|
28
|
+
id = params[:id]
|
29
|
+
service = SolidQueueMonitor::FailedJobService.new
|
30
|
+
|
31
|
+
if service.discard_job(id)
|
32
|
+
set_flash_message("Job #{id} has been discarded.", 'success')
|
33
|
+
else
|
34
|
+
set_flash_message("Failed to discard job #{id}.", 'error')
|
35
|
+
end
|
36
|
+
|
37
|
+
redirect_to params[:redirect_to].present? ? params[:redirect_to] : failed_jobs_path
|
38
|
+
end
|
39
|
+
|
40
|
+
def retry_all
|
41
|
+
result = SolidQueueMonitor::FailedJobService.new.retry_all(params[:job_ids])
|
42
|
+
|
43
|
+
if result[:success]
|
44
|
+
set_flash_message(result[:message], 'success')
|
45
|
+
else
|
46
|
+
set_flash_message(result[:message], 'error')
|
47
|
+
end
|
48
|
+
redirect_to failed_jobs_path
|
49
|
+
end
|
50
|
+
|
51
|
+
def discard_all
|
52
|
+
result = SolidQueueMonitor::FailedJobService.new.discard_all(params[:job_ids])
|
53
|
+
|
54
|
+
if result[:success]
|
55
|
+
set_flash_message(result[:message], 'success')
|
56
|
+
else
|
57
|
+
set_flash_message(result[:message], 'error')
|
58
|
+
end
|
59
|
+
redirect_to failed_jobs_path
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module SolidQueueMonitor
|
2
|
+
class InProgressJobsController < BaseController
|
3
|
+
def index
|
4
|
+
base_query = SolidQueue::ClaimedExecution.includes(:job).order(created_at: :desc)
|
5
|
+
@in_progress_jobs = paginate(filter_in_progress_jobs(base_query))
|
6
|
+
|
7
|
+
render_page('In Progress Jobs', SolidQueueMonitor::InProgressJobsPresenter.new(@in_progress_jobs[:records],
|
8
|
+
current_page: @in_progress_jobs[:current_page],
|
9
|
+
total_pages: @in_progress_jobs[:total_pages],
|
10
|
+
filters: filter_params
|
11
|
+
).render)
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def filter_in_progress_jobs(relation)
|
17
|
+
return relation unless params[:class_name].present?
|
18
|
+
|
19
|
+
if params[:class_name].present?
|
20
|
+
job_ids = SolidQueue::Job.where("class_name LIKE ?", "%#{params[:class_name]}%").pluck(:id)
|
21
|
+
relation = relation.where(job_id: job_ids)
|
22
|
+
end
|
23
|
+
|
24
|
+
relation
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module SolidQueueMonitor
|
2
|
+
class OverviewController < BaseController
|
3
|
+
def index
|
4
|
+
@stats = SolidQueueMonitor::StatsCalculator.calculate
|
5
|
+
|
6
|
+
recent_jobs_query = SolidQueue::Job.order(created_at: :desc).limit(100)
|
7
|
+
@recent_jobs = paginate(filter_jobs(recent_jobs_query))
|
8
|
+
|
9
|
+
preload_job_statuses(@recent_jobs[:records])
|
10
|
+
|
11
|
+
render_page('Overview', generate_overview_content)
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def generate_overview_content
|
17
|
+
SolidQueueMonitor::StatsPresenter.new(@stats).render +
|
18
|
+
SolidQueueMonitor::JobsPresenter.new(@recent_jobs[:records],
|
19
|
+
current_page: @recent_jobs[:current_page],
|
20
|
+
total_pages: @recent_jobs[:total_pages],
|
21
|
+
filters: filter_params
|
22
|
+
).render
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
module SolidQueueMonitor
|
2
|
+
class QueuesController < BaseController
|
3
|
+
def index
|
4
|
+
@queues = SolidQueue::Job.group(:queue_name)
|
5
|
+
.select('queue_name, COUNT(*) as job_count')
|
6
|
+
.order('job_count DESC')
|
7
|
+
|
8
|
+
render_page('Queues', SolidQueueMonitor::QueuesPresenter.new(@queues).render)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module SolidQueueMonitor
|
2
|
+
class ReadyJobsController < BaseController
|
3
|
+
def index
|
4
|
+
base_query = SolidQueue::ReadyExecution.includes(:job).order(created_at: :desc)
|
5
|
+
@ready_jobs = paginate(filter_ready_jobs(base_query))
|
6
|
+
|
7
|
+
render_page('Ready Jobs', SolidQueueMonitor::ReadyJobsPresenter.new(@ready_jobs[:records],
|
8
|
+
current_page: @ready_jobs[:current_page],
|
9
|
+
total_pages: @ready_jobs[:total_pages],
|
10
|
+
filters: filter_params
|
11
|
+
).render)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module SolidQueueMonitor
|
2
|
+
class RecurringJobsController < BaseController
|
3
|
+
def index
|
4
|
+
base_query = filter_recurring_jobs(SolidQueue::RecurringTask.order(:key))
|
5
|
+
@recurring_jobs = paginate(base_query)
|
6
|
+
|
7
|
+
render_page('Recurring Jobs', SolidQueueMonitor::RecurringJobsPresenter.new(@recurring_jobs[:records],
|
8
|
+
current_page: @recurring_jobs[:current_page],
|
9
|
+
total_pages: @recurring_jobs[:total_pages],
|
10
|
+
filters: filter_params
|
11
|
+
).render)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module SolidQueueMonitor
|
2
|
+
class ScheduledJobsController < BaseController
|
3
|
+
def index
|
4
|
+
base_query = SolidQueue::ScheduledExecution.includes(:job).order(scheduled_at: :asc)
|
5
|
+
@scheduled_jobs = paginate(filter_scheduled_jobs(base_query))
|
6
|
+
|
7
|
+
render_page('Scheduled Jobs', SolidQueueMonitor::ScheduledJobsPresenter.new(@scheduled_jobs[:records],
|
8
|
+
current_page: @scheduled_jobs[:current_page],
|
9
|
+
total_pages: @scheduled_jobs[:total_pages],
|
10
|
+
filters: filter_params
|
11
|
+
).render)
|
12
|
+
end
|
13
|
+
|
14
|
+
def create
|
15
|
+
if params[:job_ids].present?
|
16
|
+
SolidQueueMonitor::ExecuteJobService.new.execute_many(params[:job_ids])
|
17
|
+
set_flash_message('Selected jobs moved to ready queue', 'success')
|
18
|
+
else
|
19
|
+
set_flash_message('No jobs selected', 'error')
|
20
|
+
end
|
21
|
+
redirect_to scheduled_jobs_path
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -32,8 +32,12 @@ module SolidQueueMonitor
|
|
32
32
|
end
|
33
33
|
|
34
34
|
# Page links
|
35
|
-
(
|
36
|
-
|
35
|
+
visible_pages = calculate_visible_pages(current_page, total_pages)
|
36
|
+
|
37
|
+
visible_pages.each do |page|
|
38
|
+
if page == :gap
|
39
|
+
html += "<span class=\"pagination-gap\">...</span>"
|
40
|
+
elsif page == current_page
|
37
41
|
html += "<span class=\"pagination-current\">#{page}</span>"
|
38
42
|
else
|
39
43
|
html += "<a href=\"?page=#{page}#{query_params}\" class=\"pagination-link\">#{page}</a>"
|
@@ -0,0 +1,71 @@
|
|
1
|
+
module SolidQueueMonitor
|
2
|
+
class InProgressJobsPresenter < BasePresenter
|
3
|
+
include SolidQueueMonitor::Engine.routes.url_helpers
|
4
|
+
|
5
|
+
def initialize(jobs, current_page: 1, total_pages: 1, filters: {})
|
6
|
+
@jobs = jobs
|
7
|
+
@current_page = current_page
|
8
|
+
@total_pages = total_pages
|
9
|
+
@filters = filters
|
10
|
+
end
|
11
|
+
|
12
|
+
def render
|
13
|
+
section_wrapper('In Progress Jobs', generate_filter_form + generate_table + generate_pagination(@current_page, @total_pages))
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def generate_filter_form
|
19
|
+
<<-HTML
|
20
|
+
<div class="filter-form-container">
|
21
|
+
<form method="get" action="#{in_progress_jobs_path}" class="filter-form">
|
22
|
+
<div class="filter-group">
|
23
|
+
<label for="class_name">Job Class:</label>
|
24
|
+
<input type="text" name="class_name" id="class_name" value="#{@filters[:class_name]}" placeholder="Filter by class name">
|
25
|
+
</div>
|
26
|
+
|
27
|
+
<div class="filter-actions">
|
28
|
+
<button type="submit" class="filter-button">Apply Filters</button>
|
29
|
+
<a href="#{in_progress_jobs_path}" class="reset-button">Reset</a>
|
30
|
+
</div>
|
31
|
+
</form>
|
32
|
+
</div>
|
33
|
+
HTML
|
34
|
+
end
|
35
|
+
|
36
|
+
def generate_table
|
37
|
+
<<-HTML
|
38
|
+
<div class="table-container">
|
39
|
+
<table>
|
40
|
+
<thead>
|
41
|
+
<tr>
|
42
|
+
<th>Job</th>
|
43
|
+
<th>Started At</th>
|
44
|
+
<th>Process ID</th>
|
45
|
+
</tr>
|
46
|
+
</thead>
|
47
|
+
<tbody>
|
48
|
+
#{@jobs.map { |execution| generate_row(execution) }.join}
|
49
|
+
</tbody>
|
50
|
+
</table>
|
51
|
+
</div>
|
52
|
+
HTML
|
53
|
+
end
|
54
|
+
|
55
|
+
def generate_row(execution)
|
56
|
+
job = execution.job
|
57
|
+
<<-HTML
|
58
|
+
<tr>
|
59
|
+
<td>
|
60
|
+
<div class="job-class">#{job.class_name}</div>
|
61
|
+
<div class="job-meta">
|
62
|
+
<span class="job-timestamp">Queued at: #{format_datetime(job.created_at)}</span>
|
63
|
+
</div>
|
64
|
+
</td>
|
65
|
+
<td>#{format_datetime(execution.created_at)}</td>
|
66
|
+
<td>#{execution.process_id}</td>
|
67
|
+
</tr>
|
68
|
+
HTML
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -70,6 +70,7 @@ module SolidQueueMonitor
|
|
70
70
|
<th>Queue</th>
|
71
71
|
<th>Status</th>
|
72
72
|
<th>Created At</th>
|
73
|
+
<th>Actions</th>
|
73
74
|
</tr>
|
74
75
|
</thead>
|
75
76
|
<tbody>
|
@@ -82,15 +83,48 @@ module SolidQueueMonitor
|
|
82
83
|
|
83
84
|
def generate_row(job)
|
84
85
|
status = job_status(job)
|
85
|
-
|
86
|
+
|
87
|
+
# Build the row HTML
|
88
|
+
row_html = <<-HTML
|
86
89
|
<tr>
|
87
90
|
<td>#{job.id}</td>
|
88
91
|
<td>#{job.class_name}</td>
|
89
92
|
<td>#{job.queue_name}</td>
|
90
93
|
<td><span class='status-badge status-#{status}'>#{status}</span></td>
|
91
94
|
<td>#{format_datetime(job.created_at)}</td>
|
92
|
-
</tr>
|
93
95
|
HTML
|
96
|
+
|
97
|
+
# Add actions column only for failed jobs
|
98
|
+
if status == 'failed'
|
99
|
+
# Find the failed execution record for this job
|
100
|
+
failed_execution = SolidQueue::FailedExecution.find_by(job_id: job.id)
|
101
|
+
|
102
|
+
if failed_execution
|
103
|
+
row_html += <<-HTML
|
104
|
+
<td class="actions-cell">
|
105
|
+
<div class="job-actions">
|
106
|
+
<form method="post" action="#{retry_failed_job_path(id: failed_execution.id)}" class="inline-form">
|
107
|
+
<input type="hidden" name="redirect_to" value="#{root_path}">
|
108
|
+
<button type="submit" class="action-button retry-button">Retry</button>
|
109
|
+
</form>
|
110
|
+
|
111
|
+
<form method="post" action="#{discard_failed_job_path(id: failed_execution.id)}" class="inline-form"
|
112
|
+
onsubmit="return confirm('Are you sure you want to discard this job?');">
|
113
|
+
<input type="hidden" name="redirect_to" value="#{root_path}">
|
114
|
+
<button type="submit" class="action-button discard-button">Discard</button>
|
115
|
+
</form>
|
116
|
+
</div>
|
117
|
+
</td>
|
118
|
+
HTML
|
119
|
+
else
|
120
|
+
row_html += "<td></td>"
|
121
|
+
end
|
122
|
+
else
|
123
|
+
row_html += "<td></td>"
|
124
|
+
end
|
125
|
+
|
126
|
+
row_html += "</tr>"
|
127
|
+
row_html
|
94
128
|
end
|
95
129
|
|
96
130
|
def job_status(job)
|
@@ -10,12 +10,12 @@ module SolidQueueMonitor
|
|
10
10
|
<h3>Queue Statistics</h3>
|
11
11
|
<div class="stats">
|
12
12
|
#{generate_stat_card('Total Jobs', @stats[:total_jobs])}
|
13
|
-
#{generate_stat_card('Unique Queues', @stats[:unique_queues])}
|
14
13
|
#{generate_stat_card('Ready', @stats[:ready])}
|
14
|
+
#{generate_stat_card('In Progress', @stats[:in_progress])}
|
15
15
|
#{generate_stat_card('Scheduled', @stats[:scheduled])}
|
16
|
+
#{generate_stat_card('Recurring', @stats[:recurring])}
|
16
17
|
#{generate_stat_card('Failed', @stats[:failed])}
|
17
18
|
#{generate_stat_card('Completed', @stats[:completed])}
|
18
|
-
#{generate_stat_card('Recurring', @stats[:recurring])}
|
19
19
|
</div>
|
20
20
|
</div>
|
21
21
|
HTML
|
@@ -8,14 +8,12 @@ module SolidQueueMonitor
|
|
8
8
|
return { success: false, message: "Associated job not found" } unless job
|
9
9
|
|
10
10
|
ActiveRecord::Base.transaction do
|
11
|
-
# Create a ready execution for the job
|
12
11
|
SolidQueue::ReadyExecution.create!(
|
13
12
|
job_id: job.id,
|
14
13
|
queue_name: get_queue_name(failed_execution, job),
|
15
14
|
priority: job.priority
|
16
15
|
)
|
17
16
|
|
18
|
-
# Delete the failed execution
|
19
17
|
failed_execution.destroy!
|
20
18
|
end
|
21
19
|
|
@@ -30,10 +28,8 @@ module SolidQueueMonitor
|
|
30
28
|
return { success: false, message: "Associated job not found" } unless job
|
31
29
|
|
32
30
|
ActiveRecord::Base.transaction do
|
33
|
-
# Mark the job as finished
|
34
31
|
job.update!(finished_at: Time.current)
|
35
32
|
|
36
|
-
# Delete the failed execution
|
37
33
|
failed_execution.destroy!
|
38
34
|
end
|
39
35
|
|
@@ -91,11 +87,9 @@ module SolidQueueMonitor
|
|
91
87
|
private
|
92
88
|
|
93
89
|
def get_queue_name(failed_execution, job)
|
94
|
-
# Try to get queue_name from failed_execution if the method exists
|
95
90
|
if failed_execution.respond_to?(:queue_name) && failed_execution.queue_name.present?
|
96
91
|
failed_execution.queue_name
|
97
92
|
else
|
98
|
-
# Fall back to job's queue_name
|
99
93
|
job.queue_name
|
100
94
|
end
|
101
95
|
end
|
@@ -89,8 +89,9 @@ module SolidQueueMonitor
|
|
89
89
|
<nav class="navigation">
|
90
90
|
<a href="#{root_path}" class="nav-link">Overview</a>
|
91
91
|
<a href="#{ready_jobs_path}" class="nav-link">Ready Jobs</a>
|
92
|
-
<a href="#{
|
92
|
+
<a href="#{in_progress_jobs_path}" class="nav-link">In Progress Jobs</a>
|
93
93
|
<a href="#{scheduled_jobs_path}" class="nav-link">Scheduled Jobs</a>
|
94
|
+
<a href="#{recurring_jobs_path}" class="nav-link">Recurring Jobs</a>
|
94
95
|
<a href="#{failed_jobs_path}" class="nav-link">Failed Jobs</a>
|
95
96
|
<a href="#{queues_path}" class="nav-link">Queues</a>
|
96
97
|
</nav>
|
@@ -7,6 +7,7 @@ module SolidQueueMonitor
|
|
7
7
|
scheduled: SolidQueue::ScheduledExecution.count,
|
8
8
|
ready: SolidQueue::ReadyExecution.count,
|
9
9
|
failed: SolidQueue::FailedExecution.count,
|
10
|
+
in_progress: SolidQueue::ClaimedExecution.count,
|
10
11
|
completed: SolidQueue::Job.where.not(finished_at: nil).count,
|
11
12
|
recurring: SolidQueue::RecurringTask.count
|
12
13
|
}
|
data/config/routes.rb
CHANGED
@@ -1,17 +1,17 @@
|
|
1
1
|
SolidQueueMonitor::Engine.routes.draw do
|
2
|
-
root to: '
|
2
|
+
root to: 'overview#index', as: :root
|
3
3
|
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
4
|
+
resources :ready_jobs, only: [:index]
|
5
|
+
resources :scheduled_jobs, only: [:index]
|
6
|
+
resources :recurring_jobs, only: [:index]
|
7
|
+
resources :failed_jobs, only: [:index]
|
8
|
+
resources :in_progress_jobs, only: [:index]
|
9
|
+
resources :queues, only: [:index]
|
9
10
|
|
10
|
-
post 'execute_jobs', to: '
|
11
|
+
post 'execute_jobs', to: 'scheduled_jobs#create', as: :execute_jobs
|
11
12
|
|
12
|
-
#
|
13
|
-
post '
|
14
|
-
post '
|
15
|
-
post '
|
16
|
-
post 'discard_failed_jobs', to: 'monitor#discard_failed_jobs', as: 'discard_failed_jobs'
|
13
|
+
post 'retry_failed_job/:id', to: 'failed_jobs#retry', as: :retry_failed_job
|
14
|
+
post 'discard_failed_job/:id', to: 'failed_jobs#discard', as: :discard_failed_job
|
15
|
+
post 'retry_failed_jobs', to: 'failed_jobs#retry_all', as: :retry_failed_jobs
|
16
|
+
post 'discard_failed_jobs', to: 'failed_jobs#discard_all', as: :discard_failed_jobs
|
17
17
|
end
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: solid_queue_monitor
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Vishal Sadriya
|
8
8
|
bindir: exe
|
9
9
|
cert_chain: []
|
10
|
-
date: 2025-03-
|
10
|
+
date: 2025-03-28 00:00:00.000000000 Z
|
11
11
|
dependencies:
|
12
12
|
- !ruby/object:Gem::Dependency
|
13
13
|
name: rails
|
@@ -187,9 +187,18 @@ extra_rdoc_files: []
|
|
187
187
|
files:
|
188
188
|
- README.md
|
189
189
|
- Rakefile
|
190
|
+
- app/controllers/solid_queue_monitor/base_controller.rb
|
191
|
+
- app/controllers/solid_queue_monitor/failed_jobs_controller.rb
|
192
|
+
- app/controllers/solid_queue_monitor/in_progress_jobs_controller.rb
|
190
193
|
- app/controllers/solid_queue_monitor/monitor_controller.rb
|
194
|
+
- app/controllers/solid_queue_monitor/overview_controller.rb
|
195
|
+
- app/controllers/solid_queue_monitor/queues_controller.rb
|
196
|
+
- app/controllers/solid_queue_monitor/ready_jobs_controller.rb
|
197
|
+
- app/controllers/solid_queue_monitor/recurring_jobs_controller.rb
|
198
|
+
- app/controllers/solid_queue_monitor/scheduled_jobs_controller.rb
|
191
199
|
- app/presenters/solid_queue_monitor/base_presenter.rb
|
192
200
|
- app/presenters/solid_queue_monitor/failed_jobs_presenter.rb
|
201
|
+
- app/presenters/solid_queue_monitor/in_progress_jobs_presenter.rb
|
193
202
|
- app/presenters/solid_queue_monitor/jobs_presenter.rb
|
194
203
|
- app/presenters/solid_queue_monitor/queues_presenter.rb
|
195
204
|
- app/presenters/solid_queue_monitor/ready_jobs_presenter.rb
|