Sidekiq::ReliableJob
A Sidekiq extension that provides reliable job delivery by staging jobs to the database before pushing to Redis. This ensures jobs are only enqueued when database transactions commit, and provides durability during Redis outages.
Features
- Transaction Safety: Jobs are staged to the database within your transaction. If the transaction rolls back, the job is never enqueued.
- Redis Outage Resilience: Jobs continue to be accepted during Redis outages and are pushed once Redis is available.
- Reliable Delivery: A background enqueuer process polls staged jobs and pushes them to Redis.
- Automatic Cleanup: Jobs are automatically deleted from the staging table after successful completion.
- ActiveJob Support: Works with both native Sidekiq jobs and ActiveJob.
Installation
Add this line to your application's Gemfile:
gem "sidekiq-reliable_job"
Then execute:
bundle install
Setup
1. Run the generator to create the migration
rails generate sidekiq_reliable_job:install
This creates a migration for the reliable_job_outbox table.
2. Run the migration
rails db:migrate
3. Configure Sidekiq
In your Sidekiq initializer (config/initializers/sidekiq.rb):
require "sidekiq/reliable_job"
Sidekiq::ReliableJob.configure do |config|
# The ActiveRecord base class for the Outbox model (default: "ActiveRecord::Base")
config.base_class = "ApplicationRecord"
# Enable reliable job for all jobs (default: false)
config.enable_for_all_jobs = false
# Preserve dead jobs in outbox with "dead" status instead of deleting (default: false)
config.preserve_dead_jobs = false
end
Sidekiq.configure_client do |config|
Sidekiq::ReliableJob.configure_client!(config)
end
Sidekiq.configure_server do |config|
Sidekiq::ReliableJob.configure_server!(config)
end
Usage
Option 1: Enable for all jobs (recommended)
When enable_for_all_jobs is true, all Sidekiq jobs are automatically staged through the outbox. No changes to job classes required.
To opt-out a specific job from staged push:
class DirectPushJob
include Sidekiq::Job
reliable_job: false
def perform
# This job will push directly to Redis
end
end
Option 2: Enable per job (opt-in)
If enable_for_all_jobs is false (default), use sidekiq_options to opt-in specific jobs:
class MyJob
include Sidekiq::Job
reliable_job: true
def perform(user_id)
# Your job logic here
end
end
ActiveJob Support
ReliableJob works with ActiveJob. Configure the job using sidekiq_options:
class MyActiveJob < ApplicationJob
queue_as :default
reliable_job: true
def perform(user_id)
# Your job logic here
end
end
Example
When you enqueue the job within a transaction, it will be staged to the database first:
ActiveRecord::Base.transaction do
user = User.create!(name: "Alice")
MyJob.perform_async(user.id) # Staged to database, not Redis
# If an exception is raised here, the job is never enqueued
end
# Transaction committed - job is now pushed to Redis by the enqueuer
How It Works
- Client Middleware: Intercepts
perform_asyncandperform_incalls and stages jobs to thereliable_job_outboxtable instead of pushing directly to Redis. - Outbox Processor: A background thread polls for pending jobs and pushes them to Redis:
- Server Middleware: After successful job completion, deletes the staged job record from the outbox.
- Death Handler: When a job exhausts all retries, removes (or optionally preserves) the record from the outbox.
Deployment & Rollout
When enabling ReliableJob for the first time, use a two-phase deployment to avoid orphaned outbox records:
Phase 1: Deploy with ReliableJob Disabled
First, deploy the gem with all jobs disabled. This installs the middleware on all containers without affecting any jobs:
Sidekiq::ReliableJob.configure do |config|
config.enable_for_all_jobs = false # No jobs use reliable delivery yet
end
Wait for all containers to be running with the new code.
Phase 2: Enable ReliableJob
Once all containers have the middleware installed, enable reliable delivery for your jobs:
Sidekiq::ReliableJob.configure do |config|
config.enable_for_all_jobs = true # Or enable per-job with sidekiq_options
end
Why This Matters
If containers are running different versions during deployment:
- New containers may stage jobs while old containers process them
- Old containers don't have the server middleware, so they won't delete completed jobs from the outbox
- This leaves orphaned "enqueued" records in the database
Configuration Options
| Option | Default | Description |
|---|---|---|
enable_for_all_jobs |
false |
When true, all jobs are staged through the outbox |
base_class |
"ActiveRecord::Base" |
The ActiveRecord base class for the Outbox model |
preserve_dead_jobs |
false |
When true, keeps dead jobs in outbox with "dead" status instead of deleting |
Limitations
Batch Jobs (Sidekiq Pro/Enterprise)
Jobs that are part of a batch (have a bid in their payload) are automatically bypassed and pushed directly to Redis. This ensures batch callbacks and completion tracking work correctly.
Internal Sidekiq Jobs
All internal Sidekiq jobs (classes starting with Sidekiq::) are automatically bypassed. This includes:
- Batch callbacks (
Sidekiq::Batch::Callback) - Batch empty handlers (
Sidekiq::Batch::Empty) - Enterprise periodic jobs (
Sidekiq::Periodic::*) - Any other internal Sidekiq system jobs
Development
After checking out the repo, run bin/setup to install dependencies. Then, run bundle exec rspec to run the tests.
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/wealthsimple/sidekiq-reliable_job.