-
Notifications
You must be signed in to change notification settings - Fork 56
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
7091d67
commit 5d6ffe4
Showing
1 changed file
with
94 additions
and
0 deletions.
There are no files selected for viewing
94 changes: 94 additions & 0 deletions
94
docs/adr/0058-modification-du-cache-du-contenu-pedagogique.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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`. |