BlockLucky Documentation officielle GitHub

Données Redis

Redis 7.4 sert au backend pour quatre usages : les files de jobs BullMQ, l'état court (codes à usage unique, verrous, sessions révoquées, idempotence), quelques données utilisateur sans table en base, et le relais d'événements Socket.IO. Ce fonctionnement découle d'une décision d'architecture prise le 10 novembre 2026 : une seule instance Redis, toutes les clés applicatives préfixées par environnement, un délai de commande court et un repli en mémoire si Redis est indisponible. Les choix techniques d'ensemble sont présentés sur la page Choix techniques.

Tout accès passe par backend/src/config/redis.config.ts, qui exporte le singleton redis (classe RedisManager), sauf trois usages qui prennent le client brut redis.getRawClient() : BullMQ, le limiteur de débit et le relais Socket.IO.

Client et préfixe

Le client ioredis est créé ainsi, sauf quand NODE_ENV vaut test (aucun client dans ce cas) :

Option Valeur Effet
URL REDIS_URL, par défaut redis://localhost:6380 Port 6380 de l'hôte, publié par Docker Compose vers le 6379 du conteneur
lazyConnect true La connexion s'ouvre à la première commande envoyée sur le client brut (BullMQ, limiteur, abonnement) ou par la sonde de santé (ping)
maxRetriesPerRequest null Exigé par BullMQ pour ses commandes bloquantes
enableReadyCheck false
retryStrategy 200 ms, 400 ms, 600 ms, puis abandon après la troisième tentative Un Redis absent ne bloque pas le démarrage indéfiniment
connectionName blocklucky-backend Visible dans CLIENT LIST

Les méthodes get, set, setIfAbsent et del de RedisManager préfixent automatiquement chaque clé :

Texte
<REDIS_KEY_PREFIX>:<NODE_ENV>:<clé>

Avec les valeurs par défaut, la clé chain:keeper-lock devient blocklucky:development:chain:keeper-lock en développement et blocklucky:production:chain:keeper-lock en Docker Compose. Les tableaux ci-dessous donnent les clés sans ce préfixe.

Chaque commande a un délai de 500 ms (CACHE_COMMAND_TIMEOUT_MS). del utilise UNLINK, qui libère la mémoire en arrière-plan.

Gigue des durées de vie

set(clé, valeur, ttl) applique une gigue de plus ou moins 10 % à la durée de vie (withJitter) : une clé de 600 s expire entre 540 et 660 s. Cela évite que des milliers de clés créées au même instant expirent au même instant. setIfAbsent n'applique pas de gigue : les verrous gardent une durée exacte.

Les données dont la durée est critique portent leur propre échéance : le défi OTP stocke expiresAt dans sa valeur, et la révocation de session prend une marge de 20 % plus 30 s.

Clés applicatives

Authentification et sessions

Clé Valeur Durée de vie Écrite par
auth:otp:<sujet> JSON du défi : empreinte du code, usage (LOGIN, SIGNUP, SETUP, PASSWORD_RESET, EMAIL_CHANGE), tentatives, échéance, éventuelle inscription en attente avec l'empreinte bcrypt du mot de passe. Le sujet est l'identifiant du compte, un identifiant provisoire d'inscription, password-reset:<userId> ou email-change:<userId> 600 s (avec gigue) ; réécrite avec la durée restante après une erreur backend/src/services/otp-challenge.service.ts
auth:otp-failures:<sujet> Nombre d'erreurs de code ou de mot de passe. Pour une adresse sans compte, le sujet est signin:<HMAC> ou password-reset:<HMAC> de l'adresse 3 600 s (avec gigue) idem
auth:login-ticket:<HMAC> Identifiant du compte qui a franchi le premier facteur (mot de passe, code email ou Google) et doit encore saisir le code de son application TOTP. La clé contient l'empreinte HMAC du ticket, jamais le ticket lui-même : un ticket aléatoire de 256 bits encodé en base64url, renvoyé au client sous le nom loginTicket 600 s ; à usage unique, supprimée après un code TOTP ou de secours valide issueLoginTicket, lue par readLoginTicket et supprimée par revokeLoginTicket de otp-challenge.service.ts
auth:email-change:<userId> Nouvelle adresse email en attente de confirmation 600 s (avec gigue) requestEmailChange de backend/src/api/auth/auth.services.ts, effacée à la confirmation
auth:totp-used:<userId>:<compteur> 1 : fenêtre TOTP déjà consommée, contre le rejeu 120 s, setIfAbsent idem
auth:revoked:<jti> 1 : jeton de session révoqué Durée restante du jeton × 1,2 + 30 s backend/src/services/session.service.ts
auth:revoked-before:<userId> Horodatage en millisecondes : tout jeton émis avant est révoqué (déconnexion de tous les appareils, changement de mot de passe) 30 jours (avec gigue) idem

