fix(optimiseur): fallbackCode dit pourquoi on replie (correctifs brief 4, point 3)
Constat `fallbackCode` de T7 (E-T7-6) : une abstention, un plan expiré ou refusé à la réception vidaient le cache, et le cycle suivant repliait avec `no-plan` — l'app ne distinguait pas « l'optimiseur s'abstient » de « rien reçu ». Le code du verdict de réception d'une réponse non appliquée est gardé comme code de repli jusqu'à l'issue de la requête suivante ; un plan valide l'efface, un échec de requête ou une connexion perdue le ramènent à `no-plan`, qui ne dit plus que « rien reçu ». Publication seule : aucune décision ne change. INTERFACE.md : liste fermée des valeurs de fallbackCode et leur lecture (« pas d'optimiseur », « pas encore de plan », « l'optimiseur s'abstient », « plan refusé »). Tests : refoptimizer lit GetPlan.fallbackCode après chaque cycle de chaque mode (abstain, expired, invalid, unknown-load, missing-load…, puis retour normal sans fallbackCode) ; socketscheduler vérifie le code publié pour chaque refus (les tests qui épinglaient `no-plan` sont mis à jour). Contre-épreuve M3 (toujours `no-plan`) : mord. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
ce323a6e1f
commit
e3b916e249
@ -50,6 +50,7 @@ SocketScheduler::SocketScheduler(IScheduler *ombre, OptimizerClient *client, Fou
|
||||
return; // tentative échouée pendant une panne déjà notée
|
||||
m_perteNotee = true;
|
||||
oublierCache(QStringLiteral("connexion perdue"));
|
||||
m_codeReception.clear(); // plus rien ne sera reçu sur cette connexion : `no-plan`
|
||||
noterVivacite(OptimizerStatus::Vivacite::Evenement::ConnexionPerdue, m_client->lastErrorCode());
|
||||
});
|
||||
// Handshake accepté : nom, version et capacités ont changé — l'état publié se recompose.
|
||||
@ -69,7 +70,12 @@ Plan SocketScheduler::getPlan(const SurplusContext &ctx)
|
||||
envoyer(ctx);
|
||||
|
||||
if (!m_aCache) {
|
||||
noterCycle(false, QStringLiteral("no-plan"), QStringLiteral("aucun plan en cache"));
|
||||
// Pourquoi il n'y a pas de plan : le verdict de la dernière réponse non appliquée
|
||||
// (abstention, plan expiré ou refusé à la réception), sinon « rien reçu ».
|
||||
if (m_codeReception.isEmpty())
|
||||
noterCycle(false, QStringLiteral("no-plan"), QStringLiteral("aucun plan en cache"));
|
||||
else
|
||||
noterCycle(false, m_codeReception, QStringLiteral("aucun plan en cache — dernière réponse non appliquée"));
|
||||
return ombre;
|
||||
}
|
||||
// Rejugé sur le contexte DE CE CYCLE : maintenant, charges, disponibilités ont pu changer
|
||||
@ -172,6 +178,9 @@ void SocketScheduler::onPlanRecu(quint64 id, const QVariantMap &result)
|
||||
break;
|
||||
}
|
||||
|
||||
// Le verdict d'une réponse non appliquée devient le code de repli publié (fallbackCode),
|
||||
// jusqu'à l'issue de la requête suivante ; un plan valide l'efface.
|
||||
m_codeReception = v.issue == OptimizerPlan::Issue::Valide ? QString() : v.code;
|
||||
switch (v.issue) {
|
||||
case OptimizerPlan::Issue::Valide: {
|
||||
m_cache = result;
|
||||
@ -220,6 +229,7 @@ void SocketScheduler::onPlanEchoue(quint64 id, const QString &code)
|
||||
emit reponseTracee(id, QVariantMap{{QStringLiteral("error"), QVariantMap{{QStringLiteral("code"), code}}}},
|
||||
m_chronoRequete.elapsed());
|
||||
emit echecRequete(code);
|
||||
m_codeReception.clear(); // aucune réponse utilisable : le repli, s'il vient, dit `no-plan`
|
||||
noterVivacite(OptimizerStatus::Vivacite::Evenement::Echec, code);
|
||||
}
|
||||
|
||||
|
||||
@ -104,7 +104,15 @@ public:
|
||||
/*! \brief \return Vrai si le plan du dernier cycle était celui de l'optimiseur. */
|
||||
bool optimiseurAuDernierCycle() const { return m_optimiseurAuCycle; }
|
||||
|
||||
/*! \brief \return Code du dernier cycle rendu au rule-based (vide si optimiseur). */
|
||||
/*!
|
||||
* \brief Code du dernier cycle rendu au rule-based — \c GetPlan.fallbackCode.
|
||||
*
|
||||
* Plan en cache rejugé non applicable : le code de ce verdict (\c expired,
|
||||
* \c no-current-slot…). Pas de plan en cache : le verdict de la dernière réponse NON
|
||||
* appliquée (abstention, expiré ou refusé à la réception), gardé jusqu'à l'issue de la requête
|
||||
* suivante ; \c no-plan si rien n'a été reçu (aucune réponse, délai, connexion perdue).
|
||||
* \return Le code ; vide si l'optimiseur a commandé.
|
||||
*/
|
||||
QString dernierCodeDeRepli() const { return m_codeCycle; }
|
||||
|
||||
/*!
|
||||
@ -180,6 +188,7 @@ private:
|
||||
bool m_optimiseurAuCycle = false;
|
||||
QString m_codeCycle = QStringLiteral("no-plan");
|
||||
bool m_cycleNote = false;
|
||||
QString m_codeReception; // verdict de la dernière réponse non appliquée, vide sinon
|
||||
OptimizerPlan::PlanRecu m_planApplique;
|
||||
QString m_dernierPlanRecu; // planId de la dernière réponse valide annoncée
|
||||
QHash<QString, QString> m_motifVu; // loadId → dernière ligne de motif annoncée
|
||||
|
||||
@ -624,6 +624,10 @@ void TestRefOptimizer::testEachFaultModeGivesItsRow_data()
|
||||
QTest::addColumn<bool>("redemarrer");
|
||||
QTest::addColumn<QStringList>("lignes");
|
||||
QTest::addColumn<QStringList>("notifications");
|
||||
// GetPlan.fallbackCode après chacun des quatre cycles (vide = absent : l'optimiseur commande).
|
||||
// Le verdict d'une réponse non appliquée reste le code de repli jusqu'à l'issue de la requête
|
||||
// suivante ; `no-plan` ne dit que « rien reçu » (délai dépassé, connexion perdue).
|
||||
QTest::addColumn<QStringList>("replis");
|
||||
|
||||
const QStringList abstention{"optimizer|vivant|0|", "rules|vivant|0|", "rules|vivant|0|", "optimizer|vivant|0|"};
|
||||
const QStringList unePause{"attendu|vivant|rules", "attendu|vivant|optimizer"};
|
||||
@ -633,29 +637,37 @@ void TestRefOptimizer::testEachFaultModeGivesItsRow_data()
|
||||
};
|
||||
const QStringList mortPuisVivant{"attendu|vivant|rules", "attendu|mort|rules", "attendu|vivant|rules",
|
||||
"attendu|vivant|optimizer"};
|
||||
QTest::newRow("abstain") << "abstain" << false << abstention << unePause;
|
||||
QTest::newRow("expired") << "expired" << false << abstention << unePause;
|
||||
QTest::newRow("invalid") << "invalid" << false << invalide("malformed-plan") << mortPuisVivant;
|
||||
QTest::newRow("unknown-load") << "unknown-load" << false << invalide("unknown-load") << mortPuisVivant;
|
||||
QTest::newRow("forbidden-kind") << "forbidden-kind" << false << invalide("kind-mismatch") << mortPuisVivant;
|
||||
QTest::newRow("missing-load") << "missing-load" << false << invalide("incomplete-slot") << mortPuisVivant;
|
||||
auto verdict = [](const QString &code) { return QStringList{"", code, code, ""}; };
|
||||
QTest::newRow("abstain") << "abstain" << false << abstention << unePause << verdict("abstain");
|
||||
QTest::newRow("expired") << "expired" << false << abstention << unePause << verdict("expired");
|
||||
QTest::newRow("invalid") << "invalid" << false << invalide("malformed-plan") << mortPuisVivant
|
||||
<< verdict("malformed-plan");
|
||||
QTest::newRow("unknown-load") << "unknown-load" << false << invalide("unknown-load") << mortPuisVivant
|
||||
<< verdict("unknown-load");
|
||||
QTest::newRow("forbidden-kind") << "forbidden-kind" << false << invalide("kind-mismatch") << mortPuisVivant
|
||||
<< verdict("kind-mismatch");
|
||||
QTest::newRow("missing-load") << "missing-load" << false << invalide("incomplete-slot") << mortPuisVivant
|
||||
<< verdict("incomplete-slot");
|
||||
// slow (12 s > 10 s) : le premier délai dépassé garde le plan en cache, le second tue.
|
||||
QTest::newRow("slow") << "slow" << false
|
||||
<< QStringList{"optimizer|vivant|1|timeout", "rules|mort|2|timeout",
|
||||
"rules|vivant|0|timeout", "optimizer|vivant|0|timeout"}
|
||||
<< QStringList{"attendu|mort|rules", "attendu|vivant|rules", "attendu|vivant|optimizer"};
|
||||
<< QStringList{"attendu|mort|rules", "attendu|vivant|rules", "attendu|vivant|optimizer"}
|
||||
<< QStringList{"", "", "no-plan", ""};
|
||||
// drop : la connexion fermée tue tout de suite (§7 « socket fermé »), l'échec de la requête
|
||||
// en vol est compté (`disconnected`). La reconnexion qui suit n'est pas une nouvelle perte :
|
||||
// le dernier code reste celui de l'incident.
|
||||
QTest::newRow("drop") << "drop" << false
|
||||
<< QStringList{"rules|mort|1|disconnected", "rules|mort|2|disconnected",
|
||||
"rules|vivant|0|disconnected", "optimizer|vivant|0|disconnected"}
|
||||
<< QStringList{"attendu|mort|rules", "attendu|vivant|rules", "attendu|vivant|optimizer"};
|
||||
<< QStringList{"attendu|mort|rules", "attendu|vivant|rules", "attendu|vivant|optimizer"}
|
||||
<< QStringList{"", "no-plan", "no-plan", ""};
|
||||
// wrong-major : au handshake suivant (optimiseur redémarré) ; aucune requête ne part.
|
||||
QTest::newRow("wrong-major") << "wrong-major" << true
|
||||
<< QStringList{"rules|mort|0|major-mismatch", "rules|mort|0|major-mismatch",
|
||||
"rules|vivant|0|major-mismatch", "optimizer|vivant|0|major-mismatch"}
|
||||
<< QStringList{"attendu|mort|rules", "attendu|vivant|rules", "attendu|vivant|optimizer"};
|
||||
<< QStringList{"attendu|mort|rules", "attendu|vivant|rules", "attendu|vivant|optimizer"}
|
||||
<< QStringList{"no-plan", "no-plan", "no-plan", ""};
|
||||
}
|
||||
|
||||
void TestRefOptimizer::testEachFaultModeGivesItsRow()
|
||||
@ -664,6 +676,7 @@ void TestRefOptimizer::testEachFaultModeGivesItsRow()
|
||||
QFETCH(bool, redemarrer);
|
||||
QFETCH(QStringList, lignes);
|
||||
QFETCH(QStringList, notifications);
|
||||
QFETCH(QStringList, replis);
|
||||
monter(Banc::Complet);
|
||||
if (QTest::currentTestFailed())
|
||||
return;
|
||||
@ -684,6 +697,7 @@ void TestRefOptimizer::testEachFaultModeGivesItsRow()
|
||||
|
||||
QSignalSpy spy(m_arb, &EnergyArbitrator::optimizerStatusChanged);
|
||||
QStringList obtenu;
|
||||
QStringList replisObtenus;
|
||||
ecrireMode(mode);
|
||||
if (redemarrer) {
|
||||
ref.arreter();
|
||||
@ -716,13 +730,19 @@ void TestRefOptimizer::testEachFaultModeGivesItsRow()
|
||||
cycle(t);
|
||||
}
|
||||
obtenu << ligneStatut();
|
||||
const QVariantMap publie = rpc("GetPlan");
|
||||
// fallbackCode n'accompagne que le rule-based ; l'optimiseur qui commande n'en a pas.
|
||||
QCOMPARE(publie.value("strategy").toString() == "rules", publie.contains("fallbackCode"));
|
||||
replisObtenus << publie.value("fallbackCode").toString();
|
||||
}
|
||||
QCOMPARE(obtenu, lignes);
|
||||
QCOMPARE(triplets(spy), notifications);
|
||||
// Le plan publié dit qu'aucun plan d'optimiseur ne commande, et pourquoi — au cycle repli.
|
||||
// Le plan publié dit qu'aucun plan d'optimiseur ne commande, et pourquoi.
|
||||
QCOMPARE(replisObtenus, replis);
|
||||
if (mode == "slow")
|
||||
QVERIFY2(m_updateMaxMs < 250, qPrintable(QStringLiteral("update() a duré %1 ms").arg(m_updateMaxMs)));
|
||||
qInfo().noquote() << "[mode]" << mode << obtenu.join(" ; ") << "— update() max" << m_updateMaxMs << "ms";
|
||||
qInfo().noquote() << "[mode]" << mode << obtenu.join(" ; ") << "— fallbackCode" << replisObtenus.join(" ; ")
|
||||
<< "— update() max" << m_updateMaxMs << "ms";
|
||||
}
|
||||
|
||||
void TestRefOptimizer::testAProtocol10OptimizerIsAccepted()
|
||||
|
||||
@ -473,13 +473,15 @@ void TestSocketScheduler::testRefusedPlanFallsBackToRules_data()
|
||||
QTest::addColumn<QString>("cas");
|
||||
QTest::addColumn<QString>("codeJuge");
|
||||
QTest::addColumn<QString>("codeCycle");
|
||||
QTest::newRow("incomplet (Q1)") << "incomplet" << "incomplete-slot" << "no-plan";
|
||||
QTest::newRow("kind interdit") << "kind" << "kind-not-announced" << "no-plan";
|
||||
QTest::newRow("loadId inconnu") << "inconnu" << "unknown-load" << "no-plan";
|
||||
QTest::newRow("reason vide") << "reason" << "missing-reason" << "no-plan";
|
||||
QTest::newRow("expiré à la réception") << "expire" << "expired" << "no-plan";
|
||||
// codeCycle = GetPlan.fallbackCode : le verdict de la réponse non appliquée, gardé jusqu'à
|
||||
// l'issue de la requête suivante (correctifs de fin de brief 4, point 3).
|
||||
QTest::newRow("incomplet (Q1)") << "incomplet" << "incomplete-slot" << "incomplete-slot";
|
||||
QTest::newRow("kind interdit") << "kind" << "kind-not-announced" << "kind-not-announced";
|
||||
QTest::newRow("loadId inconnu") << "inconnu" << "unknown-load" << "unknown-load";
|
||||
QTest::newRow("reason vide") << "reason" << "missing-reason" << "missing-reason";
|
||||
QTest::newRow("expiré à la réception") << "expire" << "expired" << "expired";
|
||||
QTest::newRow("expiré à l'application") << "expire-tard" << "" << "expired";
|
||||
QTest::newRow("abstention") << "abstention" << "abstain" << "no-plan";
|
||||
QTest::newRow("abstention") << "abstention" << "abstain" << "abstain";
|
||||
}
|
||||
|
||||
void TestSocketScheduler::testRefusedPlanFallsBackToRules()
|
||||
@ -518,6 +520,9 @@ void TestSocketScheduler::testRefusedPlanFallsBackToRules()
|
||||
cycle(t0.addSecs(60), -3000);
|
||||
QVERIFY(!m_arb->socketScheduler()->optimiseurAuDernierCycle());
|
||||
QCOMPARE(m_arb->socketScheduler()->dernierCodeDeRepli(), codeCycle);
|
||||
const QVariantMap publie = rpc("GetPlan");
|
||||
QCOMPARE(publie.value("strategy").toString(), QString("rules"));
|
||||
QCOMPARE(publie.value("fallbackCode").toString(), codeCycle);
|
||||
// Jamais appliqué EN PARTIE : aucune charge ne porte le niveau de l'optimiseur.
|
||||
for (const QVariantMap &t : telemetrie())
|
||||
QVERIFY2(t.value("decision").toMap().value("level").toString() != "optimizer",
|
||||
@ -758,7 +763,7 @@ void TestSocketScheduler::testANewInvalidPlanReplacesTheCache()
|
||||
QCOMPARE(m_codesJuges.last(), QString("incomplete-slot"));
|
||||
cycle(t0.addSecs(120), -3000);
|
||||
QVERIFY(!m_arb->socketScheduler()->optimiseurAuDernierCycle());
|
||||
QCOMPARE(m_arb->socketScheduler()->dernierCodeDeRepli(), QString("no-plan"));
|
||||
QCOMPARE(m_arb->socketScheduler()->dernierCodeDeRepli(), QString("incomplete-slot"));
|
||||
}
|
||||
|
||||
void TestSocketScheduler::testConnectionLossDropsThePlan()
|
||||
@ -1032,7 +1037,7 @@ void TestSocketScheduler::testStatusFollowsTheFallbackTable()
|
||||
m_repondre = [](const QVariantMap &) { return QVariantMap{{"abstain", true}, {"reason", "recalcul"}}; };
|
||||
cycle(t0.addSecs(120), -3000);
|
||||
cycle(t0.addSecs(180), -3000);
|
||||
QCOMPARE(s->dernierCodeDeRepli(), QString("no-plan"));
|
||||
QCOMPARE(s->dernierCodeDeRepli(), QString("abstain"));
|
||||
} else if (cas == "expire") {
|
||||
m_repondre = nullptr;
|
||||
cycle(t0.addSecs(200), -3000); // le plan reçu à t0 + 60 valait jusqu'à t0 + 150
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user