good_job 2.2.0 → 2.3.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/CHANGELOG.md +15 -0
- data/engine/app/controllers/good_job/cron_schedules_controller.rb +0 -1
- data/engine/app/controllers/good_job/executions_controller.rb +5 -1
- data/engine/app/controllers/good_job/{active_jobs_controller.rb → jobs_controller.rb} +6 -2
- data/engine/app/filters/good_job/base_filter.rb +101 -0
- data/engine/app/filters/good_job/executions_filter.rb +40 -0
- data/engine/app/filters/good_job/jobs_filter.rb +46 -0
- data/engine/app/models/good_job/active_job_job.rb +127 -0
- data/engine/app/views/good_job/executions/index.html.erb +21 -0
- data/engine/app/views/good_job/jobs/index.html.erb +7 -0
- data/engine/app/views/good_job/{active_jobs → jobs}/show.html.erb +0 -0
- data/engine/app/views/good_job/shared/_executions_table.erb +2 -2
- data/engine/app/views/good_job/shared/_filter.erb +52 -0
- data/engine/app/views/good_job/shared/_jobs_table.erb +56 -0
- data/engine/app/views/layouts/good_job/base.html.erb +3 -0
- data/engine/config/routes.rb +2 -2
- data/lib/good_job/adapter.rb +2 -2
- data/lib/good_job/version.rb +1 -1
- metadata +12 -6
- data/engine/app/controllers/good_job/dashboards_controller.rb +0 -107
- data/engine/app/views/good_job/dashboards/index.html.erb +0 -54
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c2a09a86e822d7dfc09b5788c5790fe79fd418c5749e7ff716e79e35496d8574
|
4
|
+
data.tar.gz: d588ac1a06ea6b013834922c23de7157099f4a261b2e52422081d305f2e9e468
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8e21e8a4ded224874ac111aae669dcf2315dc63663b7942888ef22e8da9367f8b6fa973bfda86941564f6e35fff7ef22f7f403ae9f7414a48a3be2ae2cd7f2a3
|
7
|
+
data.tar.gz: 3ddcf93091ba3a221aadaf85790b8ff7edca2a317c9e23df45a5d69d07ebdcaf9143ada9bf5f157a9909aca4deb5f7ecc51c1abb80b98e90e51241e5fce0f450
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,20 @@
|
|
1
1
|
# Changelog
|
2
2
|
|
3
|
+
## [v2.3.0](https://github.com/bensheldon/good_job/tree/v2.3.0) (2021-09-25)
|
4
|
+
|
5
|
+
[Full Changelog](https://github.com/bensheldon/good_job/compare/v2.2.0...v2.3.0)
|
6
|
+
|
7
|
+
**Implemented enhancements:**
|
8
|
+
|
9
|
+
- Create an ActiveJobJob model and Dashboard [\#383](https://github.com/bensheldon/good_job/pull/383) ([bensheldon](https://github.com/bensheldon))
|
10
|
+
- Preserve page filter when deleting execution [\#381](https://github.com/bensheldon/good_job/pull/381) ([morgoth](https://github.com/morgoth))
|
11
|
+
|
12
|
+
**Merged pull requests:**
|
13
|
+
|
14
|
+
- Update GH Test Matrix with latest JRuby 9.3.0.0 [\#387](https://github.com/bensheldon/good_job/pull/387) ([tedhexaflow](https://github.com/tedhexaflow))
|
15
|
+
- Improve test support's ShellOut command's process termination and add test logs [\#385](https://github.com/bensheldon/good_job/pull/385) ([bensheldon](https://github.com/bensheldon))
|
16
|
+
- @bensheldon Add Rails 7 alpha to Appraisal; update development dependencies [\#384](https://github.com/bensheldon/good_job/pull/384) ([bensheldon](https://github.com/bensheldon))
|
17
|
+
|
3
18
|
## [v2.2.0](https://github.com/bensheldon/good_job/tree/v2.2.0) (2021-09-15)
|
4
19
|
|
5
20
|
[Full Changelog](https://github.com/bensheldon/good_job/compare/v2.1.0...v2.2.0)
|
@@ -1,10 +1,14 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
module GoodJob
|
3
3
|
class ExecutionsController < GoodJob::BaseController
|
4
|
+
def index
|
5
|
+
@filter = ExecutionsFilter.new(params)
|
6
|
+
end
|
7
|
+
|
4
8
|
def destroy
|
5
9
|
deleted_count = GoodJob::Execution.where(id: params[:id]).delete_all
|
6
10
|
message = deleted_count.positive? ? { notice: "Job execution deleted" } : { alert: "Job execution not deleted" }
|
7
|
-
|
11
|
+
redirect_back fallback_location: root_path, **message
|
8
12
|
end
|
9
13
|
end
|
10
14
|
end
|
@@ -1,10 +1,14 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
module GoodJob
|
3
|
-
class
|
3
|
+
class JobsController < GoodJob::BaseController
|
4
|
+
def index
|
5
|
+
@filter = JobsFilter.new(params)
|
6
|
+
end
|
7
|
+
|
4
8
|
def show
|
5
9
|
@executions = GoodJob::Execution.active_job_id(params[:id])
|
6
10
|
.order(Arel.sql("COALESCE(scheduled_at, created_at) DESC"))
|
7
|
-
|
11
|
+
redirect_to root_path, alert: "Executions for Active Job #{params[:id]} not found" if @executions.empty?
|
8
12
|
end
|
9
13
|
end
|
10
14
|
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module GoodJob
|
3
|
+
class BaseFilter
|
4
|
+
attr_accessor :params
|
5
|
+
|
6
|
+
def initialize(params)
|
7
|
+
@params = params
|
8
|
+
end
|
9
|
+
|
10
|
+
def records
|
11
|
+
after_scheduled_at = params[:after_scheduled_at].present? ? Time.zone.parse(params[:after_scheduled_at]) : nil
|
12
|
+
|
13
|
+
filtered_query.display_all(
|
14
|
+
after_scheduled_at: after_scheduled_at,
|
15
|
+
after_id: params[:after_id]
|
16
|
+
).limit(params.fetch(:limit, 25))
|
17
|
+
end
|
18
|
+
|
19
|
+
def last
|
20
|
+
@_last ||= records.last
|
21
|
+
end
|
22
|
+
|
23
|
+
def job_classes
|
24
|
+
base_query.group("serialized_params->>'job_class'").count
|
25
|
+
.sort_by { |name, _count| name }
|
26
|
+
.to_h
|
27
|
+
end
|
28
|
+
|
29
|
+
def queues
|
30
|
+
base_query.group(:queue_name).count
|
31
|
+
.sort_by { |name, _count| name }
|
32
|
+
.to_h
|
33
|
+
end
|
34
|
+
|
35
|
+
def states
|
36
|
+
raise NotImplementedError
|
37
|
+
end
|
38
|
+
|
39
|
+
def to_params(override)
|
40
|
+
{
|
41
|
+
state: params[:state],
|
42
|
+
job_class: params[:job_class],
|
43
|
+
}.merge(override).delete_if { |_, v| v.nil? }
|
44
|
+
end
|
45
|
+
|
46
|
+
def chart_data
|
47
|
+
count_query = Arel.sql(GoodJob::Execution.pg_or_jdbc_query(<<~SQL.squish))
|
48
|
+
SELECT *
|
49
|
+
FROM generate_series(
|
50
|
+
date_trunc('hour', $1::timestamp),
|
51
|
+
date_trunc('hour', $2::timestamp),
|
52
|
+
'1 hour'
|
53
|
+
) timestamp
|
54
|
+
LEFT JOIN (
|
55
|
+
SELECT
|
56
|
+
date_trunc('hour', scheduled_at) AS scheduled_at,
|
57
|
+
queue_name,
|
58
|
+
count(*) AS count
|
59
|
+
FROM (
|
60
|
+
#{filtered_query.except(:select).select('queue_name', 'COALESCE(good_jobs.scheduled_at, good_jobs.created_at)::timestamp AS scheduled_at').to_sql}
|
61
|
+
) sources
|
62
|
+
GROUP BY date_trunc('hour', scheduled_at), queue_name
|
63
|
+
) sources ON sources.scheduled_at = timestamp
|
64
|
+
ORDER BY timestamp ASC
|
65
|
+
SQL
|
66
|
+
|
67
|
+
current_time = Time.current
|
68
|
+
binds = [[nil, current_time - 1.day], [nil, current_time]]
|
69
|
+
executions_data = GoodJob::Execution.connection.exec_query(count_query, "GoodJob Dashboard Chart", binds)
|
70
|
+
|
71
|
+
queue_names = executions_data.map { |d| d['queue_name'] }.uniq
|
72
|
+
labels = []
|
73
|
+
queues_data = executions_data.to_a.group_by { |d| d['timestamp'] }.each_with_object({}) do |(timestamp, values), hash|
|
74
|
+
labels << timestamp.in_time_zone.strftime('%H:%M %z')
|
75
|
+
queue_names.each do |queue_name|
|
76
|
+
(hash[queue_name] ||= []) << values.find { |d| d['queue_name'] == queue_name }&.[]('count')
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
{
|
81
|
+
labels: labels,
|
82
|
+
series: queues_data.map do |queue, data|
|
83
|
+
{
|
84
|
+
name: queue,
|
85
|
+
data: data,
|
86
|
+
}
|
87
|
+
end,
|
88
|
+
}
|
89
|
+
end
|
90
|
+
|
91
|
+
private
|
92
|
+
|
93
|
+
def base_query
|
94
|
+
raise NotImplementedError
|
95
|
+
end
|
96
|
+
|
97
|
+
def filtered_query
|
98
|
+
raise NotImplementedError
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module GoodJob
|
3
|
+
class ExecutionsFilter < BaseFilter
|
4
|
+
def states
|
5
|
+
{
|
6
|
+
'finished' => base_query.finished.count,
|
7
|
+
'unfinished' => base_query.unfinished.count,
|
8
|
+
'running' => base_query.running.count,
|
9
|
+
'errors' => base_query.where.not(error: nil).count,
|
10
|
+
}
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def base_query
|
16
|
+
GoodJob::Execution.all
|
17
|
+
end
|
18
|
+
|
19
|
+
def filtered_query
|
20
|
+
query = base_query
|
21
|
+
query = query.job_class(params[:job_class]) if params[:job_class]
|
22
|
+
query = query.where(queue_name: params[:queue_name]) if params[:queue_name]
|
23
|
+
|
24
|
+
if params[:state]
|
25
|
+
case params[:state]
|
26
|
+
when 'finished'
|
27
|
+
query = query.finished
|
28
|
+
when 'unfinished'
|
29
|
+
query = query.unfinished
|
30
|
+
when 'running'
|
31
|
+
query = query.running
|
32
|
+
when 'errors'
|
33
|
+
query = query.where.not(error: nil)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
query
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module GoodJob
|
3
|
+
class JobsFilter < BaseFilter
|
4
|
+
def states
|
5
|
+
{
|
6
|
+
'scheduled' => base_query.scheduled.count,
|
7
|
+
'retried' => base_query.retried.count,
|
8
|
+
'queued' => base_query.queued.count,
|
9
|
+
'running' => base_query.running.count,
|
10
|
+
'finished' => base_query.finished.count,
|
11
|
+
'discarded' => base_query.discarded.count,
|
12
|
+
}
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def base_query
|
18
|
+
GoodJob::ActiveJobJob.all
|
19
|
+
end
|
20
|
+
|
21
|
+
def filtered_query
|
22
|
+
query = base_query
|
23
|
+
query = query.job_class(params[:job_class]) if params[:job_class]
|
24
|
+
query = query.where(queue_name: params[:queue_name]) if params[:queue_name]
|
25
|
+
|
26
|
+
if params[:state]
|
27
|
+
case params[:state]
|
28
|
+
when 'discarded'
|
29
|
+
query = query.discarded
|
30
|
+
when 'finished'
|
31
|
+
query = query.finished
|
32
|
+
when 'retried'
|
33
|
+
query = query.retried
|
34
|
+
when 'scheduled'
|
35
|
+
query = query.scheduled
|
36
|
+
when 'running'
|
37
|
+
query = query.running.select('good_jobs.*', 'pg_locks.locktype')
|
38
|
+
when 'queued'
|
39
|
+
query = query.queued
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
query
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,127 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module GoodJob
|
3
|
+
# ActiveRecord model that represents an +ActiveJob+ job.
|
4
|
+
# Is the same record data as a {GoodJob::Execution} but only the most recent execution.
|
5
|
+
# Parent class can be configured with +GoodJob.active_record_parent_class+.
|
6
|
+
# @!parse
|
7
|
+
# class ActiveJob < ActiveRecord::Base; end
|
8
|
+
class ActiveJobJob < Object.const_get(GoodJob.active_record_parent_class)
|
9
|
+
include GoodJob::Lockable
|
10
|
+
|
11
|
+
self.table_name = 'good_jobs'
|
12
|
+
self.primary_key = 'active_job_id'
|
13
|
+
self.advisory_lockable_column = 'active_job_id'
|
14
|
+
|
15
|
+
has_many :executions, -> { order(created_at: :asc) }, class_name: 'GoodJob::Execution', foreign_key: 'active_job_id'
|
16
|
+
|
17
|
+
# Only the most-recent unretried execution represents a "Job"
|
18
|
+
default_scope { where(retried_good_job_id: nil) }
|
19
|
+
|
20
|
+
# Get Jobs with given class name
|
21
|
+
# @!method job_class
|
22
|
+
# @!scope class
|
23
|
+
# @param string [String]
|
24
|
+
# Execution class name
|
25
|
+
# @return [ActiveRecord::Relation]
|
26
|
+
scope :job_class, ->(job_class) { where("serialized_params->>'job_class' = ?", job_class) }
|
27
|
+
|
28
|
+
# First execution will run in the future
|
29
|
+
scope :scheduled, -> { where(finished_at: nil).where('COALESCE(scheduled_at, created_at) > ?', DateTime.current).where("(serialized_params->>'executions')::integer < 2") }
|
30
|
+
# Execution errored, will run in the future
|
31
|
+
scope :retried, -> { where(finished_at: nil).where('COALESCE(scheduled_at, created_at) > ?', DateTime.current).where("(serialized_params->>'executions')::integer > 1") }
|
32
|
+
# Immediate/Scheduled time to run has passed, waiting for an available thread run
|
33
|
+
scope :queued, -> { where(finished_at: nil).where('COALESCE(scheduled_at, created_at) <= ?', DateTime.current).joins_advisory_locks.where(pg_locks: { locktype: nil }) }
|
34
|
+
# Advisory locked and executing
|
35
|
+
scope :running, -> { where(finished_at: nil).joins_advisory_locks.where.not(pg_locks: { locktype: nil }) }
|
36
|
+
# Completed executing successfully
|
37
|
+
scope :finished, -> { where.not(finished_at: nil).where(error: nil) }
|
38
|
+
# Errored but will not be retried
|
39
|
+
scope :discarded, -> { where.not(finished_at: nil).where.not(error: nil) }
|
40
|
+
|
41
|
+
# Get Jobs in display order with optional keyset pagination.
|
42
|
+
# @!method display_all(after_scheduled_at: nil, after_id: nil)
|
43
|
+
# @!scope class
|
44
|
+
# @param after_scheduled_at [DateTime, String, nil]
|
45
|
+
# Display records scheduled after this time for keyset pagination
|
46
|
+
# @param after_id [Numeric, String, nil]
|
47
|
+
# Display records after this ID for keyset pagination
|
48
|
+
# @return [ActiveRecord::Relation]
|
49
|
+
scope :display_all, (lambda do |after_scheduled_at: nil, after_id: nil|
|
50
|
+
query = order(Arel.sql('COALESCE(scheduled_at, created_at) DESC, id DESC'))
|
51
|
+
if after_scheduled_at.present? && after_id.present?
|
52
|
+
query = query.where(Arel.sql('(COALESCE(scheduled_at, created_at), id) < (:after_scheduled_at, :after_id)'), after_scheduled_at: after_scheduled_at, after_id: after_id)
|
53
|
+
elsif after_scheduled_at.present?
|
54
|
+
query = query.where(Arel.sql('(COALESCE(scheduled_at, created_at)) < (:after_scheduled_at)'), after_scheduled_at: after_scheduled_at)
|
55
|
+
end
|
56
|
+
query
|
57
|
+
end)
|
58
|
+
|
59
|
+
def id
|
60
|
+
active_job_id
|
61
|
+
end
|
62
|
+
|
63
|
+
def _execution_id
|
64
|
+
attributes['id']
|
65
|
+
end
|
66
|
+
|
67
|
+
def job_class
|
68
|
+
serialized_params['job_class']
|
69
|
+
end
|
70
|
+
|
71
|
+
def status
|
72
|
+
if finished_at.present?
|
73
|
+
if error.present?
|
74
|
+
:discarded
|
75
|
+
else
|
76
|
+
:finished
|
77
|
+
end
|
78
|
+
elsif (scheduled_at || created_at) > DateTime.current
|
79
|
+
if serialized_params.fetch('executions', 0) > 1
|
80
|
+
:retried
|
81
|
+
else
|
82
|
+
:scheduled
|
83
|
+
end
|
84
|
+
elsif running?
|
85
|
+
:running
|
86
|
+
else
|
87
|
+
:queued
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def head_execution
|
92
|
+
executions.last
|
93
|
+
end
|
94
|
+
|
95
|
+
def tail_execution
|
96
|
+
executions.first
|
97
|
+
end
|
98
|
+
|
99
|
+
def executions_count
|
100
|
+
aj_count = serialized_params.fetch('executions', 0)
|
101
|
+
# The execution count within serialized_params is not updated
|
102
|
+
# once the underlying execution has been executed.
|
103
|
+
if status.in? [:discarded, :finished, :running]
|
104
|
+
aj_count + 1
|
105
|
+
else
|
106
|
+
aj_count
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
def preserved_executions_count
|
111
|
+
executions.size
|
112
|
+
end
|
113
|
+
|
114
|
+
def recent_error
|
115
|
+
error.presence || executions[-2]&.error
|
116
|
+
end
|
117
|
+
|
118
|
+
def running?
|
119
|
+
# Avoid N+1 Query: `.joins_advisory_locks.select('good_jobs.*', 'pg_locks.locktype AS locktype')`
|
120
|
+
if has_attribute?(:locktype)
|
121
|
+
self['locktype'].present?
|
122
|
+
else
|
123
|
+
advisory_locked?
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
<div class="card my-3 p-6">
|
2
|
+
<%= render 'good_job/shared/chart', chart_data: @filter.chart_data %>
|
3
|
+
</div>
|
4
|
+
|
5
|
+
<%= render 'good_job/shared/filter', filter: @filter %>
|
6
|
+
|
7
|
+
<% if @filter.records.present? %>
|
8
|
+
<%= render 'good_job/shared/executions_table', executions: @filter.records %>
|
9
|
+
|
10
|
+
<nav aria-label="Job pagination" class="mt-3">
|
11
|
+
<ul class="pagination">
|
12
|
+
<li class="page-item">
|
13
|
+
<%= link_to({ after_scheduled_at: (@filter.last.scheduled_at || @filter.last.created_at), after_id: @filter.last.id }, class: "page-link") do %>
|
14
|
+
Older executions <span aria-hidden="true">»</span>
|
15
|
+
<% end %>
|
16
|
+
</li>
|
17
|
+
</ul>
|
18
|
+
</nav>
|
19
|
+
<% else %>
|
20
|
+
<em>No executions present.</em>
|
21
|
+
<% end %>
|
File without changes
|
@@ -23,12 +23,12 @@
|
|
23
23
|
<% executions.each do |execution| %>
|
24
24
|
<tr id="<%= dom_id(execution) %>">
|
25
25
|
<td>
|
26
|
-
<%= link_to
|
26
|
+
<%= link_to job_path(execution.serialized_params['job_id']) do %>
|
27
27
|
<code><%= execution.active_job_id %></code>
|
28
28
|
<% end %>
|
29
29
|
</td>
|
30
30
|
<td>
|
31
|
-
<%= link_to
|
31
|
+
<%= link_to job_path(execution.active_job_id, anchor: dom_id(execution)) do %>
|
32
32
|
<code><%= execution.id %></code>
|
33
33
|
<% end %>
|
34
34
|
</td>
|
@@ -0,0 +1,52 @@
|
|
1
|
+
<div class='card mb-2'>
|
2
|
+
<div class='card-body d-flex flex-wrap'>
|
3
|
+
|
4
|
+
<div class='me-4'>
|
5
|
+
<small>Filter by job class</small>
|
6
|
+
<br>
|
7
|
+
<% @filter.job_classes.each do |name, count| %>
|
8
|
+
<% if params[:job_class] == name %>
|
9
|
+
<%= link_to(@filter.to_params(job_class: nil), class: 'btn btn-sm btn-outline-secondary active', role: "button", "aria-pressed": true) do %>
|
10
|
+
<%= name %> (<%= count %>)
|
11
|
+
<% end %>
|
12
|
+
<% else %>
|
13
|
+
<%= link_to(@filter.to_params(job_class: name), class: 'btn btn-sm btn-outline-secondary', role: "button") do %>
|
14
|
+
<%= name %> (<%= count %>)
|
15
|
+
<% end %>
|
16
|
+
<% end %>
|
17
|
+
<% end %>
|
18
|
+
</div>
|
19
|
+
|
20
|
+
<div class='me-4'>
|
21
|
+
<small>Filter by state</small>
|
22
|
+
<br>
|
23
|
+
<% @filter.states.each do |name, count| %>
|
24
|
+
<% if params[:state] == name %>
|
25
|
+
<%= link_to(@filter.to_params(state: nil), class: 'btn btn-sm btn-outline-secondary active', role: "button", "aria-pressed": true) do %>
|
26
|
+
<%= name %> (<%= count %>)
|
27
|
+
<% end %>
|
28
|
+
<% else %>
|
29
|
+
<%= link_to(@filter.to_params(state: name), class: 'btn btn-sm btn-outline-secondary', role: "button") do %>
|
30
|
+
<%= name %> (<%= count %>)
|
31
|
+
<% end %>
|
32
|
+
<% end %>
|
33
|
+
<% end %>
|
34
|
+
</div>
|
35
|
+
|
36
|
+
<div>
|
37
|
+
<small>Filter by queue</small>
|
38
|
+
<br>
|
39
|
+
<% @filter.queues.each do |name, count| %>
|
40
|
+
<% if params[:queue_name] == name %>
|
41
|
+
<%= link_to(@filter.to_params(queue_name: nil), class: 'btn btn-sm btn-outline-secondary active', role: "button", "aria-pressed": true) do %>
|
42
|
+
<%= name %> (<%= count %>)
|
43
|
+
<% end %>
|
44
|
+
<% else %>
|
45
|
+
<%= link_to(@filter.to_params(queue_name: name), class: 'btn btn-sm btn-outline-secondary', role: "button") do %>
|
46
|
+
<%= name %> (<%= count %>)
|
47
|
+
<% end %>
|
48
|
+
<% end %>
|
49
|
+
<% end %>
|
50
|
+
</div>
|
51
|
+
</div>
|
52
|
+
</div>
|
@@ -0,0 +1,56 @@
|
|
1
|
+
<div class="card my-3">
|
2
|
+
<div class="table-responsive">
|
3
|
+
<table class="table card-table table-bordered table-hover table-sm mb-0">
|
4
|
+
<thead>
|
5
|
+
<tr>
|
6
|
+
<th>ActiveJob ID</th>
|
7
|
+
<th>State</th>
|
8
|
+
<th>Job Class</th>
|
9
|
+
<th>Queue</th>
|
10
|
+
<th>Scheduled At</th>
|
11
|
+
<th>Executions</th>
|
12
|
+
<th>Error</th>
|
13
|
+
<th>
|
14
|
+
ActiveJob Params
|
15
|
+
<%= tag.button "Toggle", type: "button", class: "btn btn-sm btn-outline-primary", role: "button",
|
16
|
+
data: { bs_toggle: "collapse", bs_target: ".job-params" },
|
17
|
+
aria: { expanded: false, controls: jobs.map { |job| "##{dom_id(job, "params")}" }.join(" ") }
|
18
|
+
%>
|
19
|
+
</th>
|
20
|
+
<th>Actions</th>
|
21
|
+
</tr>
|
22
|
+
</thead>
|
23
|
+
<tbody>
|
24
|
+
<% jobs.each do |job| %>
|
25
|
+
<tr id="<%= dom_id(job) %>">
|
26
|
+
<td>
|
27
|
+
<%= link_to job_path(job.id) do %>
|
28
|
+
<code><%= job.id %></code>
|
29
|
+
<% end %>
|
30
|
+
</td>
|
31
|
+
<td>
|
32
|
+
<span class="badge bg-secondary"><%= job.status %></span>
|
33
|
+
</td>
|
34
|
+
<td><%= job.job_class %></td>
|
35
|
+
<td><%= job.queue_name %></td>
|
36
|
+
<td><%= job.scheduled_at || job.created_at %></td>
|
37
|
+
<td><%= job.executions_count %></td>
|
38
|
+
<td class="text-break"><%= truncate(job.recent_error, length: 1_000) %></td>
|
39
|
+
<td>
|
40
|
+
<%= tag.button "Preview", type: "button", class: "btn btn-sm btn-outline-primary", role: "button",
|
41
|
+
data: { bs_toggle: "collapse", bs_target: "##{dom_id(job, 'params')}" },
|
42
|
+
aria: { expanded: false, controls: dom_id(job, "params") }
|
43
|
+
%>
|
44
|
+
<%= tag.pre JSON.pretty_generate(job.serialized_params), id: dom_id(job, "params"), class: "collapse job-params" %>
|
45
|
+
</td>
|
46
|
+
<!-- <td>-->
|
47
|
+
<%#= button_to execution_path(execution.id), method: :delete, class: "btn btn-sm btn-outline-danger", title: "Delete execution" do %>
|
48
|
+
<%#= render "good_job/shared/icons/trash" %>
|
49
|
+
<%# end %>
|
50
|
+
<!-- </td>-->
|
51
|
+
</tr>
|
52
|
+
<% end %>
|
53
|
+
</tbody>
|
54
|
+
</table>
|
55
|
+
</div>
|
56
|
+
</div>
|
@@ -25,6 +25,9 @@
|
|
25
25
|
<li class="nav-item">
|
26
26
|
<%= link_to "All Executions", root_path, class: ["nav-link", ("active" if current_page?(root_path))] %>
|
27
27
|
</li>
|
28
|
+
<li class="nav-item">
|
29
|
+
<%= link_to "All Jobs", jobs_path, class: ["nav-link", ("active" if current_page?(jobs_path))] %>
|
30
|
+
</li>
|
28
31
|
<li class="nav-item">
|
29
32
|
<%= link_to "Cron Schedules", cron_schedules_path, class: ["nav-link", ("active" if current_page?(cron_schedules_path))] %>
|
30
33
|
</li>
|
data/engine/config/routes.rb
CHANGED
@@ -1,8 +1,8 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
GoodJob::Engine.routes.draw do
|
3
|
-
root to: '
|
3
|
+
root to: 'executions#index'
|
4
4
|
resources :cron_schedules, only: %i[index]
|
5
|
-
resources :
|
5
|
+
resources :jobs, only: %i[index show]
|
6
6
|
resources :executions, only: %i[destroy]
|
7
7
|
|
8
8
|
scope controller: :assets do
|
data/lib/good_job/adapter.rb
CHANGED
@@ -101,14 +101,14 @@ module GoodJob
|
|
101
101
|
# @return [Boolean]
|
102
102
|
def execute_async?
|
103
103
|
@configuration.execution_mode == :async_all ||
|
104
|
-
@configuration.execution_mode.in?([:async, :async_server]) && in_server_process?
|
104
|
+
(@configuration.execution_mode.in?([:async, :async_server]) && in_server_process?)
|
105
105
|
end
|
106
106
|
|
107
107
|
# Whether in +:external+ execution mode.
|
108
108
|
# @return [Boolean]
|
109
109
|
def execute_externally?
|
110
110
|
@configuration.execution_mode == :external ||
|
111
|
-
@configuration.execution_mode.in?([:async, :async_server]) && !in_server_process?
|
111
|
+
(@configuration.execution_mode.in?([:async, :async_server]) && !in_server_process?)
|
112
112
|
end
|
113
113
|
|
114
114
|
# Whether in +:inline+ execution mode.
|
data/lib/good_job/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: good_job
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.
|
4
|
+
version: 2.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ben Sheldon
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-09-
|
11
|
+
date: 2021-09-25 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activejob
|
@@ -351,18 +351,24 @@ files:
|
|
351
351
|
- engine/app/assets/vendor/bootstrap/bootstrap.min.css
|
352
352
|
- engine/app/assets/vendor/chartist/chartist.css
|
353
353
|
- engine/app/assets/vendor/chartist/chartist.js
|
354
|
-
- engine/app/controllers/good_job/active_jobs_controller.rb
|
355
354
|
- engine/app/controllers/good_job/assets_controller.rb
|
356
355
|
- engine/app/controllers/good_job/base_controller.rb
|
357
356
|
- engine/app/controllers/good_job/cron_schedules_controller.rb
|
358
|
-
- engine/app/controllers/good_job/dashboards_controller.rb
|
359
357
|
- engine/app/controllers/good_job/executions_controller.rb
|
358
|
+
- engine/app/controllers/good_job/jobs_controller.rb
|
359
|
+
- engine/app/filters/good_job/base_filter.rb
|
360
|
+
- engine/app/filters/good_job/executions_filter.rb
|
361
|
+
- engine/app/filters/good_job/jobs_filter.rb
|
360
362
|
- engine/app/helpers/good_job/application_helper.rb
|
361
|
-
- engine/app/
|
363
|
+
- engine/app/models/good_job/active_job_job.rb
|
362
364
|
- engine/app/views/good_job/cron_schedules/index.html.erb
|
363
|
-
- engine/app/views/good_job/
|
365
|
+
- engine/app/views/good_job/executions/index.html.erb
|
366
|
+
- engine/app/views/good_job/jobs/index.html.erb
|
367
|
+
- engine/app/views/good_job/jobs/show.html.erb
|
364
368
|
- engine/app/views/good_job/shared/_chart.erb
|
365
369
|
- engine/app/views/good_job/shared/_executions_table.erb
|
370
|
+
- engine/app/views/good_job/shared/_filter.erb
|
371
|
+
- engine/app/views/good_job/shared/_jobs_table.erb
|
366
372
|
- engine/app/views/good_job/shared/icons/_check.html.erb
|
367
373
|
- engine/app/views/good_job/shared/icons/_exclamation.html.erb
|
368
374
|
- engine/app/views/good_job/shared/icons/_trash.html.erb
|
@@ -1,107 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
module GoodJob
|
3
|
-
class DashboardsController < GoodJob::BaseController
|
4
|
-
class ExecutionFilter
|
5
|
-
attr_accessor :params
|
6
|
-
|
7
|
-
def initialize(params)
|
8
|
-
@params = params
|
9
|
-
end
|
10
|
-
|
11
|
-
def last
|
12
|
-
@_last ||= executions.last
|
13
|
-
end
|
14
|
-
|
15
|
-
def executions
|
16
|
-
after_scheduled_at = params[:after_scheduled_at].present? ? Time.zone.parse(params[:after_scheduled_at]) : nil
|
17
|
-
sql = GoodJob::Execution.display_all(after_scheduled_at: after_scheduled_at, after_id: params[:after_id])
|
18
|
-
.limit(params.fetch(:limit, 25))
|
19
|
-
sql = sql.job_class(params[:job_class]) if params[:job_class]
|
20
|
-
if params[:state]
|
21
|
-
case params[:state]
|
22
|
-
when 'finished'
|
23
|
-
sql = sql.finished
|
24
|
-
when 'unfinished'
|
25
|
-
sql = sql.unfinished
|
26
|
-
when 'running'
|
27
|
-
sql = sql.running
|
28
|
-
when 'errors'
|
29
|
-
sql = sql.where.not(error: nil)
|
30
|
-
end
|
31
|
-
end
|
32
|
-
sql
|
33
|
-
end
|
34
|
-
|
35
|
-
def states
|
36
|
-
{
|
37
|
-
'finished' => GoodJob::Execution.finished.count,
|
38
|
-
'unfinished' => GoodJob::Execution.unfinished.count,
|
39
|
-
'running' => GoodJob::Execution.running.count,
|
40
|
-
'errors' => GoodJob::Execution.where.not(error: nil).count,
|
41
|
-
}
|
42
|
-
end
|
43
|
-
|
44
|
-
def job_classes
|
45
|
-
GoodJob::Execution.group("serialized_params->>'job_class'").count
|
46
|
-
.sort_by { |name, _count| name }
|
47
|
-
end
|
48
|
-
|
49
|
-
def to_params(override)
|
50
|
-
{
|
51
|
-
state: params[:state],
|
52
|
-
job_class: params[:job_class],
|
53
|
-
}.merge(override).delete_if { |_, v| v.nil? }
|
54
|
-
end
|
55
|
-
end
|
56
|
-
|
57
|
-
def index
|
58
|
-
@filter = ExecutionFilter.new(params)
|
59
|
-
|
60
|
-
count_query = Arel.sql(GoodJob::Execution.pg_or_jdbc_query(<<~SQL.squish))
|
61
|
-
SELECT *
|
62
|
-
FROM generate_series(
|
63
|
-
date_trunc('hour', $1::timestamp),
|
64
|
-
date_trunc('hour', $2::timestamp),
|
65
|
-
'1 hour'
|
66
|
-
) timestamp
|
67
|
-
LEFT JOIN (
|
68
|
-
SELECT
|
69
|
-
date_trunc('hour', scheduled_at) AS scheduled_at,
|
70
|
-
queue_name,
|
71
|
-
count(*) AS count
|
72
|
-
FROM (
|
73
|
-
SELECT
|
74
|
-
COALESCE(scheduled_at, created_at)::timestamp AS scheduled_at,
|
75
|
-
queue_name
|
76
|
-
FROM good_jobs
|
77
|
-
) sources
|
78
|
-
GROUP BY date_trunc('hour', scheduled_at), queue_name
|
79
|
-
) sources ON sources.scheduled_at = timestamp
|
80
|
-
ORDER BY timestamp ASC
|
81
|
-
SQL
|
82
|
-
|
83
|
-
current_time = Time.current
|
84
|
-
binds = [[nil, current_time - 1.day], [nil, current_time]]
|
85
|
-
executions_data = GoodJob::Execution.connection.exec_query(count_query, "GoodJob Dashboard Chart", binds)
|
86
|
-
|
87
|
-
queue_names = executions_data.map { |d| d['queue_name'] }.uniq
|
88
|
-
labels = []
|
89
|
-
queues_data = executions_data.to_a.group_by { |d| d['timestamp'] }.each_with_object({}) do |(timestamp, values), hash|
|
90
|
-
labels << timestamp.in_time_zone.strftime('%H:%M %z')
|
91
|
-
queue_names.each do |queue_name|
|
92
|
-
(hash[queue_name] ||= []) << values.find { |d| d['queue_name'] == queue_name }&.[]('count')
|
93
|
-
end
|
94
|
-
end
|
95
|
-
|
96
|
-
@chart = {
|
97
|
-
labels: labels,
|
98
|
-
series: queues_data.map do |queue, data|
|
99
|
-
{
|
100
|
-
name: queue,
|
101
|
-
data: data,
|
102
|
-
}
|
103
|
-
end,
|
104
|
-
}
|
105
|
-
end
|
106
|
-
end
|
107
|
-
end
|
@@ -1,54 +0,0 @@
|
|
1
|
-
<div class="card my-3 p-6">
|
2
|
-
<%= render 'good_job/shared/chart', chart_data: @chart %>
|
3
|
-
</div>
|
4
|
-
|
5
|
-
<div class='card mb-2'>
|
6
|
-
<div class='card-body d-flex flex-wrap'>
|
7
|
-
<div class='me-4'>
|
8
|
-
<small>Filter by job class</small>
|
9
|
-
<br>
|
10
|
-
<% @filter.job_classes.each do |(name, count)| %>
|
11
|
-
<% if params[:job_class] == name %>
|
12
|
-
<%= link_to(root_path(@filter.to_params(job_class: nil)), class: 'btn btn-sm btn-outline-secondary active', role: "button", "aria-pressed": true) do %>
|
13
|
-
<%= name %> (<%= count %>)
|
14
|
-
<% end %>
|
15
|
-
<% else %>
|
16
|
-
<%= link_to(root_path(@filter.to_params(job_class: name)), class: 'btn btn-sm btn-outline-secondary', role: "button") do %>
|
17
|
-
<%= name %> (<%= count %>)
|
18
|
-
<% end %>
|
19
|
-
<% end %>
|
20
|
-
<% end %>
|
21
|
-
</div>
|
22
|
-
<div>
|
23
|
-
<small>Filter by state</small>
|
24
|
-
<br>
|
25
|
-
<% @filter.states.each do |name, count| %>
|
26
|
-
<% if params[:state] == name %>
|
27
|
-
<%= link_to(root_path(@filter.to_params(state: nil)), class: 'btn btn-sm btn-outline-secondary active', role: "button", "aria-pressed": true) do %>
|
28
|
-
<%= name %> (<%= count %>)
|
29
|
-
<% end %>
|
30
|
-
<% else %>
|
31
|
-
<%= link_to(root_path(@filter.to_params(state: name)), class: 'btn btn-sm btn-outline-secondary', role: "button") do %>
|
32
|
-
<%= name %> (<%= count %>)
|
33
|
-
<% end %>
|
34
|
-
<% end %>
|
35
|
-
<% end %>
|
36
|
-
</div>
|
37
|
-
</div>
|
38
|
-
</div>
|
39
|
-
|
40
|
-
<% if @filter.executions.present? %>
|
41
|
-
<%= render 'good_job/shared/executions_table', executions: @filter.executions %>
|
42
|
-
|
43
|
-
<nav aria-label="Job pagination" class="mt-3">
|
44
|
-
<ul class="pagination">
|
45
|
-
<li class="page-item">
|
46
|
-
<%= link_to({ after_scheduled_at: (@filter.last.scheduled_at || @filter.last.created_at), after_id: @filter.last.id }, class: "page-link") do %>
|
47
|
-
Older executions <span aria-hidden="true">»</span>
|
48
|
-
<% end %>
|
49
|
-
</li>
|
50
|
-
</ul>
|
51
|
-
</nav>
|
52
|
-
<% else %>
|
53
|
-
<em>No executions present.</em>
|
54
|
-
<% end %>
|