delayed 2.2.0 → 3.0.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6d365ce7fa45762c6122da4a21a7c96e30893c15534f76bb26cbb4e3f721f7c6
4
- data.tar.gz: ca6deb755821f865e354a4ff62375d7c918c81e0b9b4be5f665aa9a347ee90e9
3
+ metadata.gz: 5fac8946441b682c8df0e1365cd446cb7d507e3831f8155d692abe6ef1d52003
4
+ data.tar.gz: 602f57430c6ac8b7c94bd248f270ffe768d65de07d50bc2e482bfed67fc1ca7c
5
5
  SHA512:
6
- metadata.gz: 936fabfd27c2ae4ee68c5f999f8b10e778a171b7f438e191a6bdadfc4c3b16da455e5f1620ff0044fc7efa257308fe0679fc74ec879342d0c5c5a248a6d657d0
7
- data.tar.gz: 5cb3cdb9e392ed6ae999d768fe910d0587fa1c5cb652cd5a2cd2f5ce2fc88a8598c105a199e831e0c3185af47cc310e82ada88bc862862fe4c9ebd291166a958
6
+ metadata.gz: 3d5a2383bd876172d81e9d0926d818afe6f989af71e4143c970daf0ea5149f63cad5ccf0029be97e59765b4ceae77dc555578a76fb955ed67f1caaaf92c71352
7
+ data.tar.gz: 0f33ed115647bbacfdfe27e9db20e075c2b7374d7ee23e57bbe1a983dacfc7d1cbea6e295fb7d672203664f98d151035675e4fccb35eeb2b627dadab921f2a4d
data/README.md CHANGED
@@ -284,11 +284,11 @@ The following events will be emitted automatically by workers as jobs are reserv
284
284
  - **delayed.job.run** - an event measuring the duration of a job's execution
285
285
  - **delayed.job.error** - an event indicating that a job has errored and may be retried (no duration attached)
286
286
  - **delayed.job.failure** - an event indicating that a job has permanently failed (no duration attached)
287
- - **delayed.job.enqueue** - an event measuring the time it takes to enqueue a job
287
+ - **delayed.job.enqueue** - an event measuring the time it takes to enqueue one or more jobs (fires once per `Delayed::Job.enqueue` call and once per `perform_all_later` / `enqueue_all` batch)
288
288
  - **delayed.worker.reserve_jobs** - an event measuring the duration of the job "pickup query"
289
289
 
290
- The "run", "error", "failure" and "enqueue" events will include a `:job` argument in the event's payload,
291
- providing access to the job instance.
290
+ The "run", "error", and "failure" events will include a `:job` argument in the event's payload,
291
+ providing access to the job instance. The "enqueue" event will include a `:jobs` array.
292
292
 
