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.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/db/migrate/20181217155351_speed_up_max_concurrent_triggers.rb +95 -0
  3. data/db/migrate/20190726154743_make_critical_columns_not_null.rb +15 -0
  4. data/db/migrate/20200330230722_add_id_to_get_delayed_jobs_index.rb +25 -0
  5. data/db/migrate/20200824222232_speed_up_max_concurrent_delete_trigger.rb +95 -0
  6. data/db/migrate/20200825011002_add_strand_order_override.rb +126 -0
  7. data/lib/delayed/backend/active_record.rb +93 -14
  8. data/lib/delayed/backend/redis/job.rb +8 -2
  9. data/lib/delayed/batch.rb +1 -1
  10. data/lib/delayed/lifecycle.rb +1 -0
  11. data/lib/delayed/periodic.rb +7 -4
  12. data/lib/delayed/server.rb +19 -0
  13. data/lib/delayed/server/helpers.rb +1 -0
  14. data/lib/delayed/server/public/js/app.js +49 -1
  15. data/lib/delayed/server/views/index.erb +16 -1
  16. data/lib/delayed/server/views/layout.erb +5 -3
  17. data/lib/delayed/settings.rb +5 -1
  18. data/lib/delayed/version.rb +1 -1
  19. data/lib/delayed/work_queue/parent_process/server.rb +14 -4
  20. data/lib/delayed/worker.rb +28 -6
  21. data/lib/delayed/worker/consul_health_check.rb +1 -1
  22. data/lib/delayed/worker/health_check.rb +9 -5
  23. data/lib/delayed/worker/null_health_check.rb +7 -1
  24. data/spec/active_record_job_spec.rb +36 -35
  25. data/spec/delayed/server_spec.rb +43 -1
  26. data/spec/delayed/work_queue/parent_process/server_spec.rb +4 -1
  27. data/spec/delayed/worker/consul_health_check_spec.rb +2 -2
  28. data/spec/delayed/worker/health_check_spec.rb +2 -2
  29. data/spec/delayed/worker_spec.rb +67 -20
  30. data/spec/gemfiles/52.gemfile +7 -0
  31. data/spec/gemfiles/60.gemfile +7 -0
  32. data/spec/shared/delayed_batch.rb +11 -0
  33. data/spec/shared/shared_backend.rb +15 -0
  34. data/spec/shared/worker.rb +4 -0
  35. data/spec/spec_helper.rb +4 -1
  36. metadata +23 -27
  37. data/spec/gemfiles/42.gemfile.lock +0 -192
  38. data/spec/gemfiles/50.gemfile.lock +0 -187
  39. 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
- job_attrs = functions.get_and_lock_next_available(worker_name, queue, min_priority, max_priority, db_time_now)
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
@@ -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
@@ -11,6 +11,7 @@ module Delayed
11
11
  :perform => [:worker, :job],
12
12
  :pop => [:worker],
13
13
  :work_queue_pop => [:work_queue, :worker_config],
14
+ :check_for_work => [:work_queue],
14
15
  }
15
16
 
16
17
  def initialize
@@ -1,4 +1,4 @@
1
- require 'rufus/scheduler'
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
- Rufus::Scheduler::CronLine.new(cron_line)
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 = Rufus::Scheduler::CronLine.new(cron_line)
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, :run_at => @cron.next_time(Delayed::Periodic.now).utc.to_time, :singleton => tag))
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
@@ -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
@@ -20,6 +20,7 @@ module Delayed
20
20
  running: url_path('running'),
21
21
  tags: url_path('tags'),
22
22
  jobs: url_path('jobs'),
23
+ bulkUpdate: url_path('bulk_update'),
23
24
  }
24
25
  }.to_json
25
26
  end
@@ -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-6">
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.6/js/jquery.dataTables.js" defer></script>
13
- <script type="text/javascript" charset="utf8" src="//cdn.datatables.net/plug-ins/1.10.6/integration/bootstrap/3/dataTables.bootstrap.js" defer></script>
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.6/integration/bootstrap/3/dataTables.bootstrap.css">
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>
@@ -23,7 +23,10 @@ module Delayed
23
23
  :worker_health_check_config,
24
24
  :worker_procname_prefix,
25
25
  ]
26
- SETTINGS_WITH_ARGS = [ :num_strands ]
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
@@ -1,3 +1,3 @@
1
1
  module Delayed
2
- VERSION = "0.15.0"
2
+ VERSION = "0.16.0"
3
3
  end
@@ -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 CHLD}
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
@@ -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
- ActionDispatch::Reloader.prepare! if reload
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
- health_check.stop
134
- work_queue.close
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} #{ job.to_json(:include_root => false, :only => %w(tag strand priority attempts created_at max_attempts source)) }"
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
- script: check_script,
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
- Delayed::Job.transaction do
36
- # double check that the job is still there. locked_by will immediately be reset
37
- # to nil in this transaction by Job#reschedule
38
- next unless Delayed::Job.where(id: job, locked_by: job.locked_by).update_all(locked_by: "abandoned job cleanup") == 1
39
- job.reschedule
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
@@ -3,7 +3,13 @@ module Delayed
3
3
  class NullHealthCheck < HealthCheck
4
4
  self.type_name = :none
5
5
 
6
- attr_reader *%i{start stop}
6
+ def start
7
+ true
8
+ end
9
+
10
+ def stop
11
+ true
12
+ end
7
13
 
8
14
  def live_workers; []; end
9
15
  end