Writing background jobs is a common task for any Ruby on Rails developer. I searched around for a post about best practices and couldn’t find one, so I decided to write one.
These are simple ideas that I stick to when writing jobs, some I got from asking around at the RoR slack, others were recommended by my tech lead during code review. I hope you find these useful.
Job should be small and do only one thing
class HutpotCreateTicketJob < ApplicationJob
queue_as :default
def perform(user_id:, hotel_id:, subject:, content:)
user = User.find(user_id)
hotel = Hotel.find(hotel_id)
Hutpot::CreateTicket.run!(
user: user,
hotel_id: hotel.id,
subject: subject,
content: content
)
end
end
It’s that simple. If this job fails you can simply ask it to retry and if it’s idempotent, you’ll have no problems.
Job should be idempotent
class HutpotCreateTicketJob < ApplicationJob
queue_as :default
def perform(user_id:, hotel_id:, subject:, content:)
user = User.find(user_id)
hotel = Hotel.find(hotel_id)
Hutpot::CreateTicket.update!(
user: user,
hotel_id: hotel.id,
subject: subject,
content: content
)
end
end
Idempotent(adj.) => “An idempotent function has an effect the first time it runs successfully and has no further effects on re-execution.”
update!
wraps its query inside a transaction. This means if something
goes wrong the update is rolled back.
This job is idempotent because on re-runs, after a successful run, it’ll not have any effect on the system.
Job should log various information
# frozen_string_literal: true
# Sets student benefit to active at the specified time.
class SetStudentBenefitToActiveJob < ApplicationJob
queue_as :default
def perform(student_benefit)
return if student_benefit.active?
unless student_benefit.enable_at.between?(Time.current.beginning_of_day,
Time.current.end_of_day)
return logger.info("#{self.class} exited because StudentBenefit##{student_benefit.id} /
enable_at has been changed to #{student_benefit.enable_at} from #{Time.current}")
end
student_benefit.update!(active: true, auto_enabled_at: Time.current, enable_at: nil)
end
end
I like to log various information from inside the job in case something goes wrong and I have to debug.
Also note the auto_enabled_at
column in student_benefit
and the job
updating it. This is also in case I need to debug something.
Two patterns for scheduling jobs
Job is scheduled via a parent scheduler
# frozen_string_literal: true
# Schedules jobs that set student benefits to active at the specifed time.
class ScheduleStudentBenefitToActiveJob < ApplicationJob
queue_as :default
def perform(*_args)
student_benefits = StudentBenefit.where(enable_at: Time.current.all_day, active: false)
student_benefits.each { SetStudentBenefitToActiveJob.set(wait_until: _1.enable_at).perform_later(_1) }
logger.info "#{self.class} processed #{student_benefits.size} seasons with ids: #{student_benefits.pluck :id}"
end
end
# Sets student benefit to active at the specified time.
class SetStudentBenefitToActiveJob < ApplicationJob
queue_as :default
def perform(student_benefit)
return if student_benefit.active?
unless student_benefit.enable_at.between?(Time.current.beginning_of_day,
Time.current.end_of_day)
return logger.info("#{self.class} exited because StudentBenefit##{student_benefit.id} /
enable_at has been changed to #{student_benefit.enable_at} from #{Time.current}")
end
student_benefit.update!(active: true, auto_enabled_at: Time.current, enable_at: nil)
end
end
# recurring.yml
activate_student_benefits:
class: ScheduleStudentBenefitToActiveJob
schedule: every day at 12am
In this pattern, a parent job (Schedule...
) is run everyday via a
cronjob. This job in turn schedules a Set...
job.
Notice that both jobs are small and do only one thing.
Job is scheduled via a callback
# student_benefit.rb
after_commit :schedule_student_benefit_to_active, on: %i[create update]
def schedule_student_benefit_to_active
return unless enabled_at.changed?
SetStudentBenefitToActiveJob.set(wait_until: self.enable_at).perform_later(self)
end
In this pattern, someone creates or updates a student_benefit
object.
If that happens we enqueue a SetStudentBenefitToActiveJob
to run at
the enable_at
time.
There are some differences between the two patterns. I prefer the second pattern because it means I don’t have to keep adding cron jobs for each new job I write.
These are just things I keep in mind when writing my jobs and till now they have served me well. I also came across this article in the sidekiq wiki, and was delightfully surprised to find that my points and the points listed there are very similar. If Sidekiq also recommends the same thing, I must be doing something right!
Hope you enjoyed this. Email me if you have any best practices you follow. I would love to know what you’re doing!