Background Jobs
🛠️ Background Jobs
🐢 The Problem With Doing Everything Inline
If the mail server is slow, or down, the user sitting on the other end of that create request waits for it too — or the request times out and the post never even gets its redirect, even though it saved successfully. The fix is the same shape as the previous three chapters: pull the risky, slow, or optional part out into its own well-defined unit, instead of leaving it tangled into the request cycle.
📮 Active Job — Rails' Abstraction Layer
Active Job is Rails' framework-provided abstraction for background work, the same role Active Record plays for databases: you write job classes against one consistent API, and a swappable adapter underneath actually handles running them — Sidekiq, Resque, Delayed Job, or Rails' built-in Async adapter for development. Switching adapters in production is a one-line config change, not a rewrite of every job.
perform(post_id) takes an integer, not the Post object itself. Jobs are serialized (to JSON, typically) and stored until a worker picks them up — by the time that happens, the in-memory Post object from the controller is long gone, and the record may have changed or even been deleted. Looking it up fresh inside perform guarantees the job always works with current data, and fails cleanly (ActiveRecord::RecordNotFound) if the record is gone rather than silently acting on stale data.
🚀 Enqueuing a Job
perform_later hands the job off to the queue and returns instantly — the controller action finishes and the user gets their redirect right away, while the actual email sends whenever a worker gets to it. perform_now (the same method every job class gets) runs the job's perform immediately and synchronously, useful for testing or for the rare case where you genuinely do need to wait for the result.
🔴 Setting Up Sidekiq
Sidekiq is the most widely used Active Job adapter in production Rails apps. It's backed by Redis rather than the primary database — jobs are pushed onto Redis-backed queues, and a separate sidekiq worker process (started independently of the Rails server itself, with bundle exec sidekiq) pulls jobs off those queues and runs them, using threads to process several jobs concurrently per process. The optional Sidekiq::Web mount gives you a live dashboard of queued, running, retrying, and failed jobs.
🔁 Retries and Failure
retry_on declares that a specific error is transient and worth trying again — a busy mail server probably isn't busy forever, so retrying with exponentially increasing delays gives it time to recover. discard_on is the opposite judgment call: if the post was deleted before the job ran, retrying RecordNotFound five more times can't fix anything, so the job gives up immediately rather than clogging the queue with retries that will never succeed.
🧪 Testing a Job
Chapter 3 built request and model specs; jobs get their own small addition on top of the same RSpec setup:
The first example checks that enqueuing happens at all, without actually running the job's code — fast, and enough to confirm the controller wires things up correctly. The second uses perform_now to run the job's logic directly and confirm its actual effect. Splitting "was it enqueued" from "does it work when it runs" mirrors the same model-spec-vs-request-spec split from Chapter 3: narrow tests for one piece of behavior, not one giant test trying to prove everything at once.
📜 Background Jobs, Side by Side
Active Job + Sidekiq (Rails) vs BullMQ (Node, node3-6)
| Concern | Node + BullMQ | Rails + Active Job/Sidekiq |
|---|---|---|
| Backing store | Redis | Redis (via Sidekiq specifically) |
| Enqueuing | queue.add("job-name", data) | SomeJob.perform_later(args) |
| Job definition | A worker function processing named jobs | One class per job, perform method |
| Retries | Configured per queue/job options | retry_on / discard_on, per error class |
| Swapping backends | Tied to BullMQ/Redis directly | Active Job is adapter-agnostic; jobs unchanged |
The underlying idea — a durable, Redis-backed queue and one or more worker processes pulling jobs off it — is the same one node3-6 already covered for message queues in general. Active Job's contribution is the same one Active Record makes for databases: one API on top, so the app's job classes don't have to know or care whether Sidekiq, Resque, or something else entirely is doing the actual work underneath.
💻 Coding Challenges
Challenge 1: A Basic Job
Write a ResizeImageJob that takes an image_id, looks up the corresponding Image record, and calls a (hypothetical) image.resize! method. Then write the one line a controller's create action would use to enqueue it after saving a new image.
Goal: Practice the perform_later pattern with an ID, not an object, passed in.
Challenge 2: Retry and Discard
Add retry_on for a hypothetical ImageProcessingTimeout error (retry up to 3 times, with exponentially increasing delays) and discard_on for ActiveRecord::RecordNotFound to the ResizeImageJob from Challenge 1.
Goal: Practice distinguishing a worth-retrying failure from a give-up-immediately failure.
Challenge 3: Testing the Job
Write two RSpec examples for ResizeImageJob: one confirming that calling perform_later enqueues the job with the correct image_id, and one confirming that perform_now actually calls resize! on the image.
Goal: Practice the enqueue-vs-actually-runs testing split from this chapter.
Background jobs trade immediacy for resilience: the user stops waiting, but they also stop knowing the outcome in real time. That tradeoff is right for a confirmation email, wrong for "did my payment go through" — anything the user needs to see the result of before the page finishes loading should usually stay inline, or use a job plus a way to poll/notify the user once it completes.
🎯 What's Next
Next chapter: APIs with Rails — API-only mode, JSON serialization, and how Rails' REST conventions carry over when the "view" is JSON instead of ERB, contrasted with Express's REST API design chapter.