Shownotes
After this episode the viewer can move slow work out of the request into an Active Job, run it with Solid Queue in development, and know where jobs get lost.
Prerequisites: demo-shop on episode-003-before, Rails 8.1, episode 002 (form objects)
Not covered: tests, recurring jobs, Mission Control, production deployment
Source: github.com/webmatze/demo-shop (branches episode-003-before / episode-003-after)
Chapters
- 0:00 The problem
- 0:40 The solution at a glance
- 1:02 Setup
- 1:42 Building it
- 2:47 Refactoring
- 3:25 Gotchas
- 3:46 Things to remember
- 4:08 Wrap-up
Transcript
The problem
Since last episode every new product goes to an external catalog. We add a Ruby Sticker, three euros, category Stickers, and hit Create. And now we wait. Three seconds, every time, because the catalog API is slow and we call it inside the request. The log confirms it: the request completed in a little over three thousand milliseconds. The user waits, the server thread is blocked, and if the API is down the product is created but the sync silently fails. That is what we fix in this episode.
The solution at a glance
The tool is Active Job with Solid Queue. Active Job is the interface: a class with a perform method, and perform later puts it on a queue. Solid Queue is the backend that Rails eight ships: the queue lives in a database table, no Redis, no extra service. The guide has the details.
Setup
Solid Queue is already in the Gemfile, but a new Rails app only wires it up for production. In development jobs run with the async adapter, in memory. We give development a second database: primary stays as it is, and a queue database with its own migrations path. Then two lines in the development config, copied from production: the queue adapter is Solid Queue, and Solid Queue connects to the queue database. We run db prepare. It creates the queue database from the schema that Rails generated on day one. Two SQLite files now, and the app is ready to queue.
Building it
We generate a job called Catalog Sync. Rails writes the class and a test stub. The job is small. Perform takes the product and calls the same service as before. And because the catalog API can fail, we tell Active Job to retry on a catalog error, five attempts, waiting longer each time. In the controller we replace the direct call with perform later. The request now only writes a row to the queue table and returns. Same product, same form: Ruby Badge, category Stickers, Create. Instant. The redirect comes back right away, and the sync has not happened yet. The log now says forty milliseconds instead of three thousand. The job sits in the queue table, one row. Nothing runs it yet, because in development nobody started a worker. We start one for a few seconds with bin jobs. It picks up the job, sleeps its three seconds, and writes to the catalog log. The user never waited for any of it.
Refactoring
This works, but the controller still knows about the catalog. A product created from the console or an import would never be synced. We take the line out of the controller. And put it where the product is born: an after create commit callback on the model. Commit, not create: the job must not start before the row is really in the database. Every new product syncs, no matter who creates it. From the console: we create a product with no form at all. The job is queued anyway. We run the worker again and the catalog log shows the third line.
Gotchas
Two things to know. The adapter is per environment: a fresh app uses async in development, which keeps jobs in memory and loses them on restart. And Solid Queue only runs while a worker runs, so in production bin jobs is a process you have to run, or you let Puma start it with a plugin.
Things to remember
A few more things. Use after commit, never after create, or the job may run before the row exists. Retry on the errors you expect, and let the rest fail loudly. Async is fine for a demo, not for anything you want to keep. And a worker is a process: no worker, no jobs.
Wrap-up
That is Solid Queue: one job class, one callback, and the request is fast again. The code is on GitHub, branches before and after. Next episode we show the user what is happening in the background, live, with Turbo Streams.