good_job 2.2.0 → 2.3.0
Sign up to get free protection for your applications and to get access to all the features.
- 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 %>
|