Async callbacks as a first class citizen

I’ve been exploring a version of this over in GitHub - kaspth/active_job-performs: ActiveJob::Performs adds the `performs` macro to set up jobs by convention.

In ActiveJob::Performs I’m making a convention of passing a model as the first argument to a job and calling an instance method on it. This is an internal pattern Active Storage and Action Mailbox that I’m hoisting to Rails apps in general.

So with my gem, your example can be rewritten as:

class Recording < ApplicationRecord
  after_create_commit :process_later

  def process
    # …
  end
  performs :process, wait: 5.seconds # Generates `process_later`
end

And this is the equivalent Ruby code that you’d write manually:

class Recording < ApplicationRecord
  class Job < ApplicationJob; end # For any shared configs, target with `performs wait: 5.seconds`
  class ProcessJob < Job
    def perform(recording) = recording.process
  end

  def process
    # …
  end
  
  def process_later
    ProcessJob.set(wait: 5.seconds).perform_later(self)
  end
end

Doing it this way generates a nicer conventional API with process_later that callbacks and other code can call. We also get a space to set up any needed configuration directly, where trying to do that with the async: true API would grow cumbersome.


performs also opens the door to other interesting app-wide patterns.

Say 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 = Invoice.first
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 I find having the option to be interesting!


There’s more examples and options in the README. I’m curious if this strikes a chord with your use-case and if the API makes sense?

1 Like