delayed_job_groups_plugin 0.14.0 → 1.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: 1a151f3024b43407ef181bc42c113e97877160c37f48f1f68b793e533f001b88
4
- data.tar.gz: d9f29810d2680e21956472ec485fe03f009765591cfadb9cb2fcaea17569c163
3
+ metadata.gz: 9d88649deb9812581de80f6a57a265ccfd265f7f9122d837eff527647f77f01b
4
+ data.tar.gz: 4979b4dd43fb9294fb622e478ac5bb36a7f688f83236183eac2fc80a668af828
5
5
  SHA512:
6
- metadata.gz: 1a9b598b3f77a3256b6fbcca5ba5d1acd47c065c7d845fc19bf541ffe236779419f0439cf70ef82783c3185697305137acb8d0baf7eadc9f51d3796933d2e65d
7
- data.tar.gz: b273960f4d058062e50c67a9144a6638f8574c97a72e5ec88a1388dc632778e3877c3e4f7218dc261789137e69382a0cd9d1600bc4800cb72b73f751c5dc93f8
6
+ metadata.gz: '083c6e1461374569d30d7538fb5a7e4d4f980cae04858bbac94793597f62d588a6078b6e55e605ee7e10ff413e0c6ef5460088cd50667663ceefdfc29543ae6a'
7
+ data.tar.gz: d4ede12f4275b606f0e0704ac3cfc255727f3796979ecff7aa87781629172d0e15ad6e1d0b96254bd845f243a7f8d7bc96f4c4bb670bf4d48be4e4a2590776a4
data/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.0.0
4
+ ### Breaking Changes
5
+ - This library will fail to load if `Delayed::Worker.destroy_failed_jobs` is set to true.
6
+ Delayed::Job sets this option to true by default, you will need to configure it to false
7
+ in order to include this library.
8
+ ### Changes
9
+ - Moves `on_cancellation` logic from the before delayed job lifecycle hook to the after hook.
10
+ - Wrapped the job group cancel hook in a lock to prevent concurrently failing jobs from enqueueing
11
+ multiple on cancellation jobs.
12
+
3
13
  ## 0.14.0
4
14
  - Reverts changes made in version 0.13.
5
15
 
data/README.md CHANGED
@@ -35,6 +35,15 @@ Run the required database migrations:
35
35
  $ rails generate delayed_job_groups_plugin:install
36
36
  $ rake db:migrate
37
37
 
38
+ ## Upgrading from 0.14.0
39
+
40
+ This library is now incompatible with Delayed::Job's default setting for `destroy_failed_jobs`.
41
+ In order to use Delayed Job Groups, you must set it to false while configuring Delayed::Job:
42
+
43
+ ```ruby
44
+ Delayed::Worker.destroy_failed_jobs = false
45
+ ```
46
+
38
47
  ## Upgrading from 0.1.2
39
48
  run the following generator to create a migration for the new configuration column.
40
49
 
@@ -42,6 +51,7 @@ run the following generator to create a migration for the new configuration colu
42
51
  # add `default: true, null: false` to the generated migration for the failure_cancels_group column
43
52
  $ rake db:migrate
44
53
 
54
+
45
55
  ## Usage
46
56
 
47
57
  Creating a job group and queueing some jobs:
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Delayed
4
+ module JobGroups
5
+ ConfigurationError = Class.new(StandardError)
6
+
7
+ class IncompatibleWithDelayedJobError < ConfigurationError
8
+ DEFAULT_MESSAGE = 'DelayedJobGroupsPlugin is incompatible with Delayed::Job ' \
9
+ 'when `destroy_failed_jobs` is set to `true`'
10
+
11
+ def initialize(msg = DEFAULT_MESSAGE)
12
+ super(msg)
13
+ end
14
+ end
15
+ end
16
+ end
@@ -62,8 +62,18 @@ module Delayed
62
62
  end
63
63
 
64
64
  def cancel
65
- Delayed::Job.enqueue(on_cancellation_job, on_cancellation_job_options || {}) if on_cancellation_job
66
- destroy
65
+ with_lock do
66
+ return if destroyed?
67
+
68
+ Delayed::Job.enqueue(on_cancellation_job, on_cancellation_job_options || {}) if on_cancellation_job
69
+ destroy
70
+ end
71
+ rescue ActiveRecord::RecordNotFound
72
+ # The first failing job to attempt cancelling the job group will enqueue the
73
+ # on cancellation job and destroy the group. Any other concurrently failing job
74
+ # in the same group can therefore silently return if the job group has already
75
+ # been destroyed.
76
+ nil
67
77
  end