293
293
  ```ruby
294
294
  ActiveSupport::Notifications.subscribe('delayed.job.run') do |*args|
@@ -56,8 +56,6 @@ module Delayed
56
56
  arel_table[:locked_at].eq(nil).or arel_table[:locked_at].lt(as_of - lock_timeout)
57
57
  end
58
58
 
59
- before_save :set_default_run_at, :set_name
60
-
61
59
  REENQUEUE_BUFFER = 30.seconds
62
60
 
63
61
  def self.set_delayed_job_table_name
@@ -281,12 +279,5 @@ module Delayed
281
279
  def attempts_alert?
282
280
  alert_attempts&.<= attempts
283
281
  end
284
-
285
- private
286
-
287
- def set_name
288
- # [feat:NameColumn] remove 'if' statement and use `||=` once the 'name' column is required.
289
- self.name = display_name if respond_to?(:name=) && attributes.fetch('name').nil?
290
- end
291
282
  end
292
283
  end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ class AddRunAtAndNameNotNullCheck < ActiveRecord::Migration[6.0]
4
+ CONSTRAINTS = {
5
+ run_at: 'chk_delayed_jobs_run_at_not_null',
6
+ name: 'chk_delayed_jobs_name_not_null',
7
+ }.freeze
8
+
9
+ def up
10
+ return unless postgres?
11
+
12
+ CONSTRAINTS.each do |column, name|
13
+ execute <<~SQL.squish
14
+ ALTER TABLE delayed_jobs
15
+ ADD CONSTRAINT #{name}
16
+ CHECK (#{column} IS NOT NULL) NOT VALID
17
+ SQL
18
+ end
19
+ end
20
+
21
+ def down
22
+ return unless postgres?
23
+
24
+ CONSTRAINTS.each_value do |name|
25
+ execute "ALTER TABLE delayed_jobs DROP CONSTRAINT IF EXISTS #{name}"
26
+ end
27
+ end
28
+
29
+ private
30
+
31
+ def postgres?
32
+ connection.adapter_name == 'PostgreSQL'
33
+ end
34
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ValidateRunAtAndNameNotNull < ActiveRecord::Migration[6.0]
4
+ CONSTRAINTS = {
5
+ run_at: 'chk_delayed_jobs_run_at_not_null',
6
+ name: 'chk_delayed_jobs_name_not_null',
7
+ }.freeze
8
+
9
+ def up
10
+ CONSTRAINTS.each do |column, name|
11
+ if postgres?
12
+ execute "ALTER TABLE delayed_jobs VALIDATE CONSTRAINT #{name}"
13
+ change_column_null :delayed_jobs, column, false
14
+ execute "ALTER TABLE delayed_jobs DROP CONSTRAINT #{name}"
15
+ else
16
+ change_column_null :delayed_jobs, column, false
17
+ end
18
+ end
19
+ end
20
+
21
+ def down
22
+ CONSTRAINTS.each do |column, name|
23
+ if postgres?
24
+ change_column_null :delayed_jobs, column, true
25
+ execute <<~SQL.squish
26
+ ALTER TABLE delayed_jobs
27
+ ADD CONSTRAINT #{name}
28
+ CHECK (#{column} IS NOT NULL) NOT VALID
29
+ SQL
30
+ else
31
+ change_column_null :delayed_jobs, column, true
32
+ end
33
+ end
34
+ end
35
+
36
+ private
37
+
38
+ def postgres?
39
+ connection.adapter_name == 'PostgreSQL'
40
+ end
41
+ end
@@ -11,24 +11,57 @@ module Delayed
11
11
  end
12
12
 
13
13
  def enqueue_at(job, timestamp)
14
- _enqueue(job, run_at: Time.at(timestamp)) # rubocop:disable Rails/TimeZone
14
+ job.scheduled_at = Time.at(timestamp) # rubocop:disable Rails/TimeZone
15
+ _enqueue(job)
16
+ end
17
+
18
+ def enqueue_all(jobs)
19
+ return 0 if jobs.empty?
20
+
21
+ assert_jobs_safe_to_enqueue!(jobs)
22
+
23
+ delayed_jobs = jobs.map { |job| build_delayed_job(job) }
24
+ Delayed::Job.enqueue_all(delayed_jobs)
25
+
26
+ perform_post_enqueue_assignments(jobs, delayed_jobs)
27
+
28
+ jobs.size
15
29
  end
16
30
 
17
31
  private
18
32
 
19
- def _enqueue(job, opts = {})
20
- if enqueue_after_transaction_commit_enabled?(job)
21
- raise UnsafeEnqueueError, "The ':delayed' ActiveJob adapter is not compatible with enqueue_after_transaction_commit"
33
+ def _enqueue(job)
34
+ job.tap { |j| enqueue_all([j]) }
35
+ end
36
+
37
+ def assert_jobs_safe_to_enqueue!(jobs)
38
+ jobs.each do |job|
39
+ if enqueue_after_transaction_commit_enabled?(job)
40
+ raise UnsafeEnqueueError, "The ':delayed' ActiveJob adapter is not compatible with enqueue_after_transaction_commit"
41
+ end
22
42
  end
43
+ end
23
44
 
24
- opts.merge!({ queue: job.queue_name, priority: job.priority }.compact)
25
- .merge!(job.provider_attributes || {})
45
+ def build_delayed_job(job)
46
+ opts = { queue: job.queue_name, priority: job.priority }.compact
47
+ opts.merge!(job.provider_attributes || {})
48
+ opts[:run_at] = coerce_scheduled_at(job.scheduled_at) if job.scheduled_at
26
49
 
27
- Delayed::Job.enqueue(JobWrapper.new(job), opts).tap do |dj|
28
- job.provider_job_id = dj.id
50
+ prepared = Delayed::Backend::JobPreparer.new(JobWrapper.new(job), opts).prepare
51
+ Delayed::Job.new(prepared)
52
+ end
53
+
54
+ def perform_post_enqueue_assignments(active_jobs, delayed_jobs)
55
+ active_jobs.zip(delayed_jobs) do |active_job, delayed_job|
56
+ active_job.successfully_enqueued = true if active_job.respond_to?(:successfully_enqueued=)
57
+ active_job.provider_job_id = delayed_job.id
29
58
  end
30
59
  end
31
60
 
61
+ def coerce_scheduled_at(value)
62
+ value.is_a?(Numeric) ? Time.at(value) : value # rubocop:disable Rails/TimeZone
63
+ end
64
+
32
65
  def enqueue_after_transaction_commit_enabled?(job)
33
66
  job.class.respond_to?(:enqueue_after_transaction_commit) &&
34
67
  [true, :always].include?(job.class.enqueue_after_transaction_commit)
@@ -14,11 +14,37 @@ module Delayed
14
14
 
15
15
  def enqueue_job(options)
16
16
  new(options).tap do |job|
17
- Delayed.lifecycle.run_callbacks(:enqueue, job) do
18
- job.hook(:enqueue)
19
- Delayed::Worker.delay_job?(job) ? job.save : job.invoke_job
17
+ assert_payload_not_active_job!(job.payload_object)
18
+ assert_no_enqueue_hook!(job.payload_object)
19
+ assert_delay_jobs_not_proc!
20
+
21
+ Delayed.lifecycle.run_callbacks(:enqueue, [job]) do
22
+ Delayed::Worker.delay_jobs ? job.save : job.invoke_job
23
+ end
24
+ end
25
+ end
26
+
27
+ # Bulk-enqueue an array of pre-built Delayed::Job instances. Callers are
28
+ # responsible for running JobPreparer and instantiating each job via
29
+ # `Delayed::Job.new(...)` before calling this method.
30
+ def enqueue_all(jobs)
31
+ return 0 if jobs.empty?
32
+
33
+ jobs.each do |job|
34
+ assert_payload_not_active_job!(job.payload_object)
35
+ assert_no_enqueue_hook!(job.payload_object)
36
+ end
37
+ assert_delay_jobs_not_proc!
38
+
39
+ Delayed.lifecycle.run_callbacks(:enqueue, jobs) do
40
+ if Delayed::Worker.delay_jobs
41
+ bulk_insert_all(jobs)
42
+ else
43
+ jobs.each(&:invoke_job)
20
44
  end
21
45
  end
46
+
47
+ jobs.size
22
48
  end
23
49
 
24
50
  def reserve(worker, max_run_time = Worker.max_run_time)
@@ -41,6 +67,38 @@ module Delayed
41
67
  warn '[DEPRECATION] `Delayed::Job.work_off` is deprecated. Use `Delayed::Worker.new.work_off instead.'
