good_job 2.6.0 → 2.6.1
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 +28 -0
- data/engine/app/assets/scripts.js +1 -0
- data/engine/app/assets/style.css +5 -0
- data/engine/app/assets/vendor/chartjs/chart.min.js +13 -0
- data/engine/app/charts/good_job/scheduled_by_queue_chart.rb +69 -0
- data/engine/app/controllers/good_job/assets_controller.rb +6 -6
- data/engine/app/filters/good_job/base_filter.rb +7 -50
- data/engine/app/filters/good_job/executions_filter.rb +9 -8
- data/engine/app/filters/good_job/jobs_filter.rb +9 -8
- data/engine/app/views/good_job/executions/index.html.erb +1 -1
- data/engine/app/views/good_job/jobs/_table.erb +1 -1
- data/engine/app/views/good_job/jobs/index.html.erb +1 -1
- data/engine/app/views/good_job/shared/_chart.erb +19 -46
- data/engine/app/views/good_job/shared/_filter.erb +18 -3
- data/engine/app/views/layouts/good_job/base.html.erb +4 -3
- data/engine/config/routes.rb +2 -2
- data/lib/good_job/active_job_job.rb +2 -19
- data/lib/good_job/execution.rb +1 -18
- data/lib/good_job/filterable.rb +42 -0
- data/lib/good_job/notifier.rb +17 -7
- data/lib/good_job/version.rb +1 -1
- metadata +14 -12
- data/engine/app/assets/vendor/chartist/chartist.css +0 -613
- data/engine/app/assets/vendor/chartist/chartist.js +0 -4516
@@ -0,0 +1,69 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GoodJob
|
4
|
+
class ScheduledByQueueChart
|
5
|
+
def initialize(filter)
|
6
|
+
@filter = filter
|
7
|
+
end
|
8
|
+
|
9
|
+
def data
|
10
|
+
end_time = Time.current
|
11
|
+
start_time = end_time - 1.day
|
12
|
+
|
13
|
+
count_query = Arel.sql(GoodJob::Execution.pg_or_jdbc_query(<<~SQL.squish))
|
14
|
+
SELECT *
|
15
|
+
FROM generate_series(
|
16
|
+
date_trunc('hour', $1::timestamp),
|
17
|
+
date_trunc('hour', $2::timestamp),
|
18
|
+
'1 hour'
|
19
|
+
) timestamp
|
20
|
+
LEFT JOIN (
|
21
|
+
SELECT
|
22
|
+
date_trunc('hour', scheduled_at) AS scheduled_at,
|
23
|
+
queue_name,
|
24
|
+
count(*) AS count
|
25
|
+
FROM (
|
26
|
+
#{@filter.filtered_query.except(:select, :order).select('queue_name', 'COALESCE(good_jobs.scheduled_at, good_jobs.created_at)::timestamp AS scheduled_at').to_sql}
|
27
|
+
) sources
|
28
|
+
GROUP BY date_trunc('hour', scheduled_at), queue_name
|
29
|
+
) sources ON sources.scheduled_at = timestamp
|
30
|
+
ORDER BY timestamp ASC
|
31
|
+
SQL
|
32
|
+
|
33
|
+
binds = [[nil, start_time], [nil, end_time]]
|
34
|
+
executions_data = GoodJob::Execution.connection.exec_query(GoodJob::Execution.pg_or_jdbc_query(count_query), "GoodJob Dashboard Chart", binds)
|
35
|
+
|
36
|
+
queue_names = executions_data.reject { |d| d['count'].nil? }.map { |d| d['queue_name'] || BaseFilter::EMPTY }.uniq
|
37
|
+
labels = []
|
38
|
+
queues_data = executions_data.to_a.group_by { |d| d['timestamp'] }.each_with_object({}) do |(timestamp, values), hash|
|
39
|
+
labels << timestamp.in_time_zone.strftime('%H:%M')
|
40
|
+
queue_names.each do |queue_name|
|
41
|
+
(hash[queue_name] ||= []) << values.find { |d| d['queue_name'] == queue_name }&.[]('count')
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
{
|
46
|
+
labels: labels,
|
47
|
+
datasets: queues_data.map do |queue, data|
|
48
|
+
label = queue || '(none)'
|
49
|
+
{
|
50
|
+
label: label,
|
51
|
+
data: data,
|
52
|
+
backgroundColor: string_to_hsl(label),
|
53
|
+
borderColor: string_to_hsl(label),
|
54
|
+
}
|
55
|
+
end,
|
56
|
+
}
|
57
|
+
end
|
58
|
+
|
59
|
+
def string_to_hsl(string)
|
60
|
+
hash_value = string.sum
|
61
|
+
|
62
|
+
hue = hash_value % 360
|
63
|
+
saturation = (hash_value % 50) + 50
|
64
|
+
lightness = '50'
|
65
|
+
|
66
|
+
"hsl(#{hue}, #{saturation}%, #{lightness}%)"
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -15,18 +15,18 @@ module GoodJob
|
|
15
15
|
render file: GoodJob::Engine.root.join("app", "assets", "vendor", "bootstrap", "bootstrap.bundle.min.js")
|
16
16
|
end
|
17
17
|
|
18
|
-
def
|
19
|
-
render file: GoodJob::Engine.root.join("app", "assets", "vendor", "
|
20
|
-
end
|
21
|
-
|
22
|
-
def chartist_js
|
23
|
-
render file: GoodJob::Engine.root.join("app", "assets", "vendor", "chartist", "chartist.js")
|
18
|
+
def chartjs_js
|
19
|
+
render file: GoodJob::Engine.root.join("app", "assets", "vendor", "chartjs", "chart.min.js")
|
24
20
|
end
|
25
21
|
|
26
22
|
def rails_ujs_js
|
27
23
|
render file: GoodJob::Engine.root.join("app", "assets", "vendor", "rails_ujs.js")
|
28
24
|
end
|
29
25
|
|
26
|
+
def scripts_js
|
27
|
+
render file: GoodJob::Engine.root.join("app", "assets", "scripts.js")
|
28
|
+
end
|
29
|
+
|
30
30
|
def style_css
|
31
31
|
render file: GoodJob::Engine.root.join("app", "assets", "style.css")
|
32
32
|
end
|
@@ -2,6 +2,7 @@
|
|
2
2
|
module GoodJob
|
3
3
|
class BaseFilter
|
4
4
|
DEFAULT_LIMIT = 25
|
5
|
+
EMPTY = '[none]'
|
5
6
|
|
6
7
|
attr_accessor :params, :base_query
|
7
8
|
|
@@ -31,7 +32,7 @@ module GoodJob
|
|
31
32
|
|
32
33
|
def queues
|
33
34
|
base_query.group(:queue_name).count
|
34
|
-
.sort_by { |name, _count| name.to_s }
|
35
|
+
.sort_by { |name, _count| name.to_s || EMPTY }
|
35
36
|
.to_h
|
36
37
|
end
|
37
38
|
|
@@ -39,58 +40,18 @@ module GoodJob
|
|
39
40
|
raise NotImplementedError
|
40
41
|
end
|
41
42
|
|
42
|
-
def to_params(override)
|
43
|
+
def to_params(override = {})
|
43
44
|
{
|
44
45
|
job_class: params[:job_class],
|
45
46
|
limit: params[:limit],
|
46
47
|
queue_name: params[:queue_name],
|
48
|
+
query: params[:query],
|
47
49
|
state: params[:state],
|
48
|
-
}.merge(override).delete_if { |_, v| v.
|
50
|
+
}.merge(override).delete_if { |_, v| v.blank? }
|
49
51
|
end
|
50
52
|
|
51
|
-
def
|
52
|
-
|
53
|
-
SELECT *
|
54
|
-
FROM generate_series(
|
55
|
-
date_trunc('hour', $1::timestamp),
|
56
|
-
date_trunc('hour', $2::timestamp),
|
57
|
-
'1 hour'
|
58
|
-
) timestamp
|
59
|
-
LEFT JOIN (
|
60
|
-
SELECT
|
61
|
-
date_trunc('hour', scheduled_at) AS scheduled_at,
|
62
|
-
queue_name,
|
63
|
-
count(*) AS count
|
64
|
-
FROM (
|
65
|
-
#{filtered_query.except(:select).select('queue_name', 'COALESCE(good_jobs.scheduled_at, good_jobs.created_at)::timestamp AS scheduled_at').to_sql}
|
66
|
-
) sources
|
67
|
-
GROUP BY date_trunc('hour', scheduled_at), queue_name
|
68
|
-
) sources ON sources.scheduled_at = timestamp
|
69
|
-
ORDER BY timestamp ASC
|
70
|
-
SQL
|
71
|
-
|
72
|
-
current_time = Time.current
|
73
|
-
binds = [[nil, current_time - 1.day], [nil, current_time]]
|
74
|
-
executions_data = GoodJob::Execution.connection.exec_query(count_query, "GoodJob Dashboard Chart", binds)
|
75
|
-
|
76
|
-
queue_names = executions_data.map { |d| d['queue_name'] }.uniq
|
77
|
-
labels = []
|
78
|
-
queues_data = executions_data.to_a.group_by { |d| d['timestamp'] }.each_with_object({}) do |(timestamp, values), hash|
|
79
|
-
labels << timestamp.in_time_zone.strftime('%H:%M %z')
|
80
|
-
queue_names.each do |queue_name|
|
81
|
-
(hash[queue_name] ||= []) << values.find { |d| d['queue_name'] == queue_name }&.[]('count')
|
82
|
-
end
|
83
|
-
end
|
84
|
-
|
85
|
-
{
|
86
|
-
labels: labels,
|
87
|
-
series: queues_data.map do |queue, data|
|
88
|
-
{
|
89
|
-
name: queue,
|
90
|
-
data: data,
|
91
|
-
}
|
92
|
-
end,
|
93
|
-
}
|
53
|
+
def filtered_query
|
54
|
+
raise NotImplementedError
|
94
55
|
end
|
95
56
|
|
96
57
|
private
|
@@ -98,9 +59,5 @@ module GoodJob
|
|
98
59
|
def default_base_query
|
99
60
|
raise NotImplementedError
|
100
61
|
end
|
101
|
-
|
102
|
-
def filtered_query
|
103
|
-
raise NotImplementedError
|
104
|
-
end
|
105
62
|
end
|
106
63
|
end
|
@@ -10,16 +10,11 @@ module GoodJob
|
|
10
10
|
}
|
11
11
|
end
|
12
12
|
|
13
|
-
private
|
14
|
-
|
15
|
-
def default_base_query
|
16
|
-
GoodJob::Execution.all
|
17
|
-
end
|
18
|
-
|
19
13
|
def filtered_query
|
20
14
|
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]
|
15
|
+
query = query.job_class(params[:job_class]) if params[:job_class].present?
|
16
|
+
query = query.where(queue_name: params[:queue_name]) if params[:queue_name].present?
|
17
|
+
query = query.search(params['query']) if params[:query].present?
|
23
18
|
|
24
19
|
if params[:state]
|
25
20
|
case params[:state]
|
@@ -36,5 +31,11 @@ module GoodJob
|
|
36
31
|
|
37
32
|
query
|
38
33
|
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def default_base_query
|
38
|
+
GoodJob::Execution.all
|
39
|
+
end
|
39
40
|
end
|
40
41
|
end
|
@@ -12,18 +12,13 @@ module GoodJob
|
|
12
12
|
}
|
13
13
|
end
|
14
14
|
|
15
|
-
private
|
16
|
-
|
17
|
-
def default_base_query
|
18
|
-
GoodJob::ActiveJobJob.all
|
19
|
-
end
|
20
|
-
|
21
15
|
def filtered_query
|
22
16
|
query = base_query.includes(:executions)
|
23
17
|
.joins_advisory_locks.select('good_jobs.*', 'pg_locks.locktype AS locktype')
|
24
18
|
|
25
|
-
query = query.job_class(params[:job_class]) if params[:job_class]
|
26
|
-
query = query.where(queue_name: params[:queue_name]) if params[:queue_name]
|
19
|
+
query = query.job_class(params[:job_class]) if params[:job_class].present?
|
20
|
+
query = query.where(queue_name: params[:queue_name]) if params[:queue_name].present?
|
21
|
+
query = query.search(params['query']) if params[:query].present?
|
27
22
|
|
28
23
|
if params[:state]
|
29
24
|
case params[:state]
|
@@ -44,5 +39,11 @@ module GoodJob
|
|
44
39
|
|
45
40
|
query
|
46
41
|
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
def default_base_query
|
46
|
+
GoodJob::ActiveJobJob.all
|
47
|
+
end
|
47
48
|
end
|
48
49
|
end
|
@@ -1,5 +1,5 @@
|
|
1
1
|
<div class="card my-3 p-6">
|
2
|
-
<%= render 'good_job/shared/chart', chart_data: @filter.
|
2
|
+
<%= render 'good_job/shared/chart', chart_data: GoodJob::ScheduledByQueueChart.new(@filter).data %>
|
3
3
|
</div>
|
4
4
|
|
5
5
|
<%= render 'good_job/shared/filter', filter: @filter %>
|
@@ -1,5 +1,5 @@
|
|
1
1
|
<div class="card my-3 p-6">
|
2
|
-
<%= render 'good_job/shared/chart', chart_data: @filter.
|
2
|
+
<%= render 'good_job/shared/chart', chart_data: GoodJob::ScheduledByQueueChart.new(@filter).data %>
|
3
3
|
</div>
|
4
4
|
|
5
5
|
<%= render 'good_job/shared/filter', filter: @filter %>
|
@@ -1,52 +1,25 @@
|
|
1
|
-
<div
|
1
|
+
<div class="chart-wrapper">
|
2
|
+
<canvas id="chart"></canvas>
|
3
|
+
</div>
|
2
4
|
|
3
5
|
<%= javascript_tag nonce: true do %>
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
6
|
+
const chartData = <%== chart_data.to_json %>;
|
7
|
+
|
8
|
+
const ctx = document.getElementById('chart').getContext('2d');
|
9
|
+
const chart = new Chart(ctx, {
|
10
|
+
type: 'line',
|
11
|
+
data: {
|
12
|
+
labels: chartData.labels,
|
13
|
+
datasets: chartData.datasets
|
11
14
|
},
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
+
options: {
|
16
|
+
responsive: true,
|
17
|
+
maintainAspectRatio: false,
|
18
|
+
scales: {
|
19
|
+
y: {
|
20
|
+
beginAtZero: true
|
21
|
+
}
|
15
22
|
}
|
16
|
-
},
|
17
|
-
axisY: {
|
18
|
-
low: 0,
|
19
|
-
onlyInteger: true
|
20
23
|
}
|
21
|
-
})
|
22
|
-
|
23
|
-
// https://www.smashingmagazine.com/2014/12/chartist-js-open-source-library-responsive-charts/
|
24
|
-
const chartEl = document.getElementById('chart');
|
25
|
-
const tooltipEl = document.createElement('div')
|
26
|
-
|
27
|
-
tooltipEl.classList.add('tooltip', 'tooltip-hidden');
|
28
|
-
chartEl.appendChild(tooltipEl);
|
29
|
-
|
30
|
-
document.body.addEventListener('mouseenter', function (event) {
|
31
|
-
if (!(event.target.matches && event.target.matches('.ct-point'))) return;
|
32
|
-
|
33
|
-
const seriesName = event.target.closest('.ct-series').getAttribute('ct:series-name');
|
34
|
-
const value = event.target.getAttribute('ct:value');
|
35
|
-
|
36
|
-
tooltipEl.innerText = seriesName + ': ' + value;
|
37
|
-
tooltipEl.classList.remove('tooltip-hidden');
|
38
|
-
}, true);
|
39
|
-
|
40
|
-
document.body.addEventListener('mouseleave', function (event) {
|
41
|
-
if (!(event.target.matches && event.target.matches('.ct-point'))) return;
|
42
|
-
|
43
|
-
tooltipEl.classList.add('tooltip-hidden');
|
44
|
-
}, true);
|
45
|
-
|
46
|
-
document.body.addEventListener('mousemove', function(event) {
|
47
|
-
if (!(event.target.matches && event.target.matches('.ct-point'))) return;
|
48
|
-
|
49
|
-
tooltipEl.style.left = (event.offsetX || event.originalEvent.layerX) + tooltipEl.offsetWidth + 10 + 'px';
|
50
|
-
tooltipEl.style.top = (event.offsetY || event.originalEvent.layerY) + tooltipEl.offsetHeight - 20 + 'px';
|
51
|
-
}, true);
|
24
|
+
});
|
52
25
|
<% end %>
|
@@ -1,6 +1,6 @@
|
|
1
1
|
<div class='card mb-2'>
|
2
2
|
<div class='card-body d-flex flex-wrap'>
|
3
|
-
<div class='me-4'>
|
3
|
+
<div class='mb-2 me-4'>
|
4
4
|
<small>Filter by job class</small>
|
5
5
|
<br>
|
6
6
|
<% filter.job_classes.each do |name, count| %>
|
@@ -16,7 +16,7 @@
|
|
16
16
|
<% end %>
|
17
17
|
</div>
|
18
18
|
|
19
|
-
<div class='me-4'>
|
19
|
+
<div class='mb-2 me-4'>
|
20
20
|
<small>Filter by state</small>
|
21
21
|
<br>
|
22
22
|
<% filter.states.each do |name, count| %>
|
@@ -32,7 +32,7 @@
|
|
32
32
|
<% end %>
|
33
33
|
</div>
|
34
34
|
|
35
|
-
<div>
|
35
|
+
<div class='mb-2 me-4'>
|
36
36
|
<small>Filter by queue</small>
|
37
37
|
<br>
|
38
38
|
<% filter.queues.each do |name, count| %>
|
@@ -47,5 +47,20 @@
|
|
47
47
|
<% end %>
|
48
48
|
<% end %>
|
49
49
|
</div>
|
50
|
+
|
51
|
+
<div class="mb-2">
|
52
|
+
<%= form_with(url: "", method: :get, local: true) do |form| %>
|
53
|
+
<% filter.to_params(query: nil).each do |key, value| %>
|
54
|
+
<%= form.hidden_field(key.to_sym, value: value) %>
|
55
|
+
<% end %>
|
56
|
+
|
57
|
+
<small><%= form.label :query, "Search" %></small>
|
58
|
+
<div class="input-group input-group-sm">
|
59
|
+
<%= form.search_field :query, value: params[:query], class: "form-control" %>
|
60
|
+
<%= form.button "Search", type: "submit", name: nil, class: "btn btn-sm btn-outline-secondary" %>
|
61
|
+
<%= link_to "Clear", filter.to_params(query: nil), class: "btn btn-sm btn-outline-secondary" %>
|
62
|
+
</div>
|
63
|
+
<% end %>
|
64
|
+
</div>
|
50
65
|
</div>
|
51
66
|
</div>
|
@@ -1,16 +1,17 @@
|
|
1
1
|
<!DOCTYPE html>
|
2
|
-
<html>
|
2
|
+
<html lang="en">
|
3
3
|
<head>
|
4
4
|
<title>Good Job Dashboard</title>
|
5
5
|
<%= csrf_meta_tags %>
|
6
6
|
<%= csp_meta_tag %>
|
7
7
|
|
8
8
|
<%= stylesheet_link_tag bootstrap_path(format: :css, v: GoodJob::VERSION) %>
|
9
|
-
<%= stylesheet_link_tag chartist_path(format: :css, v: GoodJob::VERSION) %>
|
10
9
|
<%= stylesheet_link_tag style_path(format: :css, v: GoodJob::VERSION) %>
|
11
10
|
|
12
11
|
<%= javascript_include_tag bootstrap_path(format: :js, v: GoodJob::VERSION) %>
|
13
|
-
<%= javascript_include_tag
|
12
|
+
<%= javascript_include_tag chartjs_path(format: :js, v: GoodJob::VERSION) %>
|
13
|
+
<%= javascript_include_tag scripts_path(format: :js, v: GoodJob::VERSION) %>
|
14
|
+
|
14
15
|
<%= javascript_include_tag rails_ujs_path(format: :js, v: GoodJob::VERSION) %>
|
15
16
|
</head>
|
16
17
|
<body>
|
data/engine/config/routes.rb
CHANGED
@@ -20,14 +20,14 @@ GoodJob::Engine.routes.draw do
|
|
20
20
|
scope controller: :assets do
|
21
21
|
constraints(format: :css) do
|
22
22
|
get :bootstrap, action: :bootstrap_css
|
23
|
-
get :chartist, action: :chartist_css
|
24
23
|
get :style, action: :style_css
|
25
24
|
end
|
26
25
|
|
27
26
|
constraints(format: :js) do
|
28
27
|
get :bootstrap, action: :bootstrap_js
|
29
28
|
get :rails_ujs, action: :rails_ujs_js
|
30
|
-
get :
|
29
|
+
get :chartjs, action: :chartjs_js
|
30
|
+
get :scripts, action: :scripts_js
|
31
31
|
end
|
32
32
|
end
|
33
33
|
end
|
@@ -8,7 +8,8 @@ module GoodJob
|
|
8
8
|
# @!parse
|
9
9
|
# class ActiveJob < ActiveRecord::Base; end
|
10
10
|
class ActiveJobJob < Object.const_get(GoodJob.active_record_parent_class)
|
11
|
-
include
|
11
|
+
include Filterable
|
12
|
+
include Lockable
|
12
13
|
|
13
14
|
# Raised when an inappropriate action is applied to a Job based on its state.
|
14
15
|
ActionForStateMismatchError = Class.new(StandardError)
|
@@ -47,24 +48,6 @@ module GoodJob
|
|
47
48
|
# Errored but will not be retried
|
48
49
|
scope :discarded, -> { where.not(finished_at: nil).where.not(error: nil) }
|
49
50
|
|
50
|
-
# Get Jobs in display order with optional keyset pagination.
|
51
|
-
# @!method display_all(after_scheduled_at: nil, after_id: nil)
|
52
|
-
# @!scope class
|
53
|
-
# @param after_scheduled_at [DateTime, String, nil]
|
54
|
-
# Display records scheduled after this time for keyset pagination
|
55
|
-
# @param after_id [Numeric, String, nil]
|
56
|
-
# Display records after this ID for keyset pagination
|
57
|
-
# @return [ActiveRecord::Relation]
|
58
|
-
scope :display_all, (lambda do |after_scheduled_at: nil, after_id: nil|
|
59
|
-
query = order(Arel.sql('COALESCE(scheduled_at, created_at) DESC, id DESC'))
|
60
|
-
if after_scheduled_at.present? && after_id.present?
|
61
|
-
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)
|
62
|
-
elsif after_scheduled_at.present?
|
63
|
-
query = query.where(Arel.sql('(COALESCE(scheduled_at, created_at)) < (:after_scheduled_at)'), after_scheduled_at: after_scheduled_at)
|
64
|
-
end
|
65
|
-
query
|
66
|
-
end)
|
67
|
-
|
68
51
|
# The job's ActiveJob UUID
|
69
52
|
# @return [String]
|
70
53
|
def id
|
data/lib/good_job/execution.rb
CHANGED
@@ -6,6 +6,7 @@ module GoodJob
|
|
6
6
|
# class Execution < ActiveRecord::Base; end
|
7
7
|
class Execution < Object.const_get(GoodJob.active_record_parent_class)
|
8
8
|
include Lockable
|
9
|
+
include Filterable
|
9
10
|
|
10
11
|
# Raised if something attempts to execute a previously completed Execution again.
|
11
12
|
PreviouslyPerformedError = Class.new(StandardError)
|
@@ -156,24 +157,6 @@ module GoodJob
|
|
156
157
|
end
|
157
158
|
end)
|
158
159
|
|
159
|
-
# Get Jobs in display order with optional keyset pagination.
|
160
|
-
# @!method display_all(after_scheduled_at: nil, after_id: nil)
|
161
|
-
# @!scope class
|
162
|
-
# @param after_scheduled_at [DateTime, String, nil]
|
163
|
-
# Display records scheduled after this time for keyset pagination
|
164
|
-
# @param after_id [Numeric, String, nil]
|
165
|
-
# Display records after this ID for keyset pagination
|
166
|
-
# @return [ActiveRecord::Relation]
|
167
|
-
scope :display_all, (lambda do |after_scheduled_at: nil, after_id: nil|
|
168
|
-
query = order(Arel.sql('COALESCE(scheduled_at, created_at) DESC, id DESC'))
|
169
|
-
if after_scheduled_at.present? && after_id.present?
|
170
|
-
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)
|
171
|
-
elsif after_scheduled_at.present?
|
172
|
-
query = query.where(Arel.sql('(COALESCE(scheduled_at, created_at)) < (:after_scheduled_at)'), after_scheduled_at: after_scheduled_at)
|
173
|
-
end
|
174
|
-
query
|
175
|
-
end)
|
176
|
-
|
177
160
|
# Finds the next eligible Execution, acquire an advisory lock related to it, and
|
178
161
|
# executes the job.
|
179
162
|
# @return [ExecutionResult, nil]
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module GoodJob
|
3
|
+
# Shared methods for filtering Execution/Job records from the +good_jobs+ table.
|
4
|
+
module Filterable
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
included do
|
8
|
+
# Get records in display order with optional keyset pagination.
|
9
|
+
# @!method display_all(after_scheduled_at: nil, after_id: nil)
|
10
|
+
# @!scope class
|
11
|
+
# @param after_scheduled_at [DateTime, String, nil]
|
12
|
+
# Display records scheduled after this time for keyset pagination
|
13
|
+
# @param after_id [Numeric, String, nil]
|
14
|
+
# Display records after this ID for keyset pagination
|
15
|
+
# @return [ActiveRecord::Relation]
|
16
|
+
scope :display_all, (lambda do |after_scheduled_at: nil, after_id: nil|
|
17
|
+
query = order(Arel.sql('COALESCE(scheduled_at, created_at) DESC, id DESC'))
|
18
|
+
if after_scheduled_at.present? && after_id.present?
|
19
|
+
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)
|
20
|
+
elsif after_scheduled_at.present?
|
21
|
+
query = query.where(Arel.sql('(COALESCE(scheduled_at, created_at)) < (:after_scheduled_at)'), after_scheduled_at: after_scheduled_at)
|
22
|
+
end
|
23
|
+
query
|
24
|
+
end)
|
25
|
+
|
26
|
+
# Search records by text query.
|
27
|
+
# @!method search(query)
|
28
|
+
# @!scope class
|
29
|
+
# @param query [String]
|
30
|
+
# Search Query
|
31
|
+
# @return [ActiveRecord::Relation]
|
32
|
+
scope :search, (lambda do |query|
|
33
|
+
query = query.to_s.strip
|
34
|
+
next if query.blank?
|
35
|
+
|
36
|
+
tsvector = "(to_tsvector('english', serialized_params) || to_tsvector('english', id::text) || to_tsvector('english', COALESCE(error, '')::text))"
|
37
|
+
where("#{tsvector} @@ to_tsquery(?)", query)
|
38
|
+
.order(sanitize_sql_for_order([Arel.sql("ts_rank(#{tsvector}, to_tsquery(?))"), query]) => 'DESC')
|
39
|
+
end)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
data/lib/good_job/notifier.rb
CHANGED
@@ -25,10 +25,17 @@ module GoodJob # :nodoc:
|
|
25
25
|
max_queue: 1,
|
26
26
|
fallback_policy: :discard,
|
27
27
|
}.freeze
|
28
|
-
# Seconds to wait if database cannot be connected to
|
29
|
-
RECONNECT_INTERVAL = 5
|
30
28
|
# Seconds to block while LISTENing for a message
|
31
29
|
WAIT_INTERVAL = 1
|
30
|
+
# Seconds to wait if database cannot be connected to
|
31
|
+
RECONNECT_INTERVAL = 5
|
32
|
+
# Connection errors that will wait {RECONNECT_INTERVAL} before reconnecting
|
33
|
+
CONNECTION_ERRORS = %w[
|
34
|
+
ActiveRecord::ConnectionNotEstablished
|
35
|
+
ActiveRecord::StatementInvalid
|
36
|
+
PG::UnableToSend
|
37
|
+
PG::Error
|
38
|
+
].freeze
|
32
39
|
|
33
40
|
# @!attribute [r] instances
|
34
41
|
# @!scope class
|
@@ -115,15 +122,18 @@ module GoodJob # :nodoc:
|
|
115
122
|
if thread_error
|
116
123
|
GoodJob.on_thread_error.call(thread_error) if GoodJob.on_thread_error.respond_to?(:call)
|
117
124
|
ActiveSupport::Notifications.instrument("notifier_notify_error.good_job", { error: thread_error })
|
125
|
+
|
126
|
+
connection_error = CONNECTION_ERRORS.any? do |error_string|
|
127
|
+
error_class = error_string.safe_constantize
|
128
|
+
next unless error_class
|
129
|
+
|
130
|
+
thread_error.is_a? error_class
|
131
|
+
end
|
118
132
|
end
|
119
133
|
|
120
134
|
return if shutdown?
|
121
135
|
|
122
|
-
|
123
|
-
listen(delay: RECONNECT_INTERVAL)
|
124
|
-
else
|
125
|
-
listen
|
126
|
-
end
|
136
|
+
listen(delay: connection_error ? RECONNECT_INTERVAL : 0)
|
127
137
|
end
|
128
138
|
|
129
139
|
private
|
data/lib/good_job/version.rb
CHANGED