Dans le cadre du développement d'une plateforme d'e-commerce, il est crucial de gérer le cycle de vie des commandes, notamment pour libérer le stock réservé si un utilisateur ne finalise pas son paiement dans un délai imparti. L'utilisation de Celery permet de programmer des tâches asynchrones avec un délai d'exécution précis pour automatiser cette maintenance.
Planification de l'exécution différée
Pour déclencher une action après un laps de temps spécifique (par exemple, 15 minutes après la création de la commande), on utilise l'argumnet eta (Estimated Time of Arrival) de la méthode apply_async. Cela permet de définir précisément le moment où le worker Celery doit traiter la tâche.
from datetime import datetime, timedelta
from mon_projet.tasks import traiter_expiration_commande
def programmer_annulation(id_commande, delai_minutes=15):
# Calcul de l'heure d'exécution en format UTC
moment_execution = datetime.utcnow() + timedelta(minutes=delai_minutes)
# Envoi de la tâche avec le paramètre eta
traiter_expiration_commande.apply_async(
args=[id_commande],
eta=moment_execution
)
Logique de traitement et restauration du stock
La tâche asynchrone doit vérifier si la commande est toujours en attente de paiement avant de procéder à l'annulation. Une attention particulière doit être portée à la gestion de la concurrence : il faut s'assurer qu'un utilisateur ne puisse pas valider son paiement au moment précis où le système annule la commande.
L'exemple suivant illustre l'implémentation de la tâche avec une gestion de transaciton atomique et un mécanisme de verrouillage optimiste pour la mise à jour des stocks.
import os
import django
from celery import Celery
from django.db import transaction, models
# Initialisation de l'environnement Django pour le worker
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")
django.setup()
from boutique.models import Commande, Produit, ArticleCommande
app = Celery("tasks_commerce", broker="redis://localhost:6379/0")
@app.task
@transaction.atomic
def annuler_commande_expiree(id_commande):
# Récupération de la commande si elle est toujours impayée (statut_paiement=0)
commande = Commande.objects.filter(id=id_commande, statut_paiement=0).first()
if not commande:
# La commande est déjà payée ou traitée, on interrompt la tâche
return
# Initialisation d'un point de sauvegarde pour la transaction
point_sauvegarde = transaction.savepoint()
try:
articles = ArticleCommande.objects.filter(commande=commande)
for article in articles:
produit = article.produit
tentatives = 3
succes_mise_a_jour = False
# Mécanisme de verrouillage optimiste (Optimistic Locking)
for i in range(tentatives):
stock_actuel = produit.quantite_stock
nouveau_stock = stock_actuel + article.quantite
# Mise à jour conditionnelle basée sur la version du stock au moment de la lecture
maj = Produit.objects.filter(
id=produit.id,
quantite_stock=stock_actuel
).update(quantite_stock=nouveau_stock)
if maj:
succes_mise_a_jour = True
break
if not succes_mise_a_jour:
# Si la mise à jour échoue après plusieurs tentatives, on annule tout
transaction.savepoint_rollback(point_sauvegarde)
return
# Tentative finale d'annulation de la commande
# On vérifie à nouveau le statut_paiement pour éviter les conflits de dernière seconde
lignes_affectees = Commande.objects.filter(
id=id_commande,
statut_paiement=0
).update(statut="ANNULEE")
if lignes_affectees > 0:
transaction.savepoint_commit(point_sauvegarde)
else:
# Si aucune ligne n'est modifiée, le paiement a probablement été reçu entre-temps
transaction.savepoint_rollback(point_sauvegarde)
except Exception:
transaction.savepoint_rollback(point_sauvegarde)
Points clés de l'implémentation
- Atomicité : L'utilisation de
transaction.atomicgarantit que la restauration du stock et le changement de statut de la commande sont indissociables. - Verrouillage optimiste : Plutôt que de bloquer la base de données, nous vérifions que la valeur du stock n'a pas changé entre la lecture et l'écriture.
- Condition de concurrence (Race Condition) : La vérification du nombre de lignes affectées par
update()permet de confirmer que la commande n'a pas été payée au moment exact de l'exécution de la tâche.