68
78
 
69
79
  def check_for_completion(skip_pending_jobs_check: false)
@@ -17,7 +17,13 @@ module Delayed
17
17
  end
18
18
  end
19
19
 
20
- lifecycle.before(:failure) do |_worker, job|
20
+ # In order to allow individual jobs in the JobGroup to perform cleanup activities upon
21
+ # job failure, a JobGroups on cancellation job must be enqueued in the after failure
22
+ # delayed job lifecycle hook. If both were to be performed in the same hook, the job's
23
+ # failure hook and the on cancellation job may run at the same time since hook execution
24
+ # order is never guaranteed. This is particular important if the on cancellation job
25
+ # depends on state set in the failure hook of an individual job.
26
+ lifecycle.after(:failure) do |_worker, job|
21
27
  # If a job in the job group fails, then cancel the whole job group.
22
28
  # Need to check that the job group is present since another
23
29
  # job may have concurrently cancelled it.
@@ -4,6 +4,17 @@ module Delayed
4
4
  module JobGroups
5
5
  class Railtie < ::Rails::Railtie
6
6
  config.after_initialize do
7
+
8
+ # On cancellation checks are performed in the after failure delayed job lifecycle, however
9
+ # https://github.com/collectiveidea/delayed_job/blob/master/lib/delayed/worker.rb#L268 may
10
+ # delete jobs before the hook runs. This could cause a successful job in the same group to
11
+ # complete the group instead of the group being cancelled. Therefore, we must ensure that
12
+ # the Delayed::Worker.destroy_failed_jobs is set to false, guaranteeing that the group is
13
+ # never empty if failure occurs.
14
+ if Delayed::Worker.destroy_failed_jobs && ActiveRecord::Base.connection.table_exists?(:delayed_job_groups)
15
+ raise Delayed::JobGroups::IncompatibleWithDelayedJobError
16
+ end
17
+
7
18
  Delayed::Backend::ActiveRecord::Job.include(Delayed::JobGroups::JobExtensions)
8
19
  end
9
20
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Delayed
4
4
  module JobGroups
5
- VERSION = '0.14.0'
5
+ VERSION = '1.0.0'
6
6
  end
7
7
  end
@@ -4,6 +4,7 @@ require 'active_support'
4
4
  require 'active_record'
5
5
  require 'delayed_job'
6
6
  require 'delayed_job_active_record'
7
+ require 'delayed/job_groups/errors'
7
8
  require 'delayed/job_groups/compatibility'
8
9
  require 'delayed/job_groups/complete_stuck_job_groups_job'
9
10
  require 'delayed/job_groups/job_extensions'
@@ -18,6 +19,7 @@ if defined?(Rails::Railtie)
18
19
  else
19
20
  # Do the same as in the railtie
20
21
  Delayed::Backend::ActiveRecord::Job.include(Delayed::JobGroups::JobExtensions)
22
+ raise Delayed::JobGroups::IncompatibleWithDelayedJobError if Delayed::Worker.destroy_failed_jobs
21
23
  end
22
24
 
23
25
  Delayed::Worker.plugins << Delayed::JobGroups::Plugin
@@ -18,5 +18,37 @@ module DelayedJobGroupsPlugin
18
18
  ActiveRecord::Generators::Base.next_migration_number(dirname)
19
19
  end
20
20
 
21
+ def create_initializer
22
+ initializer_file = File.join('config/initializers', 'delayed_job_config.rb')
23
+ configuration_on_matcher = /Delayed::Worker\.destroy_failed_jobs\s*=\s*true/
24
+ configuration_off_matcher = /Delayed::Worker\.destroy_failed_jobs\s*=\s*false/
25
+
26
+ say 'Attempting to initialize delayed_job_config initializer...', :green
27
+
28
+ if File.exist?(initializer_file)
29
+ say 'delayed_job_config initializer already exists... checking destroy_failed_jobs options', :green
30
+ contents = File.read(initializer_file)
31
+ if contents.match(configuration_on_matcher)
32
+ say 'Delayed::Worker.destroy_failed_jobs is set to true', :red
33
+ say 'This library requires the option to be set to false, updating config now!', :yellow
34
+
35
+ gsub_file initializer_file, configuration_on_matcher, 'Delayed::Worker.destroy_failed_jobs = false'
36
+ elsif contents.match(configuration_off_matcher)
37
+ say 'Delayed::Worker.destroy_failed_jobs is set to false; nothing to do!', :green
38
+ else
39
+ say 'Delayed::Worker.destroy_failed_jobs is not set'
40
+ say 'This library requires the option to be set to false, updating config now!', :yellow
41
+ inject_into_file initializer_file, "Delayed::Worker.destroy_failed_jobs = false\n"
42
+ end
43
+ else
44
+ create_file initializer_file do
45
+ <<~RUBY
46
+ # frozen_string_literal: true
47
+
48
+ Delayed::Worker.destroy_failed_jobs = false
49
+ RUBY
50
+ end
51
+ end
52
+ end
21
53
  end
