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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: dd267297ac5f8e889d5af7d3efb0d12f3564ddedb8c645fde37bc044e18d48d0
4
- data.tar.gz: 5fc5715400914b895fb38fc087bb7bb08ea63ffb4db8938cea9e2933b32ade36
3
+ metadata.gz: c2a09a86e822d7dfc09b5788c5790fe79fd418c5749e7ff716e79e35496d8574
4
+ data.tar.gz: d588ac1a06ea6b013834922c23de7157099f4a261b2e52422081d305f2e9e468
5
5
  SHA512:
6
- metadata.gz: d3da94d9ad1b43e5102d0795d0c3909912ba90178678187d7340170a48c2df733ba7aba4bd33da8fdb50084f82fb4ff960c71dd4db0c23bc1a68278a1d1fbd21
7
- data.tar.gz: 9810497b9bdef82a95e79bf98414e60652dc912ccb6cfe9ce0893a898708fe6e547b89d67530c62b721fff01cc2c7bfa679f450c511ca5f68d3a5ef0d0a387ca
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,5 +1,4 @@
1
1
  # frozen_string_literal: true
2
-
3
2
  module GoodJob
4
3
  class CronSchedulesController < GoodJob::BaseController
5
4
  def index
@@ -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
- redirect_to root_path, **message
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 ActiveJobsController < GoodJob::BaseController
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
- raise ActiveRecord::RecordNotFound if @executions.empty?
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">&raquo;</span>
15
+ <% end %>
16
+ </li>
17
+ </ul>
18
+ </nav>
19
+ <% else %>
20
+ <em>No executions present.</em>
21
+ <% end %>
@@ -0,0 +1,7 @@
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
+ <%= render 'good_job/shared/jobs_table', jobs: @filter.records %>
@@ -23,12 +23,12 @@
23
23
  <% executions.each do |execution| %>
24
24
  <tr id="<%= dom_id(execution) %>">
25
25
  <td>
26
- <%= link_to active_job_path(execution.serialized_params['job_id']) do %>
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 active_job_path(execution.active_job_id, anchor: dom_id(execution)) do %>
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&nbsp;
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>
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
  GoodJob::Engine.routes.draw do
3
- root to: 'dashboards#index'
3
+ root to: 'executions#index'
4
4
  resources :cron_schedules, only: %i[index]
5
- resources :active_jobs, only: %i[show]
5
+ resources :jobs, only: %i[index show]
6
6
  resources :executions, only: %i[destroy]
7
7
 
8
8
  scope controller: :assets do
@@ -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.
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
  module GoodJob
3
3
  # GoodJob gem version.
4
- VERSION = '2.2.0'
4
+ VERSION = '2.3.0'
5
5
  end
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.2.0
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-15 00:00:00.000000000 Z
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/views/good_job/active_jobs/show.html.erb
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/dashboards/index.html.erb
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">&raquo;</span>
48
- <% end %>
49
- </li>
50
- </ul>
51
- </nav>
52
- <% else %>
53
- <em>No executions present.</em>
54
- <% end %>