BUILDING TO SCALE
FR | EN
Parler de votre projet

Idempotence par conception : rendre vos jobs Sidekiq sûrs à rejouer

Sidekiq garantit une livraison au moins une fois (at-least-once), pas exactement une fois. Tout job qui n'est pas sûr à exécuter deux fois est un bug qui attend le bon chevauchement.

Publié le 5 février 2023 · 4 min de lecture

Le contrat de Sidekiq est une livraison at-least-once, pas exactly-once. Un job peut s'exécuter deux fois : un worker expire après avoir terminé mais avant d'avoir acquitté, un déploiement redémarre un processus en plein job, Redis bascule sur un autre nœud. Tout job qui n'est pas sûr à exécuter deux fois est un bug qui attend que ce chevauchement se produise.

Le cas classique : le double débit

# Non idempotent : s'exécute deux fois, débite deux fois
class ChargeCardJob
  include Sidekiq::Job
  def perform(order_id)
    order = Order.find(order_id)
    PaymentGateway.charge(order.amount_cents, order.card_token)
    order.update!(charged: true)
  end
end

Si le processus meurt après que l'appel à la passerelle a réussi mais avant que la mise à jour ne soit commitée, Sidekiq rejoue le job, et la passerelle est appelée une seconde fois sans aucun moyen de savoir que l'opération a déjà eu lieu.

Une clé d'idempotence referme la brèche

La plupart des passerelles de paiement, et bien d'autres API externes, acceptent une clé d'idempotence : si vous envoyez deux fois la même clé, le second appel renvoie le résultat du premier au lieu de s'exécuter à nouveau. Générez cette clé à partir d'un élément stable du job, pas d'une valeur aléatoire, sinon un retry produira une nouvelle clé et annulera tout l'intérêt du mécanisme.

class ChargeCardJob
  include Sidekiq::Job
  def perform(order_id)
    order = Order.find(order_id)
    return if order.charged?

    PaymentGateway.charge(
      order.amount_cents,
      order.card_token,
      idempotency_key: "charge-order-#{order.id}"
    )
    order.update!(charged: true)
  end
end

Protégez aussi l'écart entre la vérification et l'action

Le retour anticipé sur une commande déjà débitée aide, mais deux exécutions simultanées du même job peuvent toutes deux passer cette vérification avant que l'une des deux ne mette à jour l'enregistrement. Une contrainte d'unicité en base, ou une transaction verrouillée englobant la lecture et la mise à jour du drapeau, referme cette fenêtre restante au lieu de s'en remettre au hasard du timing.

L'idempotence ne concerne pas que les paiements

Envoyer un e-mail deux fois est une petite gêne ; débiter une carte deux fois, c'est un ticket au support et un chargeback. Mais la même discipline, une clé propre à l'opération et une vérification d'une exécution précédente, s'applique à tout job qui a un effet de bord en dehors de votre propre base de données : e-mails, webhooks envoyés à un partenaire, appels à des API tierces.