Skip to content

Commit

Permalink
Use the job class as the default concurrency key if none is provided (#…
Browse files Browse the repository at this point in the history
…1145)

* Add a default concurrency key if not specified

* Address review feedback

* Make the default concurrency key easibly monkey-patchable
* Support a nil concurrency key callable

* Fix typo

* Use the class name as the default concurrency key

* Note it can be disabled with a nil key

---------

Co-authored-by: Ben Sheldon [he/him] <bensheldon@gmail.com>
  • Loading branch information
Earlopain and bensheldon committed Feb 1, 2024
1 parent 9dc526a commit ad3f00f
Show file tree
Hide file tree
Showing 3 changed files with 51 additions and 5 deletions.
18 changes: 14 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -511,12 +511,22 @@ class MyJob < ApplicationJob

# A unique key to be globally locked against.
# Can be String or Lambda/Proc that is invoked in the context of the job.
#
# If a key is not provided GoodJob will use the job class name.
#
# To disable concurrency control, for example in a subclass, set the
# key explicitly to nil (e.g. `key: nil` or `key: -> { nil }`)
#
# If you provide a custom concurrency key (for example, if one of your arguments
# is transient) make sure that it is sufficiently unique across jobs and queues
# by adding the job class or queue to the key yourself, if needed.
#
# Note: Arguments passed to #perform_later can be accessed through Active Job's `arguments` method
# which is an array containing positional arguments and, optionally, a kwarg hash.
key: -> { "MyJob-#{arguments.first}-#{arguments.last[:version]}" } # MyJob.perform_later("Alice", version: 'v2') => "MyJob-Alice-v2"
key: -> { "#{self.class.name}-#{queue_name}-#{arguments.first}-#{arguments.last[:version]}" } # MyJob.perform_later("Alice", version: 'v2') => "MyJob-default-Alice-v2"
)

def perform(first_name)
def perform(first_name, version:)
# do work
end
end
Expand All @@ -525,8 +535,8 @@ end
When testing, the resulting concurrency key value can be inspected:

```ruby
job = MyJob.perform_later("Alice")
job.good_job_concurrency_key #=> "MyJob-Alice"
job = MyJob.perform_later("Alice", version: 'v1')
job.good_job_concurrency_key #=> "MyJob-default-Alice-v1"
```

#### How concurrency controls work
Expand Down
8 changes: 8 additions & 0 deletions lib/good_job/active_job_extensions/concurrency.rb
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,8 @@ def good_job_concurrency_key
# Generates the concurrency key from the configuration
# @return [Object] concurrency key
def _good_job_concurrency_key
return _good_job_default_concurrency_key unless self.class.good_job_concurrency_config.key?(:key)

key = self.class.good_job_concurrency_config[:key]
return if key.blank?

Expand All @@ -105,6 +107,12 @@ def _good_job_concurrency_key
key
end

# Generates the default concurrency key when the configuration doesn't provide one
# @return [String] concurrency key
def _good_job_default_concurrency_key
self.class.name.to_s
end

private

def good_job_enqueue_concurrency_check(job, on_abort:, on_enqueue:)
Expand Down
30 changes: 29 additions & 1 deletion spec/lib/good_job/active_job_extensions/concurrency_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ def perform(name:)
end
end

describe 'when concurrency key is nil' do
describe 'when concurrency key returns nil' do
it 'does not limit concurrency' do
TestJob.good_job_control_concurrency_with(
total_limit: -> { 1 },
Expand All @@ -38,6 +38,18 @@ def perform(name:)
end
end

describe 'when concurrency key is nil' do
it 'does not limit concurrency' do
TestJob.good_job_control_concurrency_with(
total_limit: -> { 1 },
key: nil
)

expect(TestJob.perform_later(name: "Alice")).to be_present
expect(TestJob.perform_later(name: "Alice")).to be_present
end
end

describe '.good_job_control_concurrency_with' do
describe 'total_limit:', :skip_rails_5 do
before do
Expand Down Expand Up @@ -194,6 +206,22 @@ def perform
end
end

context 'when no key is specified' do
before do
stub_const 'TestJob', (Class.new(ActiveJob::Base) do
include GoodJob::ActiveJobExtensions::Concurrency

def perform(name)
end
end)
end

it 'uses the class name as the default concurrency key' do
job = TestJob.perform_later("Alice")
expect(job.good_job_concurrency_key).to eq('TestJob')
end
end

describe '#perform_later' do
before do
stub_const 'TestJob', (Class.new(ActiveJob::Base) do
Expand Down

0 comments on commit ad3f00f

Please sign in to comment.