delayed_job_groups_plugin 0.12.0 → 0.13.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: 1614cbedffbd4c2f49972dce192e778aa05b371eb95418f9dc8d1960b0ad749f
4
- data.tar.gz: f25ddd68ba02ff554f4ee34c7e84e78f5fc8b41d7fb95dd602d87eb2caeffc69
3
+ metadata.gz: 9773f978178a522b2f7d150230d2aec54bdd5a3bd11b13a980e93ebd71a0bf8c
4
+ data.tar.gz: 9d7ae98668fc83a5d7aa516138470585dc5f711a52796c297f8c408969cb2903
5
5
  SHA512:
6
- metadata.gz: eca2537bb58924b5d701e3b80e655c5ce1b01f1267f959131691f789bc398c9e872df8dc9bec7675362bb73a4c6389b67ffc9336285efdd04e216bdf1e88a279
7
- data.tar.gz: ebfe107eed1975870c85884766d756ca0e790e4ea194d84013b7a0840c1f9306a6dd6081258f5086b0e31c2cabba1cd3d3d8c9973bf3695514f3b15d501f3171
6
+ metadata.gz: 964f5ea0c140c4d9122ed4602e2e059263f8e2ba71242aa741f079a195a8f7a39c42938380b8794f40b7ebb0b464c640ae1945c7a4174150d5299df9af381bd0
7
+ data.tar.gz: 98ff4ae8816c181e05aea9ad267d105e0d0397f3d02a50ef7fa3c61bfc9c53100d8e0a3a10aee298b512ae80a55f753f08e7407dafaabfaf6f5f40080428f600
data/Appraisals CHANGED
@@ -4,6 +4,7 @@ appraise 'rails-7.0' do
4
4
  gem 'activerecord', '~> 7.0.8'
5
5
  gem 'activesupport', '~> 7.0.8'
6
6
  gem 'sqlite3', '~> 1.7'
7
+ gem 'concurrent-ruby', '1.3.4'
7
8
  end
8
9
 
9
10
  appraise 'rails-7.1' do
data/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.13.0
4
+ - Moves `on_cancellation` logic from the before delayed job lifecycle hook to the after hook.
5
+ - Gem will now fail to load if `Delayed::Worker.destroy_failed_jobs` is set to true.
6
+ - Wrapped the job group cancel hook in a lock to prevent concurrently failing jobs from enqueueing
7
+ multiple on cancellation jobs.
8
+
3
9
  ## 0.12.0
4
10
  - Add support for Rails 8.0.
5
11
  - Drop support for Rails 6.1
@@ -5,5 +5,6 @@ source "https://rubygems.org"
5
5
  gem "activerecord", "~> 7.0.8"
6
6
  gem "activesupport", "~> 7.0.8"
7
7
  gem "sqlite3", "~> 1.7"
8
+ gem 'concurrent-ruby', '1.3.4'
8
9
 
9
10
  gemspec path: "../"
@@ -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,15 @@ 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
+ raise Delayed::JobGroups::IncompatibleWithDelayedJobError if Delayed::Worker.destroy_failed_jobs
15
+
7
16
  Delayed::Backend::ActiveRecord::Job.include(Delayed::JobGroups::JobExtensions)
8
17
  end
9
18
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Delayed
4
4
  module JobGroups
5
- VERSION = '0.12.0'
5
+ VERSION = '0.13.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
@@ -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.12.0
4
+ version: 0.13.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: 2024-11-12 00:00:00.000000000 Z
12
+ date: 2025-09-24 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
@@ -312,7 +313,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
312
313
  - !ruby/object:Gem::Version
313
314
  version: '0'
314
315
  requirements: []
315
- rubygems_version: 3.3.26
316
+ rubygems_version: 3.4.1
316
317
  signing_key:
317
318
  specification_version: 4
318
319
  summary: Delayed::Job job groups plugin