inst-jobs 0.15.0 → 0.16.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/db/migrate/20181217155351_speed_up_max_concurrent_triggers.rb +95 -0
- data/db/migrate/20190726154743_make_critical_columns_not_null.rb +15 -0
- data/db/migrate/20200330230722_add_id_to_get_delayed_jobs_index.rb +25 -0
- data/db/migrate/20200824222232_speed_up_max_concurrent_delete_trigger.rb +95 -0
- data/db/migrate/20200825011002_add_strand_order_override.rb +126 -0
- data/lib/delayed/backend/active_record.rb +93 -14
- data/lib/delayed/backend/redis/job.rb +8 -2
- data/lib/delayed/batch.rb +1 -1
- data/lib/delayed/lifecycle.rb +1 -0
- data/lib/delayed/periodic.rb +7 -4
- data/lib/delayed/server.rb +19 -0
- data/lib/delayed/server/helpers.rb +1 -0
- data/lib/delayed/server/public/js/app.js +49 -1
- data/lib/delayed/server/views/index.erb +16 -1
- data/lib/delayed/server/views/layout.erb +5 -3
- data/lib/delayed/settings.rb +5 -1
- data/lib/delayed/version.rb +1 -1
- data/lib/delayed/work_queue/parent_process/server.rb +14 -4
- data/lib/delayed/worker.rb +28 -6
- data/lib/delayed/worker/consul_health_check.rb +1 -1
- data/lib/delayed/worker/health_check.rb +9 -5
- data/lib/delayed/worker/null_health_check.rb +7 -1
- data/spec/active_record_job_spec.rb +36 -35
- data/spec/delayed/server_spec.rb +43 -1
- data/spec/delayed/work_queue/parent_process/server_spec.rb +4 -1
- data/spec/delayed/worker/consul_health_check_spec.rb +2 -2
- data/spec/delayed/worker/health_check_spec.rb +2 -2
- data/spec/delayed/worker_spec.rb +67 -20
- data/spec/gemfiles/52.gemfile +7 -0
- data/spec/gemfiles/60.gemfile +7 -0
- data/spec/shared/delayed_batch.rb +11 -0
- data/spec/shared/shared_backend.rb +15 -0
- data/spec/shared/worker.rb +4 -0
- data/spec/spec_helper.rb +4 -1
- metadata +23 -27
- data/spec/gemfiles/42.gemfile.lock +0 -192
- data/spec/gemfiles/50.gemfile.lock +0 -187
- data/spec/gemfiles/51.gemfile.lock +0 -187
@@ -223,7 +223,8 @@ class Job
|
|
223
223
|
min_priority = Delayed::MIN_PRIORITY,
|
224
224
|
max_priority = Delayed::MAX_PRIORITY,
|
225
225
|
prefetch: nil,
|
226
|
-
prefetch_owner: nil
|
226
|
+
prefetch_owner: nil,
|
227
|
+
forced_latency: nil)
|
227
228
|
|
228
229
|
check_queue(queue)
|
229
230
|
check_priorities(min_priority, max_priority)
|
@@ -234,7 +235,9 @@ class Job
|
|
234
235
|
|
235
236
|
# as an optimization this lua function returns the hash of job attributes,
|
236
237
|
# rather than just a job id, saving a round trip
|
237
|
-
|
238
|
+
now = db_time_now
|
239
|
+
now -= forced_latency if forced_latency
|
240
|
+
job_attrs = functions.get_and_lock_next_available(worker_name, queue, min_priority, max_priority, now)
|
238
241
|
job = instantiate_from_attrs(job_attrs) # will return nil if the attrs are blank
|
239
242
|
if multiple_workers
|
240
243
|
if job.nil?
|
@@ -450,7 +453,10 @@ class Job
|
|
450
453
|
[singleton.run_at, run_at].min
|
451
454
|
when :overwrite
|
452
455
|
run_at
|
456
|
+
when :loose
|
457
|
+
singleton.run_at
|
453
458
|
end
|
459
|
+
singleton.handler = self.handler if self.on_conflict == :overwrite
|
454
460
|
singleton.save! if singleton.changed?
|
455
461
|
COLUMNS.each { |c| send("#{c}=", singleton.send(c)) }
|
456
462
|
end
|
data/lib/delayed/batch.rb
CHANGED
@@ -30,7 +30,7 @@ module Delayed
|
|
30
30
|
private
|
31
31
|
def prepare_batches(mode, opts)
|
32
32
|
raise "nested batching is not supported" if Delayed::Job.batches
|
33
|
-
Delayed::Job.batches = Hash.new { |h,k| h[k] =
|
33
|
+
Delayed::Job.batches = Hash.new { |h,k| h[k] = Set.new }
|
34
34
|
batch_enqueue_args = [:queue]
|
35
35
|
batch_enqueue_args << :priority unless opts[:priority]
|
36
36
|
Delayed::Job.batch_enqueue_args = batch_enqueue_args
|
data/lib/delayed/lifecycle.rb
CHANGED
data/lib/delayed/periodic.rb
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
require '
|
1
|
+
require 'fugit'
|
2
2
|
|
3
3
|
module Delayed
|
4
4
|
class Periodic
|
@@ -15,7 +15,7 @@ class Periodic
|
|
15
15
|
def self.add_overrides(overrides)
|
16
16
|
overrides.each do |name, cron_line|
|
17
17
|
# throws error if the line is malformed
|
18
|
-
|
18
|
+
Fugit.do_parse_cron(cron_line)
|
19
19
|
end
|
20
20
|
self.overrides.merge!(overrides)
|
21
21
|
end
|
@@ -41,13 +41,16 @@ class Periodic
|
|
41
41
|
|
42
42
|
def initialize(name, cron_line, job_args, block)
|
43
43
|
@name = name
|
44
|
-
@cron =
|
44
|
+
@cron = Fugit.do_parse_cron(cron_line)
|
45
45
|
@job_args = { :priority => Delayed::LOW_PRIORITY }.merge(job_args.symbolize_keys)
|
46
46
|
@block = block
|
47
47
|
end
|
48
48
|
|
49
49
|
def enqueue
|
50
|
-
Delayed::Job.enqueue(self, @job_args.merge(:max_attempts => 1,
|
50
|
+
Delayed::Job.enqueue(self, @job_args.merge(:max_attempts => 1,
|
51
|
+
:run_at => @cron.next_time(Delayed::Periodic.now).utc.to_time,
|
52
|
+
:singleton => tag,
|
53
|
+
on_conflict: :patient))
|
51
54
|
end
|
52
55
|
|
53
56
|
def perform
|
data/lib/delayed/server.rb
CHANGED
@@ -16,12 +16,18 @@ module Delayed
|
|
16
16
|
if using_active_record? && !ActiveRecord::Base.connected?
|
17
17
|
ActiveRecord::Base.establish_connection(ENV['DATABASE_URL'])
|
18
18
|
end
|
19
|
+
|
20
|
+
@allow_update = args.length > 0 && args[0][:update]
|
19
21
|
end
|
20
22
|
|
21
23
|
def using_active_record?
|
22
24
|
Delayed::Job == Delayed::Backend::ActiveRecord::Job
|
23
25
|
end
|
24
26
|
|
27
|
+
def allow_update
|
28
|
+
@allow_update
|
29
|
+
end
|
30
|
+
|
25
31
|
# Ensure we're connected to the DB before processing the request
|
26
32
|
before do
|
27
33
|
if ActiveRecord::Base.respond_to?(:verify_active_connections!) && using_active_record?
|
@@ -100,6 +106,19 @@ module Delayed
|
|
100
106
|
})
|
101
107
|
end
|
102
108
|
|
109
|
+
post '/bulk_update' do
|
110
|
+
content_type :json
|
111
|
+
|
112
|
+
halt 403 unless @allow_update
|
113
|
+
|
114
|
+
payload = JSON.parse(request.body.read).symbolize_keys
|
115
|
+
Delayed::Job.bulk_update(payload[:action], { ids: payload[:ids] })
|
116
|
+
|
117
|
+
json({
|
118
|
+
success: true
|
119
|
+
})
|
120
|
+
end
|
121
|
+
|
103
122
|
private
|
104
123
|
|
105
124
|
def extract_page_size
|
@@ -97,7 +97,8 @@
|
|
97
97
|
{"data": "priority"},
|
98
98
|
{"data": "strand"},
|
99
99
|
{"data": "run_at"}
|
100
|
-
]
|
100
|
+
],
|
101
|
+
select: true
|
101
102
|
});
|
102
103
|
|
103
104
|
$('body').on('click', '.refresh_jobs_link', function(event) {
|
@@ -106,6 +107,53 @@
|
|
106
107
|
return true
|
107
108
|
});
|
108
109
|
|
110
|
+
function bulkUpdate(action) {
|
111
|
+
var selectedRows = jobsTable.rows( { selected: true } );
|
112
|
+
var n = selectedRows.count();
|
113
|
+
if(action == 'destroy') {
|
114
|
+
if(!confirm("Are you sure you want to delete " + n + " jobs?")) {
|
115
|
+
return;
|
116
|
+
}
|
117
|
+
}
|
118
|
+
var itemIds = [];
|
119
|
+
var i;
|
120
|
+
for (i = 0; i < n; ++i) {
|
121
|
+
itemIds.push(selectedRows.data()[i].id);
|
122
|
+
}
|
123
|
+
var data = {
|
124
|
+
action: action,
|
125
|
+
items: itemIds
|
126
|
+
}
|
127
|
+
$.ajax({
|
128
|
+
type: "POST",
|
129
|
+
contentType : 'application/json',
|
130
|
+
url: ENV.Routes.bulkUpdate,
|
131
|
+
data: JSON.stringify(data),
|
132
|
+
success: function( ) {
|
133
|
+
jobsTable.rows().deselect();
|
134
|
+
jobsTable.ajax.reload();
|
135
|
+
},
|
136
|
+
});
|
137
|
+
}
|
138
|
+
|
139
|
+
$('body').on('click', '.hold_selection_link', function(event) {
|
140
|
+
event.preventDefault();
|
141
|
+
bulkUpdate('hold');
|
142
|
+
return true
|
143
|
+
});
|
144
|
+
|
145
|
+
$('body').on('click', '.unhold_selection_link', function(event) {
|
146
|
+
event.preventDefault();
|
147
|
+
bulkUpdate('unhold');
|
148
|
+
return true
|
149
|
+
});
|
150
|
+
|
151
|
+
$('body').on('click', '.delete_selection_link', function(event) {
|
152
|
+
event.preventDefault();
|
153
|
+
bulkUpdate('destroy');
|
154
|
+
return true
|
155
|
+
});
|
156
|
+
|
109
157
|
$('body').on('change', 'select#current_jobs_flavor', function (event) {
|
110
158
|
event.preventDefault();
|
111
159
|
$selectBox = $(this);
|
@@ -60,13 +60,28 @@
|
|
60
60
|
</span>
|
61
61
|
</div>
|
62
62
|
</div>
|
63
|
-
<div class="col-md
|
63
|
+
<div class="col-md-<%= allow_update ? 2 : 6 %>">
|
64
64
|
</div>
|
65
65
|
<div class="col-md-1">
|
66
66
|
<a href="" class="btn btn-default pull-right refresh_jobs_link" aria-label="Refresh Jobs Table">
|
67
67
|
<span class="glyphicon glyphicon-refresh"></span> Refresh
|
68
68
|
</a>
|
69
69
|
</div>
|
70
|
+
<% if allow_update %>
|
71
|
+
<div class="col-md-4">
|
72
|
+
<span>With selection: </span>
|
73
|
+
<br/>
|
74
|
+
<a href="" class="btn btn-default hold_selection_link" aria-label="Hold Selected Jobs">
|
75
|
+
<span class="glyphicon glyphicon-pause"></span> Hold
|
76
|
+
</a>
|
77
|
+
<a href="" class="btn btn-default unhold_selection_link" aria-label="Un-hold Selected Jobs">
|
78
|
+
<span class="glyphicon glyphicon-play"></span> Un-Hold
|
79
|
+
</a>
|
80
|
+
<a href="" class="btn btn-default delete_selection_link" aria-label="Delete Selected Jobs">
|
81
|
+
<span class="glyphicon glyphicon-trash"></span> Delete
|
82
|
+
</a>
|
83
|
+
</div>
|
84
|
+
<% end %>
|
70
85
|
</div>
|
71
86
|
</div>
|
72
87
|
|
@@ -9,12 +9,14 @@ window.ENV = <%= render_javascript_env %>
|
|
9
9
|
</script>
|
10
10
|
<script type="text/javascript" charset="utf8 "src="//code.jquery.com/jquery-2.1.3.min.js" defer></script>
|
11
11
|
<script type="text/javascript" charset="utf8" src="//maxcdn.bootstrapcdn.com/bootstrap/3.3.4/js/bootstrap.min.js" defer></script>
|
12
|
-
<script type="text/javascript" charset="utf8" src="//cdn.datatables.net/1.10.
|
13
|
-
<script type="text/javascript" charset="utf8" src="//cdn.datatables.net/
|
12
|
+
<script type="text/javascript" charset="utf8" src="//cdn.datatables.net/1.10.19/js/jquery.dataTables.js" defer></script>
|
13
|
+
<script type="text/javascript" charset="utf8" src="//cdn.datatables.net/select/1.2.7/js/dataTables.select.min.js" defer></script>
|
14
|
+
<script type="text/javascript" charset="utf8" src="//cdn.datatables.net/plug-ins/1.10.19/integration/bootstrap/3/dataTables.bootstrap.js" defer></script>
|
14
15
|
<script type="text/javascript" charset="utf8" src="<%= url_path 'js/app.js' %>" defer></script>
|
15
16
|
|
16
17
|
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.4/css/bootstrap.min.css">
|
17
|
-
<link rel="stylesheet" href="//cdn.datatables.net/plug-ins/1.10.
|
18
|
+
<link rel="stylesheet" href="//cdn.datatables.net/plug-ins/1.10.19/integration/bootstrap/3/dataTables.bootstrap.css">
|
19
|
+
<link rel="stylesheet" href="//cdn.datatables.net/select/1.2.7/css/select.dataTables.min.css">
|
18
20
|
<link rel="stylesheet" href="<%= url_path 'css/app.css' %>">
|
19
21
|
|
20
22
|
<title>Delayed Jobs</title>
|
data/lib/delayed/settings.rb
CHANGED
@@ -23,7 +23,10 @@ module Delayed
|
|
23
23
|
:worker_health_check_config,
|
24
24
|
:worker_procname_prefix,
|
25
25
|
]
|
26
|
-
SETTINGS_WITH_ARGS = [
|
26
|
+
SETTINGS_WITH_ARGS = [
|
27
|
+
:job_detailed_log_format,
|
28
|
+
:num_strands
|
29
|
+
]
|
27
30
|
|
28
31
|
SETTINGS.each do |setting|
|
29
32
|
mattr_writer(setting)
|
@@ -65,6 +68,7 @@ module Delayed
|
|
65
68
|
|
66
69
|
self.num_strands = ->(strand_name){ nil }
|
67
70
|
self.default_job_options = ->{ Hash.new }
|
71
|
+
self.job_detailed_log_format = ->(job){ job.to_json(include_root: false, only: %w(tag strand priority attempts created_at max_attempts source)) }
|
68
72
|
|
69
73
|
# Send workers KILL after QUIT if they haven't exited within the
|
70
74
|
# slow_exit_timeout
|
data/lib/delayed/version.rb
CHANGED
@@ -5,7 +5,7 @@ class ParentProcess
|
|
5
5
|
attr_reader :clients, :listen_socket
|
6
6
|
|
7
7
|
include Delayed::Logging
|
8
|
-
SIGNALS = %i{INT TERM QUIT
|
8
|
+
SIGNALS = %i{INT TERM QUIT}
|
9
9
|
|
10
10
|
def initialize(listen_socket, parent_pid: nil, config: Settings.parent_process)
|
11
11
|
@listen_socket = listen_socket
|
@@ -59,12 +59,20 @@ class ParentProcess
|
|
59
59
|
|
60
60
|
def run_once
|
61
61
|
handles = @clients.keys + [@listen_socket, @self_pipe[0]]
|
62
|
+
# if we're currently idle, then force a "latency" to job fetching - don't
|
63
|
+
# fetch recently queued jobs, allowing busier workers to fetch them first.
|
64
|
+
# if they're not keeping up, the jobs will slip back in time, and suddenly we'll become
|
65
|
+
# active and quickly pick up all the jobs we can. The latency is calculated to ensure that
|
66
|
+
# an active worker is guaranteed to have attempted to fetch new jobs in the meantime
|
67
|
+
forced_latency = Settings.sleep_delay + Settings.sleep_delay_stagger * 2 if all_workers_idle?
|
62
68
|
timeout = Settings.sleep_delay + (rand * Settings.sleep_delay_stagger)
|
63
69
|
readable, _, _ = IO.select(handles, nil, nil, timeout)
|
64
70
|
if readable
|
65
71
|
readable.each { |s| handle_read(s) }
|
66
72
|
end
|
67
|
-
check_for_work
|
73
|
+
Delayed::Worker.lifecycle.run_callbacks(:check_for_work, self) do
|
74
|
+
check_for_work(forced_latency: forced_latency)
|
75
|
+
end
|
68
76
|
unlock_timed_out_prefetched_jobs
|
69
77
|
end
|
70
78
|
|
@@ -111,7 +119,7 @@ class ParentProcess
|
|
111
119
|
drop_socket(socket)
|
112
120
|
end
|
113
121
|
|
114
|
-
def check_for_work
|
122
|
+
def check_for_work(forced_latency: nil)
|
115
123
|
@waiting_clients.each do |(worker_config, workers)|
|
116
124
|
prefetched_jobs = @prefetched_jobs[worker_config] ||= []
|
117
125
|
logger.debug("I have #{prefetched_jobs.length} jobs for #{workers.length} waiting workers")
|
@@ -124,6 +132,7 @@ class ParentProcess
|
|
124
132
|
workers.unshift(client)
|
125
133
|
next
|
126
134
|
end
|
135
|
+
client.working = true
|
127
136
|
begin
|
128
137
|
logger.debug("Sending prefetched job #{job.id} to #{client.name}")
|
129
138
|
client_timeout { Marshal.dump(job, client.socket) }
|
@@ -148,7 +157,8 @@ class ParentProcess
|
|
148
157
|
worker_config[:min_priority],
|
149
158
|
worker_config[:max_priority],
|
150
159
|
prefetch: Settings.fetch_batch_size * (worker_config[:workers] || 1) - recipients.length,
|
151
|
-
prefetch_owner: prefetch_owner
|
160
|
+
prefetch_owner: prefetch_owner,
|
161
|
+
forced_latency: forced_latency)
|
152
162
|
logger.debug("Fetched and locked #{response.values.flatten.size} new jobs for workers (#{response.keys.join(', ')}).")
|
153
163
|
response.each do |(worker_name, job)|
|
154
164
|
if worker_name == prefetch_owner
|
data/lib/delayed/worker.rb
CHANGED
@@ -63,11 +63,19 @@ class Worker
|
|
63
63
|
if app && !app.config.cache_classes
|
64
64
|
Delayed::Worker.lifecycle.around(:perform) do |worker, job, &block|
|
65
65
|
reload = app.config.reload_classes_only_on_change != true || app.reloaders.map(&:updated?).any?
|
66
|
-
|
66
|
+
|
67
|
+
if reload
|
68
|
+
if defined?(ActiveSupport::Reloader)
|
69
|
+
Rails.application.reloader.reload!
|
70
|
+
else
|
71
|
+
ActionDispatch::Reloader.prepare!
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
67
75
|
begin
|
68
76
|
block.call(worker, job)
|
69
77
|
ensure
|
70
|
-
ActionDispatch::Reloader.cleanup! if reload
|
78
|
+
ActionDispatch::Reloader.cleanup! if reload && !defined?(ActiveSupport::Reloader)
|
71
79
|
end
|
72
80
|
end
|
73
81
|
end
|
@@ -101,7 +109,7 @@ class Worker
|
|
101
109
|
trap(sig) { @signal_queue << sig; wake_up }
|
102
110
|
end
|
103
111
|
|
104
|
-
health_check.start
|
112
|
+
raise 'Could not register health_check' unless health_check.start
|
105
113
|
|
106
114
|
signal_processor = Thread.new do
|
107
115
|
loop do
|
@@ -110,6 +118,7 @@ class Worker
|
|
110
118
|
when :INT, :TERM
|
111
119
|
@exit = true # get the main thread to bail early if it's waiting for a job
|
112
120
|
work_thread.raise(SystemExit) # Force the main thread to bail out of the current job
|
121
|
+
cleanup! # we're going to get SIGKILL'd in a moment, so clean up asap
|
113
122
|
break
|
114
123
|
when :QUIT
|
115
124
|
@exit = true
|
@@ -130,13 +139,26 @@ class Worker
|
|
130
139
|
Rails.logger.fatal("Child process died: #{e.inspect}") rescue nil
|
131
140
|
self.class.lifecycle.run_callbacks(:exceptional_exit, self, e) { }
|
132
141
|
ensure
|
133
|
-
|
134
|
-
|
142
|
+
cleanup!
|
143
|
+
|
135
144
|
if signal_processor
|
136
145
|
signal_processor.kill
|
137
146
|
signal_processor.join
|
138
147
|
end
|
148
|
+
end
|
149
|
+
|
150
|
+
def cleanup!
|
151
|
+
return if cleaned?
|
152
|
+
|
153
|
+
health_check.stop
|
154
|
+
work_queue.close
|
139
155
|
Delayed::Job.clear_locks!(name)
|
156
|
+
|
157
|
+
@cleaned = true
|
158
|
+
end
|
159
|
+
|
160
|
+
def cleaned?
|
161
|
+
@cleaned
|
140
162
|
end
|
141
163
|
|
142
164
|
def run
|
@@ -231,7 +253,7 @@ class Worker
|
|
231
253
|
def log_job(job, format = :short)
|
232
254
|
case format
|
233
255
|
when :long
|
234
|
-
"#{job.full_name} #{
|
256
|
+
"#{job.full_name} #{ Settings.job_detailed_log_format.call(job) }"
|
235
257
|
else
|
236
258
|
job.full_name
|
237
259
|
end
|
@@ -59,7 +59,7 @@ module Delayed
|
|
59
59
|
|
60
60
|
def check_attributes
|
61
61
|
{
|
62
|
-
|
62
|
+
args: ['bash', '-c', check_script],
|
63
63
|
status: 'passing',
|
64
64
|
interval: @config.fetch(:check_interval, '5m'),
|
65
65
|
deregister_critical_service_after: @config.fetch(:deregister_service_delay, '10m'),
|
@@ -32,11 +32,15 @@ module Delayed
|
|
32
32
|
# prefetched jobs have their own way of automatically unlocking themselves
|
33
33
|
next if job.locked_by.start_with?("prefetch:")
|
34
34
|
unless live_workers.include?(job.locked_by)
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
35
|
+
begin
|
36
|
+
Delayed::Job.transaction do
|
37
|
+
# double check that the job is still there. locked_by will immediately be reset
|
38
|
+
# to nil in this transaction by Job#reschedule
|
39
|
+
next unless Delayed::Job.where(id: job, locked_by: job.locked_by).update_all(locked_by: "abandoned job cleanup") == 1
|
40
|
+
job.reschedule
|
41
|
+
end
|
42
|
+
rescue
|
43
|
+
::Rails.logger.error "Failure rescheduling abandoned job #{job.id} #{$!.inspect}"
|
40
44
|
end
|
41
45
|
end
|
42
46
|
end
|