diff --git a/CHANGELOG.md b/CHANGELOG.md index 104fe84..1c5d7a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## [Unreleased](https://github.com/veeqo/activejob-uniqueness/compare/v0.2.1...HEAD) +### Added +- [#31](https://github.com/veeqo/activejob-uniqueness/pull/31) Add ability to set a custom runtime lock key for `:until_and_while_executing` strategy + ## [0.2.1](https://github.com/veeqo/activejob-uniqueness/compare/v0.2.0...v0.2.1) - 2021-08-24 ### Added diff --git a/README.md b/README.md index f445464..8456baf 100644 --- a/README.md +++ b/README.md @@ -116,6 +116,10 @@ class MyJob < ActiveJob::Base def lock_key 'qux' # completely custom lock key end + + def runtime_lock_key + 'quux' # completely custom runtime lock key for :until_and_while_executing + end end ``` diff --git a/lib/active_job/uniqueness/active_job_patch.rb b/lib/active_job/uniqueness/active_job_patch.rb index efb8769..fd39832 100644 --- a/lib/active_job/uniqueness/active_job_patch.rb +++ b/lib/active_job/uniqueness/active_job_patch.rb @@ -55,7 +55,7 @@ def unlock!(*arguments) end def lock_strategy - @lock_strategy ||= lock_strategy_class.new(**lock_options.merge(lock_key: lock_key, job: self)) + @lock_strategy ||= lock_strategy_class.new(job: self) end # Override in your job class if you want to customize arguments set for a digest. @@ -64,11 +64,11 @@ def lock_key_arguments end # Override lock_key method in your job class if you want to build completely custom lock key. - delegate :lock_key, to: :lock_key_generator + delegate :lock_key, :runtime_lock_key, to: :lock_key_generator def lock_key_generator - ActiveJob::Uniqueness::LockKey.new job_class_name: self.class.name, - arguments: lock_key_arguments + @lock_key_generator ||= ActiveJob::Uniqueness::LockKey.new job_class_name: self.class.name, + arguments: lock_key_arguments end end diff --git a/lib/active_job/uniqueness/lock_key.rb b/lib/active_job/uniqueness/lock_key.rb index c984c9b..efb5e1d 100644 --- a/lib/active_job/uniqueness/lock_key.rb +++ b/lib/active_job/uniqueness/lock_key.rb @@ -28,6 +28,14 @@ def lock_key ].join(':') end + # used only by :until_and_while_executing strategy + def runtime_lock_key + [ + lock_key, + 'runtime' + ].join(':') + end + def wildcard_key [ lock_prefix, diff --git a/lib/active_job/uniqueness/strategies/base.rb b/lib/active_job/uniqueness/strategies/base.rb index e0edabf..81bbee3 100644 --- a/lib/active_job/uniqueness/strategies/base.rb +++ b/lib/active_job/uniqueness/strategies/base.rb @@ -13,10 +13,10 @@ class Base attr_reader :lock_key, :lock_ttl, :on_conflict, :job - def initialize(lock_key:, lock_ttl: nil, on_conflict: nil, job: nil) - @lock_key = lock_key - @lock_ttl = (lock_ttl || config.lock_ttl).to_i * 1000 # ms - @on_conflict = on_conflict || config.on_conflict + def initialize(job:) + @lock_key = job.lock_key + @lock_ttl = (job.lock_options[:lock_ttl] || config.lock_ttl).to_i * 1000 # ms + @on_conflict = job.lock_options[:on_conflict] || config.on_conflict @job = job end diff --git a/lib/active_job/uniqueness/strategies/until_and_while_executing.rb b/lib/active_job/uniqueness/strategies/until_and_while_executing.rb index 1df7205..4d5e975 100644 --- a/lib/active_job/uniqueness/strategies/until_and_while_executing.rb +++ b/lib/active_job/uniqueness/strategies/until_and_while_executing.rb @@ -9,12 +9,16 @@ module Strategies class UntilAndWhileExecuting < Base include LockingOnEnqueue - attr_reader :runtime_lock_ttl, :on_runtime_conflict + attr_reader :runtime_lock_key, :runtime_lock_ttl, :on_runtime_conflict - def initialize(runtime_lock_ttl: nil, on_runtime_conflict: nil, **params) - super(**params) - @runtime_lock_ttl = runtime_lock_ttl.present? ? runtime_lock_ttl.to_i * 1000 : lock_ttl - @on_runtime_conflict = on_runtime_conflict || on_conflict + def initialize(job:) + super + @runtime_lock_key = job.runtime_lock_key + + runtime_lock_ttl_option = job.lock_options[:runtime_lock_ttl] + @runtime_lock_ttl = runtime_lock_ttl_option.present? ? runtime_lock_ttl_option.to_i * 1000 : lock_ttl + + @on_runtime_conflict = job.lock_options[:on_runtime_conflict] || on_conflict end def before_perform @@ -33,10 +37,6 @@ def around_perform(block) ensure unlock(resource: runtime_lock_key, event: :runtime_unlock) unless @job_aborted end - - def runtime_lock_key - [lock_key, 'runtime'].join(':') - end end end end diff --git a/spec/active_job/uniqueness/lock_key/runtime_lock_key_spec.rb b/spec/active_job/uniqueness/lock_key/runtime_lock_key_spec.rb new file mode 100644 index 0000000..bcadf01 --- /dev/null +++ b/spec/active_job/uniqueness/lock_key/runtime_lock_key_spec.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +describe ActiveJob::Uniqueness::LockKey, '#runtime_lock_key' do + subject { lock_key.runtime_lock_key } + + let(:lock_key) { described_class.new(job_class_name: job_class_name, arguments: arguments) } + let(:job_class_name) { 'FooBarJob' } + let(:arguments) { ['baz'] } + + context 'when default configuration is used' do + it { is_expected.to eq 'activejob_uniqueness:foo_bar_job:143654a5f0a059a178924baf9b815ea6:runtime' } + end + + context 'when job class has namespace' do + let(:job_class_name) { 'Foo::BarJob' } + + it { is_expected.to eq 'activejob_uniqueness:foo/bar_job:143654a5f0a059a178924baf9b815ea6:runtime' } + end + + context 'when custom lock_prefix is set' do + before { allow(ActiveJob::Uniqueness.config).to receive(:lock_prefix).and_return('custom') } + + it { is_expected.to eq 'custom:foo_bar_job:143654a5f0a059a178924baf9b815ea6:runtime' } + end + + context 'when custom digest_method is set' do + before { allow(ActiveJob::Uniqueness.config).to receive(:digest_method).and_return(OpenSSL::Digest::SHA1) } + + it { is_expected.to eq 'activejob_uniqueness:foo_bar_job:c8246148dacbed08f65913be488195317569f8dd:runtime' } + end + + context 'when nil arguments given' do + let(:arguments) { nil } + + it { is_expected.to eq 'activejob_uniqueness:foo_bar_job:no_arguments:runtime' } + end + + context 'when [] arguments given' do + let(:arguments) { [] } + + it { is_expected.to eq 'activejob_uniqueness:foo_bar_job:no_arguments:runtime' } + end +end diff --git a/spec/active_job/uniqueness/strategies/until_and_while_executing_spec.rb b/spec/active_job/uniqueness/strategies/until_and_while_executing_spec.rb index 5bb86e6..b75754b 100644 --- a/spec/active_job/uniqueness/strategies/until_and_while_executing_spec.rb +++ b/spec/active_job/uniqueness/strategies/until_and_while_executing_spec.rb @@ -194,4 +194,96 @@ def perform(number1, number2) end end end + + describe 'lock keys' do + let(:job) { job_class.new(2, 1) } + + describe 'on enqueuing' do + before { job.lock_strategy.before_enqueue } + + context 'when the job has no custom #lock_key defined' do + let(:job_class) do + stub_active_job_class do + unique :until_and_while_executing + + def perform(number1, number2) + number1 / number2 + end + end + end + + it 'locks the job with the default lock key' do + expect(locks.size).to eq 1 + expect(locks.first).to match(/\Aactivejob_uniqueness:my_job:[^:]+\z/) + end + end + + context 'when the job has a custom #lock_key defined' do + let(:job_class) do + stub_active_job_class do + unique :until_and_while_executing + + def perform(number1, number2) + number1 / number2 + end + + def lock_key + 'activejob_uniqueness:whatever' + end + end + end + + it 'locks the job with the custom runtime lock key' do + expect(locks.size).to eq 1 + expect(locks.first).to eq 'activejob_uniqueness:whatever' + end + end + end + + describe 'while executing' do + before { job.lock_strategy.before_perform } + + context 'when the job has no custom #runtime_lock_key defined' do + let(:job_class) do + stub_active_job_class do + unique :until_and_while_executing + + def perform(number1, number2) + number1 / number2 + end + end + end + + it 'locks the job with the default runtime lock key' do + job.lock_strategy.around_perform lambda { + expect(locks.size).to eq 1 + expect(locks.first).to match(/\Aactivejob_uniqueness:my_job:[^:]+:runtime\z/) + } + end + end + + context 'when the job has a custom #runtime_lock_key defined' do + let(:job_class) do + stub_active_job_class do + unique :until_and_while_executing + + def perform(number1, number2) + number1 / number2 + end + + def runtime_lock_key + 'activejob_uniqueness:whatever' + end + end + end + + it 'locks the job with the custom runtime lock key' do + job.lock_strategy.around_perform lambda { + expect(locks.size).to eq 1 + expect(locks.first).to eq 'activejob_uniqueness:whatever' + } + end + end + end + end end diff --git a/spec/active_job/uniqueness/strategies/until_executed_spec.rb b/spec/active_job/uniqueness/strategies/until_executed_spec.rb index 2d67f66..f2c2b86 100644 --- a/spec/active_job/uniqueness/strategies/until_executed_spec.rb +++ b/spec/active_job/uniqueness/strategies/until_executed_spec.rb @@ -44,4 +44,48 @@ def perform(number1, number2) end end end + + describe 'lock key' do + let(:job) { job_class.new(2, 1) } + + before { job.lock_strategy.before_enqueue } + + context 'when the job has no custom #lock_key defined' do + let(:job_class) do + stub_active_job_class do + unique :until_executed + + def perform(number1, number2) + number1 / number2 + end + end + end + + it 'locks the job with the default lock key' do + expect(locks.size).to eq 1 + expect(locks.first).to match(/\Aactivejob_uniqueness:my_job:[^:]+\z/) + end + end + + context 'when the job has a custom #lock_key defined' do + let(:job_class) do + stub_active_job_class do + unique :until_executed + + def perform(number1, number2) + number1 / number2 + end + + def lock_key + 'activejob_uniqueness:whatever' + end + end + end + + it 'locks the job with the custom lock key' do + expect(locks.size).to eq 1 + expect(locks.first).to eq 'activejob_uniqueness:whatever' + end + end + end end diff --git a/spec/active_job/uniqueness/strategies/until_executing_spec.rb b/spec/active_job/uniqueness/strategies/until_executing_spec.rb index 9b9f7b0..d1e8c95 100644 --- a/spec/active_job/uniqueness/strategies/until_executing_spec.rb +++ b/spec/active_job/uniqueness/strategies/until_executing_spec.rb @@ -44,4 +44,48 @@ def perform(number1, number2) end end end + + describe 'lock key' do + let(:job) { job_class.new(2, 1) } + + before { job.lock_strategy.before_enqueue } + + context 'when the job has no custom #lock_key defined' do + let(:job_class) do + stub_active_job_class do + unique :until_executing + + def perform(number1, number2) + number1 / number2 + end + end + end + + it 'locks the job with the default lock key' do + expect(locks.size).to eq 1 + expect(locks.first).to match(/\Aactivejob_uniqueness:my_job:[^:]+\z/) + end + end + + context 'when the job has a custom #lock_key defined' do + let(:job_class) do + stub_active_job_class do + unique :until_executing + + def perform(number1, number2) + number1 / number2 + end + + def lock_key + 'activejob_uniqueness:whatever' + end + end + end + + it 'locks the job with the custom lock key' do + expect(locks.size).to eq 1 + expect(locks.first).to eq 'activejob_uniqueness:whatever' + end + end + end end diff --git a/spec/active_job/uniqueness/strategies/until_expired_spec.rb b/spec/active_job/uniqueness/strategies/until_expired_spec.rb index f5c598f..3ce2445 100644 --- a/spec/active_job/uniqueness/strategies/until_expired_spec.rb +++ b/spec/active_job/uniqueness/strategies/until_expired_spec.rb @@ -44,4 +44,48 @@ def perform(number1, number2) end end end + + describe 'lock key' do + let(:job) { job_class.new(2, 1) } + + before { job.lock_strategy.before_enqueue } + + context 'when the job has no custom #lock_key defined' do + let(:job_class) do + stub_active_job_class do + unique :until_expired + + def perform(number1, number2) + number1 / number2 + end + end + end + + it 'locks the job with the default lock key' do + expect(locks.size).to eq 1 + expect(locks.first).to match(/\Aactivejob_uniqueness:my_job:[^:]+\z/) + end + end + + context 'when the job has a custom #lock_key defined' do + let(:job_class) do + stub_active_job_class do + unique :until_expired + + def perform(number1, number2) + number1 / number2 + end + + def lock_key + 'activejob_uniqueness:whatever' + end + end + end + + it 'locks the job with the custom lock key' do + expect(locks.size).to eq 1 + expect(locks.first).to eq 'activejob_uniqueness:whatever' + end + end + end end diff --git a/spec/active_job/uniqueness/strategies/while_executing_spec.rb b/spec/active_job/uniqueness/strategies/while_executing_spec.rb index 8ed16a7..327479f 100644 --- a/spec/active_job/uniqueness/strategies/while_executing_spec.rb +++ b/spec/active_job/uniqueness/strategies/while_executing_spec.rb @@ -132,4 +132,52 @@ def perform(number1, number2) end end end + + describe 'lock key' do + let(:job) { job_class.new(2, 1) } + + before { job.lock_strategy.before_perform } + + context 'when the job has no custom #lock_key defined' do + let(:job_class) do + stub_active_job_class do + unique :while_executing + + def perform(number1, number2) + number1 / number2 + end + end + end + + it 'locks the job with the default lock key' do + job.lock_strategy.around_perform lambda { + expect(locks.size).to eq 1 + expect(locks.first).to match(/\Aactivejob_uniqueness:my_job:[^:]+\z/) + } + end + end + + context 'when the job has a custom #lock_key defined' do + let(:job_class) do + stub_active_job_class do + unique :while_executing + + def perform(number1, number2) + number1 / number2 + end + + def lock_key + 'activejob_uniqueness:whatever' + end + end + end + + it 'locks the job with the custom lock key' do + job.lock_strategy.around_perform lambda { + expect(locks.size).to eq 1 + expect(locks.first).to eq 'activejob_uniqueness:whatever' + } + end + end + end end