DEV Community

Cover image for Redis cache clé / valeur ou comment éviter l'anti-pattern Singleton dans un contexte Web réparti
DUVAL Olivier
DUVAL Olivier

Posted on • Updated on

Redis cache clé / valeur ou comment éviter l'anti-pattern Singleton dans un contexte Web réparti

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 :

Image description

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 :

Image description

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
    }
}
Enter fullscreen mode Exit fullscreen mode

0️⃣ un énuméré est utilisé pour le type de stratégie

class StrategyType(IntEnum):    
    BDD = 1
    API= 2
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

2️⃣ pour les autres requêtes, on lit la stratégie :

def _get_cache_strategy(uid_connected):
    return cache.get(uid_connected)
Enter fullscreen mode Exit fullscreen mode

Simple et efficace, plus de problème !

Top comments (0)