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.
Techniquement, un pool de jetons est stocké dans une liste Redis. Chaque slot de concurrence autorisé correspond à un jeton dans cette liste :
BLPOP).LPUSH).wait_timeout, la requête reçoit une réponse 503 avec un en-tête Retry-After.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.
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ètre | Type | Défaut | Description |
|---|---|---|---|
limit | int | None | Auto-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_timeout | int | 10 | Durée maximale d'attente en secondes avant de renvoyer un 503. |
@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."""
...
Le décorateur est un no-op dans les contextes suivants :
bench execute, scripts)Cela évite tout blocage involontaire lors des opérations de maintenance ou de développement.
Retry-AfterLorsque 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.
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.
| Situation | Recommandation |
|---|---|
| Génération de rapport analytique lourd | limit=2, wait_timeout=60 |
| Export CSV/Excel de grande volumétrie | limit=3, wait_timeout=30 |
| Appel à une API externe lente | limit=5, wait_timeout=15 |
| Calcul de prix avec beaucoup de règles | limit=None (auto), wait_timeout=10 |
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.frappe/concurrency_limiter.pyfrappe/utils/redis_semaphore.pyfrappe.exceptions.TooManyConcurrentRequestsfrappe/tests/test_concurrency_limiter.py