Files
_Assistant_Lead_Tech/knowledge/backend/risques/redis.md
T
MaksTinyWorkshop f1b783407a docs(knowledge): capitalisation backend — intégration du triage local (mai-juin 2026)
Triage et intégration des propositions backend du buffer 95_a_capitaliser.md
(lot local RL799_V2 + app-alexandrie, mai-juin 2026), distinct de la capitalisation
remote antérieure (triage 2026-05-02).

~73 entrées intégrées sur knowledge/backend/, dont :
- patterns/auth.md : série "membrane d'auth fédérée BFF/OIDC" (9 patterns) + jose algo whitelist
- patterns/prisma.md : recette fusionnée "Migration String/Int → enum" (backfill + Cas A/B/C),
  row réactivable, endpoint replace atomique, updateMany conditionnel, etc.
- risques/general.md : 19 risques (epoch s vs ms, keepAliveTimeout=0, upsert+filtre liste,
  fail-safe catch-all, retrait asymétrique front/back, anti-énumération rate-limit, etc.)
- patterns/general, async, nestjs, contracts, tests + risques/auth, contracts, prisma, redis, stripe, tests
- compléments d'entrées existantes (authorize-after-fetch, P3014, cursor opaque, DI swc, Stripe v20...)
- README patterns/risques mis à jour

Doublons internes corrigés en relecture (suppression-champ .map() → general seul ;
e2e DB-based → tests.md seul). Doublons hors backend / entrées projet / rejets non intégrés.
Source 95_a_capitaliser.md non purgée à ce stade (purge en fin de capitalisation complète).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 11:25:02 +02:00

7.8 KiB

Backend — Risques & vigilance : Redis

Extrait de la base de connaissance Lead_tech. Voir knowledge/backend/risques/README.md pour l'index complet.


Redis — thrash de connexion sous charge

Risques

  • Connexions concurrentes multiples si connect() est appelé "à la demande" sans lock
  • Spam logs + saturation connexions quand Redis est down ou lent

Symptômes

  • N appels simultanés → N tentatives de connexion en parallèle
  • Logs "Redis connection failed" en rafale au démarrage ou lors d'un restart Redis

Bonnes pratiques / mitigations

// Pattern single-flight + cooldown + fallback DB best-effort
if (!this.connectPromise) {
  this.connectPromise = this.client.connect().finally(() => { this.connectPromise = null; });
}
await this.connectPromise;
// Si échec → nextConnectRetryAtMs = now + 1000 → return false → fallback DB
  • Contexte technique : Redis / NestJS — 09-03-2026

Entitlements — TTL cache supérieur au SLA de propagation

Risques

  • TTL cache > SLA propagation → un webhook raté viole mécaniquement le SLA (accès stale plus long que garanti)
  • Utilisateur avec accès périmé ou sans accès dû, pendant toute la durée du TTL résiduel

Symptômes

  • Accès premium encore actif après annulation (ou inversement)
  • NFR "propagation ≤ 60s" non respecté en cas de webhook manqué

Bonnes pratiques / mitigations

  • TTL cache ≤ SLA cible (ex : NFR "≤ 60s" → TTL = 60s max)
  • Toujours coupler TTL + invalidation explicite via webhook (les deux, pas l'un ou l'autre)
  • Contexte technique : Redis / entitlements / NestJS — 09-03-2026

Compteurs in-memory ≠ métriques persistées

Risques

  • Compteurs in-memory remis à zéro au restart (perte de données)
  • Non agrégables sur plusieurs instances (données partielles par pod)

Symptômes

  • Métriques qui "repartent de 0" à chaque déploiement
  • Dashboards incorrects en environnement multi-instance

Bonnes pratiques / mitigations

  • V1 low-cost : Redis INCRBY best-effort par eventType → persisté et agrégé multi-instances
  • Évolutif vers Prometheus/OTel sans changer l'interface (abstraction dès le départ)
  • Contexte technique : Redis / NestJS — 09-03-2026

TTL Redis quota calculé en heure locale (dérive jusqu'à ±12h)

Risques

  • Le reset du quota journalier dérive selon le timezone du serveur, pouvant aller jusqu'à ±12h d'écart par rapport à minuit UTC

Symptômes

  • Quota qui se remet à zéro à des heures inattendues selon l'environnement de déploiement
  • Comportement différent en dev local (TZ machine) et en prod (TZ container)

Bonnes pratiques / mitigations

// ✅ CORRECT — UTC midnight garanti
const midnight = new Date(
  Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate() + 1),
);
const ttlMs = midnight.getTime() - now.getTime();

