One guard was never the whole job

Free preview 4 min to read

Two courses already pointed here by name. The mid-level course "Pattern: idempotency keys" built a reserve, work, complete guard around a whole operation, and closed by describing this one: "That course asks a harder question: how to design a job so it's safe to retry any number of times, including every point inside perform where it can fail partway." The mid-level course "Solid Queue: job architecture" traced what happens when a worker dies mid-job. The claim becomes a failed row, and nothing re-runs by itself. The retry, it said, is a decision for retry_on, for a manual retry, "or for a job written so that running twice is harmless".

Both assume you know what idempotency is and why at-least-once delivery makes it necessary. The mid-level course "Service objects: idempotency", earlier in this series, and the intern-level HTTP verb courses built that vocabulary. This course starts past it.

The simplest job in this app, and the guard it already has

The simplest job under app/jobs/ is Partners::FetchSiteIconJob:

module Partners
  class FetchSiteIconJob < ApplicationJob
    queue_as :default

    def perform(partner_id)
      partner = ::Partners::Partner.find_by(id: partner_id)
      ::Partners::FetchSiteIconService.call(partner: partner) if partner && !partner.logo.attached?
    end
  end
end

One line of work, guarded by !partner.logo.attached?. Run this job once, or run it five times because a retry or a manual re-run delivered it again. After the first successful run, every later run finds partner.logo.attached? true and does nothing. That single condition is the whole idempotency mechanism for this job, and it costs one call to ActiveStorage::Attached::One#attached?.

It isn't the only check. The enqueue site, Admin::Partners::SavePartnerService, tests the same thing before it calls perform_later:

::Partners::FetchSiteIconJob.perform_later(partner.id) unless partner.logo.attached?

Two checks of the same condition, at two different times. That isn't redundancy; they close two different races. The enqueue-site check stops a job from being queued at all when there's obviously nothing to do. The check inside perform is the one that matters for idempotency, because it runs again on every re-run. That may be minutes or hours after the enqueue-site check passed, and the world may have changed: an admin could have uploaded a logo by hand in the meantime. A guard checked once, at enqueue time, and trusted for the rest of the job's life doesn't protect against retries. Only the one inside perform does.

What this job doesn't have to solve

FetchSiteIconJob gets away with one if. That's not luck. The job does one thing, and that thing already has a natural yes-or-no state, attached?, on the record it's about to change.

Most of what makes idempotency feel hard shows up when a job's work stops being one thing. If perform did two things, say attach a logo and then tell someone it happened, a crash between them leaves the first done and the second undone. A naive retry then has to decide correctly what to redo and what to skip.

This app has a job like that. Partners::DeliverWebhookJob posts to a partner's endpoint and then records the success, and its own comment explains why a repeat is tolerable: the partner deduplicates on the certificate code. Lesson 6 comes back to that kind of safety. The rest of this course builds the multi-step case on FetchSiteIconJob, with one clearly hypothetical extra step.

Key takeaways

  • This course picks up where "Pattern: idempotency keys" and "Solid Queue: job architecture" said it would: not what idempotency means, but how to build it into a job's own perform, step by step.
  • Partners::FetchSiteIconJob is idempotent because of one condition, !partner.logo.attached?. It's checked once at enqueue time in SavePartnerService, and, more importantly, every time perform runs.
  • The enqueue-time check and the check inside perform close two different races. Only the one inside perform protects against a retry that happens after the world has changed.
  • A job that does one thing can often lean on a natural state check. A job that does more than one thing needs a separate answer for each, and that's the subject of the rest of this course.
Lesson 1 of 7 · free

Keep going with lesson 2

Three ways to make one step safe to redo

Covers checking state before acting, letting a unique index refuse duplicates, and writing absolute end states instead of relative changes.

A free account also gives you the quizzes, your progress and notes, and the discussion.

Type to start searching...