Ressources

Limitation de concurrence

Protégez vos endpoints coûteux contre les surcharges avec le décorateur @frappe.concurrent_limit.

Limitation de concurrence

Certaines méthodes exposées via @frappe.whitelist() peuvent monopoliser un worker Gunicorn pendant toute leur durée d'exécution (génération de rapports complexes, requêtes analytiques lourdes, exports volumineux…). Une rafale de requêtes simultanées peut épuiser tous les workers disponibles et rendre le site complètement inaccessible.

Le décorateur @frappe.concurrent_limit() résout ce problème en limitant le nombre d'exécutions simultanées d'une méthode donnée.

Principe de fonctionnement

Techniquement, un pool de jetons est stocké dans une liste Redis. Chaque slot de concurrence autorisé correspond à un jeton dans cette liste :

  • Au démarrage d'une requête, un jeton est extrait de la liste (BLPOP).
  • À la fin de l'exécution (succès ou erreur), le jeton est restitué (LPUSH).
  • Si aucun jeton n'est disponible avant l'expiration du délai wait_timeout, la requête reçoit une réponse 503 avec un en-tête Retry-After.

Auto-guérison après crash

En cas de crash d'un worker (le jeton n'est jamais restitué), le pool se reconstruit automatiquement grâce à une TTL d'une heure sur la clé de capacité Redis. Le site se rétablit sans intervention manuelle.

Usage

Décoration d'une méthode exposée

import frappe

@frappe.whitelist()
@frappe.concurrent_limit(limit=3)
def endpoint_couteux(*args, **kwargs):
    # traitement long...
    pass
@frappe.concurrent_limit() doit toujours être placé après@frappe.whitelist() (c'est-à-dire plus proche de la définition de la fonction).

Paramètres

ParamètreTypeDéfautDescription
limitint | NoneAuto-détectéNombre maximal d'exécutions simultanées. Si None, calculé automatiquement : workers × threads ÷ 2, dérivé de la ligne de commande du master Gunicorn.
wait_timeoutint10Durée maximale d'attente en secondes avant de renvoyer un 503.

Exemple avec délai d'attente personnalisé

@frappe.whitelist()
@frappe.concurrent_limit(limit=5, wait_timeout=30)
def export_rapport_annuel(annee: int):
    """Export pouvant prendre jusqu'à 20 secondes — on tolère une attente de 30 s."""
    ...

Comportement hors requête HTTP

Le décorateur est un no-op dans les contextes suivants :

  • Jobs en arrière-plan (workers)
  • Commandes CLI (bench execute, scripts)
  • Tests automatisés

Cela évite tout blocage involontaire lors des opérations de maintenance ou de développement.

Réponse 503 et en-tête Retry-After

Lorsque le délai d'attente est dépassé, le framework renvoie automatiquement :

HTTP/1.1 503 Service Unavailable
Retry-After: 10
Content-Type: application/json

{"exc_type": "TooManyConcurrentRequests", "exception": "...", ...}

Les clients (applications front-end, intégrations tierces) peuvent lire cet en-tête pour planifier une nouvelle tentative.

Introspection : obtenir les statistiques de concurrence

Une fonction utilitaire get_stats permet de consulter l'état courant du pool pour une fonction donnée :

from frappe.concurrency_limiter import get_stats

stats = get_stats("mon_app.api.endpoint_couteux")
# Exemple de retour :
# {"capacity": 3, "available": 1, "in_use": 2}

Cela peut être utile pour des tableaux de bord de supervision ou des alertes opérationnelles.

Cas d'usage recommandés

SituationRecommandation
Génération de rapport analytique lourdlimit=2, wait_timeout=60
Export CSV/Excel de grande volumétrielimit=3, wait_timeout=30
Appel à une API externe lentelimit=5, wait_timeout=15
Calcul de prix avec beaucoup de règleslimit=None (auto), wait_timeout=10

Points d'attention

La limite automatique (limit=None) est calculée une seule fois au démarrage du worker, à partir de la ligne de commande du master Gunicorn. Si vous modifiez le nombre de workers sans redémarrer l'application, la limite ne sera pas recalculée automatiquement.
N'appliquez ce décorateur qu'aux méthodes réellement coûteuses. Sur des endpoints rapides, le passage par Redis introduit une latence inutile.

Référence technique

  • Module principal : frappe/concurrency_limiter.py
  • Sémaphore Redis : frappe/utils/redis_semaphore.py
  • Exception levée : frappe.exceptions.TooManyConcurrentRequests
  • Tests : frappe/tests/test_concurrency_limiter.py