diff --git a/docs/adr/0058-modification-du-cache-du-contenu-pedagogique.md b/docs/adr/0058-modification-du-cache-du-contenu-pedagogique.md new file mode 100644 index 00000000000..dcf850bfdb4 --- /dev/null +++ b/docs/adr/0058-modification-du-cache-du-contenu-pedagogique.md @@ -0,0 +1,94 @@ +# 57. Modification du fonctionnement du cache de contenu pédagogique +Date : 2024-12-16 + +## État +Validée + +## Historique +Pour ceux qui veulent toute l'histoire, voici le lien vers les précédentes ADR expliquant le fonctionnement du cache de contenu pédagogique aujourd'hui : + +- [ADR de la mise en place du cache](0005-ajout-d-un-cache-memoire-distribute-pour-le-contenu-pedagogique.md) +- [ADR traitant du stockage en clé unique dans Redis et en mémoire vive](0016-stockage-du-referentiel-en-cache.md) + +## Contexte +Le fonctionnement aujourd'hui consiste à stocker l'entièreté du contenu pédagogique en deux endroits : + +- en mémoire vive +- sur Redis, de sorte que une machine qui vient de lancer un serveur récupère rapidement la donnée + +### Limitations et problèmes +#### Crash mémoire +Le contenu pédagogique est un ensemble dont la taille grossit, doucement mais sûrement. Cela fait plusieurs semaines qu'on constate un dépassement mémoire sur les containers +lorsqu'ils traitent des opérations liées au cache de contenu pédagogique. Ces dépassements correspondent aux moments de lecture et d'écriture, depuis et vers le cache Redis. +Voici un extrait du fichier `RedisCache.js` : +```js +class RedisCache extends Cache { + // Lecture depuis le cache Redis + async get(key, generator) { + const value = await this._client.get(key); + + if (value) { + const parsed = JSON.parse(value); // Dépassement mémoire ici + const patches = await this._client.lrange(`${key}:${PATCHES_KEY}`, 0, -1); + patches.map((patchJSON) => JSON.parse(patchJSON)).forEach((patch) => applyPatch(parsed, patch)); + return parsed; + } + + return this._manageValueNotFoundInCache(key, generator); + } + // Ecriture vers le cache Redis + async set(key, object) { + const objectAsString = JSON.stringify(object); // Dépassement mémoire ici + + logger.info({ key, length: objectAsString.length }, 'Setting Redis key'); + + await this._client.set(key, objectAsString); + await this._client.del(`${key}:${PATCHES_KEY}`); + + return object; + } + /* ... */ +} +``` +Redis stocke des chaînes de caractères, tandis qu'en mémoire vive nous conservons le contenu pédagogique en **_POJO_** (**_plain old javascript object_**). Cette conversion dans +les deux sens est donc faite dans le code (via `JSON.stringify` et `JSON.parse`), ce qui signifie qu'à un moment donné, dans la pile mémoire de traitement de la fonction, +on a simultanément le contenu pédagogique en **_POJO_** et en chaînes de caractères. Sachant que, ce jour, le contenu pédagogique fait environ 40 Mo, on peut estimer à, au +minimum, 80 Mo de données, sans parler des allocations diverses et variées nécessaires à l'exécution du code. + +#### Peu optimisé +Le contenu pédagogique est stocké dans une seule clé. Cela pose deux défauts majeurs. + +D'une part, nous n'avons pas le choix de ce qui mérite d'être promu dans le cache mémoire ou pas. Tout y est. +Pourtant, on sait qu'un certain nombre d'entités sont très peu ou pas consultés dans le fonctionnement des applications Pix, voici une liste non exhaustive : +- Les données sur des entités peu affichées (`frameworks` ou `thematics`) +- Les épreuves non jouables (car périmées ou en atelier) +- Les épreuves dans une langue encore peu jouée sur Pix + + +D'autre part, et on pense en particulier aux acquis et aux épreuves, il est fréquent de vouloir récupérer toujours le même sous-ensemble. +Par exemple, tous les utilisateurs qui se positionnent sur la même compétence, dans le code pour récupérer la prochaine épreuve, +on récupère les mêmes acquis et les mêmes épreuves juste avant de dérouler l'algorithme du choix d'épreuve. Aujourd'hui, on effectue donc les mêmes boucles et les mêmes filtres. + +## Solution +### Corriger les dépassements en mémoire en remplaçant Redis par PG +Le plus urgent était de réparer le problème des dépassements en mémoire. +Nous l'avons vu, ces dépassements sont directement liés à l'usage de Redis pour stocker le contenu pédagogique. +Nous avons donc décidé de stocker le contenu pédagogique dans la base de données PG. Et plutôt que de stocker la donnée d'un seul tenant, +nous avons créé une table par entité. +Liste des migrations : +- [Toutes les tables](../../api/db/migrations/20241120132349_create-learningcontent-schema-and-tables.js) +- [Retrait de contraintes de clés étrangères](../../api/db/migrations/20241125150331_remove-learningcontent-foreignkeys.js) +- [Correction d'un type de clé primaire](../../api/db/migrations/20241127142253_alter-table-column-id-missions-to-integer.js) + +Via l'utilisation de PG, notamment de `knex`, le problème de dépassement de mémoire sera résolu. Les résultats des requêtes effectuées +auprès de la base sont directement retournés en objets. Aucune transformation n'est nécessaire. + +#### Écriture +Les écritures dans les tables se produisent à trois occasions distinctes : +- Lors d'un rafraîchissement du cache (planifié par cron ou ponctuel via PixAdmin), durant lequel on récupère la dernière release +- Lors d'une création forcée de nouvelle release (via PixAdmin), durant laquelle on va demander à l'API LCMS de créer une nouvelle release et de nous la retourner +- Lors d'un patch d'une entité (opération effectuée seulement sur l'environnement de recette) + +Les `repositories` en charge des écritures sont appelés à ces trois occasions dans une transaction. +Ils procèdent à des `upserts`, c'est-à-dire à des insertions ou des modifications, mais pas de suppression. +Ces nouveaux `repositories` d'écriture ont été placé dans le dossier `src/learning-content`.