# Scénario 13: Cross-System Integration (IO + DataNode) **Priorité**: ⭐⭐ SHOULD HAVE **Phase**: 2 (SHOULD HAVE) **Durée estimée**: ~6 minutes **Effort implémentation**: ~6-8 heures --- ## 🎯 Objectif Valider que les systèmes IO (IntraIO) et DataNode (IDataTree) fonctionnent correctement **ensemble** dans des cas d'usage réels: - Config hot-reload → republish via IO - State persistence via DataNode + message routing via IO - Multi-module coordination (Module A publie state → Module B lit via DataNode) - Concurrent access (IO threads + DataNode threads) - Integration avec hot-reload de modules - Performance du système complet **Note**: Ce test valide l'intégration complète du moteur, pas les composants isolés. --- ## 📋 Description ### Setup Initial 1. Créer IDataTree avec structure complète: - **config/** - Configuration modules (units, gameplay, network) - **data/** - State persistence (player, world, economy) - **runtime/** - State temporaire (fps, metrics, active_entities) 2. Créer 4 modules avec IO + DataNode: - **ConfigWatcherModule** - Surveille config/, publie changements via IO - **PlayerModule** - Gère state joueur, persiste via data/, publie events - **EconomyModule** - Souscrit à player events, met à jour economy data/ - **MetricsModule** - Collecte metrics dans runtime/, publie stats 3. Total: 4 modules communicant via IO et partageant data via DataNode ### Test Séquence #### Test 1: Config Hot-Reload → IO Broadcast (60s) 1. ConfigWatcherModule souscrit à hot-reload callbacks 2. Modifier `config/gameplay.json` (changer difficulty) 3. Quand callback déclenché: - ConfigWatcherModule publie "config:gameplay:changed" via IO - PlayerModule souscrit et reçoit notification - PlayerModule lit nouvelle config via DataNode - PlayerModule ajuste son comportement 4. Vérifier: - Callback → publish → subscribe → read chain fonctionne - Nouvelle config appliquée dans PlayerModule - Latence totale < 100ms #### Test 2: State Persistence + Event Publishing (60s) 1. PlayerModule crée state: - `data/player/profile` - {name, level, gold} - `data/player/inventory` - {items[]} 2. PlayerModule sauvegarde via `tree->saveNode()` 3. PlayerModule publie "player:level_up" via IO 4. EconomyModule souscrit à "player:*" 5. EconomyModule reçoit event, lit player data via DataNode 6. EconomyModule calcule bonus, met à jour `data/economy/bonuses` 7. EconomyModule sauvegarde via `tree->saveNode()` 8. Vérifier: - Save → publish → subscribe → read → save chain fonctionne - Data persistence correcte - Pas de race conditions #### Test 3: Multi-Module State Synchronization (90s) 1. PlayerModule met à jour `data/player/gold` = 1000 2. PlayerModule publie "player:gold:updated" avec {gold: 1000} 3. EconomyModule reçoit event via IO 4. EconomyModule lit `data/player/gold` via DataNode 5. Vérifier cohérence: - Valeur dans message IO = valeur dans DataNode - Pas de désynchronisation - Order des events préservé 6. Répéter 100 fois avec updates rapides 7. Vérifier consistency finale #### Test 4: Runtime Metrics Collection (60s) 1. MetricsModule collecte metrics toutes les 100ms: - `runtime/fps` - FPS actuel - `runtime/memory` - Memory usage - `runtime/message_count` - Messages IO 2. MetricsModule publie "metrics:snapshot" toutes les secondes 3. ConfigWatcherModule souscrit et log metrics 4. Vérifier: - Runtime data pas persisté (pas de fichiers) - Metrics publishing fonctionne - Low-frequency batching optimise (pas 10 msg/s mais 1 msg/s) #### Test 5: Concurrent Access (IO + DataNode) (90s) 1. Lancer 4 threads: - Thread 1: PlayerModule publie events à 100 Hz - Thread 2: EconomyModule lit data/ à 50 Hz - Thread 3: MetricsModule écrit runtime/ à 100 Hz - Thread 4: ConfigWatcherModule lit config/ à 10 Hz 2. Exécuter pendant 60 secondes 3. Vérifier: - Aucun crash - Aucune corruption de data - Aucun deadlock - Performance acceptable (< 10% overhead) #### Test 6: Hot-Reload Module + Preserve State (90s) 1. PlayerModule a state actif: - 50 entities dans `runtime/entities` - Gold = 5000 dans `data/player/gold` - Active quest dans `runtime/quest` 2. Déclencher hot-reload de PlayerModule: - `getState()` extrait tout (data/ + runtime/) - Recompile module - `setState()` restaure 3. Pendant reload: - EconomyModule continue de publier via IO - Messages accumulés dans queue PlayerModule 4. Après reload: - PlayerModule pull messages accumulés - Vérifie state préservé (50 entities, 5000 gold, quest) - Continue processing normalement 5. Vérifier: - State complet préservé (DataNode + runtime) - Messages pas perdus (IO queue) - Pas de corruption #### Test 7: Config Change Cascades (60s) 1. Modifier `config/gameplay.json` → difficulty = "hard" 2. ConfigWatcherModule détecte → publie "config:gameplay:changed" 3. PlayerModule reçoit → reload config → ajuste HP multiplier 4. PlayerModule publie "player:config:updated" 5. EconomyModule reçoit → reload config → ajuste prices 6. EconomyModule publie "economy:config:updated" 7. MetricsModule reçoit → log cascade 8. Vérifier: - Cascade complète en < 500ms - Tous modules synchronisés - Ordre des events correct #### Test 8: Large State + High-Frequency IO (60s) 1. Créer large DataNode tree (1000 nodes) 2. Publier 10k messages/s via IO 3. Modules lisent DataNode pendant IO flood 4. Mesurer: - Latence IO: < 10ms p99 - Latence DataNode read: < 5ms p99 - Memory growth: < 20MB - CPU usage: < 80% 5. Vérifier: - Systèmes restent performants - Pas de dégradation mutuelle --- ## 🏗️ Implémentation ### ConfigWatcherModule Structure ```cpp // ConfigWatcherModule.h class ConfigWatcherModule : public IModule { public: void initialize(std::shared_ptr config) override; void process(float deltaTime) override; std::shared_ptr getState() const override; void setState(std::shared_ptr state) override; bool isIdle() const override { return true; } private: std::shared_ptr io; std::shared_ptr tree; void onConfigReloaded(); void publishConfigChange(const std::string& configName); }; ``` ### PlayerModule Structure ```cpp // PlayerModule.h class PlayerModule : public IModule { public: void initialize(std::shared_ptr config) override; void process(float deltaTime) override; std::shared_ptr getState() const override; void setState(std::shared_ptr state) override; bool isIdle() const override { return true; } private: std::shared_ptr io; std::shared_ptr tree; int gold = 0; int level = 1; std::vector inventory; void handleConfigChange(); void savePlayerData(); void publishLevelUp(); }; ``` ### Test Principal ```cpp // test_13_cross_system.cpp #include "helpers/TestMetrics.h" #include "helpers/TestAssertions.h" #include "helpers/TestReporter.h" #include #include int main() { TestReporter reporter("Cross-System Integration"); TestMetrics metrics; // === SETUP === std::filesystem::create_directories("test_cross/config"); std::filesystem::create_directories("test_cross/data"); auto tree = std::make_shared("test_cross"); DebugEngine engine; engine.setDataTree(tree); // Charger modules engine.loadModule("ConfigWatcherModule", "build/modules/libConfigWatcherModule.so"); engine.loadModule("PlayerModule", "build/modules/libPlayerModule.so"); engine.loadModule("EconomyModule", "build/modules/libEconomyModule.so"); engine.loadModule("MetricsModule", "build/modules/libMetricsModule.so"); auto config = createJsonConfig({ {"transport", "intra"}, {"instanceId", "test_cross"} }); engine.initializeModule("ConfigWatcherModule", config); engine.initializeModule("PlayerModule", config); engine.initializeModule("EconomyModule", config); engine.initializeModule("MetricsModule", config); // ======================================================================== // TEST 1: Config Hot-Reload → IO Broadcast // ======================================================================== std::cout << "\n=== TEST 1: Config Hot-Reload → IO Broadcast ===\n"; // Créer config initial nlohmann::json gameplayConfig = { {"difficulty", "normal"}, {"hpMultiplier", 1.0} }; std::ofstream configFile("test_cross/config/gameplay.json"); configFile << gameplayConfig.dump(2); configFile.close(); tree->loadConfigFile("gameplay.json"); // Setup reload callback std::atomic configChangedEvents{0}; auto playerIO = engine.getModuleIO("PlayerModule"); playerIO->subscribe("config:gameplay:changed", {}); // ConfigWatcherModule setup callback tree->onTreeReloaded([&]() { std::cout << " → Config reloaded, publishing event...\n"; auto watcherIO = engine.getModuleIO("ConfigWatcherModule"); auto data = std::make_unique(nlohmann::json{ {"config", "gameplay"}, {"timestamp", std::time(nullptr)} }); watcherIO->publish("config:gameplay:changed", std::move(data)); }); // Modifier config std::this_thread::sleep_for(std::chrono::milliseconds(100)); gameplayConfig["difficulty"] = "hard"; gameplayConfig["hpMultiplier"] = 1.5; std::ofstream configFile2("test_cross/config/gameplay.json"); configFile2 << gameplayConfig.dump(2); configFile2.close(); auto reloadStart = std::chrono::high_resolution_clock::now(); // Trigger reload tree->reloadIfChanged(); // Process pour permettre IO routing engine.update(1.0f/60.0f); // PlayerModule vérifie message if (playerIO->hasMessages() > 0) { auto msg = playerIO->pullMessage(); configChangedEvents++; // PlayerModule lit nouvelle config auto gameplay = tree->getConfigRoot()->getChild("gameplay"); std::string difficulty = gameplay->getString("difficulty"); double hpMult = gameplay->getDouble("hpMultiplier"); std::cout << " PlayerModule received config change: difficulty=" << difficulty << ", hpMult=" << hpMult << "\n"; ASSERT_EQ(difficulty, "hard", "Difficulty should be updated"); ASSERT_EQ(hpMult, 1.5, "HP multiplier should be updated"); } auto reloadEnd = std::chrono::high_resolution_clock::now(); float reloadLatency = std::chrono::duration(reloadEnd - reloadStart).count(); std::cout << "Total latency (reload + publish + subscribe + read): " << reloadLatency << "ms\n"; ASSERT_LT(reloadLatency, 100.0f, "Total latency should be < 100ms"); ASSERT_EQ(configChangedEvents, 1, "Should receive exactly 1 config change event"); reporter.addMetric("config_reload_latency_ms", reloadLatency); reporter.addAssertion("config_hotreload_chain", true); std::cout << "✓ TEST 1 PASSED\n"; // ======================================================================== // TEST 2: State Persistence + Event Publishing // ======================================================================== std::cout << "\n=== TEST 2: State Persistence + Event Publishing ===\n"; auto dataRoot = tree->getDataRoot(); // PlayerModule crée state auto player = std::make_shared("player", nlohmann::json::object()); auto profile = std::make_shared("profile", nlohmann::json{ {"name", "TestPlayer"}, {"level", 5}, {"gold", 1000} }); player->setChild("profile", profile); dataRoot->setChild("player", player); // Save tree->saveNode("data/player"); // Verify file ASSERT_TRUE(std::filesystem::exists("test_cross/data/player/profile.json"), "Profile should be saved"); // PlayerModule publie level up auto levelUpData = std::make_unique(nlohmann::json{ {"event", "level_up"}, {"newLevel", 6}, {"goldBonus", 500} }); playerIO->publish("player:level_up", std::move(levelUpData)); // EconomyModule souscrit auto economyIO = engine.getModuleIO("EconomyModule"); economyIO->subscribe("player:*", {}); engine.update(1.0f/60.0f); // EconomyModule reçoit et traite if (economyIO->hasMessages() > 0) { auto msg = economyIO->pullMessage(); std::cout << " EconomyModule received: " << msg.topic << "\n"; // EconomyModule lit player data auto playerData = tree->getDataRoot()->getChild("player")->getChild("profile"); int gold = playerData->getInt("gold"); std::cout << " Player gold: " << gold << "\n"; // EconomyModule calcule bonus int goldBonus = 500; int newGold = gold + goldBonus; // Update data playerData->setInt("gold", newGold); // Create economy bonuses auto economy = std::make_shared("economy", nlohmann::json::object()); auto bonuses = std::make_shared("bonuses", nlohmann::json{ {"levelUpBonus", goldBonus}, {"appliedAt", std::time(nullptr)} }); economy->setChild("bonuses", bonuses); dataRoot->setChild("economy", economy); // Save economy data tree->saveNode("data/economy"); std::cout << " EconomyModule updated bonuses and saved\n"; } // Verify full chain ASSERT_TRUE(std::filesystem::exists("test_cross/data/economy/bonuses.json"), "Economy bonuses should be saved"); reporter.addAssertion("state_persistence_chain", true); std::cout << "✓ TEST 2 PASSED\n"; // ======================================================================== // TEST 3: Multi-Module State Synchronization // ======================================================================== std::cout << "\n=== TEST 3: Multi-Module State Synchronization ===\n"; int syncErrors = 0; for (int i = 0; i < 100; i++) { // PlayerModule met à jour gold int goldValue = 1000 + i * 10; auto playerProfile = tree->getDataRoot()->getChild("player")->getChild("profile"); playerProfile->setInt("gold", goldValue); // PlayerModule publie event avec valeur auto goldUpdate = std::make_unique(nlohmann::json{ {"event", "gold_updated"}, {"gold", goldValue} }); playerIO->publish("player:gold:updated", std::move(goldUpdate)); engine.update(1.0f/60.0f); // EconomyModule reçoit et vérifie cohérence if (economyIO->hasMessages() > 0) { auto msg = economyIO->pullMessage(); auto* msgData = dynamic_cast(msg.data.get()); int msgGold = msgData->getJsonData()["gold"]; // Lire DataNode auto playerData = tree->getDataRoot()->getChild("player")->getChild("profile"); int dataGold = playerData->getInt("gold"); if (msgGold != dataGold) { std::cerr << " SYNC ERROR: msg=" << msgGold << " data=" << dataGold << "\n"; syncErrors++; } } } std::cout << "Synchronization errors: " << syncErrors << " / 100\n"; ASSERT_EQ(syncErrors, 0, "Should have zero synchronization errors"); reporter.addMetric("sync_errors", syncErrors); reporter.addAssertion("state_synchronization", syncErrors == 0); std::cout << "✓ TEST 3 PASSED\n"; // ======================================================================== // TEST 4: Runtime Metrics Collection // ======================================================================== std::cout << "\n=== TEST 4: Runtime Metrics Collection ===\n"; auto runtimeRoot = tree->getRuntimeRoot(); auto metricsIO = engine.getModuleIO("MetricsModule"); // MetricsModule publie metrics avec low-freq batching IIO::SubscriptionConfig metricsConfig; metricsConfig.replaceable = true; metricsConfig.batchInterval = 1000; // 1 second playerIO->subscribeLowFreq("metrics:*", metricsConfig); // Simulate 3 seconds de metrics collection for (int sec = 0; sec < 3; sec++) { for (int i = 0; i < 10; i++) { // MetricsModule collecte metrics auto metrics = std::make_shared("metrics", nlohmann::json{ {"fps", 60.0}, {"memory", 125000000 + i * 1000}, {"messageCount", i * 100} }); runtimeRoot->setChild("metrics", metrics); // Publie snapshot auto snapshot = std::make_unique(nlohmann::json{ {"fps", 60.0}, {"memory", 125000000 + i * 1000}, {"timestamp", std::time(nullptr)} }); metricsIO->publish("metrics:snapshot", std::move(snapshot)); std::this_thread::sleep_for(std::chrono::milliseconds(100)); engine.update(1.0f/60.0f); } } // Vérifier batching int snapshotsReceived = 0; while (playerIO->hasMessages() > 0) { playerIO->pullMessage(); snapshotsReceived++; } std::cout << "Snapshots received: " << snapshotsReceived << " (expected ~3 due to batching)\n"; ASSERT_TRUE(snapshotsReceived >= 2 && snapshotsReceived <= 4, "Should receive ~3 batched snapshots"); // Vérifier runtime pas persisté ASSERT_FALSE(std::filesystem::exists("test_cross/runtime"), "Runtime data should not be persisted"); reporter.addMetric("batched_snapshots", snapshotsReceived); reporter.addAssertion("runtime_metrics", true); std::cout << "✓ TEST 4 PASSED\n"; // ======================================================================== // TEST 5: Concurrent Access (IO + DataNode) // ======================================================================== std::cout << "\n=== TEST 5: Concurrent Access ===\n"; std::atomic running{true}; std::atomic publishCount{0}; std::atomic readCount{0}; std::atomic writeCount{0}; std::atomic errors{0}; // Thread 1: PlayerModule publie events std::thread pubThread([&]() { while (running) { try { auto data = std::make_unique(nlohmann::json{{"id", publishCount++}}); playerIO->publish("concurrent:test", std::move(data)); std::this_thread::sleep_for(std::chrono::milliseconds(10)); } catch (...) { errors++; } } }); // Thread 2: EconomyModule lit data/ std::thread readThread([&]() { while (running) { try { auto playerData = tree->getDataRoot()->getChild("player"); if (playerData) { auto profile = playerData->getChild("profile"); if (profile) { int gold = profile->getInt("gold", 0); readCount++; } } std::this_thread::sleep_for(std::chrono::milliseconds(20)); } catch (...) { errors++; } } }); // Thread 3: MetricsModule écrit runtime/ std::thread writeThread([&]() { while (running) { try { auto metrics = std::make_shared("metrics", nlohmann::json{ {"counter", writeCount++} }); runtimeRoot->setChild("metrics", metrics); std::this_thread::sleep_for(std::chrono::milliseconds(10)); } catch (...) { errors++; } } }); // Thread 4: ConfigWatcherModule lit config/ std::thread configThread([&]() { while (running) { try { auto gameplay = tree->getConfigRoot()->getChild("gameplay"); if (gameplay) { std::string diff = gameplay->getString("difficulty", "normal"); } std::this_thread::sleep_for(std::chrono::milliseconds(100)); } catch (...) { errors++; } } }); // Run for 5 seconds auto concurrentStart = std::chrono::high_resolution_clock::now(); std::this_thread::sleep_for(std::chrono::seconds(5)); running = false; auto concurrentEnd = std::chrono::high_resolution_clock::now(); pubThread.join(); readThread.join(); writeThread.join(); configThread.join(); float duration = std::chrono::duration(concurrentEnd - concurrentStart).count(); std::cout << "Concurrent test ran for " << duration << "s\n"; std::cout << " Publishes: " << publishCount << "\n"; std::cout << " Reads: " << readCount << "\n"; std::cout << " Writes: " << writeCount << "\n"; std::cout << " Errors: " << errors << "\n"; ASSERT_EQ(errors, 0, "Should have zero errors during concurrent access"); ASSERT_GT(publishCount, 0, "Should have published messages"); ASSERT_GT(readCount, 0, "Should have read data"); ASSERT_GT(writeCount, 0, "Should have written data"); reporter.addMetric("concurrent_publishes", publishCount); reporter.addMetric("concurrent_reads", readCount); reporter.addMetric("concurrent_writes", writeCount); reporter.addMetric("concurrent_errors", errors); reporter.addAssertion("concurrent_access", errors == 0); std::cout << "✓ TEST 5 PASSED\n"; // ======================================================================== // TEST 6: Hot-Reload Module + Preserve State // ======================================================================== std::cout << "\n=== TEST 6: Hot-Reload Module + Preserve State ===\n"; // PlayerModule crée state complexe auto entities = std::make_shared("entities", nlohmann::json::array()); for (int i = 0; i < 50; i++) { entities->getJsonData().push_back({{"id", i}, {"hp", 100}}); } runtimeRoot->setChild("entities", entities); auto playerGold = tree->getDataRoot()->getChild("player")->getChild("profile"); playerGold->setInt("gold", 5000); tree->saveNode("data/player/profile"); auto quest = std::make_shared("quest", nlohmann::json{ {"active", true}, {"questId", 42} }); runtimeRoot->setChild("quest", quest); std::cout << "State before reload: 50 entities, 5000 gold, quest #42 active\n"; // EconomyModule publie messages pendant reload std::thread spamThread([&]() { for (int i = 0; i < 100; i++) { auto data = std::make_unique(nlohmann::json{{"spam", i}}); economyIO->publish("player:spam", std::move(data)); std::this_thread::sleep_for(std::chrono::milliseconds(10)); } }); // Trigger hot-reload de PlayerModule std::this_thread::sleep_for(std::chrono::milliseconds(200)); auto stateBefore = engine.getModuleState("PlayerModule"); modifySourceFile("tests/modules/PlayerModule.cpp", "v1.0", "v2.0"); system("cmake --build build --target PlayerModule 2>&1 > /dev/null"); engine.reloadModule("PlayerModule"); spamThread.join(); // Vérifier state après reload auto stateAfter = engine.getModuleState("PlayerModule"); auto entitiesAfter = runtimeRoot->getChild("entities"); int entityCount = entitiesAfter->getJsonData().size(); std::cout << "Entities after reload: " << entityCount << "\n"; ASSERT_EQ(entityCount, 50, "Should preserve 50 entities"); auto goldAfter = tree->getDataRoot()->getChild("player")->getChild("profile"); int goldValue = goldAfter->getInt("gold"); std::cout << "Gold after reload: " << goldValue << "\n"; ASSERT_EQ(goldValue, 5000, "Should preserve 5000 gold"); auto questAfter = runtimeRoot->getChild("quest"); bool questActive = questAfter->getBool("active"); int questId = questAfter->getInt("questId"); std::cout << "Quest after reload: active=" << questActive << ", id=" << questId << "\n"; ASSERT_EQ(questActive, true, "Quest should still be active"); ASSERT_EQ(questId, 42, "Quest ID should be preserved"); // Vérifier messages pas perdus int spamReceived = 0; while (playerIO->hasMessages() > 0) { playerIO->pullMessage(); spamReceived++; } std::cout << "Spam messages received after reload: " << spamReceived << "\n"; ASSERT_GT(spamReceived, 0, "Should receive queued messages after reload"); reporter.addAssertion("hotreload_preserve_state", true); reporter.addMetric("spam_messages_queued", spamReceived); std::cout << "✓ TEST 6 PASSED\n"; // ======================================================================== // TEST 7: Config Change Cascades // ======================================================================== std::cout << "\n=== TEST 7: Config Change Cascades ===\n"; // Subscribe chain playerIO->subscribe("config:*", {}); economyIO->subscribe("player:*", {}); metricsIO->subscribe("economy:*", {}); auto cascadeStart = std::chrono::high_resolution_clock::now(); // 1. Modifier config gameplayConfig["difficulty"] = "extreme"; std::ofstream configFile3("test_cross/config/gameplay.json"); configFile3 << gameplayConfig.dump(2); configFile3.close(); // 2. Trigger reload tree->reloadIfChanged(); auto watcherIO = engine.getModuleIO("ConfigWatcherModule"); watcherIO->publish("config:gameplay:changed", std::make_unique(nlohmann::json{{"config", "gameplay"}})); engine.update(1.0f/60.0f); // 3. PlayerModule reçoit et publie if (playerIO->hasMessages() > 0) { playerIO->pullMessage(); playerIO->publish("player:config:updated", std::make_unique(nlohmann::json{{"hpMult", 2.0}})); } engine.update(1.0f/60.0f); // 4. EconomyModule reçoit et publie if (economyIO->hasMessages() > 0) { economyIO->pullMessage(); economyIO->publish("economy:config:updated", std::make_unique(nlohmann::json{{"pricesMult", 1.5}})); } engine.update(1.0f/60.0f); // 5. MetricsModule reçoit et log if (metricsIO->hasMessages() > 0) { metricsIO->pullMessage(); std::cout << " → Cascade complete!\n"; } auto cascadeEnd = std::chrono::high_resolution_clock::now(); float cascadeTime = std::chrono::duration(cascadeEnd - cascadeStart).count(); std::cout << "Cascade latency: " << cascadeTime << "ms\n"; ASSERT_LT(cascadeTime, 500.0f, "Cascade should complete in < 500ms"); reporter.addMetric("cascade_latency_ms", cascadeTime); reporter.addAssertion("config_cascade", true); std::cout << "✓ TEST 7 PASSED\n"; // ======================================================================== // TEST 8: Large State + High-Frequency IO // ======================================================================== std::cout << "\n=== TEST 8: Large State + High-Frequency IO ===\n"; // Créer large tree (1000 nodes) auto largeRoot = tree->getDataRoot(); for (int i = 0; i < 100; i++) { auto category = std::make_shared("cat_" + std::to_string(i), nlohmann::json::object()); for (int j = 0; j < 10; j++) { auto item = std::make_shared("item_" + std::to_string(j), nlohmann::json{ {"id", i * 10 + j}, {"value", (i * 10 + j) * 100} }); category->setChild("item_" + std::to_string(j), item); } largeRoot->setChild("cat_" + std::to_string(i), category); } std::cout << "Created large DataNode tree (1000 nodes)\n"; // High-frequency IO + concurrent DataNode reads std::atomic ioPublished{0}; std::atomic dataReads{0}; std::vector ioLatencies; std::vector dataLatencies; running = true; std::thread ioThread([&]() { while (running) { auto start = std::chrono::high_resolution_clock::now(); auto data = std::make_unique(nlohmann::json{{"id", ioPublished++}}); playerIO->publish("stress:test", std::move(data)); auto end = std::chrono::high_resolution_clock::now(); float latency = std::chrono::duration(end - start).count(); ioLatencies.push_back(latency); // Target: 10k msg/s = 0.1ms interval std::this_thread::sleep_for(std::chrono::microseconds(100)); } }); std::thread dataThread([&]() { while (running) { auto start = std::chrono::high_resolution_clock::now(); auto cat = largeRoot->getChild("cat_50"); if (cat) { auto item = cat->getChild("item_5"); if (item) { int value = item->getInt("value", 0); dataReads++; } } auto end = std::chrono::high_resolution_clock::now(); float latency = std::chrono::duration(end - start).count(); dataLatencies.push_back(latency); std::this_thread::sleep_for(std::chrono::microseconds(500)); } }); auto memBefore = getCurrentMemoryUsage(); std::this_thread::sleep_for(std::chrono::seconds(5)); running = false; ioThread.join(); dataThread.join(); auto memAfter = getCurrentMemoryUsage(); long memGrowth = static_cast(memAfter) - static_cast(memBefore); // Calculate p99 latencies std::sort(ioLatencies.begin(), ioLatencies.end()); std::sort(dataLatencies.begin(), dataLatencies.end()); float ioP99 = ioLatencies[static_cast(ioLatencies.size() * 0.99)]; float dataP99 = dataLatencies[static_cast(dataLatencies.size() * 0.99)]; std::cout << "Performance results:\n"; std::cout << " IO published: " << ioPublished << " messages\n"; std::cout << " IO p99 latency: " << ioP99 << "ms\n"; std::cout << " DataNode reads: " << dataReads << "\n"; std::cout << " DataNode p99 latency: " << dataP99 << "ms\n"; std::cout << " Memory growth: " << (memGrowth / 1024.0 / 1024.0) << "MB\n"; ASSERT_LT(ioP99, 10.0f, "IO p99 latency should be < 10ms"); ASSERT_LT(dataP99, 5.0f, "DataNode p99 latency should be < 5ms"); ASSERT_LT(memGrowth, 20 * 1024 * 1024, "Memory growth should be < 20MB"); reporter.addMetric("io_p99_latency_ms", ioP99); reporter.addMetric("datanode_p99_latency_ms", dataP99); reporter.addMetric("memory_growth_mb", memGrowth / 1024.0 / 1024.0); reporter.addAssertion("performance_under_load", true); std::cout << "✓ TEST 8 PASSED\n"; // ======================================================================== // CLEANUP // ======================================================================== std::filesystem::remove_all("test_cross"); // ======================================================================== // RAPPORT FINAL // ======================================================================== metrics.printReport(); reporter.printFinalReport(); return reporter.getExitCode(); } ``` --- ## 📊 Métriques Collectées | Métrique | Description | Seuil | |----------|-------------|-------| | **config_reload_latency_ms** | Latence reload→publish→subscribe→read | < 100ms | | **sync_errors** | Erreurs synchronisation IO/DataNode | 0 | | **batched_snapshots** | Snapshots reçus avec batching | 2-4 | | **concurrent_publishes** | Messages publiés en concurrence | > 0 | | **concurrent_reads** | Lectures DataNode concurrentes | > 0 | | **concurrent_writes** | Écritures DataNode concurrentes | > 0 | | **concurrent_errors** | Erreurs pendant concurrence | 0 | | **spam_messages_queued** | Messages queued pendant reload | > 0 | | **cascade_latency_ms** | Latence cascade config changes | < 500ms | | **io_p99_latency_ms** | P99 latence IO sous charge | < 10ms | | **datanode_p99_latency_ms** | P99 latence DataNode sous charge | < 5ms | | **memory_growth_mb** | Croissance mémoire sous charge | < 20MB | --- ## ✅ Critères de Succès ### MUST PASS 1. ✅ Config hot-reload chain fonctionne (< 100ms) 2. ✅ State persistence + event publishing chain fonctionne 3. ✅ Synchronization IO/DataNode sans erreurs 4. ✅ Runtime metrics avec batching 5. ✅ Concurrent access sans crashes/corruption 6. ✅ Hot-reload préserve state complet 7. ✅ Messages IO pas perdus pendant reload 8. ✅ Config cascades propagent correctement 9. ✅ Performance acceptable sous charge ### NICE TO HAVE 1. ✅ Config reload latency < 50ms (optimal) 2. ✅ Cascade latency < 200ms (optimal) 3. ✅ IO p99 < 5ms (optimal) 4. ✅ DataNode p99 < 2ms (optimal) --- ## 🐛 Cas d'Erreur Attendus | Erreur | Cause | Action | |--------|-------|--------| | Config change pas propagé | Callback pas déclenché | FAIL - fix onTreeReloaded | | Sync errors > 0 | Race condition IO/DataNode | FAIL - add locking | | Messages perdus | Queue overflow pendant reload | WARN - increase queue size | | Concurrent crashes | Missing mutex | FAIL - add thread safety | | State corrompu après reload | setState() bug | FAIL - fix state restoration | | Cascade timeout | Deadlock dans chain | FAIL - fix event routing | | Performance degradation | O(n²) algorithm | FAIL - optimize | | Memory leak | Resources not freed | FAIL - fix destructors | --- ## 📝 Output Attendu ``` ================================================================================ TEST: Cross-System Integration (IO + DataNode) ================================================================================ === TEST 1: Config Hot-Reload → IO Broadcast === → Config reloaded, publishing event... PlayerModule received config change: difficulty=hard, hpMult=1.5 Total latency (reload + publish + subscribe + read): 87ms ✓ TEST 1 PASSED === TEST 2: State Persistence + Event Publishing === EconomyModule received: player:level_up Player gold: 1000 EconomyModule updated bonuses and saved ✓ TEST 2 PASSED === TEST 3: Multi-Module State Synchronization === Synchronization errors: 0 / 100 ✓ TEST 3 PASSED === TEST 4: Runtime Metrics Collection === Snapshots received: 3 (expected ~3 due to batching) ✓ TEST 4 PASSED === TEST 5: Concurrent Access === Concurrent test ran for 5.001s Publishes: 487 Reads: 243 Writes: 489 Errors: 0 ✓ TEST 5 PASSED === TEST 6: Hot-Reload Module + Preserve State === State before reload: 50 entities, 5000 gold, quest #42 active Entities after reload: 50 Gold after reload: 5000 Quest after reload: active=true, id=42 Spam messages received after reload: 94 ✓ TEST 6 PASSED === TEST 7: Config Change Cascades === → Cascade complete! Cascade latency: 234ms ✓ TEST 7 PASSED === TEST 8: Large State + High-Frequency IO === Created large DataNode tree (1000 nodes) Performance results: IO published: 48723 messages IO p99 latency: 8.3ms DataNode reads: 9745 DataNode p99 latency: 3.2ms Memory growth: 14.7MB ✓ TEST 8 PASSED ================================================================================ METRICS ================================================================================ Config reload latency: 87ms (threshold: < 100ms) ✓ Sync errors: 0 (threshold: 0) ✓ Batched snapshots: 3 Concurrent publishes: 487 Concurrent reads: 243 Concurrent writes: 489 Concurrent errors: 0 (threshold: 0) ✓ Spam messages queued: 94 Cascade latency: 234ms (threshold: < 500ms) ✓ IO p99 latency: 8.3ms (threshold: < 10ms) ✓ DataNode p99 latency: 3.2ms (threshold: < 5ms) ✓ Memory growth: 14.7MB (threshold: < 20MB) ✓ ================================================================================ ASSERTIONS ================================================================================ ✓ config_hotreload_chain ✓ state_persistence_chain ✓ state_synchronization ✓ runtime_metrics ✓ concurrent_access ✓ hotreload_preserve_state ✓ config_cascade ✓ performance_under_load Result: ✅ PASSED (8/8 tests) ================================================================================ ``` --- ## 📅 Planning **Jour 1 (4h):** - Implémenter ConfigWatcherModule, PlayerModule, EconomyModule, MetricsModule - Setup IDataTree avec structure config/data/runtime - Tests 1-3 (config reload, persistence, sync) **Jour 2 (4h):** - Tests 4-6 (metrics, concurrent, hot-reload) - Tests 7-8 (cascades, performance) - Debug + validation --- **Conclusion**: Ces 3 nouveaux scénarios (11, 12, 13) complètent la suite de tests d'intégration en couvrant les systèmes IO et DataNode, ainsi que leur intégration.