job_harbor 0.6.0 → 0.7.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 +136 -16
- data/app/assets/stylesheets/job_harbor/application.css +1 -1
- data/app/assets/stylesheets/job_harbor/application.tailwind.css +8 -0
- data/app/components/job_harbor/badge_component.rb +5 -3
- data/app/components/job_harbor/chart_component.rb +2 -4
- data/app/components/job_harbor/failure_rates_component.rb +19 -7
- data/app/components/job_harbor/job_row_component.rb +1 -1
- data/app/components/job_harbor/per_page_selector_component.rb +1 -3
- data/app/components/job_harbor/queue_card_component.rb +3 -2
- data/app/components/job_harbor/refresh_selector_component.rb +9 -4
- data/app/components/job_harbor/theme_toggle_component.rb +1 -2
- data/app/components/job_harbor/worker_card_component.rb +19 -12
- data/app/controllers/job_harbor/application_controller.rb +7 -3
- data/app/controllers/job_harbor/dashboard_controller.rb +1 -1
- data/app/controllers/job_harbor/jobs_controller.rb +46 -39
- data/app/controllers/job_harbor/queues_controller.rb +4 -5
- data/app/controllers/job_harbor/recurring_tasks_controller.rb +36 -8
- data/app/controllers/job_harbor/workers_controller.rb +1 -1
- data/app/models/job_harbor/chart_data.rb +22 -63
- data/app/models/job_harbor/dashboard_stats.rb +3 -20
- data/app/models/job_harbor/failure_stats.rb +8 -44
- data/app/models/job_harbor/job_presenter.rb +80 -131
- data/app/models/job_harbor/page_info.rb +9 -0
- data/app/models/job_harbor/processes.rb +25 -0
- data/app/models/job_harbor/queue_stats.rb +15 -12
- data/app/models/job_harbor/recurring_task_arguments.rb +29 -0
- data/app/models/job_harbor/sensitive_value_filter.rb +68 -0
- data/app/views/job_harbor/jobs/index.html.erb +12 -8
- data/app/views/job_harbor/jobs/search.html.erb +3 -3
- data/app/views/job_harbor/jobs/show.html.erb +15 -5
- data/app/views/job_harbor/queues/show.html.erb +9 -12
- data/app/views/job_harbor/recurring_tasks/show.html.erb +6 -6
- data/app/views/layouts/job_harbor/application.html.erb +58 -27
- data/lib/job_harbor/configuration.rb +30 -3
- data/lib/job_harbor/engine.rb +4 -11
- data/lib/job_harbor/version.rb +1 -1
- metadata +19 -12
- data/lib/tasks/solidqueue_dashboard_tasks.rake +0 -4
- data/lib/tasks/tailwind.rake +0 -129
|
@@ -2,13 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
module JobHarbor
|
|
4
4
|
class JobsController < ApplicationController
|
|
5
|
+
BULK_BATCH_SIZE = 500
|
|
6
|
+
|
|
5
7
|
before_action :set_job, only: [ :show, :retry, :discard ]
|
|
8
|
+
before_action :reject_malformed_filters, only: [ :retry_all, :discard_all ]
|
|
6
9
|
|
|
7
10
|
def index
|
|
8
11
|
@status = params[:status]
|
|
9
12
|
@per_page = per_page_param
|
|
10
|
-
@class_name =
|
|
11
|
-
@queue_name =
|
|
13
|
+
@class_name = filter_param(:class_name)
|
|
14
|
+
@queue_name = filter_param(:queue_name)
|
|
12
15
|
|
|
13
16
|
@pagy, @jobs = JobPresenter.all_with_status(
|
|
14
17
|
@status,
|
|
@@ -18,6 +21,7 @@ module JobHarbor
|
|
|
18
21
|
queue_name: @queue_name
|
|
19
22
|
)
|
|
20
23
|
@counts = job_counts
|
|
24
|
+
@bulk_count = bulk_scope(@status)&.count
|
|
21
25
|
@filter_data = filter_data
|
|
22
26
|
set_page_title @status ? "#{@status.titleize} Jobs" : "All Jobs"
|
|
23
27
|
end
|
|
@@ -47,24 +51,23 @@ module JobHarbor
|
|
|
47
51
|
end
|
|
48
52
|
|
|
49
53
|
def discard
|
|
50
|
-
if @job.can_discard?
|
|
51
|
-
perform_discard(@job)
|
|
54
|
+
if @job.can_discard? && @job.discard
|
|
52
55
|
redirect_to jobs_path(status: params[:return_status]), notice: "Job has been discarded."
|
|
53
56
|
else
|
|
54
57
|
redirect_to job_path(@job), alert: "This job cannot be discarded."
|
|
55
58
|
end
|
|
59
|
+
rescue SolidQueue::Execution::UndiscardableError
|
|
60
|
+
redirect_to job_path(@job), alert: "This job cannot be discarded."
|
|
56
61
|
end
|
|
57
62
|
|
|
58
63
|
def retry_all
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
redirect_to jobs_path(status: status), notice: "#{count} jobs queued for retry."
|
|
64
|
+
count = retry_all_jobs(params[:status])
|
|
65
|
+
redirect_to bulk_redirect_path, notice: "#{helpers.pluralize(count, "job")} queued for retry."
|
|
62
66
|
end
|
|
63
67
|
|
|
64
68
|
def discard_all
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
redirect_to jobs_path(status: status), notice: "#{count} jobs discarded."
|
|
69
|
+
count = discard_all_jobs(params[:status])
|
|
70
|
+
redirect_to bulk_redirect_path, notice: "#{helpers.pluralize(count, "job")} discarded."
|
|
68
71
|
end
|
|
69
72
|
|
|
70
73
|
private
|
|
@@ -107,44 +110,48 @@ module JobHarbor
|
|
|
107
110
|
failed_execution.retry
|
|
108
111
|
end
|
|
109
112
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
SolidQueue::
|
|
114
|
-
SolidQueue::
|
|
115
|
-
|
|
113
|
+
# Nil for tabs without bulk actions.
|
|
114
|
+
def bulk_scope(status)
|
|
115
|
+
executions = case status
|
|
116
|
+
when "failed" then SolidQueue::FailedExecution
|
|
117
|
+
when "blocked" then SolidQueue::BlockedExecution
|
|
118
|
+
end
|
|
119
|
+
return unless executions
|
|
120
|
+
|
|
121
|
+
JobPresenter.apply_filters(
|
|
122
|
+
SolidQueue::Job.where(id: executions.select(:job_id)),
|
|
123
|
+
class_name: filter_param(:class_name),
|
|
124
|
+
queue_name: filter_param(:queue_name)
|
|
125
|
+
)
|
|
126
|
+
end
|
|
116
127
|
|
|
117
|
-
|
|
118
|
-
|
|
128
|
+
def reject_malformed_filters
|
|
129
|
+
head :bad_request unless %i[ class_name queue_name ].all? { |key| params[key].nil? || params[key].is_a?(String) }
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def filter_param(key)
|
|
133
|
+
params[key] if params[key].is_a?(String)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def bulk_redirect_path
|
|
137
|
+
jobs_path(status: params[:status], class_name: filter_param(:class_name).presence, queue_name: filter_param(:queue_name).presence)
|
|
119
138
|
end
|
|
120
139
|
|
|
121
140
|
def retry_all_jobs(status)
|
|
122
141
|
return 0 unless status == "failed"
|
|
123
142
|
|
|
124
|
-
|
|
125
|
-
SolidQueue::FailedExecution.find_each do |fe|
|
|
126
|
-
fe.retry
|
|
127
|
-
count += 1
|
|
128
|
-
end
|
|
129
|
-
count
|
|
143
|
+
bulk_scope(status).in_batches(of: BULK_BATCH_SIZE).sum { |jobs| SolidQueue::FailedExecution.retry_all(jobs) }
|
|
130
144
|
end
|
|
131
145
|
|
|
132
146
|
def discard_all_jobs(status)
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
count = SolidQueue::BlockedExecution.count
|
|
142
|
-
job_ids = SolidQueue::BlockedExecution.pluck(:job_id)
|
|
143
|
-
SolidQueue::BlockedExecution.delete_all
|
|
144
|
-
SolidQueue::Job.where(id: job_ids).update_all(finished_at: Time.current)
|
|
145
|
-
count
|
|
146
|
-
else
|
|
147
|
-
0
|
|
147
|
+
scope = bulk_scope(status)
|
|
148
|
+
return 0 unless scope
|
|
149
|
+
|
|
150
|
+
executions = status == "failed" ? SolidQueue::FailedExecution : SolidQueue::BlockedExecution
|
|
151
|
+
scope.in_batches(of: BULK_BATCH_SIZE).sum do |jobs|
|
|
152
|
+
ids = jobs.ids
|
|
153
|
+
executions.discard_all_from_jobs(jobs)
|
|
154
|
+
ids.size - SolidQueue::Job.where(id: ids).count
|
|
148
155
|
end
|
|
149
156
|
end
|
|
150
157
|
end
|
|
@@ -13,20 +13,19 @@ module JobHarbor
|
|
|
13
13
|
@pagy, @jobs = JobPresenter.all_with_status(
|
|
14
14
|
nil,
|
|
15
15
|
page: params[:page],
|
|
16
|
-
per_page: sq_config.jobs_per_page
|
|
16
|
+
per_page: sq_config.jobs_per_page,
|
|
17
|
+
queue_name: @queue.name
|
|
17
18
|
)
|
|
18
|
-
# Filter to only jobs in this queue
|
|
19
|
-
@jobs = @jobs.select { |j| j.queue_name == @queue.name }
|
|
20
19
|
set_page_title "Queue: #{@queue.name}"
|
|
21
20
|
end
|
|
22
21
|
|
|
23
22
|
def pause
|
|
24
|
-
@queue.pause
|
|
23
|
+
@queue.pause
|
|
25
24
|
redirect_to queues_path, notice: "Queue '#{@queue.name}' has been paused."
|
|
26
25
|
end
|
|
27
26
|
|
|
28
27
|
def resume
|
|
29
|
-
@queue.resume
|
|
28
|
+
@queue.resume
|
|
30
29
|
redirect_to queues_path, notice: "Queue '#{@queue.name}' has been resumed."
|
|
31
30
|
end
|
|
32
31
|
|
|
@@ -2,7 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
module JobHarbor
|
|
4
4
|
class RecurringTasksController < ApplicationController
|
|
5
|
+
RECENT_EXECUTIONS_LIMIT = 20
|
|
6
|
+
|
|
5
7
|
before_action :set_task, only: [ :show, :enqueue_now ]
|
|
8
|
+
before_action :ensure_job_class_defined, only: :enqueue_now
|
|
6
9
|
|
|
7
10
|
def index
|
|
8
11
|
@tasks = SolidQueue::RecurringTask.order(:key)
|
|
@@ -10,22 +13,47 @@ module JobHarbor
|
|
|
10
13
|
end
|
|
11
14
|
|
|
12
15
|
def show
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
16
|
+
executions = SolidQueue::RecurringExecution.where(task_key: @task.key)
|
|
17
|
+
@task_arguments = RecurringTaskArguments.new(@task)
|
|
18
|
+
@executions_count = executions.count
|
|
19
|
+
@last_run_at = executions.maximum(:run_at)
|
|
20
|
+
@recent_executions = executions.order(run_at: :desc).limit(RECENT_EXECUTIONS_LIMIT)
|
|
17
21
|
set_page_title "Task: #{@task.key}"
|
|
18
22
|
end
|
|
19
23
|
|
|
20
24
|
def enqueue_now
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
+
if enqueued?(at: Time.current.change(usec: 0))
|
|
26
|
+
redirect_to recurring_task_path(@task), notice: "Task '#{@task.key}' has been enqueued."
|
|
27
|
+
else
|
|
28
|
+
redirect_to recurring_task_path(@task),
|
|
29
|
+
alert: "Task '#{@task.key}' was not enqueued. It may have just run, or the job refused to enqueue; check the logs."
|
|
30
|
+
end
|
|
25
31
|
end
|
|
26
32
|
|
|
27
33
|
private
|
|
28
34
|
|
|
35
|
+
# Whole seconds, like cron ticks, so same-second clicks hit SQ's unique (task_key, run_at).
|
|
36
|
+
# SQ 1.0.x records a halted job with a nil job_id; the NotNullViolation rolls back.
|
|
37
|
+
def enqueued?(at:)
|
|
38
|
+
job = @task.enqueue(at: at)
|
|
39
|
+
job.present? && job.successfully_enqueued?
|
|
40
|
+
rescue ActiveRecord::NotNullViolation => error
|
|
41
|
+
Rails.error.report(error, handled: true)
|
|
42
|
+
false
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# SQ's job_class is nil for an undefined class; blank class_name uses SQ's default job.
|
|
46
|
+
def ensure_job_class_defined
|
|
47
|
+
class_name = @task.class_name
|
|
48
|
+
return if class_name.blank?
|
|
49
|
+
|
|
50
|
+
job_class = class_name.safe_constantize
|
|
51
|
+
return if job_class.is_a?(Class) && job_class < ActiveJob::Base
|
|
52
|
+
|
|
53
|
+
redirect_to recurring_task_path(@task),
|
|
54
|
+
alert: "Task '#{@task.key}' can't run: #{class_name} isn't a job class defined in this app."
|
|
55
|
+
end
|
|
56
|
+
|
|
29
57
|
def set_task
|
|
30
58
|
@task = SolidQueue::RecurringTask.find(params[:id])
|
|
31
59
|
rescue ActiveRecord::RecordNotFound
|
|
@@ -4,7 +4,7 @@ module JobHarbor
|
|
|
4
4
|
class WorkersController < ApplicationController
|
|
5
5
|
def index
|
|
6
6
|
@workers = SolidQueue::Process.order(last_heartbeat_at: :desc)
|
|
7
|
-
@active_count =
|
|
7
|
+
@active_count = Processes.alive.count
|
|
8
8
|
@stale_count = @workers.count - @active_count
|
|
9
9
|
set_page_title "Workers"
|
|
10
10
|
end
|
|
@@ -13,7 +13,7 @@ module JobHarbor
|
|
|
13
13
|
def initialize(range: "24h")
|
|
14
14
|
@range = RANGES[range] || RANGES["24h"]
|
|
15
15
|
@duration = @range[:duration]
|
|
16
|
-
@interval = @range[:interval]
|
|
16
|
+
@interval = @range[:interval].to_i
|
|
17
17
|
@cutoff = Time.current - @duration
|
|
18
18
|
end
|
|
19
19
|
|
|
@@ -22,83 +22,42 @@ module JobHarbor
|
|
|
22
22
|
end
|
|
23
23
|
|
|
24
24
|
def series
|
|
25
|
-
@series ||=
|
|
25
|
+
@series ||= {
|
|
26
|
+
labels: bucket_starts.map { |time| format_label(time) },
|
|
27
|
+
completed: counts_per_bucket(SolidQueue::Job, :finished_at),
|
|
28
|
+
failed: counts_per_bucket(SolidQueue::FailedExecution, :created_at),
|
|
29
|
+
enqueued: counts_per_bucket(SolidQueue::Job, :created_at)
|
|
30
|
+
}
|
|
26
31
|
end
|
|
27
32
|
|
|
28
33
|
private
|
|
29
34
|
|
|
30
|
-
def
|
|
31
|
-
|
|
32
|
-
labels = buckets.map { |t| format_label(t) }
|
|
33
|
-
|
|
34
|
-
completed = count_by_bucket(completed_jobs, :finished_at, buckets)
|
|
35
|
-
failed = count_by_bucket(failed_jobs, :created_at, buckets)
|
|
36
|
-
enqueued = count_by_bucket(enqueued_jobs, :created_at, buckets)
|
|
37
|
-
|
|
38
|
-
{
|
|
39
|
-
labels: labels,
|
|
40
|
-
completed: completed,
|
|
41
|
-
failed: failed,
|
|
42
|
-
enqueued: enqueued
|
|
43
|
-
}
|
|
35
|
+
def bucket_count
|
|
36
|
+
@duration.to_i / @interval
|
|
44
37
|
end
|
|
45
38
|
|
|
46
|
-
def
|
|
47
|
-
|
|
48
|
-
current = @cutoff
|
|
49
|
-
while current <= Time.current
|
|
50
|
-
buckets << current
|
|
51
|
-
current += @interval
|
|
52
|
-
end
|
|
53
|
-
buckets
|
|
39
|
+
def bucket_starts
|
|
40
|
+
Array.new(bucket_count) { |index| @cutoff + index * @interval }
|
|
54
41
|
end
|
|
55
42
|
|
|
56
43
|
def format_label(time)
|
|
57
|
-
|
|
58
|
-
time.strftime("%H:%M")
|
|
59
|
-
elsif @duration <= 24.hours
|
|
60
|
-
time.strftime("%H:%M")
|
|
61
|
-
else
|
|
62
|
-
time.strftime("%b %d")
|
|
63
|
-
end
|
|
44
|
+
@duration <= 24.hours ? time.strftime("%H:%M") : time.strftime("%b %d")
|
|
64
45
|
end
|
|
65
46
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
.group_by_period(@interval, time_field)
|
|
47
|
+
# One row of SUM(CASE ...) per series: ANSI SQL, so it needs no per-database date truncation.
|
|
48
|
+
def counts_per_bucket(model, time_field)
|
|
49
|
+
column = "#{model.quoted_table_name}.#{model.connection.quote_column_name(time_field)}"
|
|
50
|
+
sums = bucket_starts.each_with_index.map { |start, index| bucket_sum(model, column, start, last: index == bucket_count - 1) }
|
|
71
51
|
|
|
72
|
-
|
|
73
|
-
buckets.map do |bucket_start|
|
|
74
|
-
bucket_end = bucket_start + @interval
|
|
75
|
-
counts.count { |time, _| time >= bucket_start && time < bucket_end }
|
|
76
|
-
end
|
|
77
|
-
rescue => e
|
|
78
|
-
# Fallback to simple counting if group_by_period isn't available
|
|
79
|
-
buckets.map do |bucket_start|
|
|
80
|
-
bucket_end = bucket_start + @interval
|
|
81
|
-
scope.where("#{time_field} >= ? AND #{time_field} < ?", bucket_start, bucket_end).count
|
|
82
|
-
end
|
|
52
|
+
model.where(time_field => @cutoff..).pick(*sums).map(&:to_i)
|
|
83
53
|
end
|
|
84
54
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
def failed_jobs
|
|
90
|
-
SolidQueue::FailedExecution.all
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
def enqueued_jobs
|
|
94
|
-
SolidQueue::Job.all
|
|
95
|
-
end
|
|
55
|
+
# The last bucket is open-ended so a job stamped at the instant of the query still counts.
|
|
56
|
+
def bucket_sum(model, column, start, last:)
|
|
57
|
+
within = last ? "#{column} >= ?" : "#{column} >= ? AND #{column} < ?"
|
|
58
|
+
binds = last ? [ start ] : [ start, start + @interval ]
|
|
96
59
|
|
|
97
|
-
|
|
98
|
-
# Simple implementation - group records by time bucket
|
|
99
|
-
scope.pluck(time_field).group_by do |time|
|
|
100
|
-
time.to_i / interval.to_i * interval.to_i
|
|
101
|
-
end.transform_keys { |ts| Time.at(ts) }
|
|
60
|
+
Arel.sql(model.sanitize_sql_array([ "SUM(CASE WHEN #{within} THEN 1 ELSE 0 END)", *binds ]))
|
|
102
61
|
end
|
|
103
62
|
end
|
|
104
63
|
end
|
|
@@ -11,10 +11,6 @@ module JobHarbor
|
|
|
11
11
|
calculate_stats
|
|
12
12
|
end
|
|
13
13
|
|
|
14
|
-
def total_jobs
|
|
15
|
-
pending_count + scheduled_count + in_progress_count + failed_count + blocked_count
|
|
16
|
-
end
|
|
17
|
-
|
|
18
14
|
private
|
|
19
15
|
|
|
20
16
|
def calculate_stats
|
|
@@ -51,32 +47,19 @@ module JobHarbor
|
|
|
51
47
|
end
|
|
52
48
|
|
|
53
49
|
def finished_jobs_count
|
|
54
|
-
|
|
55
|
-
if SolidQueue::Job.column_names.include?("finished_at")
|
|
56
|
-
SolidQueue::Job.where("finished_at > ?", 24.hours.ago).count
|
|
57
|
-
else
|
|
58
|
-
0
|
|
59
|
-
end
|
|
50
|
+
SolidQueue::Job.where("finished_at > ?", 24.hours.ago).count
|
|
60
51
|
end
|
|
61
52
|
|
|
62
53
|
def active_workers_count
|
|
63
|
-
|
|
54
|
+
Processes.live_workers.count
|
|
64
55
|
end
|
|
65
56
|
|
|
66
57
|
def queues_count_calc
|
|
67
|
-
SolidQueue::Queue.count
|
|
68
|
-
rescue
|
|
69
|
-
# Fallback: count unique queue names from jobs
|
|
70
58
|
SolidQueue::Job.distinct.count(:queue_name)
|
|
71
59
|
end
|
|
72
60
|
|
|
73
61
|
def calculate_throughput
|
|
74
|
-
|
|
75
|
-
if SolidQueue::Job.column_names.include?("finished_at")
|
|
76
|
-
SolidQueue::Job.where("finished_at > ?", 1.hour.ago).count
|
|
77
|
-
else
|
|
78
|
-
0
|
|
79
|
-
end
|
|
62
|
+
SolidQueue::Job.where("finished_at > ?", 1.hour.ago).count
|
|
80
63
|
end
|
|
81
64
|
|
|
82
65
|
def fetch_recent_failures
|
|
@@ -3,61 +3,25 @@
|
|
|
3
3
|
module JobHarbor
|
|
4
4
|
class FailureStats
|
|
5
5
|
WINDOW = 24.hours
|
|
6
|
-
LOW_THRESHOLD = 5
|
|
7
|
-
MEDIUM_THRESHOLD = 20
|
|
8
|
-
|
|
9
|
-
def initialize(window: WINDOW)
|
|
10
|
-
@window = window
|
|
11
|
-
@cutoff = Time.current - @window
|
|
12
|
-
end
|
|
13
6
|
|
|
14
7
|
def stats
|
|
15
8
|
@stats ||= calculate_stats
|
|
16
9
|
end
|
|
17
10
|
|
|
18
|
-
def self.rate_badge_class(rate)
|
|
19
|
-
case rate
|
|
20
|
-
when 0...LOW_THRESHOLD
|
|
21
|
-
"sqd-rate-low"
|
|
22
|
-
when LOW_THRESHOLD...MEDIUM_THRESHOLD
|
|
23
|
-
"sqd-rate-medium"
|
|
24
|
-
else
|
|
25
|
-
"sqd-rate-high"
|
|
26
|
-
end
|
|
27
|
-
end
|
|
28
|
-
|
|
29
11
|
private
|
|
30
12
|
|
|
31
13
|
def calculate_stats
|
|
32
|
-
|
|
33
|
-
recent_jobs = SolidQueue::Job.where(
|
|
34
|
-
|
|
35
|
-
# Group by class_name and calculate totals
|
|
36
|
-
job_counts = recent_jobs.group(:class_name).count
|
|
37
|
-
|
|
38
|
-
# Get failed job counts (jobs that have failed executions)
|
|
39
|
-
failed_job_ids = SolidQueue::FailedExecution
|
|
40
|
-
.joins(:job)
|
|
41
|
-
.where("solid_queue_jobs.created_at >= ?", @cutoff)
|
|
42
|
-
.pluck(:job_id)
|
|
14
|
+
since = Time.current - WINDOW
|
|
15
|
+
recent_jobs = SolidQueue::Job.where(created_at: since..)
|
|
43
16
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
.group(:class_name)
|
|
47
|
-
.count
|
|
17
|
+
totals = recent_jobs.group(:class_name).count
|
|
18
|
+
failures = recent_jobs.joins(:failed_execution).group(:class_name).count
|
|
48
19
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
failed = failed_counts[class_name] || 0
|
|
52
|
-
rate = total > 0 ? (failed.to_f / total * 100).round(1) : 0.0
|
|
20
|
+
totals.map do |class_name, total|
|
|
21
|
+
failed = failures.fetch(class_name, 0)
|
|
53
22
|
|
|
54
|
-
{
|
|
55
|
-
|
|
56
|
-
total: total,
|
|
57
|
-
failed: failed,
|
|
58
|
-
rate: rate
|
|
59
|
-
}
|
|
60
|
-
end.sort_by { |s| -s[:rate] }
|
|
23
|
+
{ class_name: class_name, total: total, failed: failed, rate: (failed.to_f / total * 100).round(1) }
|
|
24
|
+
end.sort_by { |stat| -stat[:rate] }
|
|
61
25
|
end
|
|
62
26
|
end
|
|
63
27
|
end
|