Skip to content

Commit

Permalink
docs(api): ADR
Browse files Browse the repository at this point in the history
  • Loading branch information
laura-bergoens committed Dec 16, 2024
1 parent 7091d67 commit 5d6ffe4
Showing 1 changed file with 94 additions and 0 deletions.
94 changes: 94 additions & 0 deletions docs/adr/0058-modification-du-cache-du-contenu-pedagogique.md
Original file line number Diff line number Diff line change
@@ -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`.

0 comments on commit 5d6ffe4

Please sign in to comment.