From 36c63db39afbe9c4738df2d4eddc75e4624be4ae Mon Sep 17 00:00:00 2001 From: will89 Date: Mon, 22 Feb 2021 09:55:43 -0500 Subject: [PATCH] Add ruby 3 to test matrix and drop ruby < 2.6 support (#21) --- .circleci/config.yml | 31 +++-- .rubocop.yml | 4 +- CHANGELOG.md | 3 + delayed_job_groups.gemspec | 10 +- lib/delayed/job_groups/compatibility.rb | 4 - lib/delayed/job_groups/job_extensions.rb | 4 - lib/delayed/job_groups/job_group.rb | 9 +- lib/delayed/job_groups/plugin.rb | 4 +- lib/delayed/job_groups/version.rb | 2 +- lib/delayed/job_groups/yaml_loader.rb | 2 + spec/delayed/job_groups/job_group_spec.rb | 4 + spec/delayed/job_groups/plugin_spec.rb | 119 ++++++++------------ spec/delayed/job_groups/yaml_loader_spec.rb | 10 +- spec/support/test_jobs.rb | 33 ++++++ 14 files changed, 122 insertions(+), 117 deletions(-) create mode 100644 spec/support/test_jobs.rb diff --git a/.circleci/config.yml b/.circleci/config.yml index 8d3ad75..afa47db 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -2,14 +2,14 @@ version: 2.1 jobs: lint: docker: - - image: salsify/ruby_ci:2.5.8 + - image: salsify/ruby_ci:2.6.6 working_directory: ~/delayed_job_groups steps: - checkout - restore_cache: keys: - - v1-gems-ruby-2.5.8-{{ checksum "delayed_job_groups.gemspec" }}-{{ checksum "Gemfile" }} - - v1-gems-ruby-2.5.8- + - v1-gems-ruby-2.6.6-{{ checksum "delayed_job_groups.gemspec" }}-{{ checksum "Gemfile" }} + - v1-gems-ruby-2.6.6- - run: name: Install Gems command: | @@ -18,7 +18,7 @@ jobs: bundle clean fi - save_cache: - key: v1-gems-ruby-2.5.8-{{ checksum "delayed_job_groups.gemspec" }}-{{ checksum "Gemfile" }} + key: v1-gems-ruby-2.6.6-{{ checksum "delayed_job_groups.gemspec" }}-{{ checksum "Gemfile" }} paths: - "vendor/bundle" - "gemfiles/vendor/bundle" @@ -29,8 +29,10 @@ jobs: parameters: gemfile: type: string + ruby_version: + type: string docker: - - image: salsify/ruby_ci:2.5.8 + - image: salsify/ruby_ci:<< parameters.ruby_version >> environment: CIRCLE_TEST_REPORTS: "test-results" BUNDLE_GEMFILE: << parameters.gemfile >> @@ -39,8 +41,8 @@ jobs: - checkout - restore_cache: keys: - - v1-gems-ruby-2.5.8-{{ checksum "delayed_job_groups.gemspec" }}-{{ checksum "<< parameters.gemfile >>" }} - - v1-gems-ruby-2.5.8- + - v1-gems-ruby-<< parameters.ruby_version >>-{{ checksum "delayed_job_groups.gemspec" }}-{{ checksum "<< parameters.gemfile >>" }} + - v1-gems-ruby-<< parameters.ruby_version >>- - run: name: Install Gems command: | @@ -49,7 +51,7 @@ jobs: bundle clean fi - save_cache: - key: v1-gems-ruby-2.5.8-{{ checksum "delayed_job_groups.gemspec" }}-{{ checksum "<< parameters.gemfile >>" }} + key: v1-gems-ruby-<< parameters.ruby_version >>-{{ checksum "delayed_job_groups.gemspec" }}-{{ checksum "<< parameters.gemfile >>" }} paths: - "vendor/bundle" - "gemfiles/vendor/bundle" @@ -67,6 +69,13 @@ workflows: matrix: parameters: gemfile: - - "gemfiles/rails_5.2.gemfile" - - "gemfiles/rails_6.0.gemfile" - - "gemfiles/rails_6.1.gemfile" + - "gemfiles/rails_5.2.gemfile" + - "gemfiles/rails_6.0.gemfile" + - "gemfiles/rails_6.1.gemfile" + ruby_version: + - "2.6.6" + - "2.7.2" + - "3.0.0" + exclude: + - gemfile: "gemfiles/rails_5.2.gemfile" + ruby_version: "3.0.0" diff --git a/.rubocop.yml b/.rubocop.yml index d53c8e8..a1a9f86 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -2,10 +2,10 @@ inherit_gem: salsify_rubocop: conf/rubocop.yml AllCops: - TargetRubyVersion: 2.5 + TargetRubyVersion: 2.6 Exclude: - 'vendor/**/*' - - 'gemfiles/vendor/**/*' + - 'gemfiles/**/*' Style/FrozenStringLiteralComment: Enabled: true diff --git a/CHANGELOG.md b/CHANGELOG.md index 732e470..2fd1e42 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,7 @@ # Changelog +### 0.7.0 +* Add support for ruby 3 +* Drop support for ruby < 2.6 ### 0.6.2 * Defer including extension until delayed_job_active_record is loaded diff --git a/delayed_job_groups.gemspec b/delayed_job_groups.gemspec index 806b938..b168a25 100644 --- a/delayed_job_groups.gemspec +++ b/delayed_job_groups.gemspec @@ -1,7 +1,6 @@ - # frozen_string_literal: true -lib = File.expand_path('../lib', __FILE__) +lib = File.expand_path('lib', __dir__) $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) require 'delayed/job_groups/version' @@ -19,12 +18,13 @@ Gem::Specification.new do |spec| spec.test_files = Dir.glob('spec/**/*') spec.require_paths = ['lib'] - spec.required_ruby_version = '>= 2.5' + spec.required_ruby_version = '>= 2.6' spec.add_dependency 'delayed_job', '>= 4.1' spec.add_dependency 'delayed_job_active_record', '>= 4.1' - spec.post_install_message = 'See https://github.com/salsify/delayed_job_groups_plugin#installation for upgrade/installation notes.' + spec.post_install_message = 'See https://github.com/salsify/delayed_job_groups_plugin#installation '\ + 'for upgrade/installation notes.' spec.add_development_dependency 'appraisal' spec.add_dependency 'activerecord', '>= 5.2', '< 7' @@ -35,7 +35,7 @@ Gem::Specification.new do |spec| spec.add_development_dependency 'rspec', '~> 3' spec.add_development_dependency 'rspec-its' spec.add_development_dependency 'rspec_junit_formatter' - spec.add_development_dependency 'salsify_rubocop', '0.52.1.1' + spec.add_development_dependency 'salsify_rubocop', '~> 1.0.1' spec.add_development_dependency 'simplecov' spec.add_development_dependency 'sqlite3' spec.add_development_dependency 'timecop' diff --git a/lib/delayed/job_groups/compatibility.rb b/lib/delayed/job_groups/compatibility.rb index cd13450..10f2f77 100644 --- a/lib/delayed/job_groups/compatibility.rb +++ b/lib/delayed/job_groups/compatibility.rb @@ -7,10 +7,6 @@ module Delayed module JobGroups module Compatibility - def self.mass_assignment_security_enabled? - defined?(::ActiveRecord::MassAssignmentSecurity) - end - end end end diff --git a/lib/delayed/job_groups/job_extensions.rb b/lib/delayed/job_groups/job_extensions.rb index 739524d..b55fe3a 100644 --- a/lib/delayed/job_groups/job_extensions.rb +++ b/lib/delayed/job_groups/job_extensions.rb @@ -11,10 +11,6 @@ def ready_to_run(worker_name, max_run_time) end included do - if Delayed::JobGroups::Compatibility.mass_assignment_security_enabled? - attr_accessible :job_group_id, :blocked - end - belongs_to :job_group, class_name: 'Delayed::JobGroups::JobGroup', required: false class << self diff --git a/lib/delayed/job_groups/job_group.rb b/lib/delayed/job_groups/job_group.rb index 08c0a19..d1bdb1c 100644 --- a/lib/delayed/job_groups/job_group.rb +++ b/lib/delayed/job_groups/job_group.rb @@ -8,11 +8,6 @@ class JobGroup < ActiveRecord::Base self.table_name = "#{ActiveRecord::Base.table_name_prefix}delayed_job_groups" - if Delayed::JobGroups::Compatibility.mass_assignment_security_enabled? - attr_accessible :on_completion_job, :on_completion_job_options, :blocked, :on_cancellation_job, - :on_cancellation_job_options, :failure_cancels_group - end - serialize :on_completion_job, Delayed::JobGroups::YamlLoader serialize :on_completion_job_options, Hash serialize :on_cancellation_job, Delayed::JobGroups::YamlLoader @@ -30,6 +25,7 @@ class JobGroup < ActiveRecord::Base def mark_queueing_complete with_lock do raise 'JobGroup has already completed queueing' if queueing_complete? + update_column(:queueing_complete, true) complete if ready_for_completion? end @@ -66,13 +62,14 @@ def self.check_for_completion(job_group_id) # zero will queue the job group's completion job and destroy the job group so # other jobs need to handle the job group having been destroyed already. job_group = where(id: job_group_id).lock(true).first - job_group.send(:complete) if job_group && job_group.send(:ready_for_completion?) + job_group.send(:complete) if job_group&.send(:ready_for_completion?) end end def self.has_pending_jobs?(job_group_ids) # rubocop:disable Naming/PredicateName job_group_ids = Array(job_group_ids) return false if job_group_ids.empty? + Delayed::Job.where(job_group_id: job_group_ids, failed_at: nil).exists? end diff --git a/lib/delayed/job_groups/plugin.rb b/lib/delayed/job_groups/plugin.rb index 1376d63..99495bc 100644 --- a/lib/delayed/job_groups/plugin.rb +++ b/lib/delayed/job_groups/plugin.rb @@ -21,9 +21,7 @@ def job.max_attempts # If a job in the job group fails, then cancel the whole job group. # Need to check that the job group is present since another # job may have concurrently cancelled it. - if job.in_job_group? && job.job_group && job.job_group.failure_cancels_group? - job.job_group.cancel - end + job.job_group.cancel if job.in_job_group? && job.job_group&.failure_cancels_group? end lifecycle.after(:perform) do |_worker, job| diff --git a/lib/delayed/job_groups/version.rb b/lib/delayed/job_groups/version.rb index 7f26640..f1ac4b7 100644 --- a/lib/delayed/job_groups/version.rb +++ b/lib/delayed/job_groups/version.rb @@ -2,6 +2,6 @@ module Delayed module JobGroups - VERSION = '0.6.2' + VERSION = '0.7.0' end end diff --git a/lib/delayed/job_groups/yaml_loader.rb b/lib/delayed/job_groups/yaml_loader.rb index 77fc57c..e46be59 100644 --- a/lib/delayed/job_groups/yaml_loader.rb +++ b/lib/delayed/job_groups/yaml_loader.rb @@ -5,11 +5,13 @@ module JobGroups module YamlLoader def self.load(yaml) return yaml unless yaml.is_a?(String) && /^---/.match(yaml) + YAML.load_dj(yaml) end def self.dump(object) return if object.nil? + YAML.dump(object) end end diff --git a/spec/delayed/job_groups/job_group_spec.rb b/spec/delayed/job_groups/job_group_spec.rb index 457324d..e142954 100644 --- a/spec/delayed/job_groups/job_group_spec.rb +++ b/spec/delayed/job_groups/job_group_spec.rb @@ -50,6 +50,7 @@ before { job_group.mark_queueing_complete } it { is_expected.to be_queueing_complete } + it_behaves_like "the job group was completed" end @@ -59,6 +60,7 @@ before { job_group.mark_queueing_complete } it { is_expected.to be_queueing_complete } + it_behaves_like "the job group was not completed" end @@ -69,6 +71,7 @@ end it { is_expected.to be_queueing_complete } + it_behaves_like "the job group was not completed" end end @@ -199,6 +202,7 @@ end its(:blocked?) { is_expected.to be(false) } + it_behaves_like "the job group was completed" end end diff --git a/spec/delayed/job_groups/plugin_spec.rb b/spec/delayed/job_groups/plugin_spec.rb index e4dedf4..6418ce7 100644 --- a/spec/delayed/job_groups/plugin_spec.rb +++ b/spec/delayed/job_groups/plugin_spec.rb @@ -5,56 +5,56 @@ @old_max_attempts = Delayed::Worker.max_attempts Delayed::Worker.max_attempts = 2 - CompletionJob.invoked = false - CancellationJob.invoked = false + TestJobs::CompletionJob.invoked = false + TestJobs::CancellationJob.invoked = false end after do Delayed::Worker.max_attempts = @old_max_attempts end - let!(:job_group) { Delayed::JobGroups::JobGroup.create!(on_completion_job: CompletionJob.new) } + let!(:job_group) { Delayed::JobGroups::JobGroup.create!(on_completion_job: TestJobs::CompletionJob.new) } it "runs the completion job after completing other jobs" do - job_group.enqueue(NoOpJob.new) - job_group.enqueue(NoOpJob.new) + job_group.enqueue(TestJobs::NoOpJob.new) + job_group.enqueue(TestJobs::NoOpJob.new) job_group.mark_queueing_complete expect(job_group_count).to eq 1 expect(queued_job_count).to eq 2 # Run our first job Delayed::Worker.new.work_off(1) - expect(CompletionJob.invoked).to be(false) + expect(TestJobs::CompletionJob.invoked).to be(false) expect(job_group_count).to eq 1 expect(queued_job_count).to eq 1 # Run our second job which should enqueue the completion job Delayed::Worker.new.work_off(1) - expect(CompletionJob.invoked).to be(false) + expect(TestJobs::CompletionJob.invoked).to be(false) expect(job_group_count).to eq 0 expect(queued_job_count).to eq 1 # Now we should run the completion job Delayed::Worker.new.work_off(1) - expect(CompletionJob.invoked).to be(true) + expect(TestJobs::CompletionJob.invoked).to be(true) expect(queued_job_count).to eq 0 end it "only runs the completion job after queueing is completed" do - job_group.enqueue(NoOpJob.new) - job_group.enqueue(NoOpJob.new) + job_group.enqueue(TestJobs::NoOpJob.new) + job_group.enqueue(TestJobs::NoOpJob.new) expect(job_group_count).to eq 1 expect(queued_job_count).to eq 2 # Run our first job Delayed::Worker.new.work_off(1) - expect(CompletionJob.invoked).to be(false) + expect(TestJobs::CompletionJob.invoked).to be(false) expect(job_group_count).to eq 1 expect(queued_job_count).to eq 1 # Run our second job Delayed::Worker.new.work_off(1) - expect(CompletionJob.invoked).to be(false) + expect(TestJobs::CompletionJob.invoked).to be(false) expect(job_group_count).to eq 1 expect(queued_job_count).to eq 0 @@ -65,7 +65,7 @@ # Now we should run the completion job Delayed::Worker.new.work_off(1) - expect(CompletionJob.invoked).to be(true) + expect(TestJobs::CompletionJob.invoked).to be(true) expect(queued_job_count).to eq 0 end @@ -75,8 +75,8 @@ it "cancels the group" do Delayed::Worker.max_attempts = 1 - job_group.enqueue(FailingJob.new) - job_group.enqueue(NoOpJob.new) + job_group.enqueue(TestJobs::FailingJob.new) + job_group.enqueue(TestJobs::NoOpJob.new) job_group.mark_queueing_complete expect(queued_job_count).to eq 2 expect(job_group_count).to eq 1 @@ -84,7 +84,7 @@ # Run the job which should fail and cancel the JobGroup Delayed::Worker.new.work_off(1) # Completion job is not invoked - expect(CompletionJob.invoked).to be(false) + expect(TestJobs::CompletionJob.invoked).to be(false) expect(failed_job_count).to eq 1 expect(queued_job_count).to eq 0 expect(job_group_count).to eq 0 @@ -98,15 +98,15 @@ it "does not cancel the group" do Delayed::Worker.max_attempts = 1 - job_group.enqueue(FailingJob.new) - job_group.enqueue(NoOpJob.new) + job_group.enqueue(TestJobs::FailingJob.new) + job_group.enqueue(TestJobs::NoOpJob.new) job_group.mark_queueing_complete expect(queued_job_count).to eq 2 expect(job_group_count).to eq 1 # Run the job which should fail don't cancel the JobGroup Delayed::Worker.new.work_off(1) - expect(CancellationJob.invoked).to be(false) + expect(TestJobs::CancellationJob.invoked).to be(false) expect(failed_job_count).to eq 1 expect(queued_job_count).to eq 1 expect(job_group_count).to eq 1 @@ -120,7 +120,7 @@ # Run the completion job Delayed::Worker.new.work_off(1) # Completion job is invoked - expect(CompletionJob.invoked).to be(true) + expect(TestJobs::CompletionJob.invoked).to be(true) expect(failed_job_count).to eq 1 expect(queued_job_count).to eq 0 expect(job_group_count).to eq 0 @@ -129,8 +129,8 @@ it "runs completion job if last job failed" do Delayed::Worker.max_attempts = 2 - job_group.enqueue(NoOpJob.new) - job_group.enqueue(FailingJob.new) + job_group.enqueue(TestJobs::NoOpJob.new) + job_group.enqueue(TestJobs::FailingJob.new) job_group.mark_queueing_complete expect(queued_job_count).to eq 2 expect(job_group_count).to eq 1 @@ -144,7 +144,7 @@ # Run the job which should error Delayed::Worker.new.work_off(1) # Completion job is not invoked - expect(CompletionJob.invoked).to be(false) + expect(TestJobs::CompletionJob.invoked).to be(false) expect(failed_job_count).to eq 0 expect(queued_job_count).to eq 1 expect(job_group_count).to eq 1 @@ -159,7 +159,7 @@ # Run the completion job Delayed::Worker.new.work_off(1) # Completion job is invoked - expect(CompletionJob.invoked).to be(true) + expect(TestJobs::CompletionJob.invoked).to be(true) expect(failed_job_count).to eq 1 expect(queued_job_count).to eq 0 expect(job_group_count).to eq 0 @@ -169,7 +169,7 @@ it "doesn't retry failed jobs if the job group has been canceled" do job_group.cancel - Delayed::Job.enqueue(FailingJob.new, job_group_id: job_group.id) + Delayed::Job.enqueue(TestJobs::FailingJob.new, job_group_id: job_group.id) expect(queued_job_count).to eq 1 # Run the job which should fail and should not queue a retry @@ -182,8 +182,8 @@ job_group.blocked = true job_group.save! - job_group.enqueue(NoOpJob.new) - job_group.enqueue(NoOpJob.new) + job_group.enqueue(TestJobs::NoOpJob.new) + job_group.enqueue(TestJobs::NoOpJob.new) job_group.mark_queueing_complete expect(Delayed::Job.count).to eq 2 @@ -197,41 +197,41 @@ # Run our first job Delayed::Worker.new.work_off(1) - expect(CompletionJob.invoked).to be(false) + expect(TestJobs::CompletionJob.invoked).to be(false) expect(job_group_count).to eq 1 expect(Delayed::Job.count).to eq 1 # Run our second job which should enqueue the completion job Delayed::Worker.new.work_off(1) - expect(CompletionJob.invoked).to be(false) + expect(TestJobs::CompletionJob.invoked).to be(false) expect(job_group_count).to eq 0 expect(Delayed::Job.count).to eq 1 # Now we should run the completion job Delayed::Worker.new.work_off(1) - expect(CompletionJob.invoked).to be(true) + expect(TestJobs::CompletionJob.invoked).to be(true) expect(Delayed::Job.count).to eq 0 end context "when a cancellation job is provided" do let!(:job_group) do - Delayed::JobGroups::JobGroup.create!(on_completion_job: CompletionJob.new, - on_cancellation_job: CancellationJob.new) + Delayed::JobGroups::JobGroup.create!(on_completion_job: TestJobs::CompletionJob.new, + on_cancellation_job: TestJobs::CancellationJob.new) end it "runs the cancellation job after a job error causes cancellation" do Delayed::Worker.max_attempts = 1 - job_group.enqueue(FailingJob.new) - job_group.enqueue(NoOpJob.new) + job_group.enqueue(TestJobs::FailingJob.new) + job_group.enqueue(TestJobs::NoOpJob.new) job_group.mark_queueing_complete expect(queued_job_count).to eq 2 expect(job_group_count).to eq 1 # Run the job which should fail and cancel the JobGroup Delayed::Worker.new.work_off(1) - expect(CompletionJob.invoked).to be(false) - expect(CancellationJob.invoked).to be(false) + expect(TestJobs::CompletionJob.invoked).to be(false) + expect(TestJobs::CancellationJob.invoked).to be(false) expect(failed_job_count).to eq 1 expect(queued_job_count).to eq 1 @@ -239,24 +239,24 @@ # Now we should run the cancellation job Delayed::Worker.new.work_off(1) - expect(CompletionJob.invoked).to be(false) - expect(CancellationJob.invoked).to be(true) + expect(TestJobs::CompletionJob.invoked).to be(false) + expect(TestJobs::CancellationJob.invoked).to be(true) expect(queued_job_count).to eq 0 end it "runs the cancellation job after the job group is cancelled" do - job_group.enqueue(NoOpJob.new) - job_group.enqueue(FailingJob.new) + job_group.enqueue(TestJobs::NoOpJob.new) + job_group.enqueue(TestJobs::FailingJob.new) job_group.mark_queueing_complete job_group.cancel # cancellation job should be queued expect(queued_job_count).to eq 1 - expect(CancellationJob.invoked).to be(false) + expect(TestJobs::CancellationJob.invoked).to be(false) # Run the cancellation job Delayed::Worker.new.work_off(1) - expect(CancellationJob.invoked).to be(true) + expect(TestJobs::CancellationJob.invoked).to be(true) expect(queued_job_count).to eq 0 end end @@ -265,8 +265,8 @@ let!(:job_group) { Delayed::JobGroups::JobGroup.create! } it "doesn't queue a non-existent completion job" do - job_group.enqueue(NoOpJob.new) - job_group.enqueue(NoOpJob.new) + job_group.enqueue(TestJobs::NoOpJob.new) + job_group.enqueue(TestJobs::NoOpJob.new) job_group.mark_queueing_complete expect(job_group_count).to eq 1 expect(queued_job_count).to eq 2 @@ -286,37 +286,6 @@ end end - class FailingJob - - def perform - raise 'Test failure' - end - - end - - class NoOpJob - - def perform - - end - end - - class CompletionJob - cattr_accessor :invoked - - def perform - CompletionJob.invoked = true - end - end - - class CancellationJob - cattr_accessor :invoked - - def perform - CancellationJob.invoked = true - end - end - def job_group_count Delayed::JobGroups::JobGroup.count end diff --git a/spec/delayed/job_groups/yaml_loader_spec.rb b/spec/delayed/job_groups/yaml_loader_spec.rb index 741120d..994b5c5 100644 --- a/spec/delayed/job_groups/yaml_loader_spec.rb +++ b/spec/delayed/job_groups/yaml_loader_spec.rb @@ -1,14 +1,12 @@ # frozen_string_literal: true describe Delayed::JobGroups::YamlLoader do - class Foo; end - describe "#load" do context "with a correct yaml object representation" do - let(:yaml) { '--- !ruby/object:Foo {}' } + let(:yaml) { '--- !ruby/object:TestJobs::Foo {}' } it "deserializes from YAML properly" do - expect(Delayed::JobGroups::YamlLoader.load(yaml)).to be_a(Foo) + expect(Delayed::JobGroups::YamlLoader.load(yaml)).to be_a(TestJobs::Foo) end end @@ -25,10 +23,10 @@ class Foo; end describe "#dump" do context "with an object" do - let(:object) { Foo.new } + let(:object) { TestJobs::Foo.new } it "serializes into YAML properly" do - expect(Delayed::JobGroups::YamlLoader.dump(object)).to eq("--- !ruby/object:Foo {}\n") + expect(Delayed::JobGroups::YamlLoader.dump(object)).to eq("--- !ruby/object:TestJobs::Foo {}\n") end end diff --git a/spec/support/test_jobs.rb b/spec/support/test_jobs.rb new file mode 100644 index 0000000..76ccf87 --- /dev/null +++ b/spec/support/test_jobs.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +module TestJobs + class Foo; end + + class FailingJob + def perform + raise 'Test failure' + end + end + + class NoOpJob + def perform + + end + end + + class CompletionJob + cattr_accessor :invoked + + def perform + CompletionJob.invoked = true + end + end + + class CancellationJob + cattr_accessor :invoked + + def perform + CancellationJob.invoked = true + end + end +end