Ruby on Rails is a powerful web framework, but sometimes you need to handle tasks that take too long to process during a typical web request. That’s where background jobs come in handy. They let you offload time-consuming work to be processed later, keeping your app responsive.
Active Job is Rails’ built-in framework for declaring and running background jobs. It provides a consistent interface for various queuing backends like Sidekiq and Delayed Job. This means you can switch between different job processors without changing your application code.
Let’s dive into how to use Active Job with Sidekiq, one of the most popular background job processors for Ruby. First, you’ll need to add Sidekiq to your Gemfile:
gem 'sidekiq'
Then, configure Active Job to use Sidekiq as its backend in config/application.rb:
config.active_job.queue_adapter = :sidekiq
Now you’re ready to create your first job. Rails provides a generator to create job classes:
rails generate job process_payment
This creates a new job file in app/jobs/process_payment_job.rb:
class ProcessPaymentJob < ApplicationJob
queue_as :default
def perform(order_id)
order = Order.find(order_id)
# Process payment logic here
puts "Processing payment for order #{order_id}"
end
end
The perform method is where the magic happens. It’s called when the job is executed and contains the logic for your background task. In this case, it’s processing a payment for an order.
To enqueue this job, you can call:
ProcessPaymentJob.perform_later(order.id)
This adds the job to the queue to be processed asynchronously. If you want to run the job immediately, you can use perform_now instead.
One cool thing about Active Job is that it handles serialization for you. You can pass ActiveRecord objects directly to your job, and Active Job will serialize them correctly:
ProcessPaymentJob.perform_later(order)
Sidekiq uses Redis as its backend, so make sure you have Redis installed and running. You’ll also need to start the Sidekiq worker process:
bundle exec sidekiq
Now, let’s talk about some advanced features. Active Job supports job priorities, allowing you to specify which jobs should be processed first:
class HighPriorityJob < ApplicationJob
queue_as :high_priority
def perform(task)
# Important task logic
end
end
You can also set up error handling for your jobs. It’s a good practice to rescue and log errors, and possibly retry the job:
class ProcessPaymentJob < ApplicationJob
retry_on NetworkError, wait: 5.seconds, attempts: 3
def perform(order_id)
# Payment processing logic
rescue SomeOtherError => e
ErrorNotifier.notify(e)
end
end
This setup will automatically retry the job up to 3 times with a 5-second delay between attempts if a NetworkError occurs. For other errors, it will notify an error tracking service.
Another useful feature is job callbacks. These allow you to run code before, after, or around job execution:
class ProcessPaymentJob < ApplicationJob
before_perform :log_job_start
after_perform :send_confirmation
around_perform :track_time
def perform(order_id)
# Payment processing logic
end
private
def log_job_start
Rails.logger.info "Starting to process payment for order #{order_id}"
end
def send_confirmation
# Send confirmation email
end
def track_time
start_time = Time.now
yield
end_time = Time.now
Rails.logger.info "Job took #{end_time - start_time} seconds"
end
end
These callbacks can be super handy for logging, tracking, or any other tasks you want to perform around your job execution.
Now, let’s talk about testing. Active Job provides test helpers that make it easy to test your background jobs. Here’s an example using RSpec:
RSpec.describe ProcessPaymentJob, type: :job do
describe "#perform_later" do
it "enqueues the job" do
expect {
ProcessPaymentJob.perform_later(1)
}.to have_enqueued_job(ProcessPaymentJob).with(1)
end
end
describe "#perform" do
it "processes the payment" do
order = create(:order)
expect(Order).to receive(:find).with(order.id).and_return(order)
expect(order).to receive(:process_payment)
ProcessPaymentJob.new.perform(order.id)
end
end
end
These tests ensure that your job is enqueued correctly and that it performs the expected actions when executed.
One thing to keep in mind when working with background jobs is idempotency. Your jobs should be designed to be safely re-run in case of failures. For example, instead of directly charging a credit card in your job, you might want to check if the payment has already been processed:
def perform(order_id)
order = Order.find(order_id)
return if order.paid?
charge_result = payment_gateway.charge(order.total, order.credit_card)
if charge_result.success?
order.update(paid: true)
else
raise PaymentError, charge_result.error_message
end
end
This way, if the job is retried due to a temporary failure, you won’t end up charging the customer twice.
Another advanced technique is job batching. While not natively supported by Active Job, you can implement it using Sidekiq Pro or by rolling your own solution. Job batching allows you to group related jobs together and track their progress as a unit.
Here’s a simple example of how you might implement basic job batching:
class BatchJob < ApplicationJob
def perform(batch_id, *args)
Batch.find(batch_id).increment!(:completed_jobs)
# Perform the actual job logic here
end
end
class Batch < ApplicationRecord
def enqueue_jobs(job_args)
job_args.each do |args|
BatchJob.perform_later(id, *args)
end
update(total_jobs: job_args.size)
end
def progress
completed_jobs.to_f / total_jobs
end
def complete?
completed_jobs == total_jobs
end
end
This allows you to create a batch of jobs and track their progress:
batch = Batch.create
batch.enqueue_jobs([[1], [2], [3]])
You can then check the batch’s progress or completion status:
puts batch.progress # Outputs a number between 0 and 1
puts batch.complete? # Outputs true or false
As your application grows, you might find yourself needing to scale your job processing. Sidekiq makes this easy - you can simply run multiple Sidekiq processes on different machines to increase your job processing capacity.
You might also want to consider using separate queues for different types of jobs. This allows you to prioritize certain jobs or dedicate workers to specific tasks:
class HighPriorityJob < ApplicationJob
queue_as :high_priority
end
class LowPriorityJob < ApplicationJob
queue_as :low_priority
end
Then you can start Sidekiq workers dedicated to specific queues:
bundle exec sidekiq -q high_priority -q default
bundle exec sidekiq -q low_priority
This setup ensures that your high priority jobs are processed quickly, while still allowing lower priority jobs to be handled.
Remember, while background jobs are powerful, they’re not a silver bullet. They introduce additional complexity to your application and can make debugging more challenging. Use them judiciously for tasks that truly need to be run asynchronously.
In my experience, background jobs have been a game-changer for handling complex, time-consuming tasks in web applications. I once worked on an e-commerce platform where we used background jobs for everything from processing orders to generating complex reports. It allowed us to keep the user interface snappy and responsive, even when dealing with large amounts of data or complex calculations.
One particularly tricky issue we faced was dealing with jobs that depended on external APIs. We found that wrapping these jobs in retries with exponential backoff was crucial for handling temporary network issues or API rate limits.
Another lesson learned the hard way: always make sure your background jobs are idempotent. We once had a job that sent welcome emails to new users, but didn’t properly check if the email had already been sent. During a particularly unlucky deployment, we ended up sending multiple welcome emails to some users. Needless to say, we quickly added checks to prevent this from happening again!
Background job processing is a vast topic, and there’s always more to learn. As you dive deeper, you might want to explore topics like job scheduling (running jobs at specific times), job priorities (ensuring critical jobs are processed first), and monitoring and observability (keeping an eye on your job queues and processing times).
Remember, the key to successfully using background jobs is to think carefully about what tasks truly need to be asynchronous, and to design your jobs to be robust and fault-tolerant. With Active Job and a solid queuing backend like Sidekiq, you’ll be well-equipped to handle complex, time-consuming tasks in your Rails applications. Happy coding!