Sommaire
Préambule TL;DR
💥 Dernièrement, lors d'un développement, on a eu un bug bloquant, dû à une variable qui devait préciser la stratégie à adopter post-connexion de l'utilisateur.
📌 Si le connecté est de type A alors utiliser la stratégie A (consultation BDD), sinon prendre la stratégie P (consultation API) car il est de type P.
Schéma du déroulé :
1- connexion ➙ stratégie à adopter, stocker dans une variable type_strategie ➙ obtention des informations selon la stratégie
N- une fois connecté ➙ lecture de la stratégie via la variable ➙ les informations varient (un Ctrl-F5 ou un retour sur la page)
On reconnait ici une variante du pattern Strategy, où la variable (calculée à l'authentification) qui en précise le type était en...global, autrement dit, un singleton, et cela a causé des effets de bord pour tous les connectés : pour le même connecté, la page affichant des données issus de la stratégie...changeait, parfois aller chercher les informations de la stratégie A et d'autres la stratégie B voire pire, le dernier connecté changeait la stratégie pour tous les autres, "incompréhensible" ! 🥵
Que se passe-t-il Houston ?
➢ Dans un contexte Web multi-nœuds, chaque serveur Web a son espace mémoire, indépendant des autres. Le stockage d'une valeur dans une variable le sera uniquement sur le nœud / serveur Web atteint. A chaque requête Web, celle-ci peut changer de nœuds, selon la répartition de charge.
➢ Nous utilisons Gunicorn qui est un serveur Web WSGI, cela permet d'exécuter un serveur d'applications python comme Django. Gunicorn est architecturé en plusieurs workers (ou noeuds), chaque worker a son espace mémoire isolé. Dans un contexte de production, on pourrait également avoir Gunicorn réparti sur plusieurs serveurs VM / noeuds : les requêtes Web sont non seulement réparties sur les différents serveurs, mais aussi réparties au sein de Gunicorn, on ne sait pas par avance où ira la requête (sur quel noeud et sur quel worker), pour schématiser ce type d'architecture ops :
source : https://excalidraw.com/#json=CXDUttmskxjwScBjEr35x,BavDLiX5xlisHZB5viyrPQ
💣 ⚠ Il y a autant de versions différentes de VAR qu'il existe de workers / noeuds.
💣 ⚠ Corolaire : Cette variable VAR est unique et partagée (singleton) à tous les connectés et non selon leur profil propre, l'effet "le dernier qui dit qui a raison" s'appliquera.
Un utilisateur ira par exemple sur le noeud 1 puis sur le worker 2, et la prochaine fois, peut être sur le noeud 2 puis sur le worker 1, ainsi de suite : la stratégie à adopter pouvait donc changer selon le noeud / worker sur lequel la requête tombait 🎭 l'état de la variable n'est pas conservée.
Solution élégante
1️⃣ La 1ère solution venant en tête est d'utiliser les variables de session pour stocker le type de stratégie propre au connecté.
session['strategy'] = A
...sauf que cette idée était difficile à mettre en place car non triviale dans le code et cette variable de type de stratégie est utilisé côté frontoffice mais aussi côté backoffice.
2️⃣ C'est là que Redis vient à la rescousse ! Redis est déjà utilisé comme message broker / PubSub, cet article décrit son usage dans un contexte de lancement de tâches différées. Redis permet aussi de l'utiliser en base NoSql clé / valeur pour du cache par exemple, autant prendre cette possibilité. Django fournit un provider par défaut pour Redis.
Que cela soit côté Frontoffice ou Backoffice, on stocke la stratégie dans une clé Redis. La clé est ici l'identifiant unique du connecté.
Le scénario se déroule maintenant de la façon suivante :
1- connexion ➙ stratégie à adopter, stocker dans une clé(uid_connecté) Redis ➙ obtention des informations selon la stratégie
N- une fois connecté ➙ lecture de la stratégie via la clé(uid_connecté) Redis ➙ la bonne stratégie est prise en compte à chaque fois, les informations sont bonnes à chaque requête
La grande différence réside à ce que VAR est maintenant partagée par tous les noeuds / workers, sa clé d'accès est un identifiant unique du connecté
Schématiquement :
source : https://excalidraw.com/#json=tuOIxkk9k1D3Zu8qVNyvD,JPrfOfaUnFnzCQVngYOvjQ
Technique
Configuration pour le provider de cache Redis, du settings Django
CACHES = {
"default": {
"BACKEND": "django.core.cache.backends.redis.RedisCache",
"LOCATION": REDIS_CACHE # par ex. redis://api-redis-db:6379
}
}
0️⃣ un énuméré est utilisé pour le type de stratégie
class StrategyType(IntEnum):
BDD = 1
API= 2
1️⃣ à la connexion, stockage :
def _set_cache_strategy(uid_connected, strategy: StrategyType):
"""
pas de timeout
sinon par défaut 300 s https://docs.djangoproject.com/fr/5.0/ref/settings/#std-setting-CACHES-TIMEOUT)
"""
cache.set(uid_connected, strategy, timeout=None)
2️⃣ pour les autres requêtes, on lit la stratégie :
def _get_cache_strategy(uid_connected):
return cache.get(uid_connected)
Simple et efficace, plus de problème !
Top comments (0)