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