22
54
  end
@@ -299,11 +299,28 @@ describe Delayed::JobGroups::JobGroup do
299
299
  end
300
300
 
301
301
  describe "#cancel" do
302
+ subject(:job_group) do
303
+ create(
304
+ :job_group,
305
+ on_completion_job: on_completion_job,
306
+ on_completion_job_options: on_completion_job_options,
307
+ on_cancellation_job: on_cancellation_job,
308
+ on_cancellation_job_options: on_cancellation_job_options,
309
+ blocked: blocked
310
+ )
311
+ end
312
+
302
313
  let!(:queued_job) { Delayed::Job.create!(job_group_id: job_group.id) }
303
314
  let!(:running_job) { Delayed::Job.create!(job_group_id: job_group.id, locked_at: Time.now, locked_by: 'test') }
315
+ let(:before_hook) {}
316
+ let(:on_cancellation_job) { 'dummy job' }
317
+ let(:on_cancellation_job_options) do
318
+ { foo: 'bar' }
319
+ end
320
+ let(:cancel) { true }
304
321
 
305
322
  before do
306
- job_group.cancel
323
+ job_group.cancel if cancel
307
324
  end
308
325
 
309
326
  it "destroys the job group" do
@@ -317,6 +334,21 @@ describe Delayed::JobGroups::JobGroup do
317
334
  it "does not destroy running jobs" do
318
335
  expect(running_job).not_to have_been_destroyed
319
336
  end
337
+
338
+ context "when already cancelled" do
339
+ let(:cancel) { false }
340
+
341
+ it "skips cancel block if already cancelled" do
342
+ other = Delayed::JobGroups::JobGroup.find(job_group.id)
343
+ job_group.cancel
344
+
345
+ other.cancel
346
+ other.cancel
347
+
348
+ expect(Delayed::Job)
349
+ .to have_received(:enqueue).with(on_cancellation_job, on_cancellation_job_options).once
350
+ end
351
+ end
320
352
  end
321
353
 
322
354
  describe "#failure_cancels_group?" do
data/spec/spec_helper.rb CHANGED
@@ -13,6 +13,11 @@ end
13
13
 
14
14
  require 'rspec/its'
15
15
  require 'database_cleaner'
16
+ require 'delayed_job'
17
+
18
+ Delayed::Worker.read_ahead = 1
19
+ Delayed::Worker.destroy_failed_jobs = false
20
+
16
21
  require 'delayed_job_groups_plugin'
17
22
  require 'factory_bot'
18
23
  require 'yaml'
@@ -23,9 +28,6 @@ Dir["#{spec_dir}/support/**/*.rb"].sort.each { |f| require f }
23
28
 
24
29
  FileUtils.makedirs('log')
25
30
 
26
- Delayed::Worker.read_ahead = 1
27
- Delayed::Worker.destroy_failed_jobs = false
28
-
29
31
  Delayed::Worker.logger = Logger.new('log/test.log')
30
32
  Delayed::Worker.logger.level = Logger::DEBUG
31
33
  ActiveRecord::Base.logger = Delayed::Worker.logger
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: delayed_job_groups_plugin
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.14.0
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Joel Turkel
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2025-09-25 00:00:00.000000000 Z
12
+ date: 2025-10-08 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: activerecord
@@ -269,6 +269,7 @@ files:
269
269
  - gemfiles/rails_8.0.gemfile
270
270
  - lib/delayed/job_groups/compatibility.rb
271
271
  - lib/delayed/job_groups/complete_stuck_job_groups_job.rb
272
+ - lib/delayed/job_groups/errors.rb
272
273
  - lib/delayed/job_groups/job_extensions.rb
273
274
  - lib/delayed/job_groups/job_group.rb
274
275
  - lib/delayed/job_groups/plugin.rb