ActiveJob::Performs
ActiveJob::Performs adds a performs class method to make the model + job loop vastly more conventional. You use it like this:
class Post < ApplicationRecord
performs :publish
# Or `performs def publish`!
def publish
# Some logic to publish a post
end
end
Here's what performs generates under the hood:
class Post < ApplicationRecord
class Job < ApplicationJob; end # We build a general Job class to share configuration between method jobs.
# Individual method jobs inherit from the `Post::Job` defined above.
class PublishJob < Job
# We generate the required `perform` method passing in the `post` and calling `publish` on it.
def perform(post, *, **) = post.publish(*, **)
end
# On Rails 7.1+, where `ActiveJob.perform_all_later` exists, we also generate
# a bulk method to enqueue many jobs at once. So you can do this:
#
# Post.unpublished.in_batches.each(&:publish_later_bulk)
def self.publish_later_bulk(set = all)
ActiveJob.perform_all_later set.map { PublishJob.new(_1) }
end
# We generate `publish_later` to wrap the job execution forwarding arguments and options.
def publish_later(*, **) = PublishJob.perform_later(self, *, **)
def publish
# Some logic to publish a post.
end
end
Benefits
- Conventional Jobs: they'll now mostly call instance methods like
publish_later->publish. - Follows Rails' internal conventions: this borrows from
ActionMailbox::InboundEmail#process_latercallingprocessandActionMailer::Base#deliver_latercallingdeliver. - Clarity & less guess work: the
_latermethods standardize how you call jobs throughout your app, so you can instantly tell what's happening. - Less tedium: getting an instance method run in the background is just now a
performscall with some potential configuration. - Fewer files to manage: you don't have to dig up something in
app/jobsjust to learn almost nothing from the boilerplate in there. - Remaining jobs stand out:
app/jobsis way lighter, so any jobs in there that don't fit theperformspattern now stand out way more. - More consolidated logic: sometimes Job classes house model-level logic, but now it's all the way out in
app/jobsinstead ofapp/models, huh?
[!TIP] On that last point,
performsdoes put more logic back within your Active Records, so if you need further encapsulation to prevent them growing too large, consider checking out active_record-associated_object.
Used in production & praise from people
The https://www.rubyevents.org team uses ActiveJob::Performs quite a bit:
Here's what @claudiob had to say after using ActiveJob::Performs:
I’ve been using active_job-performs for the last month and I love it love it love it!!
Your thought process behind it is so thorough. I have a bunch of jobs now attached to models and my app/jobs folder… is empty!!
This saves me a lot of mental hoops, I don’t have to switch between files anymore, everything is self-contained. Thank you!!!
From @andycroll in a writeup about launching UsingRails:
I’ve also adopted a couple of gems—with exceptional Rails-level taste and author pedigree—that I hadn’t used in anger before, including
active_job-performsfrom Kasper […]. Would recommend both.
And @nshki after trying it:
Spent some time playing with @kaspth's
ActiveRecord::AssociatedObjectandActiveJob::Performsand wow! The conventions these gems put in place help simplify a codebase drastically. I particularly loveActiveJob::Performs—it helped me refactor out allApplicationJobclasses I had and keep important context in the right domain model.
Usage
with ActiveRecord::Base & other GlobalID::Identification objects
ActiveJob::Performs works with any object that has include GlobalID::Identification and responds to that interface.
ActiveRecord::Base implements this, so here's how that looks:
class Post < ActiveRecord::Base
extend ActiveJob::Performs # We technically auto-extend ActiveRecord::Base, but other object hierarchies need this.
# `performs` builds a `Post::PublishJob` and routes configs over to it.
performs :publish, queue_adapter: :sidekiq, queue_as: :important, discard_on: SomeError do
retry_on TimeoutError, wait: :polynomially_longer
end
def publish
Here's what performs generates under the hood:
class Post < ActiveRecord::Base
# We setup a general Job class that's shared between method jobs.
class Job < ApplicationJob; end
# Individual method jobs inherit from the `Post::Job` defined above.
class PublishJob < Job
self.queue_adapter = :sidekiq
queue_as :important
discard_on SomeError
retry_on TimeoutError, wait: :polynomially_longer
# We generate `perform` passing in the `post` and calling `publish` on it.
def perform(post, *arguments, **)
post.publish(*arguments, **)
end
end
# On Rails 7.1, where `ActiveJob.perform_all_later` exists, we also generate
# a bulk method to enqueue many jobs at once. So you can do this:
#
# Post.unpublished.in_batches.each(&:publish_later_bulk)
#
# Or pass in a subset of posts as an argument:
#
# Post.publish_later_bulk Post.unpublished
def self.publish_later_bulk(set = all)
ActiveJob.perform_all_later set.map { PublishJob.new(_1) }
end
# We generate `publish_later` to wrap the job execution.
def publish_later(*arguments, **)
PublishJob.perform_later(self, *arguments, **)
end
def publish
[!NOTE] We prefer & call
{name}=setter methods, but fall back to getters. That's how we supportself.queue_adapter=, but alsoqueue_aswhich is not configured viaqueue_as=.
We generate the Post::Job class above to share configuration between method level jobs. E.g. if you had a retract method that was setup very similar, you could do:
class Post < ActiveRecord::Base
performs queue_as: :important
performs :publish
performs :retract
def publish
Which would then become:
class Post < ActiveRecord::Base
class Job < ApplicationJob
queue_as :important
end
class PublishJob < Job
Establishing patterns across your app
If there's an Active Record method that you'd like any model to be able to run from a background job, you can set them up in your ApplicationRecord:
class ApplicationRecord < ActiveRecord::Base
self.abstract_class = true
# We're passing specific queues for monitoring, but you may not need or want them.
performs :touch, queue_as: "active_record.touch"
performs :update, queue_as: "active_record.update"
performs :destroy, queue_as: "active_record.destroy"
end
Then a model could now run things like:
record.touch_later
record.touch_later :reminded_at, time: 5.minutes.from_now # Pass supported arguments to `touch`
record.update_later reminded_at: 1.year.ago
# Particularly handy to use on a record with many `dependent: :destroy` associations.
# Plus if anything fails, the transaction will rollback and the job fails, so you can retry it later!
record.destroy_later
You may not want this for touch and update, and maybe you'd rather architect your system in such a way that they don't have so many side-effects, but having the option can be handy!
Also, I haven't tested all the Active Record methods, so please file an issue if you encounter any.
Method suffixes
ActiveJob::Performs supports Ruby's stylistic method suffixes, i.e. ? and ! respectively.
class Post < ActiveRecord::Base
performs :publish! # Generates `publish_later!` which calls `publish!`.
performs :retract? # Generates `retract_later?` which calls `retract?`.
def publish!
Private methods
ActiveJob::Performs also works with private methods in case you only want to expose the generated _later method.
class Post < ActiveRecord::Base
performs :publish # Generates the public `publish_later` instance method.
# Private implementation, only call `publish_later` please!
private def publish
Additionally, in case the job is meant to be internal to the object, performs :some_method returns :some_method_later which you can pass to private.
E.g. private performs :some_method will generate a private some_method_later method.
Overriding the generated instance _later method
The instance level _later methods, like publish_later above, are generated into an included module. So in case you have a condition where you'd like to prevent the enqueue, you can override the method and call super:
class Post < ApplicationRecord
performs def publish
# …
end
def publish_later = some_condition? && super
end
Usage with ActiveRecord::AssociatedObject
The ActiveRecord::AssociatedObject gem also implements GlobalID::Identification, so you use performs exactly like you would on Active Records:
class Post::Publisher < ActiveRecord::AssociatedObject
extend ActiveJob::Performs # We technically auto-extend ActiveRecord::AssociatedObject, but other object hierarchies need this.
performs queue_as: :important
performs :publish
performs :retract
def publish
[!NOTE] There's one difference with Active Record: you must pass in a set to
_later_bulkmethods. Like so:
Post::Publisher.publish_later_bulk Post::Publisher.first(10)
Passing wait to performs
If there's a job you want to defer, performs can set it for each invocation:
class Post < ActiveRecord::Base
mattr_reader :config, default: Rails.application.config_for(:posts)
performs :social_media_boost, wait: config.
performs :social_media_boost, wait: 5.minutes # Alternatively, this works too.
# Additionally, a block can be passed to have access to the `post`:
performs :social_media_boost, wait: -> post { post. }
end
Now, social_media_boost_later can be called immediately, but automatically run after the grace period.
wait_until is also supported:
class Post < ActiveRecord::Base
performs :publish, wait_until: -> post { Date.tomorrow.noon if post.graceful? }
end
Installation
Install the gem and add to the application's Gemfile by executing:
$ bundle add active_job-performs
If bundler is not being used to manage dependencies, install the gem by executing:
$ gem install active_job-performs
Development
After checking out the repo, run bin/setup to install dependencies. Then, run rake test to run the tests. You can also run bin/console for an interactive prompt that will allow you to experiment.
To install this gem onto your local machine, run bundle exec rake install. To release a new version, update the version number in version.rb, and then run bundle exec rake release, which will create a git tag for the version, push git commits and the created tag, and push the .gem file to rubygems.org.
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/kaspth/active_job-performs.
License
The gem is available as open source under the terms of the MIT License.