- Create new docs/plans/ directory with organized structure - Add comprehensive PLAN_deadlock_detection_prevention.md (15h plan) - ThreadSanitizer integration (2h) - Helgrind validation (3h) - std::scoped_lock refactoring (4h) - std::shared_mutex optimization (6h) - Migrate 16 plans from planTI/ to docs/plans/ - Rename all files to PLAN_*.md convention - Update README.md with index and statuses - Remove old planTI/ directory - Add run_all_tests.sh script for test automation Plans now include: - 1 active development plan (deadlock prevention) - 3 test architecture plans - 13 integration test scenario plans 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
562 lines
18 KiB
Markdown
562 lines
18 KiB
Markdown
# Scénario 8: Config Hot-Reload
|
|
|
|
**Priorité**: ⭐ NICE TO HAVE
|
|
**Phase**: 3 (NICE TO HAVE)
|
|
**Durée estimée**: ~1 minute
|
|
**Effort implémentation**: ~3-4 heures
|
|
|
|
---
|
|
|
|
## 🎯 Objectif
|
|
|
|
Valider que le système peut modifier la configuration d'un module à la volée sans redémarrage, avec:
|
|
- Changement de paramètres en temps réel
|
|
- Reconfiguration sans perte de state
|
|
- Support de multiples types de configuration (scalaires, listes, objets)
|
|
- Validation des valeurs et rollback en cas d'erreur
|
|
|
|
---
|
|
|
|
## 📋 Description
|
|
|
|
### Setup Initial
|
|
1. Charger `ConfigurableModule` avec configuration de base:
|
|
- `spawnRate`: 10 entités/seconde
|
|
- `maxEntities`: 50
|
|
- `entitySpeed`: 5.0
|
|
- `colors`: ["red", "blue"]
|
|
- `physics.gravity`: 9.8
|
|
- `physics.friction`: 0.5
|
|
2. Spawner des entités pendant 10 secondes
|
|
3. Vérifier que les entités respectent la config initiale
|
|
|
|
### Phase 1: Config Hot-Reload Simple
|
|
1. À t=10s, modifier configuration:
|
|
- `spawnRate`: 20 (doubler le taux de spawn)
|
|
- `entitySpeed`: 10.0 (doubler la vitesse)
|
|
2. Appliquer config via `updateConfig()` sans reload du module
|
|
3. Vérifier que:
|
|
- Nouvelles entités utilisent nouvelle config
|
|
- Entités existantes **gardent** leur config d'origine (continuité)
|
|
- Pas de perte de state
|
|
|
|
### Phase 2: Config Hot-Reload Complexe
|
|
1. À t=20s, modifier configuration avancée:
|
|
- `maxEntities`: 100 (augmenter limite)
|
|
- `colors`: ["green", "yellow", "purple"] (nouvelles couleurs)
|
|
- `physics.gravity`: 1.6 (gravité lunaire)
|
|
2. Vérifier que:
|
|
- Nouvelles entités spawnent avec nouvelles couleurs
|
|
- Physique appliquée correctement
|
|
- Limite respectée
|
|
|
|
### Phase 3: Config Invalide + Rollback
|
|
1. À t=30s, tenter d'appliquer config invalide:
|
|
- `spawnRate`: -5 (négatif = invalide)
|
|
- `maxEntities`: 1000000 (trop grand)
|
|
2. Vérifier que:
|
|
- Config invalide rejetée
|
|
- Config précédente (valide) restaurée
|
|
- Aucune corruption de state
|
|
- Erreur loggée clairement
|
|
|
|
### Phase 4: Config Partielle
|
|
1. À t=40s, modifier seulement une partie de la config:
|
|
- `entitySpeed`: 2.0
|
|
- (laisser tous les autres paramètres inchangés)
|
|
2. Vérifier que:
|
|
- Seul `entitySpeed` est modifié
|
|
- Autres paramètres préservés
|
|
- Merge correct
|
|
|
|
---
|
|
|
|
## 🏗️ Implémentation
|
|
|
|
### ConfigurableModule Structure
|
|
|
|
```cpp
|
|
// ConfigurableModule.h
|
|
class ConfigurableModule : public IModule {
|
|
public:
|
|
struct Entity {
|
|
float x, y;
|
|
float vx, vy;
|
|
std::string color;
|
|
float speed; // Config snapshot à la création
|
|
int id;
|
|
};
|
|
|
|
struct Config {
|
|
int spawnRate; // Entités par seconde
|
|
int maxEntities; // Limite totale
|
|
float entitySpeed; // Vitesse de déplacement
|
|
std::vector<std::string> colors; // Couleurs disponibles
|
|
|
|
struct Physics {
|
|
float gravity;
|
|
float friction;
|
|
} physics;
|
|
|
|
// Validation
|
|
bool validate(std::string& errorMsg) const {
|
|
if (spawnRate < 0 || spawnRate > 1000) {
|
|
errorMsg = "spawnRate must be in [0, 1000]";
|
|
return false;
|
|
}
|
|
if (maxEntities < 1 || maxEntities > 10000) {
|
|
errorMsg = "maxEntities must be in [1, 10000]";
|
|
return false;
|
|
}
|
|
if (entitySpeed < 0.0f) {
|
|
errorMsg = "entitySpeed must be >= 0";
|
|
return false;
|
|
}
|
|
if (colors.empty()) {
|
|
errorMsg = "colors list cannot be empty";
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
};
|
|
|
|
void initialize(std::shared_ptr<IDataNode> config) override;
|
|
void process(float deltaTime) override;
|
|
std::shared_ptr<IDataNode> getState() const override;
|
|
void setState(std::shared_ptr<IDataNode> state) override;
|
|
|
|
// NOUVELLE API: Config hot-reload
|
|
bool updateConfig(std::shared_ptr<IDataNode> newConfig);
|
|
|
|
bool isIdle() const override { return true; }
|
|
|
|
private:
|
|
Config currentConfig;
|
|
Config previousConfig; // Pour rollback
|
|
std::vector<Entity> entities;
|
|
float spawnAccumulator = 0.0f;
|
|
int nextEntityId = 0;
|
|
|
|
void spawnEntity();
|
|
void updateEntity(Entity& entity, float dt);
|
|
Config parseConfig(std::shared_ptr<IDataNode> configNode);
|
|
};
|
|
```
|
|
|
|
### Configuration Format (JSON)
|
|
|
|
```json
|
|
{
|
|
"spawnRate": 10,
|
|
"maxEntities": 50,
|
|
"entitySpeed": 5.0,
|
|
"colors": ["red", "blue"],
|
|
"physics": {
|
|
"gravity": 9.8,
|
|
"friction": 0.5
|
|
}
|
|
}
|
|
```
|
|
|
|
### Test Principal
|
|
|
|
```cpp
|
|
// test_08_config_hotreload.cpp
|
|
#include "helpers/TestMetrics.h"
|
|
#include "helpers/TestAssertions.h"
|
|
#include "helpers/TestReporter.h"
|
|
|
|
int main() {
|
|
TestReporter reporter("Config Hot-Reload");
|
|
TestMetrics metrics;
|
|
|
|
// === SETUP ===
|
|
DebugEngine engine;
|
|
engine.loadModule("ConfigurableModule", "build/modules/libConfigurableModule.so");
|
|
|
|
// Config initiale
|
|
auto config = createJsonConfig({
|
|
{"spawnRate", 10},
|
|
{"maxEntities", 50},
|
|
{"entitySpeed", 5.0},
|
|
{"colors", json::array({"red", "blue"})},
|
|
{"physics", {
|
|
{"gravity", 9.8},
|
|
{"friction", 0.5}
|
|
}}
|
|
});
|
|
engine.initializeModule("ConfigurableModule", config);
|
|
|
|
std::cout << "=== Phase 0: Initial config (10s) ===\n";
|
|
|
|
// === PHASE 0: Baseline (10s) ===
|
|
for (int i = 0; i < 600; i++) { // 10s * 60 FPS
|
|
engine.update(1.0f/60.0f);
|
|
metrics.recordFPS(60.0f);
|
|
}
|
|
|
|
auto state0 = engine.getModuleState("ConfigurableModule");
|
|
auto json0 = getJsonFromState(state0);
|
|
int entityCount0 = json0["entities"].size();
|
|
|
|
// On devrait avoir ~100 entités (10/s * 10s)
|
|
ASSERT_WITHIN(entityCount0, 100, 10, "Should have ~100 entities after 10s");
|
|
reporter.addAssertion("initial_spawn_rate", true);
|
|
|
|
// Vérifier vitesse des entités
|
|
for (const auto& entity : json0["entities"]) {
|
|
float speed = entity["speed"];
|
|
ASSERT_EQ(speed, 5.0f, "Initial entity speed should be 5.0");
|
|
}
|
|
|
|
std::cout << "✓ Baseline: " << entityCount0 << " entities spawned\n";
|
|
|
|
// === PHASE 1: Simple Config Change (10s) ===
|
|
std::cout << "\n=== Phase 1: Doubling spawn rate and speed (10s) ===\n";
|
|
|
|
auto newConfig1 = createJsonConfig({
|
|
{"spawnRate", 20}, // Double spawn rate
|
|
{"maxEntities", 50},
|
|
{"entitySpeed", 10.0}, // Double speed
|
|
{"colors", json::array({"red", "blue"})},
|
|
{"physics", {
|
|
{"gravity", 9.8},
|
|
{"friction", 0.5}
|
|
}}
|
|
});
|
|
|
|
bool updateResult1 = engine.updateModuleConfig("ConfigurableModule", newConfig1);
|
|
ASSERT_TRUE(updateResult1, "Config update should succeed");
|
|
reporter.addAssertion("config_update_simple", updateResult1);
|
|
|
|
for (int i = 0; i < 600; i++) { // 10s * 60 FPS
|
|
engine.update(1.0f/60.0f);
|
|
}
|
|
|
|
auto state1 = engine.getModuleState("ConfigurableModule");
|
|
auto json1 = getJsonFromState(state1);
|
|
int entityCount1 = json1["entities"].size();
|
|
|
|
// On devrait maintenant avoir ~100 (initial) + ~200 (20/s * 10s) = ~300 entités
|
|
// Mais max = 50, donc on devrait avoir exactement 50
|
|
ASSERT_EQ(entityCount1, 50, "Should respect maxEntities limit");
|
|
reporter.addAssertion("max_entities_respected", entityCount1 == 50);
|
|
|
|
// Vérifier que les NOUVELLES entités ont speed = 10.0
|
|
int newEntityCount = 0;
|
|
for (const auto& entity : json1["entities"]) {
|
|
if (entity["id"] >= entityCount0) { // Nouvelle entité
|
|
float speed = entity["speed"];
|
|
ASSERT_EQ(speed, 10.0f, "New entities should have speed 10.0");
|
|
newEntityCount++;
|
|
}
|
|
}
|
|
std::cout << "✓ Config applied: " << newEntityCount << " new entities with speed 10.0\n";
|
|
|
|
// === PHASE 2: Complex Config Change (10s) ===
|
|
std::cout << "\n=== Phase 2: Complex config changes (10s) ===\n";
|
|
|
|
auto newConfig2 = createJsonConfig({
|
|
{"spawnRate", 15},
|
|
{"maxEntities", 100}, // Augmenter limite
|
|
{"entitySpeed", 7.5},
|
|
{"colors", json::array({"green", "yellow", "purple"})}, // Nouvelles couleurs
|
|
{"physics", {
|
|
{"gravity", 1.6}, // Gravité lunaire
|
|
{"friction", 0.2}
|
|
}}
|
|
});
|
|
|
|
bool updateResult2 = engine.updateModuleConfig("ConfigurableModule", newConfig2);
|
|
ASSERT_TRUE(updateResult2, "Config update 2 should succeed");
|
|
|
|
int entitiesBeforePhase2 = entityCount1;
|
|
|
|
for (int i = 0; i < 600; i++) { // 10s * 60 FPS
|
|
engine.update(1.0f/60.0f);
|
|
}
|
|
|
|
auto state2 = engine.getModuleState("ConfigurableModule");
|
|
auto json2 = getJsonFromState(state2);
|
|
int entityCount2 = json2["entities"].size();
|
|
|
|
// Vérifier que la limite a augmenté
|
|
ASSERT_LT(entitiesBeforePhase2, entityCount2, "Entity count should have increased");
|
|
ASSERT_LE(entityCount2, 100, "Should respect new maxEntities = 100");
|
|
|
|
// Vérifier couleurs des nouvelles entités
|
|
std::set<std::string> newColors;
|
|
for (const auto& entity : json2["entities"]) {
|
|
if (entity["id"] >= entitiesBeforePhase2) {
|
|
newColors.insert(entity["color"]);
|
|
}
|
|
}
|
|
|
|
bool hasNewColors = newColors.count("green") || newColors.count("yellow") || newColors.count("purple");
|
|
ASSERT_TRUE(hasNewColors, "New entities should use new color palette");
|
|
reporter.addAssertion("new_colors_applied", hasNewColors);
|
|
|
|
std::cout << "✓ Complex config applied: " << entityCount2 << " total entities, new colors: ";
|
|
for (const auto& color : newColors) std::cout << color << " ";
|
|
std::cout << "\n";
|
|
|
|
// === PHASE 3: Invalid Config + Rollback (5s) ===
|
|
std::cout << "\n=== Phase 3: Invalid config rejection (5s) ===\n";
|
|
|
|
auto invalidConfig = createJsonConfig({
|
|
{"spawnRate", -5}, // INVALIDE: négatif
|
|
{"maxEntities", 1000000}, // INVALIDE: trop grand
|
|
{"entitySpeed", 5.0},
|
|
{"colors", json::array({"red"})},
|
|
{"physics", {
|
|
{"gravity", 9.8},
|
|
{"friction", 0.5}
|
|
}}
|
|
});
|
|
|
|
bool updateResult3 = engine.updateModuleConfig("ConfigurableModule", invalidConfig);
|
|
ASSERT_FALSE(updateResult3, "Invalid config should be rejected");
|
|
reporter.addAssertion("invalid_config_rejected", !updateResult3);
|
|
|
|
// Continuer l'exécution - devrait utiliser la config précédente (valide)
|
|
for (int i = 0; i < 300; i++) { // 5s * 60 FPS
|
|
engine.update(1.0f/60.0f);
|
|
}
|
|
|
|
auto state3 = engine.getModuleState("ConfigurableModule");
|
|
auto json3 = getJsonFromState(state3);
|
|
|
|
// Vérifier que la config précédente (Phase 2) est toujours active
|
|
// On devrait avoir spawn à 15/s, pas -5 ni 0
|
|
int entityCount3 = json3["entities"].size();
|
|
ASSERT_GT(entityCount3, entityCount2, "Should continue spawning with previous valid config");
|
|
reporter.addAssertion("config_rollback_works", entityCount3 > entityCount2);
|
|
|
|
std::cout << "✓ Rollback successful: config unchanged, " << (entityCount3 - entityCount2) << " entities spawned\n";
|
|
|
|
// === PHASE 4: Partial Config Update (5s) ===
|
|
std::cout << "\n=== Phase 4: Partial config update (5s) ===\n";
|
|
|
|
auto partialConfig = createJsonConfig({
|
|
{"entitySpeed", 2.0} // Modifier seulement la vitesse
|
|
});
|
|
|
|
bool updateResult4 = engine.updateModuleConfigPartial("ConfigurableModule", partialConfig);
|
|
ASSERT_TRUE(updateResult4, "Partial config update should succeed");
|
|
|
|
for (int i = 0; i < 300; i++) { // 5s * 60 FPS
|
|
engine.update(1.0f/60.0f);
|
|
}
|
|
|
|
auto state4 = engine.getModuleState("ConfigurableModule");
|
|
auto json4 = getJsonFromState(state4);
|
|
|
|
// Vérifier que les nouvelles entités ont speed = 2.0
|
|
// Et que les autres paramètres (colors, etc.) sont inchangés de Phase 2
|
|
bool foundNewSpeed = false;
|
|
bool foundOldColors = false;
|
|
|
|
for (const auto& entity : json4["entities"]) {
|
|
if (entity["id"] >= entityCount3) {
|
|
if (entity["speed"] == 2.0f) foundNewSpeed = true;
|
|
std::string color = entity["color"];
|
|
if (color == "green" || color == "yellow" || color == "purple") {
|
|
foundOldColors = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
ASSERT_TRUE(foundNewSpeed, "New entities should have updated speed");
|
|
ASSERT_TRUE(foundOldColors, "Colors should be preserved from Phase 2");
|
|
reporter.addAssertion("partial_update_works", foundNewSpeed && foundOldColors);
|
|
|
|
std::cout << "✓ Partial update: speed changed, other params preserved\n";
|
|
|
|
// === VÉRIFICATIONS FINALES ===
|
|
|
|
// Memory stability
|
|
size_t memGrowth = metrics.getMemoryGrowth();
|
|
ASSERT_LT(memGrowth, 10 * 1024 * 1024, "Memory growth should be < 10MB");
|
|
reporter.addMetric("memory_growth_mb", memGrowth / (1024.0f * 1024.0f));
|
|
|
|
// No crashes
|
|
reporter.addAssertion("no_crashes", true);
|
|
|
|
// === RAPPORT FINAL ===
|
|
metrics.printReport();
|
|
reporter.printFinalReport();
|
|
|
|
return reporter.getExitCode();
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 📊 Métriques Collectées
|
|
|
|
| Métrique | Description | Seuil |
|
|
|----------|-------------|-------|
|
|
| **config_update_time_ms** | Temps d'application de nouvelle config | < 50ms |
|
|
| **memory_growth_mb** | Croissance mémoire totale | < 10MB |
|
|
| **config_rollback_time_ms** | Temps de rollback si invalide | < 10ms |
|
|
| **partial_update_accuracy** | Précision du merge partiel | 100% |
|
|
|
|
---
|
|
|
|
## ✅ Critères de Succès
|
|
|
|
### MUST PASS
|
|
1. ✅ Config update appliquée en < 50ms
|
|
2. ✅ Nouvelles entités utilisent nouvelle config
|
|
3. ✅ Entités existantes préservent leur config (continuité)
|
|
4. ✅ Config invalide rejetée + rollback automatique
|
|
5. ✅ Partial update fusionne correctement
|
|
6. ✅ Aucun crash
|
|
|
|
### NICE TO HAVE
|
|
1. ✅ Config update en < 10ms (très rapide)
|
|
2. ✅ Validation détaillée avec messages d'erreur clairs
|
|
3. ✅ Support de nested objects (physics.gravity)
|
|
4. ✅ Historique des configs (undo/redo)
|
|
|
|
---
|
|
|
|
## 🔧 API Nécessaires dans IModule
|
|
|
|
### Nouvelle méthode à ajouter
|
|
|
|
```cpp
|
|
class IModule {
|
|
public:
|
|
// Méthodes existantes
|
|
virtual void initialize(std::shared_ptr<IDataNode> config) = 0;
|
|
virtual void process(float deltaTime) = 0;
|
|
virtual std::shared_ptr<IDataNode> getState() const = 0;
|
|
virtual void setState(std::shared_ptr<IDataNode> state) = 0;
|
|
virtual bool isIdle() const = 0;
|
|
|
|
// NOUVELLE: Config hot-reload
|
|
virtual bool updateConfig(std::shared_ptr<IDataNode> newConfig) {
|
|
// Default implementation: reject
|
|
return false;
|
|
}
|
|
|
|
// NOUVELLE: Partial config update
|
|
virtual bool updateConfigPartial(std::shared_ptr<IDataNode> partialConfig) {
|
|
// Default implementation: delegate to full update
|
|
return updateConfig(partialConfig);
|
|
}
|
|
};
|
|
```
|
|
|
|
### DebugEngine Support
|
|
|
|
```cpp
|
|
class DebugEngine {
|
|
public:
|
|
// Nouvelle méthode
|
|
bool updateModuleConfig(const std::string& moduleName, std::shared_ptr<IDataNode> newConfig) {
|
|
auto it = modules.find(moduleName);
|
|
if (it == modules.end()) return false;
|
|
|
|
return it->second->updateConfig(newConfig);
|
|
}
|
|
|
|
bool updateModuleConfigPartial(const std::string& moduleName, std::shared_ptr<IDataNode> partialConfig) {
|
|
auto it = modules.find(moduleName);
|
|
if (it == modules.end()) return false;
|
|
|
|
return it->second->updateConfigPartial(partialConfig);
|
|
}
|
|
};
|
|
```
|
|
|
|
---
|
|
|
|
## 🐛 Cas d'Erreur Attendus
|
|
|
|
| Erreur | Cause | Action |
|
|
|--------|-------|--------|
|
|
| Config rejetée alors que valide | Validation trop stricte | FAIL - ajuster règles |
|
|
| Config invalide acceptée | Pas de validation | FAIL - implémenter validate() |
|
|
| State corrompu après config change | Mauvaise gestion | FAIL - séparer config et state |
|
|
| Rollback échoue | Pas de backup | FAIL - sauvegarder prev config |
|
|
| Partial update écrase tout | Merge incorrect | FAIL - implémenter merge proper |
|
|
|
|
---
|
|
|
|
## 📝 Output Attendu
|
|
|
|
```
|
|
================================================================================
|
|
TEST: Config Hot-Reload
|
|
================================================================================
|
|
|
|
=== Phase 0: Initial config (10s) ===
|
|
✓ Baseline: 102 entities spawned
|
|
|
|
=== Phase 1: Doubling spawn rate and speed (10s) ===
|
|
✓ Config applied: 47 new entities with speed 10.0
|
|
|
|
=== Phase 2: Complex config changes (10s) ===
|
|
✓ Complex config applied: 98 total entities, new colors: green purple yellow
|
|
|
|
=== Phase 3: Invalid config rejection (5s) ===
|
|
[ERROR] Config validation failed: spawnRate must be in [0, 1000]
|
|
[ERROR] Config validation failed: maxEntities must be in [1, 10000]
|
|
✓ Rollback successful: config unchanged, 73 entities spawned
|
|
|
|
=== Phase 4: Partial config update (5s) ===
|
|
✓ Partial update: speed changed, other params preserved
|
|
|
|
================================================================================
|
|
METRICS
|
|
================================================================================
|
|
Config update time: 8ms (threshold: < 50ms) ✓
|
|
Memory growth: 4.2MB (threshold: < 10MB) ✓
|
|
|
|
================================================================================
|
|
ASSERTIONS
|
|
================================================================================
|
|
✓ initial_spawn_rate
|
|
✓ config_update_simple
|
|
✓ max_entities_respected
|
|
✓ new_colors_applied
|
|
✓ invalid_config_rejected
|
|
✓ config_rollback_works
|
|
✓ partial_update_works
|
|
✓ no_crashes
|
|
|
|
Result: ✅ PASSED
|
|
|
|
================================================================================
|
|
```
|
|
|
|
---
|
|
|
|
## 📅 Planning
|
|
|
|
**Jour 8 (3h):**
|
|
- Implémenter `updateConfig()` et `updateConfigPartial()` dans IModule
|
|
- Implémenter ConfigurableModule avec validation
|
|
|
|
**Jour 9 (1h):**
|
|
- Implémenter test_08_config_hotreload.cpp
|
|
- Debug + validation
|
|
|
|
---
|
|
|
|
## 🎯 Valeur Ajoutée
|
|
|
|
Ce test valide une fonctionnalité **cruciale pour le développement** :
|
|
- **Tweaking en temps réel** : Ajuster paramètres de gameplay sans redémarrer
|
|
- **A/B testing** : Tester différentes configs rapidement
|
|
- **Debug facilité** : Modifier valeurs pour reproduire bugs
|
|
- **Production-ready** : Hot-config peut servir en prod (feature flags, tuning)
|
|
|
|
Contrairement au hot-reload de code (recompilation), le hot-reload de config est **instantané** et ne nécessite pas de rebuild.
|
|
|
|
---
|
|
|
|
**Prochaine étape**: Implémentation
|