mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-11 17:26:57 +02:00
Moteur : une bascule plus récente périme la vérification du rendu, plan et retour en POST
La vérification du rendu du gabarit après une mise à jour attend jusqu'à vingt minutes que le nouveau moteur réponde. Un retour à la version précédente, une autre version installée ou une seconde mise à jour pendant cette attente la laissaient courir : elle comparait alors l'ancien moteur à celui d'une autre bascule, ou écrasait le relevé de la suivante. - Chaque bascule ouvre une génération ; une vérification périmée s'arrête sans rien ranger, et son relevé n'écrase plus le suivant. Retour et « utiliser » effacent le relevé, qui parlerait d'un moteur arrêté. - /api/engine/plan (registre) et /api/engine/rollback (configuration et redémarrage) refusent tout autre verbe que POST — l'interface n'utilise que POST. - Tests : vérification annulée en attente, relevé périmé ignoré, GET refusé. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
5b834d54b2
commit
5d37bef0ce
3 files changed
+122
-6
No files matched your search
@@ -194,6 +194,8 @@ func engineRunUpdate(tag string, preserveOff bool) {
|
|||||||
ref, haveBefore := engineRenderBefore(ctx)
|
ref, haveBefore := engineRenderBefore(ctx)
|
||||||
cancel()
|
cancel()
|
||||||
|
|
||||||
|
// La vérification d'une bascule précédente parlerait du moteur qu'on quitte.
|
||||||
|
engineRenderCancel()
|
||||||
if err := engineSwitchTo(bin); err != nil {
|
if err := engineSwitchTo(bin); err != nil {
|
||||||
lcFail(err)
|
lcFail(err)
|
||||||
return
|
return
|
||||||
@@ -280,6 +282,7 @@ func handleEngineUse(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
engineRememberPrevious(left, bin)
|
engineRememberPrevious(left, bin)
|
||||||
|
engineRenderCancel()
|
||||||
if serviceIsActive() {
|
if serviceIsActive() {
|
||||||
_ = serviceAction("restart")
|
_ = serviceAction("restart")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -265,6 +265,12 @@ type engineRenderCheck struct {
|
|||||||
var (
|
var (
|
||||||
engineRenderMu sync.Mutex
|
engineRenderMu sync.Mutex
|
||||||
engineRenderLast *engineRenderCheck
|
engineRenderLast *engineRenderCheck
|
||||||
|
// engineRenderGen : la vérification en cours. Une bascule plus récente
|
||||||
|
// (autre mise à jour, retour à la version précédente, autre version
|
||||||
|
// installée) la périme : une vérification d'avant, encore à attendre son
|
||||||
|
// moteur, ne doit ni écraser le relevé suivant ni comparer l'ancien
|
||||||
|
// moteur au moteur d'une autre bascule.
|
||||||
|
engineRenderGen uint64
|
||||||
// engineRenderWait : délai laissé au nouveau moteur pour charger le modèle
|
// engineRenderWait : délai laissé au nouveau moteur pour charger le modèle
|
||||||
// (un gros MoE en mmap peut demander plusieurs minutes). Variable pour les
|
// (un gros MoE en mmap peut demander plusieurs minutes). Variable pour les
|
||||||
// tests.
|
// tests.
|
||||||
@@ -272,9 +278,40 @@ var (
|
|||||||
engineRenderPoll = 5 * time.Second
|
engineRenderPoll = 5 * time.Second
|
||||||
)
|
)
|
||||||
|
|
||||||
func engineRenderSet(c engineRenderCheck) {
|
// engineRenderBegin ouvre une vérification : celles d'avant sont périmées.
|
||||||
|
func engineRenderBegin() uint64 {
|
||||||
|
engineRenderMu.Lock()
|
||||||
|
defer engineRenderMu.Unlock()
|
||||||
|
engineRenderGen++
|
||||||
|
return engineRenderGen
|
||||||
|
}
|
||||||
|
|
||||||
|
// engineRenderCancel : une bascule sans vérification (retour à la version
|
||||||
|
// précédente, autre version installée) périme la vérification en cours et
|
||||||
|
// efface son relevé, qui parlerait d'un moteur qui ne tourne plus.
|
||||||
|
func engineRenderCancel() {
|
||||||
|
engineRenderMu.Lock()
|
||||||
|
defer engineRenderMu.Unlock()
|
||||||
|
engineRenderGen++
|
||||||
|
engineRenderLast = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// engineRenderCurrent : la vérification gen est-elle encore la dernière ?
|
||||||
|
func engineRenderCurrent(gen uint64) bool {
|
||||||
|
engineRenderMu.Lock()
|
||||||
|
defer engineRenderMu.Unlock()
|
||||||
|
return gen == engineRenderGen
|
||||||
|
}
|
||||||
|
|
||||||
|
// engineRenderSet range le relevé de la vérification gen — rien si une
|
||||||
|
// bascule plus récente l'a périmée.
|
||||||
|
func engineRenderSet(gen uint64, c engineRenderCheck) {
|
||||||
c.At = time.Now()
|
c.At = time.Now()
|
||||||
engineRenderMu.Lock()
|
engineRenderMu.Lock()
|
||||||
|
if gen != engineRenderGen {
|
||||||
|
engineRenderMu.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
engineRenderLast = &c
|
engineRenderLast = &c
|
||||||
engineRenderMu.Unlock()
|
engineRenderMu.Unlock()
|
||||||
if c.Status != "pending" {
|
if c.Status != "pending" {
|
||||||
@@ -331,7 +368,8 @@ func engineRenderBefore(ctx context.Context) (engineRenderRef, bool) {
|
|||||||
// avant : un ancien moteur qui aurait survécu à l'arrêt répondrait aussi, et
|
// avant : un ancien moteur qui aurait survécu à l'arrêt répondrait aussi, et
|
||||||
// comparer l'ancien à lui-même conclurait à tort « identique ».
|
// comparer l'ancien à lui-même conclurait à tort « identique ».
|
||||||
func engineRenderAfter(ref engineRenderRef, tag string) {
|
func engineRenderAfter(ref engineRenderRef, tag string) {
|
||||||
engineRenderSet(engineRenderCheck{Status: "pending", Tag: tag})
|
gen := engineRenderBegin()
|
||||||
|
engineRenderSet(gen, engineRenderCheck{Status: "pending", Tag: tag})
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), engineRenderWait)
|
ctx, cancel := context.WithTimeout(context.Background(), engineRenderWait)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
p := tplLocalProber()
|
p := tplLocalProber()
|
||||||
@@ -346,22 +384,25 @@ func engineRenderAfter(ref engineRenderRef, tag string) {
|
|||||||
return err == nil && props.BuildInfo != ref.build
|
return err == nil && props.BuildInfo != ref.build
|
||||||
}
|
}
|
||||||
for !ready() {
|
for !ready() {
|
||||||
|
if !engineRenderCurrent(gen) {
|
||||||
|
return // bascule plus récente : plus rien à comparer
|
||||||
|
}
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
engineRenderSet(engineRenderCheck{Status: "unknown", Tag: tag, Detail: "le nouveau moteur n'a pas répondu à temps : rendu non vérifié"})
|
engineRenderSet(gen, engineRenderCheck{Status: "unknown", Tag: tag, Detail: "le nouveau moteur n'a pas répondu à temps : rendu non vérifié"})
|
||||||
return
|
return
|
||||||
case <-time.After(engineRenderPoll):
|
case <-time.After(engineRenderPoll):
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
after, err := tplRenderPrint(ctx, p, ref.shape)
|
after, err := tplRenderPrint(ctx, p, ref.shape)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
engineRenderSet(engineRenderCheck{Status: "unknown", Tag: tag, Detail: "rendu impossible sur le nouveau moteur : " + err.Error()})
|
engineRenderSet(gen, engineRenderCheck{Status: "unknown", Tag: tag, Detail: "rendu impossible sur le nouveau moteur : " + err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if same, d := tplRenderDiff(ref.text, after); same {
|
if same, d := tplRenderDiff(ref.text, after); same {
|
||||||
engineRenderSet(engineRenderCheck{Status: "same", Tag: tag})
|
engineRenderSet(gen, engineRenderCheck{Status: "same", Tag: tag})
|
||||||
} else {
|
} else {
|
||||||
engineRenderSet(engineRenderCheck{Status: "changed", Tag: tag, Detail: d})
|
engineRenderSet(gen, engineRenderCheck{Status: "changed", Tag: tag, Detail: d})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -410,6 +451,11 @@ func enginePrevious(cur string) map[string]any {
|
|||||||
// changerait au prompt. {recommended:true} : la plus récente ≥ b10864
|
// changerait au prompt. {recommended:true} : la plus récente ≥ b10864
|
||||||
// réellement publiée ; sinon la dernière publiée. Rien n'est téléchargé.
|
// réellement publiée ; sinon la dernière publiée. Rien n'est téléchargé.
|
||||||
func handleEnginePlan(w http.ResponseWriter, r *http.Request) {
|
func handleEnginePlan(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Interroge le registre : POST seulement, comme le clic qui l'appelle.
|
||||||
|
if r.Method != http.MethodPost {
|
||||||
|
sendJSON(w, http.StatusMethodNotAllowed, map[string]any{"ok": false, "error": "POST requis"})
|
||||||
|
return
|
||||||
|
}
|
||||||
if !engineOCISupported() {
|
if !engineOCISupported() {
|
||||||
sendJSON(w, 200, map[string]any{"ok": false, "error": engineUnsupportedWhy()})
|
sendJSON(w, 200, map[string]any{"ok": false, "error": engineUnsupportedWhy()})
|
||||||
return
|
return
|
||||||
@@ -454,6 +500,11 @@ func handleEnginePlan(w http.ResponseWriter, r *http.Request) {
|
|||||||
// réseau. Le moteur quitté devient à son tour « le précédent » : deux clics
|
// réseau. Le moteur quitté devient à son tour « le précédent » : deux clics
|
||||||
// ramènent où l'on était.
|
// ramènent où l'on était.
|
||||||
func handleEngineRollback(w http.ResponseWriter, r *http.Request) {
|
func handleEngineRollback(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Change la configuration et redémarre le moteur : POST seulement.
|
||||||
|
if r.Method != http.MethodPost {
|
||||||
|
sendJSON(w, http.StatusMethodNotAllowed, map[string]any{"ok": false, "error": "POST requis"})
|
||||||
|
return
|
||||||
|
}
|
||||||
if tuneDenyHTTP(w) {
|
if tuneDenyHTTP(w) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -469,6 +520,7 @@ func handleEngineRollback(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
engineRememberPrevious(cur, bin)
|
engineRememberPrevious(cur, bin)
|
||||||
|
engineRenderCancel()
|
||||||
if serviceIsActive() {
|
if serviceIsActive() {
|
||||||
_ = serviceAction("restart")
|
_ = serviceAction("restart")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -183,6 +183,67 @@ func TestRetourVersionPrecedente(t *testing.T) {
|
|||||||
if rec.Code != 400 {
|
if rec.Code != 400 {
|
||||||
t.Errorf("rollback sans précédente : HTTP %d, attendu 400", rec.Code)
|
t.Errorf("rollback sans précédente : HTTP %d, attendu 400", rec.Code)
|
||||||
}
|
}
|
||||||
|
// Les deux routes nouvelles changent la configuration ou interrogent le
|
||||||
|
// registre : jamais sur un GET.
|
||||||
|
for _, h := range []http.HandlerFunc{handleEngineRollback, handleEnginePlan} {
|
||||||
|
rec = httptest.NewRecorder()
|
||||||
|
h(rec, httptest.NewRequest("GET", "/api/engine/x", nil))
|
||||||
|
if rec.Code != http.StatusMethodNotAllowed {
|
||||||
|
t.Errorf("GET : HTTP %d, attendu 405", rec.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Une bascule plus récente périme la vérification du rendu encore en attente
|
||||||
|
// de son moteur : elle s'arrête sans rien ranger, et son relevé n'écrase pas
|
||||||
|
// celui de la bascule suivante.
|
||||||
|
func TestVerificationDuRenduPerimee(t *testing.T) {
|
||||||
|
testHome(t)
|
||||||
|
freshTplProbe(t)
|
||||||
|
oldPoll, oldWait := engineRenderPoll, engineRenderWait
|
||||||
|
engineRenderPoll, engineRenderWait = time.Millisecond, 10*time.Second
|
||||||
|
t.Cleanup(func() {
|
||||||
|
engineRenderPoll, engineRenderWait = oldPoll, oldWait
|
||||||
|
engineRenderMu.Lock()
|
||||||
|
engineRenderLast = nil
|
||||||
|
engineRenderMu.Unlock()
|
||||||
|
})
|
||||||
|
f := &fakeTpl{mode: "qwen3", build: "b10678-old"}
|
||||||
|
f.start(t)
|
||||||
|
ref, ok := engineRenderBefore(context.Background())
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("rendu de référence impossible")
|
||||||
|
}
|
||||||
|
// L'ancien moteur répond toujours : la vérification attend.
|
||||||
|
done := make(chan struct{})
|
||||||
|
go func() { engineRenderAfter(ref, "server-cuda-b11351"); close(done) }()
|
||||||
|
deadline := time.Now().Add(5 * time.Second)
|
||||||
|
for {
|
||||||
|
if c := engineRenderSnapshot(); c != nil && c.Status == "pending" {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if time.Now().After(deadline) {
|
||||||
|
t.Fatal("vérification jamais commencée")
|
||||||
|
}
|
||||||
|
time.Sleep(time.Millisecond)
|
||||||
|
}
|
||||||
|
// Retour à la version précédente : vérification périmée, relevé effacé.
|
||||||
|
engineRenderCancel()
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
case <-time.After(5 * time.Second):
|
||||||
|
t.Fatal("vérification périmée toujours en attente")
|
||||||
|
}
|
||||||
|
if c := engineRenderSnapshot(); c != nil {
|
||||||
|
t.Errorf("relevé d'une bascule annulée : %+v", c)
|
||||||
|
}
|
||||||
|
// Un relevé d'une génération passée n'écrase rien.
|
||||||
|
gen := engineRenderBegin()
|
||||||
|
engineRenderSet(gen, engineRenderCheck{Status: "same", Tag: "neuf"})
|
||||||
|
engineRenderSet(gen-1, engineRenderCheck{Status: "changed", Tag: "ancien"})
|
||||||
|
if c := engineRenderSnapshot(); c == nil || c.Tag != "neuf" {
|
||||||
|
t.Errorf("relevé écrasé par une vérification périmée : %+v", c)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Le verdict « le prompt rendu va changer » : seulement quand c'est établi
|
// Le verdict « le prompt rendu va changer » : seulement quand c'est établi
|
||||||
|
|||||||
Reference in new issue
Block a user