// ❌ RISQUÉ — heure locale du serveur
const endOfDay = new Date();
endOfDay.setHours(23, 59, 59, 999); // dérive selon TZ serveur
  • Règle : tout expireAt ou TTL de quota journalier doit utiliser Date.UTC() — vérifier systématiquement en review

  • Contexte technique : Redis / NestJS — app-alexandrie 20-03-2026


Compensation incrBy(-1) non-atomique après dépassement de quota

Risques

  • Pattern courant de quota Redis : INCR → vérifier > limit → si oui, décrémenter (incrBy(-1)) et lever 429. La compensation n'est PAS atomique avec le check.
  • Si Redis devient indisponible (flap) entre l'incrément et la compensation, l'appel de décrément échoue silencieusement et retourne null. Le compteur reste incrémenté → quota fantôme jusqu'à l'expiration de la clé.

Symptômes

  • Utilisateur bloqué par le quota alors qu'il n'a pas atteint la limite réelle
  • Aucune erreur visible : la valeur de retour null du décrément n'est pas vérifiée
  • Comportement intermittent corrélé aux instabilités Redis

Bonnes pratiques / mitigations

// ❌ Compensation non vérifiée — échec silencieux possible
await redis.incr(key);
if (current > limit) {
  await redis.incrBy(key, -1); // retourne null si Redis flap → compensation perdue
  throw quotaExceeded();
}

// ✅ Vérifier le retour de la compensation et loguer
const compensated = await redis.incrBy(key, -1);
if (compensated === null) {
  logger.error({ type: 'quota', event: 'compensation_failed', key });
}
  • Règle : toujours vérifier le retour du décrément de compensation et loguer explicitement si null. Documenter ce choix dans les Dev Notes de la story (comportement intentionnel vs bug).
  • Solution robuste : encapsuler incrément + compensation conditionnelle dans le même pipeline MULTI/EXEC ou un script Lua (atomicité garantie), au prix d'une complexité plus élevée.

Compenser AUSSI quand la transaction DB échoue (pas seulement au dépassement)

Le même compteur doit être compensé quand l'écriture DB qui suit le quota échoue. Pattern récurrent : consumeDailyQuota (incrément Redis) est appelé avant une transaction DB. Si la transaction throw, le compteur du user est consommé sans avoir produit le side-effect attendu → quota fantôme. Le piège : la compensation incrBy(-1) existante n'est souvent déclenchée que sur dépassement de quota, pas sur exception de la transaction.

// ❌ si $transaction throw, le compteur reste incrémenté → quota fantôme
await consumeDailyQuota({ ..., action: 'dm-message' });
const message = await prisma.$transaction(async (tx) => { /* INSERT, peut échouer */ });

// ✅ compensation systématique sur exception de la transaction
await consumeDailyQuota({ ... });
try {
  const message = await prisma.$transaction(...);
} catch (err) {
  await redis.incrBy(quotaKey, -1).catch(() => {});
  throw err;
}
  • Trade-off : garder l'ordre quota → tx → compensation (et non « tx puis quota ») garantit qu'on ne dépasse pas la limite sous charge concurrente (deux requêtes simultanées qui passeraient toutes deux le check). La compensation sur catch est donc obligatoire.

  • Règle : tout flow consumeDailyQuota puis écriture DB doit compenser sur exception. Vérifier aussi les autres actions partageant ce flow (comment, post, support_ticket).

  • Contexte technique : Redis / NestJS / Prisma — app-alexandrie 01-04-2026, complété 13-05-2026 (story 10.2)


Rate-limit à compteur partagé entre endpoints jumeaux

Risques

  • Un helper de rate-limit dont la clé Redis omet un discriminant d'endpoint (quota:${action}:${userId}:${jour}) fait partager un unique compteur à plusieurs endpoints réutilisant le même action et le même discriminant (ex : l'IP).
  • Les seuils respectifs des endpoints perdent tout sens : un excès sur l'un consomme le quota de l'autre. Ex : un excès de login bloque un reset de mot de passe légitime.

Symptômes

  • Un endpoint renvoie 429 alors que son propre seuil n'est pas atteint, à cause du trafic sur un endpoint jumeau.
  • Le bug est invisible en test : chaque e2e exerce un seul endpoint à la fois, donc le compteur n'est jamais partagé pendant un test.

Bonnes pratiques / mitigations

  • Règle de clé : clé = quota:<action>:<endpoint>:<identité>:<fenêtre>. La clé DOIT inclure un discriminant d'endpoint, pas seulement l'identité de l'appelant.

  • Test de non-régression : exercer DEUX endpoints jumeaux jusqu'au seuil dans le même test — un test mono-endpoint ne révèle jamais ce bug.

  • Contexte technique : Redis / NestJS — app-alexandrie 22-05-2026