Multi-session pédagogique¶
PerfShop permet à plusieurs étudiants en parallèle (jusqu'à 500) de jouer un parcours pédagogique sur la même instance, sans interférer entre eux. Chaque étudiant a son propre token UUID, sa propre progression et ses propres tentatives, persistés en base de données.
L'architecture est qualifiée de write-through : la base de données est toujours la source de vérité, et un cache mémoire optionnel peut être activé à chaud par le formateur pour servir un cas d'usage pédagogique précis (démonstration de fuite mémoire visible dans un heap dump).
Source de vérité
Cette page reflète le contrat de l'interface PedagogiqueSessionService.java et son implémentation DefaultPedagogiqueSessionService.java, ainsi que l'entité PedagogiqueSessionEntity.java et la migration V36__add_pedagogique_sessions_table.sql.
Vue d'ensemble¶
flowchart TB
subgraph CTRL["Couche contrôleur"]
CSC["ChaosStudentController<br/>(endpoints /pedagogique/**)"]
OC["OrderController<br/>(getAgentCodeForToken)"]
end
subgraph SVC["Couche service"]
PSS["PedagogiqueSessionService<br/>(interface)"]
DPS["DefaultPedagogiqueSessionService"]
PSS -.implémente.-> DPS
end
subgraph DATA["Couche données"]
direction TB
CACHE[/"Cache mémoire optionnel<br/>ConcurrentHashMap<token, MutableSession>"/]
DB[("MySQL<br/>pedagogique_sessions")]
end
CTRL --> PSS
DPS -->|toujours| DB
DPS -.cache activé.-> CACHE
CACHE -.fallback.-> DB
Deux modes coexistent dans la même implémentation :
- Mode database (défaut) : toutes les opérations passent directement par la base. Aucun objet
MutableSessionne vit en heap au-delà de la durée d'une requête HTTP. C'est le mode normal, résilient aux redémarrages du backend. - Mode database + cache mémoire (opt-in formateur) : chaque session créée est aussi stockée dans une
ConcurrentHashMapcôté JVM. Les lectures consultent d'abord le cache (rapide), puis la base en fallback. Les sessions vivent en heap, ce qui les rend visibles dans les heap dumps générés par le chaos mémoire — c'est précisément la démonstration pédagogique recherchée.
Pourquoi un cache mémoire optionnel ?¶
Le cache mémoire n'est pas une optimisation de performance. C'est un support pédagogique : quand le formateur active le chaos mémoire (heap leak) et déclenche un heap dump via /actuator/heapdump, il veut que les étudiants découvrent dans le dump des objets contenant leurs propres tokens, alias et progressions. Sans cache mémoire, la table pedagogique_sessions est la seule structure qui contient ces données — elles restent invisibles dans un heap dump.
flowchart LR
subgraph A["Mode database<br/>(défaut)"]
direction TB
R1["Requête HTTP étudiant"] --> S1["MutableSession<br/>(éphémère, locale<br/>à la requête)"]
S1 --> DB1[("DB seule")]
S1 -.GC immédiat.-> X1[/"❌ Invisible<br/>dans heap dump"/]
end
subgraph B["Mode database + cache"]
direction TB
R2["Requête HTTP étudiant"] --> S2["MutableSession<br/>(persistante en heap)"]
S2 --> CH2[/"ConcurrentHashMap<br/>en JVM"/]
S2 --> DB2[("DB toujours mise à jour")]
CH2 -.heap dump.-> V2[/"✅ Visible :<br/>tokens, alias,<br/>extractionAnswerHash"/]
end
C'est la valeur pédagogique du chaos mémoire : montrer concrètement qu'une fuite mémoire peut exposer des données utilisateur qui auraient dû rester opaques. Les étudiants peuvent ouvrir le heap dump avec Eclipse MAT ou VisualVM, chercher la classe PedagogiqueSessionService$MutableSession, et lire les tokens des camarades en clair.
Contrat de l'interface PedagogiqueSessionService¶
L'interface définit le cycle de vie complet d'une session pédagogique. Toutes les méthodes ont une sémantique write-through : elles persistent en base inconditionnellement, et mettent à jour le cache si activé.
Méthodes principales¶
| Méthode | Rôle |
|---|---|
createSession(alias, level, timerSeconds) |
Crée une session, calcule agentCode et extractionAnswerHash, persiste en DB, ajoute au cache si activé |
getSession(token) |
Retourne la session par token (cache d'abord si activé, sinon DB), null si inconnu |
saveSession(session, level) |
Persiste les mutations (step, completedAt, attempts) — appelé après chaque mutation |
getAgentCodeForToken(token) |
Récupère le code agent pour OrderController (sans charger toute la session) |
clearAll() |
Vide la table ET le cache |
isEmpty() / count() |
Stats globales |
getAllSessions() |
Vue lecture seule de toutes les sessions actives |
setMemoryCacheEnabled(enabled) |
Bascule le cache à chaud (appelé par ChaosStudentController selon le choix formateur) |
isMemoryCacheEnabled() |
État courant, exposé dans /status pour synchroniser l'UI formateur |
Inner class MutableSession¶
L'objet métier en mémoire (et dans le cache quand actif) :
classDiagram
class MutableSession {
+String token "UUID, immuable"
+String alias "immuable, peut être null"
+long joinedAt "epoch ms, immuable"
+AtomicInteger step "incrémentation concurrente"
+volatile long completedAt "0 si non terminé"
+ConcurrentHashMap~String,Integer~ attempts "tentatives par étape"
+volatile String agentCode "ex: A3F7"
+volatile String extractionAnswerHash "SHA-256"
+volatile String logiqueQuestionIndices "ex: 15,10,18,19,14"
+volatile String logiqueExpectedHash "SHA-256"
+displayAlias() String "alias ou Agent-XXXX"
}
class PedagogiqueSessionEntity {
+String token "PK, UUID"
+String alias
+String level "bac1..bac5"
+long joinedAt
+long completedAt
+int currentStep
+String agentCode
+String extractionAnswerHash
+String logiqueQuestionIndices
+String logiqueExpectedHash
+String attemptsJson "{ \"bac1-0\": 2, ... }"
+LocalDateTime createdAt
+displayAlias() String
}
MutableSession <..> PedagogiqueSessionEntity : "conversion bidirectionnelle<br/>(serializeAttempts / deserializeAttempts)"
Thread-safety de MutableSession¶
| Champ | Type | Pourquoi ? |
|---|---|---|
token, alias, joinedAt |
final (immuables) |
Posés au constructeur, jamais modifiés |
step |
AtomicInteger |
Incrémentations concurrentes potentielles si l'étudiant clique deux fois rapidement |
completedAt, agentCode, extractionAnswerHash, logiqueQuestionIndices, logiqueExpectedHash |
volatile |
Écriture unique, lecture concurrente — volatile garantit la visibilité |
attempts |
ConcurrentHashMap |
Mutations concurrentes sur des clés différentes ("bac1-0", "bac1-1"…) |
Aucun synchronized n'est utilisé : la composition final + volatile + AtomicInteger + ConcurrentHashMap couvre tous les patterns d'accès observés.
Conversion MutableSession ↔ PedagogiqueSessionEntity¶
La couche service traduit en permanence entre les deux représentations. Voici la correspondance complète :
MutableSession (Java) |
PedagogiqueSessionEntity (JPA) |
Note |
|---|---|---|
token (UUID) |
token (VARCHAR(36) PK) |
Identique |
alias (String) |
alias (VARCHAR(100)) |
null accepté |
| (porté par le contrôleur) | level (VARCHAR(10)) |
MutableSession ne porte pas le niveau ; il est passé en paramètre à createSession() et à saveSession() |
joinedAt (long ms) |
joined_at (BIGINT) |
Identique |
completedAt (long ms) |
completed_at (BIGINT, défaut 0) |
Identique |
step.get() (AtomicInteger) |
current_step (INT) |
int simple côté DB |
agentCode (String) |
agent_code (VARCHAR(10)) |
Identique |
extractionAnswerHash (String) |
extraction_answer_hash (VARCHAR(64)) |
SHA-256 hex |
logiqueQuestionIndices (String) |
logique_question_indices (VARCHAR(20)) |
Format "15,10,18,19,14" |
logiqueExpectedHash (String) |
logique_expected_hash (VARCHAR(64)) |
SHA-256 hex |
attempts (ConcurrentHashMap) |
attempts_json (TEXT) |
Sérialisé en JSON via Jackson |
Pas de DEFAULT sur TEXT
MySQL 8 en mode strict refuse DEFAULT '{}' sur une colonne TEXT. La valeur initiale "{}" pour attempts_json est donc gérée côté Java (serializeAttempts retourne "{}" quand la map est vide). C'est documenté en commentaire dans V36__add_pedagogique_sessions_table.sql.
Diagrammes de séquence¶
createSession() — création au /join¶
sequenceDiagram
autonumber
participant CTL as ChaosStudentController
participant SVC as DefaultPedagogiqueSessionService
participant DB as MySQL<br/>(pedagogique_sessions)
participant CACHE as ConcurrentHashMap<br/>(optionnel)
CTL->>SVC: createSession(alias, "bac3", 1800)
SVC->>SVC: token = UUID.randomUUID()
SVC->>SVC: agentCode = computeAgentCode(level, token)
SVC->>SVC: extractionAnswerHash = SHA-256(...)
SVC->>SVC: indices = computeLogiqueIndices(token)
SVC->>SVC: logiqueExpectedHash = SHA-256(answers)
SVC->>SVC: ms = new MutableSession(alias)
SVC->>SVC: ms.agentCode = agentCode (volatile)
SVC->>SVC: ms.extractionAnswerHash = ... (volatile)
SVC->>SVC: ms.logiqueQuestionIndices = indices
SVC->>SVC: ms.logiqueExpectedHash = ...
SVC->>SVC: entity = new PedagogiqueSessionEntity(token, alias, level, joinedAt)
SVC->>SVC: copyFields(entity, ms, level)
SVC->>DB: INSERT pedagogique_sessions
alt isMemoryCacheEnabled()
SVC->>CACHE: put(token, ms)
end
SVC-->>CTL: ms
getSession() — lecture avec stratégie cache¶
sequenceDiagram
autonumber
participant CTL as Contrôleur
participant SVC as DefaultPedagogiqueSessionService
participant CACHE as ConcurrentHashMap
participant DB as MySQL
CTL->>SVC: getSession(token)
alt isMemoryCacheEnabled()
SVC->>CACHE: get(token)
alt Hit cache
CACHE-->>SVC: MutableSession
SVC-->>CTL: ms
else Miss cache
SVC->>DB: SELECT WHERE token = ?
DB-->>SVC: PedagogiqueSessionEntity ou null
alt null
SVC-->>CTL: null
else trouvé
SVC->>SVC: ms = entityToMutable(entity)
SVC->>CACHE: put(token, ms)
SVC-->>CTL: ms
end
end
else cache désactivé
SVC->>DB: SELECT WHERE token = ?
DB-->>SVC: entity ou null
SVC->>SVC: ms = entityToMutable(entity) si non null
SVC-->>CTL: ms ou null
end
saveSession() — write-through¶
sequenceDiagram
autonumber
participant CTL as Contrôleur
participant SVC as DefaultPedagogiqueSessionService
participant CACHE as ConcurrentHashMap
participant DB as MySQL
CTL->>SVC: saveSession(ms, "bac3")
Note over SVC: Le cache pointe déjà sur le<br/>même objet ms — rien à faire<br/>pour les mutations atomiques<br/>(step, attempts).<br/>On persiste juste en DB.
SVC->>SVC: entity = mutableToEntity(ms, "bac3")
SVC->>SVC: entity.attemptsJson = ObjectMapper.writeValueAsString(ms.attempts)
SVC->>DB: UPDATE pedagogique_sessions
SVC-->>CTL: void
L'optimisation clé : le cache n'est jamais explicitement mis à jour par saveSession(). Il pointe directement sur l'objet MutableSession que le contrôleur vient de muter — donc tout est déjà à jour en mémoire. Seule la base nécessite l'écriture explicite.
Activation/désactivation à chaud¶
Le formateur bascule le mode via l'interface chaos-admin (chaos pédagogique). Côté backend, l'appel transite par ChaosStudentController qui invoque pedagogiqueSessionService.setMemoryCacheEnabled(enabled). L'état est exposé en lecture dans la réponse de /api/chaos/student/status (champ memoryCacheEnabled) pour que l'UI formateur puisse refléter l'état courant.
stateDiagram-v2
[*] --> DatabaseOnly : démarrage backend
DatabaseOnly --> WithCache : setMemoryCacheEnabled(true)
WithCache --> DatabaseOnly : setMemoryCacheEnabled(false)
state WithCache {
[*] --> Empty
Empty --> Populated : premier /join
Populated --> Populated : /join supplémentaires
Populated --> Populated : getSession (hit ou miss + hydratation)
}
state DatabaseOnly {
[*] --> NoHeap
NoHeap --> NoHeap : toutes les opérations<br/>passent par la DB
}
Bascule cache → database
Quand le formateur désactive le cache, la ConcurrentHashMap est vidée. Les sessions existantes ne sont pas perdues (elles restent en base), mais elles disparaissent du heap. Les requêtes suivantes les rechargent depuis la base à la demande. C'est cohérent avec l'invariant write-through : la DB est toujours la source de vérité.
Limites et garde-fous¶
| Limite | Valeur | Pourquoi ? |
|---|---|---|
| Sessions actives simultanées | 500 maximum | Largement au-dessus du besoin réel (60 étudiants/TP). Au-delà, /join retourne 429 Too Many Requests. |
| Limite mémoire JVM | -Xms256m -Xmx1g (cf. JAVA_OPTS) |
500 sessions × ~2 KB ≈ 1 Mo de cache. Largement compatible avec la heap limit. |
| Purge | Pas de purge automatique | Les sessions restent en DB après la fin du TP. Le formateur peut appeler clearAll() via l'admin pour repartir d'un état propre. |
Pour aller plus loin¶
- Authentification — mécanisme
X-Student-Token - Schéma de données — table
pedagogique_sessions(V36) - Section Parcours pédagogique (LOT 5) — détail des 5 niveaux et des énigmes
- Section Chaos engineering (LOT 3) — chaos mémoire et heap dumps