42
68
  Delayed::Worker.new.work_off(num)
43
69
  end
70
+
71
+ def name_assignable?
72
+ column_names.include?('name')
73
+ end
74
+
75
+ private
76
+
77
+ def bulk_insert_all(jobs)
78
+ now = db_time_now
79
+ jobs.each { |job| job.created_at = job.updated_at = now }
80
+ rows = jobs.map { |job| job.attributes.compact }
81
+ result = insert_all(rows) # rubocop:disable Rails/SkipsModelValidations
82
+ return unless connection.supports_insert_returning?
83
+
84
+ ids = result.rows.map(&:first)
85
+ jobs.zip(ids) { |job, id| job.id = id }
86
+ end
87
+
88
+ def assert_payload_not_active_job!(payload)
89
+ raise "Delayed::Job enqueue methods do not accept ActiveJobs, use perform_later instead" if payload.is_a?(ActiveJob::Base)
90
+ end
91
+
92
+ def assert_no_enqueue_hook!(payload)
93
+ return if payload.is_a?(Delayed::JobWrapper)
94
+ return unless payload.respond_to?(:enqueue)
95
+
96
+ raise ":enqueue hook on #{payload.class} is no longer supported"
97
+ end
98
+
99
+ def assert_delay_jobs_not_proc!
100
+ raise 'Delayed::Worker.delay_jobs may not be a Proc' if Delayed::Worker.delay_jobs.is_a?(Proc)
101
+ end
44
102
  end
45
103
 
46
104
  attr_reader :error
@@ -58,7 +116,7 @@ module Delayed
58
116
  ParseObjectFromYaml = %r{!ruby/\w+:([^\s]+)} # rubocop:disable Naming/ConstantName
59
117
 
60
118
  def name
61
- if self.class.column_names.include?('name')
119
+ if self.class.name_assignable?
62
120
  super || display_name
63
121
  else
64
122
  display_name # [feat:NameColumn] remove fallback once the "name" column is required.
@@ -105,9 +163,11 @@ module Delayed
105
163
 
106
164
  def hook(name, *args)
