good_job 2.6.0 → 2.7.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 +70 -0
- data/README.md +57 -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/controllers/good_job/base_controller.rb +19 -0
- 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/cli.rb +8 -0
- data/lib/good_job/configuration.rb +7 -0
- data/lib/good_job/execution.rb +1 -18
- data/lib/good_job/filterable.rb +42 -0
- data/lib/good_job/job_performer.rb +2 -5
- data/lib/good_job/lockable.rb +2 -4
- data/lib/good_job/notifier.rb +17 -7
- data/lib/good_job/probe_server.rb +51 -0
- data/lib/good_job/version.rb +1 -1
- metadata +29 -12
- data/engine/app/assets/vendor/chartist/chartist.css +0 -613
- data/engine/app/assets/vendor/chartist/chartist.js +0 -4516
@@ -5,6 +5,25 @@ module GoodJob
|
|
5
5
|
|
6
6
|
around_action :switch_locale
|
7
7
|
|
8
|
+
content_security_policy do |policy|
|
9
|
+
policy.default_src(:none) if policy.default_src(*policy.default_src).blank?
|
10
|
+
policy.connect_src(:self) if policy.connect_src(*policy.connect_src).blank?
|
11
|
+
policy.base_uri(:none) if policy.base_uri(*policy.base_uri).blank?
|
12
|
+
policy.font_src(:self) if policy.font_src(*policy.font_src).blank?
|
13
|
+
policy.img_src(:self, :data) if policy.img_src(*policy.img_src).blank?
|
14
|
+
policy.object_src(:none) if policy.object_src(*policy.object_src).blank?
|
15
|
+
policy.script_src(:self) if policy.script_src(*policy.script_src).blank?
|
16
|
+
policy.style_src(:self) if policy.style_src(*policy.style_src).blank?
|
17
|
+
policy.form_action(:self) if policy.form_action(*policy.form_action).blank?
|
18
|
+
policy.frame_ancestors(:none) if policy.frame_ancestors(*policy.frame_ancestors).blank?
|
19
|
+
end
|
20
|
+
|
21
|
+
before_action do
|
22
|
+
next if request.content_security_policy_nonce_generator
|
23
|
+
|
24
|
+
request.content_security_policy_nonce_generator = ->(_request) { SecureRandom.base64(16) }
|
25
|
+
end
|
26
|
+
|
8
27
|
private
|
9
28
|
|
10
29
|
def switch_locale(&action)
|
@@ -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_text(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_text(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/cli.rb
CHANGED
@@ -79,6 +79,9 @@ module GoodJob
|
|
79
79
|
method_option :pidfile,
|
80
80
|
type: :string,
|
81
81
|
desc: "Path to write daemonized Process ID (env var: GOOD_JOB_PIDFILE, default: tmp/pids/good_job.pid)"
|
82
|
+
method_option :probe_port,
|
83
|
+
type: :numeric,
|
84
|
+
desc: "Port for http health check (env var: GOOD_JOB_PROBE_PORT, default: nil)"
|
82
85
|
|
83
86
|
def start
|
84
87
|
set_up_application!
|
@@ -93,6 +96,10 @@ module GoodJob
|
|
93
96
|
poller.recipients << [scheduler, :create_thread]
|
94
97
|
|
95
98
|
cron_manager = GoodJob::CronManager.new(configuration.cron_entries, start_on_initialize: true) if configuration.enable_cron?
|
99
|
+
if configuration.probe_port
|
100
|
+
probe_server = GoodJob::ProbeServer.new(port: configuration.probe_port)
|
101
|
+
probe_server.start
|
102
|
+
end
|
96
103
|
|
97
104
|
@stop_good_job_executable = false
|
98
105
|
%w[INT TERM].each do |signal|
|
@@ -106,6 +113,7 @@ module GoodJob
|
|
106
113
|
|
107
114
|
executors = [notifier, poller, cron_manager, scheduler].compact
|
108
115
|
GoodJob._shutdown_all(executors, timeout: configuration.shutdown_timeout)
|
116
|
+
probe_server&.stop
|
109
117
|
end
|
110
118
|
|
111
119
|
default_task :start
|
@@ -195,6 +195,13 @@ module GoodJob
|
|
195
195
|
Rails.application.root.join('tmp', 'pids', 'good_job.pid')
|
196
196
|
end
|
197
197
|
|
198
|
+
# Port of the probe server
|
199
|
+
# @return [nil,Integer]
|
200
|
+
def probe_port
|
201
|
+
options[:probe_port] ||
|
202
|
+
env['GOOD_JOB_PROBE_PORT']
|
203
|
+
end
|
204
|
+
|
198
205
|
private
|
199
206
|
|
200
207
|
def rails_config
|
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_text(query)
|
28
|
+
# @!scope class
|
29
|
+
# @param query [String]
|
30
|
+
# Search Query
|
31
|
+
# @return [ActiveRecord::Relation]
|
32
|
+
scope :search_text, (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
|
@@ -13,9 +13,6 @@ module GoodJob
|
|
13
13
|
# @param queue_string [String] Queues to execute jobs from
|
14
14
|
def initialize(queue_string)
|
15
15
|
@queue_string = queue_string
|
16
|
-
|
17
|
-
@job_query = Concurrent::Delay.new { GoodJob::Execution.queue_string(queue_string) }
|
18
|
-
@parsed_queues = Concurrent::Delay.new { GoodJob::Execution.queue_parser(queue_string) }
|
19
16
|
end
|
20
17
|
|
21
18
|
# A meaningful name to identify the performer in logs and for debugging.
|
@@ -65,11 +62,11 @@ module GoodJob
|
|
65
62
|
attr_reader :queue_string
|
66
63
|
|
67
64
|
def job_query
|
68
|
-
@
|
65
|
+
@_job_query ||= GoodJob::Execution.queue_string(queue_string)
|
69
66
|
end
|
70
67
|
|
71
68
|
def parsed_queues
|
72
|
-
@
|
69
|
+
@_parsed_queues ||= GoodJob::Execution.queue_parser(queue_string)
|
73
70
|
end
|
74
71
|
end
|
75
72
|
end
|
data/lib/good_job/lockable.rb
CHANGED
@@ -24,7 +24,7 @@ module GoodJob
|
|
24
24
|
|
25
25
|
included do
|
26
26
|
# Default column to be used when creating Advisory Locks
|
27
|
-
class_attribute :advisory_lockable_column, instance_accessor: false, default:
|
27
|
+
class_attribute :advisory_lockable_column, instance_accessor: false, default: nil
|
28
28
|
|
29
29
|
# Default Postgres function to be used for Advisory Locks
|
30
30
|
class_attribute :advisory_lockable_function, default: "pg_try_advisory_lock"
|
@@ -161,10 +161,8 @@ module GoodJob
|
|
161
161
|
end
|
162
162
|
end
|
163
163
|
|
164
|
-
# Allow advisory_lockable_column to be a `Concurrent::Delay`
|
165
164
|
def _advisory_lockable_column
|
166
|
-
|
167
|
-
column.respond_to?(:value) ? column.value : column
|
165
|
+
advisory_lockable_column || primary_key
|
168
166
|
end
|
169
167
|
|
170
168
|
def supports_cte_materialization_specifiers?
|
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
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GoodJob
|
4
|
+
class ProbeServer
|
5
|
+
RACK_SERVER = 'webrick'
|
6
|
+
|
7
|
+
def self.task_observer(time, output, thread_error) # rubocop:disable Lint/UnusedMethodArgument
|
8
|
+
return if thread_error.is_a? Concurrent::CancelledOperationError
|
9
|
+
|
10
|
+
GoodJob.on_thread_error.call(thread_error) if thread_error && GoodJob.on_thread_error.respond_to?(:call)
|
11
|
+
end
|
12
|
+
|
13
|
+
def initialize(port:)
|
14
|
+
@port = port
|
15
|
+
end
|
16
|
+
|
17
|
+
def start
|
18
|
+
@handler = Rack::Handler.get(RACK_SERVER)
|
19
|
+
@future = Concurrent::Future.new(args: [@handler, @port, GoodJob.logger]) do |thr_handler, thr_port, thr_logger|
|
20
|
+
thr_handler.run(self, Port: thr_port, Logger: thr_logger, AccessLog: [])
|
21
|
+
end
|
22
|
+
@future.add_observer(self.class, :task_observer)
|
23
|
+
@future.execute
|
24
|
+
end
|
25
|
+
|
26
|
+
def running?
|
27
|
+
@handler&.instance_variable_get(:@server)&.status == :Running
|
28
|
+
end
|
29
|
+
|
30
|
+
def stop
|
31
|
+
@handler&.shutdown
|
32
|
+
@future&.value # wait for Future to exit
|
33
|
+
end
|
34
|
+
|
35
|
+
def call(env)
|
36
|
+
case Rack::Request.new(env).path
|
37
|
+
when '/', '/status'
|
38
|
+
[200, {}, ["OK"]]
|
39
|
+
when '/status/started'
|
40
|
+
started = GoodJob::Scheduler.instances.any? && GoodJob::Scheduler.instances.all?(&:running?)
|
41
|
+
started ? [200, {}, ["Started"]] : [503, {}, ["Not started"]]
|
42
|
+
when '/status/connected'
|
43
|
+
connected = GoodJob::Scheduler.instances.any? && GoodJob::Scheduler.instances.all?(&:running?) &&
|
44
|
+
GoodJob::Notifier.instances.any? && GoodJob::Notifier.instances.all?(&:listening?)
|
45
|
+
connected ? [200, {}, ["Connected"]] : [503, {}, ["Not connected"]]
|
46
|
+
else
|
47
|
+
[404, {}, ["Not found"]]
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
data/lib/good_job/version.rb
CHANGED