Requêtes et webhooks

Clé Valeur Durée de vie Écrite par
idempotency:<compte>:<méthode>:<chemin>:<clé> PENDING pendant le traitement, puis JSON du statut et du corps de la réponse 60 s pour le verrou (setIfAbsent), 24 h pour la réponse ; supprimée si la réponse est une 5xx backend/src/middlewares/idempotency.middleware.ts
webhook:seen:<signature> 1 : signature déjà reçue, contre le rejeu 7 jours, setIfAbsent backend/src/api/webhooks/webhook-signature.ts

Blockchain

Clé Valeur Durée de vie Écrite par
chain:keeper-lock 1 : un process exécute le keeper max(5, KEEPER_INTERVAL_MS / 1000) s, 10 s par défaut ; supprimée en fin de passe backend/src/blockchain/lottery.keeper.ts
chain:draw-ready-at:<tour> Instant, en millisecondes, où le tour est devenu prêt au tirage ; début de la salle d'attente 7 jours, setIfAbsent backend/src/blockchain/draw-reveal.ts, appelé par le keeper et GET /lottery/status
wallet:grant-lock:<userId> 1 : dotation de bienvenue en cours 120 s ; supprimée en cas d'échec backend/src/blockchain/wallet.service.ts
blockchain:circuit_breaker true ou false : disjoncteur, en mode simulé seulement Aucune backend/src/services/blockchain.service.ts
blockchain:fallback_active true ou false Aucune idem ; écrite mais jamais relue

Données utilisateur et partenaires

Ces clés n'ont pas de durée de vie et n'ont pas d'équivalent en base. Elles sont donc des données persistantes, sauvegardées seulement par la persistance AOF de Redis.

Clé Valeur Écrite par Lue par
user:prefs:<userId> JSON des préférences : thème, langue, pushNotificationsEnabled et notifications (quatre catégories) updatePreferences readPreferences, appelé par getProfile, updatePreferences et getPushNotificationsHistory
user:quests:<userId> JSON de la liste des quêtes sociales réalisées completeSocialQuest getSocialQuests, completeSocialQuest
partner:codes:<rewardId> JSON du lot de codes promo importés pour une offre, consommé à chaque échange POST /partners/codes/upload Échange de récompense
partner:offer_expiry:<offreId> Date d'expiration d'une offre Création d'offre Jamais relue

Attention

Une perte des données Redis efface les préférences, l'historique des quêtes (une quête pourrait être validée une seconde fois) et les codes promo importés non encore distribués. Ces données ne sont pas couvertes par pg_dump ; voir Performance et sauvegardes.

L'ancienne clé user:ticket_limit:<userId>, qui portait le plafond personnel de jeu responsable, n'est plus ni écrite ni lue depuis le retrait de PUT /user/responsible-limit : avec un ticket par participant, le profil renvoie toujours maxTicketsPerRound: 1. Les valeurs déjà présentes dans une instance Redis existante restent sans effet et peuvent être supprimées (SCAN sur le motif *:user:ticket_limit:*, puis DEL).

Clés hors RedisManager

Clé ou canal Préfixe Usage
<REDIS_KEY_PREFIX>:<NODE_ENV>:ratelimit:global:<ip> Ajouté explicitement Compteur du limiteur global, fenêtre de 15 min, 300 requêtes (10 000 en test)
<REDIS_KEY_PREFIX>:<NODE_ENV>:ratelimit:auth:<ip> Ajouté explicitement Compteur du limiteur d'authentification, fenêtre de 10 min, 30 requêtes
<REDIS_KEY_PREFIX>:<NODE_ENV>:ratelimit:identity:<ip> Ajouté explicitement Compteur des envois de pièce d'identité, fenêtre d'une heure, 10 requêtes
<REDIS_KEY_PREFIX>:<NODE_ENV>:ratelimit:support:user:<userId> Ajouté explicitement Compteur des messages au support, fenêtre d'une heure, 5 requêtes par compte (ip:<ip> sans utilisateur)
Canal <REDIS_KEY_PREFIX>:<NODE_ENV>:socket-events Ajouté explicitement Pub/sub du relais temps réel ; aucune donnée stockée
bull:<file>:* Préfixe propre à BullMQ Files de jobs (voir plus bas)

