trace : une requête non envoyée n'est tracée que par sa taille (Q7)
Avec les traces actives, la ligne d'un cycle recopiait la requête GetPlan même quand le client ne l'avait pas écrite (ligne de plus de 1 Mio, request-too-long) : environ 1 Mio par cycle jusqu'au plafond journalier, pour une requête que personne n'a reçue, et que le rejeu aurait renvoyée. - La ligne porte alors `requestBytes` (taille de la ligne, octets) à la place de `request` ; sa réponse reste l'erreur request-too-long. Même règle pour la requête qui SERAIT partie sans optimiseur. - La décision « part ou ne part pas » est celle du client : OptimizerClient::getPlanLine(), extraite de requestPlan() sans changer son comportement, est lue par la trace — pas une seconde estimation. - Le commentaire « la requête RÉELLEMENT envoyée » est corrigé, la doc de la ligne de trace (optimizertrace.h) aussi. Preuve : testARequestNotSentIsTracedBySizeOnly (avec et sans optimiseur ; contre-épreuve : la requête envoyée est recopiée). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
8988af0a16
commit
14b86de3c9
@ -1733,7 +1733,7 @@ void EnergyArbitrator::provisionEvChargerConfig(EvCharger *ev)
|
|||||||
}
|
}
|
||||||
m_provisionRefuse.remove(id);
|
m_provisionRefuse.remove(id);
|
||||||
|
|
||||||
qCInfo(dcNymeaEnergy()) << "[EnergyArbitrator] Borne" << ev->name()
|
qCInfo(dcNymeaEnergy()) << "[EnergyArbitrator] Borne" << c.label()
|
||||||
<< "ajoutée à la configuration des charges (rang" << c.priority()
|
<< "ajoutée à la configuration des charges (rang" << c.priority()
|
||||||
<< ", domaine ev, rankOrigin=auto) — entrée créée automatiquement, "
|
<< ", domaine ev, rankOrigin=auto) — entrée créée automatiquement, "
|
||||||
"insérée EN QUEUE sans déplacer personne. Le rang est un défaut, "
|
"insérée EN QUEUE sans déplacer personne. Le rang est un défaut, "
|
||||||
@ -3122,15 +3122,29 @@ void EnergyArbitrator::tracerCycle(const SurplusContext &ctx, const Plan &plan,
|
|||||||
ligne.insert(QStringLiteral("t"), OptimizerTrace::horodatage(now));
|
ligne.insert(QStringLiteral("t"), OptimizerTrace::horodatage(now));
|
||||||
const bool optimiseur = plan.strategy == QLatin1String(OptimizerPlan::StrategieOptimiseur);
|
const bool optimiseur = plan.strategy == QLatin1String(OptimizerPlan::StrategieOptimiseur);
|
||||||
const quint64 id = m_socketScheduler ? m_socketScheduler->idRequeteDuCycle() : 0;
|
const quint64 id = m_socketScheduler ? m_socketScheduler->idRequeteDuCycle() : 0;
|
||||||
|
// Q7 — une requête trop longue pour la ligne du protocole n'est PAS partie : la trace n'en
|
||||||
|
// garde que la TAILLE (`requestBytes`, octets de la ligne). La recopier écrirait plus de 1 Mio
|
||||||
|
// par cycle, jusqu'au plafond journalier, pour une requête que personne n'a reçue — et le
|
||||||
|
// rejeu la renverrait à un optimiseur qui la refuserait. La règle est celle du client
|
||||||
|
// (getPlanLine, kMaxLineBytes), pas une seconde estimation.
|
||||||
|
auto requeteOuTaille = [&ligne](quint64 idFil, const QVariantMap &requete) {
|
||||||
|
const qsizetype octets = OptimizerClient::getPlanLine(idFil, requete).size();
|
||||||
|
if (octets > OptimizerClient::kMaxLineBytes)
|
||||||
|
ligne.insert(QStringLiteral("requestBytes"), octets);
|
||||||
|
else
|
||||||
|
ligne.insert(QStringLiteral("request"), requete);
|
||||||
|
};
|
||||||
if (id != 0) {
|
if (id != 0) {
|
||||||
// La requête RÉELLEMENT envoyée ; sa réponse complétera la ligne (ou « no-response »).
|
// La requête soumise au client ; sa réponse complétera la ligne (ou « no-response »).
|
||||||
ligne.insert(QStringLiteral("request"), m_socketScheduler->requeteDuCycle());
|
// Trop longue, elle n'a pas été écrite et sa réponse sera l'erreur `request-too-long`.
|
||||||
|
requeteOuTaille(id, m_socketScheduler->requeteDuCycle());
|
||||||
} else {
|
} else {
|
||||||
// Aucune n'est partie (pas d'endpoint, client non connecté) : celle qui serait partie,
|
// Aucune n'est partie (pas d'endpoint, client non connecté) : celle qui serait partie,
|
||||||
// même fonction, même fil — et c'est le rule-based qui y a répondu.
|
// même fonction, même fil — et c'est le rule-based qui y a répondu. Même règle de taille,
|
||||||
|
// calculée sous un identifiant nul : aucun n'a été attribué.
|
||||||
OptimizerProtocol::ContexteFil fil = contexteFil(ctx);
|
OptimizerProtocol::ContexteFil fil = contexteFil(ctx);
|
||||||
fil.trigger = m_socketScheduler ? m_socketScheduler->declencheurEnAttente() : QStringLiteral("cycle");
|
fil.trigger = m_socketScheduler ? m_socketScheduler->declencheurEnAttente() : QStringLiteral("cycle");
|
||||||
ligne.insert(QStringLiteral("request"), OptimizerProtocol::getPlanParams(ctx, fil));
|
requeteOuTaille(0, OptimizerProtocol::getPlanParams(ctx, fil));
|
||||||
ligne.insert(QStringLiteral("response"),
|
ligne.insert(QStringLiteral("response"),
|
||||||
optimiseur ? QVariantMap{{QStringLiteral("error"),
|
optimiseur ? QVariantMap{{QStringLiteral("error"),
|
||||||
QVariantMap{{QStringLiteral("code"), QStringLiteral("not-sent")}}}}
|
QVariantMap{{QStringLiteral("code"), QStringLiteral("not-sent")}}}}
|
||||||
|
|||||||
@ -254,12 +254,7 @@ quint64 OptimizerClient::requestPlan(const QVariantMap ¶ms)
|
|||||||
|
|
||||||
const quint64 id = ++m_lastId;
|
const quint64 id = ++m_lastId;
|
||||||
m_pendingId = id;
|
m_pendingId = id;
|
||||||
const QByteArray line = serialize(QVariantMap{
|
const QByteArray line = getPlanLine(id, params);
|
||||||
{QStringLiteral("jsonrpc"), QStringLiteral("2.0")},
|
|
||||||
{QStringLiteral("id"), id},
|
|
||||||
{QStringLiteral("method"), QStringLiteral("GetPlan")},
|
|
||||||
{QStringLiteral("params"), params},
|
|
||||||
});
|
|
||||||
|
|
||||||
// §2 (1.2, Q7) : le moteur n'émet jamais de ligne de plus de maxLineBytes. Un optimiseur
|
// §2 (1.2, Q7) : le moteur n'émet jamais de ligne de plus de maxLineBytes. Un optimiseur
|
||||||
// conforme la jetterait (-32600 à id null) et la requête finirait en délai dépassé, la cause
|
// conforme la jetterait (-32600 à id null) et la requête finirait en délai dépassé, la cause
|
||||||
@ -704,6 +699,16 @@ void OptimizerClient::countProtocolError(const QString &code, const QString &det
|
|||||||
emit protocolError(code);
|
emit protocolError(code);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QByteArray OptimizerClient::getPlanLine(quint64 id, const QVariantMap ¶ms)
|
||||||
|
{
|
||||||
|
return serialize(QVariantMap{
|
||||||
|
{QStringLiteral("jsonrpc"), QStringLiteral("2.0")},
|
||||||
|
{QStringLiteral("id"), id},
|
||||||
|
{QStringLiteral("method"), QStringLiteral("GetPlan")},
|
||||||
|
{QStringLiteral("params"), params},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
QByteArray OptimizerClient::serialize(const QVariantMap &message)
|
QByteArray OptimizerClient::serialize(const QVariantMap &message)
|
||||||
{
|
{
|
||||||
return QJsonDocument(QJsonObject::fromVariantMap(message)).toJson(QJsonDocument::Compact);
|
return QJsonDocument(QJsonObject::fromVariantMap(message)).toJson(QJsonDocument::Compact);
|
||||||
|
|||||||
@ -235,6 +235,15 @@ public:
|
|||||||
*/
|
*/
|
||||||
quint64 requestPlan(const QVariantMap ¶ms);
|
quint64 requestPlan(const QVariantMap ¶ms);
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* \brief La ligne \c GetPlan exacte que \c requestPlan() écrirait, sans son \c '\\n'.
|
||||||
|
* \param id Identifiant JSON-RPC de la requête. \param params Ses \c params.
|
||||||
|
* \return La ligne sérialisée. Sa taille comparée à \c kMaxLineBytes dit si la requête part :
|
||||||
|
* c'est la règle de \c requestPlan(), qui l'appelle — la trace de décision la lit aussi,
|
||||||
|
* pour ne pas recopier une requête qui n'est pas partie.
|
||||||
|
*/
|
||||||
|
static QByteArray getPlanLine(quint64 id, const QVariantMap ¶ms);
|
||||||
|
|
||||||
/*! \brief \return Identifiant de la requête attendue, 0 si aucune. */
|
/*! \brief \return Identifiant de la requête attendue, 0 si aucune. */
|
||||||
quint64 pendingRequestId() const { return m_pendingId; }
|
quint64 pendingRequestId() const { return m_pendingId; }
|
||||||
|
|
||||||
|
|||||||
@ -30,7 +30,9 @@
|
|||||||
*
|
*
|
||||||
* \par Ce qu'une ligne porte (annexe D)
|
* \par Ce qu'une ligne porte (annexe D)
|
||||||
* \c t (instant du cycle, heure locale avec décalage), \c request (les paramètres \c GetPlan
|
* \c t (instant du cycle, heure locale avec décalage), \c request (les paramètres \c GetPlan
|
||||||
* envoyés, ou ceux qui l'auraient été), \c response (ce qui a répondu à CETTE requête),
|
* envoyés, ou ceux qui l'auraient été) — ou, à sa place, \c requestBytes (taille de la ligne,
|
||||||
|
* en octets) pour une requête trop longue pour partir (\c request-too-long, Q7), \c response
|
||||||
|
* (ce qui a répondu à CETTE requête),
|
||||||
* \c executed (actions appliquées au cycle), \c strategy (\c optimizer | \c rules | \c none),
|
* \c executed (actions appliquées au cycle), \c strategy (\c optimizer | \c rules | \c none),
|
||||||
* \c latencyMs (optimiseur seulement). Jamais le token : il ne voyage que dans le handshake ;
|
* \c latencyMs (optimiseur seulement). Jamais le token : il ne voyage que dans le handshake ;
|
||||||
* le client masque toute réponse qui le recopierait (\c OptimizerSecret), et l'écrivain masque
|
* le client masque toute réponse qui le recopierait (\c OptimizerSecret), et l'écrivain masque
|
||||||
@ -195,7 +197,8 @@ public:
|
|||||||
|
|
||||||
/*!
|
/*!
|
||||||
* \brief Ligne d'un cycle.
|
* \brief Ligne d'un cycle.
|
||||||
* \param ligne \c t, \c request, \c response (si connue), \c executed, \c strategy.
|
* \param ligne \c t, \c request (ou \c requestBytes), \c response (si connue), \c executed,
|
||||||
|
* \c strategy.
|
||||||
* \param t Instant du cycle (choisit le fichier).
|
* \param t Instant du cycle (choisit le fichier).
|
||||||
* \param idRequete Identifiant de la requête envoyée ce cycle ; 0 = aucune (la ligne part).
|
* \param idRequete Identifiant de la requête envoyée ce cycle ; 0 = aucune (la ligne part).
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -14,6 +14,7 @@
|
|||||||
#include "energysettings.h"
|
#include "energysettings.h"
|
||||||
#include "etm/energyarbitrator.h"
|
#include "etm/energyarbitrator.h"
|
||||||
#include "etm/config/loadconfigstore.h"
|
#include "etm/config/loadconfigstore.h"
|
||||||
|
#include "etm/scheduler/optimizerclient.h"
|
||||||
#include "etm/scheduler/socketscheduler.h"
|
#include "etm/scheduler/socketscheduler.h"
|
||||||
#include "etm/scheduler/optimizerstatus.h"
|
#include "etm/scheduler/optimizerstatus.h"
|
||||||
#include "etm/scheduler/publishedplan.h"
|
#include "etm/scheduler/publishedplan.h"
|
||||||
@ -143,6 +144,8 @@ private slots:
|
|||||||
void testTokenIsInNoTrace();
|
void testTokenIsInNoTrace();
|
||||||
void testTraceLineSize();
|
void testTraceLineSize();
|
||||||
void testTracerPairsTheResponseByRequestId();
|
void testTracerPairsTheResponseByRequestId();
|
||||||
|
//! Q7, suite : une requête trop longue pour partir n'est tracée que par sa TAILLE.
|
||||||
|
void testARequestNotSentIsTracedBySizeOnly();
|
||||||
// Brief 4 T7 — correctif A : un token recopié par l'optimiseur ne sort nulle part
|
// Brief 4 T7 — correctif A : un token recopié par l'optimiseur ne sort nulle part
|
||||||
void testAnEchoedTokenLeaksNowhere();
|
void testAnEchoedTokenLeaksNowhere();
|
||||||
// Lot contrat OpenRPC — le moteur parle le contrat qu'il sert
|
// Lot contrat OpenRPC — le moteur parle le contrat qu'il sert
|
||||||
@ -1629,6 +1632,64 @@ void TestSocketScheduler::testInheritedOutOfRuleConfigWhatGetPlanSends()
|
|||||||
jugerLesRequetesDuMoteur();
|
jugerLesRequetesDuMoteur();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void TestSocketScheduler::testARequestNotSentIsTracedBySizeOnly()
|
||||||
|
{
|
||||||
|
// Q7, suite : la trace recopiait la requête du cycle, même quand le client ne l'avait PAS
|
||||||
|
// écrite (ligne de plus de 1 Mio, request-too-long) — environ 1 Mio par cycle jusqu'au plafond
|
||||||
|
// journalier, pour une requête que personne n'a reçue. Seule sa TAILLE y figure désormais.
|
||||||
|
monter();
|
||||||
|
const QString d = activerTraces();
|
||||||
|
m_repondre = [](const QVariantMap &) { return QVariantMap{{"abstain", true}, {"reason", "test"}}; };
|
||||||
|
const QDateTime t0 = utcDateTime(QDate(2026, 6, 8), QTime(13, 0, 0));
|
||||||
|
cycle(t0, -3000); // CONTRE-ÉPREUVE : une requête envoyée est recopiée
|
||||||
|
const int recues = m_requetes.count();
|
||||||
|
|
||||||
|
// Une configuration héritée au libellé démesuré : SetLoadConfig la refuserait, le chargement
|
||||||
|
// la garde, et le contexte dépasse la ligne du protocole.
|
||||||
|
const QString demesure(OptimizerClient::kMaxLineBytes + 1000, QLatin1Char('L'));
|
||||||
|
const QVariantList charges{
|
||||||
|
QVariantMap{{"id", "ecs"}, {"label", "ECS"}, {"adapter", "relay-router"}, {"mode", "fixed"},
|
||||||
|
{"priority", 1}, {"enabled", true}, {"minOnS", 0}, {"minOffS", 0},
|
||||||
|
{"relays", QVariantList{QVariantMap{{"thingId", m_relais.toString()}, {"powerW", 1000}}}}},
|
||||||
|
QVariantMap{{"id", m_var}, {"label", demesure}, {"adapter", "etmvariableload"}, {"mode", "dynamic"},
|
||||||
|
{"maxPowerW", 3000}, {"priority", 2}, {"enabled", true}}};
|
||||||
|
chargerConfigHeritee(charges);
|
||||||
|
m_compteur->setStateValue("currentPower", -3000);
|
||||||
|
m_arb->recordMeterUpdate(t0.addSecs(60));
|
||||||
|
m_arb->simulationCallUpdate(t0.addSecs(60));
|
||||||
|
QVERIFY(waitFor([&] { return m_codesEchec.contains(QStringLiteral("request-too-long")); }));
|
||||||
|
settle();
|
||||||
|
QCOMPARE(m_requetes.count(), recues); // rien n'est parti
|
||||||
|
|
||||||
|
QList<QVariantMap> l = traces(d, 2);
|
||||||
|
QCOMPARE(l.count(), 2);
|
||||||
|
QVERIFY(l.at(0).contains("request"));
|
||||||
|
QVERIFY(!l.at(0).contains("requestBytes"));
|
||||||
|
QVERIFY(!l.at(1).contains("request"));
|
||||||
|
const qint64 octets = l.at(1).value("requestBytes").toLongLong();
|
||||||
|
QVERIFY2(octets > OptimizerClient::kMaxLineBytes && octets < 2 * OptimizerClient::kMaxLineBytes,
|
||||||
|
qPrintable(QString::number(octets)));
|
||||||
|
QCOMPARE(l.at(1).value("response").toMap(),
|
||||||
|
(QVariantMap{{"error", QVariantMap{{"code", "request-too-long"}}}}));
|
||||||
|
// Le libellé n'est NULLE PART dans la trace : le fichier entier est loin du mégaoctet.
|
||||||
|
qint64 taille = 0;
|
||||||
|
for (const QString &f : fichiersTrace(d))
|
||||||
|
taille += QFileInfo(QDir(d).filePath(f)).size();
|
||||||
|
QVERIFY2(taille < 200 * 1024, qPrintable(QString::number(taille)));
|
||||||
|
|
||||||
|
// Sans optimiseur : la requête qui SERAIT partie suit la même règle.
|
||||||
|
monter(false);
|
||||||
|
const QString d2 = activerTraces();
|
||||||
|
chargerConfigHeritee(charges);
|
||||||
|
cycle(t0.addSecs(120), -3000);
|
||||||
|
l = traces(d2, 1);
|
||||||
|
QCOMPARE(l.count(), 1);
|
||||||
|
QVERIFY(!l.at(0).contains("request"));
|
||||||
|
QVERIFY(l.at(0).value("requestBytes").toLongLong() > OptimizerClient::kMaxLineBytes);
|
||||||
|
QCOMPARE(l.at(0).value("strategy").toString(), QStringLiteral("rules"));
|
||||||
|
QVERIFY(l.at(0).value("response").toMap().contains("result"));
|
||||||
|
}
|
||||||
|
|
||||||
void TestSocketScheduler::testARequestTooLongCountsAsAFailure()
|
void TestSocketScheduler::testARequestTooLongCountsAsAFailure()
|
||||||
{
|
{
|
||||||
// Protocole 1.2, Q7 : une requête que le client n'émet pas (ligne de plus de 1 Mio) est un
|
// Protocole 1.2, Q7 : une requête que le client n'émet pas (ligne de plus de 1 Mio) est un
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user