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 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 %>