Le limiteur de débit est défini dans backend/src/middlewares/rateLimiter.middleware.ts avec le magasin rate-limit-redis. Il est réglé en passOnStoreError: true : si Redis ne répond pas, la requête passe (choix fail-open, pour que la panne de Redis n'arrête pas l'API).

Files BullMQ

Trois files partagent les options de backend/src/queues/queue.options.ts : 5 tentatives, attente exponentielle à partir de 1 s avec gigue de 50 %, jobs terminés conservés 24 h (1 000 au plus), jobs en échec conservés 14 jours.

File Nom des jobs Producteur Worker Concurrence
emails-queue send-email backend/src/queues/emails.queue.ts startEmailWorker WORKER_CONCURRENCY
ledger-queue ledger-op backend/src/queues/ledger.queue.ts startLedgerWorker 1
chain-actions MINT_BADGE, CLAIM_PRIZE, CLAIM_REFUND backend/src/blockchain/chain-actions.ts startChainActionsWorker 1

Les files ne passent pas par RedisManager : leurs clés ne portent pas le préfixe REDIS_KEY_PREFIX, mais le préfixe par défaut de BullMQ, bull. Par exemple bull:chain-actions:wait, bull:chain-actions:failed, bull:ledger-queue:<jobId>. Deux environnements qui partageraient une même instance Redis partageraient donc leurs files.

La file chain-actions est détaillée sur la page File d'actions on-chain, les deux autres sur la page Files et workers.

Repli en mémoire

Chaque méthode de RedisManager teste d'abord si le client est prêt (status === 'ready'). S'il ne l'est pas, ou si la commande échoue ou dépasse 500 ms, elle utilise une Map locale au process :

Méthode Comportement de repli
get Lit la Map, en supprimant une entrée expirée
set Écrit dans la Map avec l'échéance (gigue comprise)
setIfAbsent Écrit seulement si aucune entrée non expirée n'existe
del Supprime de la Map ; la suppression est aussi tentée sur Redis

Le repli permet à l'API de continuer à servir quand Redis tombe. Ses limites :

  • la Map est propre à chaque process : les verrous (chain:keeper-lock, wallet:grant-lock) et l'idempotence ne protègent plus entre l'API et le worker ou entre plusieurs instances ;
  • les données écrites pendant la panne ne sont pas recopiées dans Redis au retour, et réciproquement : une clé écrite en mémoire n'est plus lue une fois Redis revenu ;
  • la Map n'a pas de limite de taille et est perdue au redémarrage.

Pour les files, le repli consiste à exécuter le travail directement dans le process (envoi d'email, opération ledger, action on-chain), sans réessai. Sans client Redis du tout, le process worker refuse de démarrer (« Aucun worker démarré : Redis est requis pour le process worker »).

Dans les tests unitaires, redis.config est remplacé par backend/__tests__/__mocks__/redis.mock.ts, une Map sans durée de vie dont getRawClient() renvoie null.

Configuration du serveur Redis

Le service redis de docker-compose.yml lance redis:7.4-alpine avec :

Option Valeur Raison
--appendonly yes Persistance AOF : les données utilisateur ci-dessus et les jobs survivent à un redémarrage
--appendfsync everysec Au plus une seconde d'écritures perdue en cas d'arrêt brutal
--maxmemory 256mb Plafond mémoire
--maxmemory-policy noeviction Redis refuse les écritures plutôt que d'évincer des clés : BullMQ l'exige, une éviction pourrait supprimer un job
--slowlog-log-slower-than 10000 Journalise les commandes de plus de 10 ms

Les données sont dans le volume redisdata. Le port n'est publié que sur 127.0.0.1:6380.

Inspecter les clés

Terminal
docker compose exec redis redis-cli --scan --pattern 'blocklucky:production:*'
docker compose exec redis redis-cli TTL blocklucky:production:chain:keeper-lock
docker compose exec redis redis-cli --scan --pattern 'bull:chain-actions:*'

--scan parcourt les clés par lots, sans bloquer le serveur comme le ferait KEYS.