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.
Articles liés
Le pattern outbox dans Rails : publier des événements sans perdre de données
L'écart entre l'enregistrement d'une donnée et la publication du message qui l'annonce est l'endroit où les événements disparaissent discrètement. Voici comment le combler.
Kafka ou Sidekiq ? Choisir le bon outil asynchrone pour votre backend Ruby on Rails
Un chemin de décision concret pour savoir quand une application Rails a réellement besoin d'un message broker, et quand Sidekiq couvre déjà tout ce dont vous avez besoin.