107
165
  if payload_object.respond_to?(name)
108
- if payload_object.is_a?(Delayed::JobWrapper)
109
- return if name == :enqueue # this callback is not supported due to method naming conflicts.
166
+ if name == :enqueue
167
+ raise ':enqueue hook is no longer supported'
168
+ end
110
169
 
170
+ if payload_object.is_a?(Delayed::JobWrapper)
111
171
  warn '[DEPRECATION] Job hook methods (`before`, `after`, `success`, etc) are deprecated. Use ActiveJob callbacks instead.'
112
172
  end
113
173
 
@@ -158,17 +218,13 @@ module Delayed
158
218
  @display_name ||= payload_object.display_name if payload_object.respond_to?(:display_name)
159
219
  @display_name ||= payload_object.class.name
160
220
  rescue DeserializationError # [feat:NameColumn] remove this `rescue` once the "name" column is required.
161
- raise if !persisted? && self.class.column_names.include?('name')
221
+ raise if !persisted? && self.class.name_assignable?
162
222
 
163
223
  ParseObjectFromYaml.match(handler)[1]
164
224
  end
165
225
 
166
226
  protected
167
227
 
168
- def set_default_run_at
169
- self.run_at ||= self.class.db_time_now
170
- end
171
-
172
228
  # Call during reload operation to clear out internal state
173
229
  def reset
174
230
  @payload_object = nil
@@ -12,6 +12,8 @@ module Delayed
12
12
  set_payload
13
13
  set_queue_name
14
14
  set_priority
15
+ set_run_at
16
+ set_name
15
17
  handle_dst
16
18
  reject_stale_run_at
17
19
  handle_deprecation
@@ -34,6 +36,24 @@ module Delayed
34
36
  options[:priority] ||= Delayed::Worker.default_priority
35
37
  end
36
38
 
39
+ def set_run_at
40
+ options[:run_at] ||= Job.db_time_now
41
+ end
42
+
43
+ def set_name
44
+ return if options[:name] || !Job.name_assignable?
45
+
46
+ payload = options[:payload_object]
47
+ options[:name] =
48
+ if payload.respond_to?(:job_data)
49
+ payload.job_data['job_class']
50
+ elsif payload.respond_to?(:display_name)
51
+ payload.display_name
52
+ else
53
+ payload.class.name
54
+ end
55
+ end
56
+
37
57
  def scheduled_into_fall_back_hour?
38
58
  options[:run_at] &&
39
59
  !options[:run_at].in_time_zone.dst? &&
@@ -4,7 +4,7 @@ module Delayed
4
4
  class Lifecycle
5
5
  EVENTS = {
6
6
  execute: [nil],
7
- enqueue: [:job],
7
+ enqueue: [:jobs],
8
8
  perform: %i(worker job),
9
9
  error: %i(worker job),
10
10
  failure: %i(worker job),
@@ -2,9 +2,9 @@ module Delayed
2
2
  module Plugins
3
3
  class Instrumentation < Plugin
4
4
  callbacks do |lifecycle|
5
- lifecycle.around(:enqueue) do |job, *args, &block|
6
- ActiveSupport::Notifications.instrument('delayed.job.enqueue', active_support_notifications_tags(job)) do
7
- block.call(job, *args)
5
+ lifecycle.around(:enqueue) do |jobs, &block|
6
+ ActiveSupport::Notifications.instrument('delayed.job.enqueue', bulk_enqueue_tags(jobs)) do
7
+ block.call(jobs)
8
8
  end
9
9
  end
10
10
 
@@ -34,6 +34,12 @@ module Delayed
34
34
  job: job,
35
35
  }
36
36
  end
37
+
38
+ def self.bulk_enqueue_tags(jobs)
39
+ {
40
+ jobs: jobs.map { |job| active_support_notifications_tags(job) },
41
+ }
42
+ end
37
43
  end
38
44
  end
39
45
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Delayed
4
- VERSION = '2.2.0'
4
+ VERSION = '3.0.0'
5
5
  end
@@ -37,14 +37,6 @@ module Delayed
37
37
  :default_log_level, :default_log_level=, to: Delayed
38
38
  end
39
39
 
40
- def self.delay_job?(job)
41
- if delay_jobs.is_a?(Proc)
42
- delay_jobs.arity == 1 ? delay_jobs.call(job) : delay_jobs.call
43
- else
44
- delay_jobs
45
- end
46
- end
47
-
48
40
  def initialize
49
41
  @failed_reserve_count = 0
50
42