diff --git a/README.md b/README.md index afa98a6..fe19047 100644 --- a/README.md +++ b/README.md @@ -679,8 +679,10 @@ Ajoutées par ce fork : claire ; les tâches planifiées attendent. Un verrou dont le propriétaire est mort (Loki tué en plein essai) est écarté avant tout démarrage du moteur : l'essai orphelin n'est arrêté que si PID, instant de démarrage ET binaire - concordent (`taskkill /T` sous Windows), et le moteur est relancé au - démarrage de l'interface s'il tournait avant. **Refus d'entrée** : preset + concordent (`taskkill /T` sous Windows), et le moteur est relancé s'il + tournait avant — au démarrage de l'interface, puis toutes les 30 s par le + processus web (un `stat` tant qu'aucun verrou n'existe) : un `loki tune` tué + par `kill -9` ne laisse plus le moteur arrêté. **Refus d'entrée** : preset externe, aucun preset actif, génération, tâche, bench ou job `bash_bg` en cours, moteur occupé (`/slots`) — en ligne de commande, seul ce dernier contrôle voit le processus web : préférer le bouton. **Essais** (descente étape par étape depuis diff --git a/internal/loki/backend_tune_lock.go b/internal/loki/backend_tune_lock.go index aa45f53..76590bb 100644 --- a/internal/loki/backend_tune_lock.go +++ b/internal/loki/backend_tune_lock.go @@ -332,15 +332,25 @@ func tuneUndoApply(o tuneOwner) { fmt.Fprintf(os.Stderr, "[loki tune] application interrompue avant sa vérification : ancienne version du preset rétablie\n") } -// tuneRecoverAtBoot : au démarrage du processus web, une optimisation -// interrompue (Loki tué en plein essai) a laissé le vrai moteur arrêté et peut- +// tuneRecoverStale : une optimisation interrompue (Loki redémarré en plein +// essai, « loki tune » tué par kill -9) a laissé le vrai moteur arrêté et peut- // être un essai en VRAM. On arrête l'essai, puis on relance le moteur s'il -// tournait avant — ce que l'optimisation aurait fait en finissant. -func tuneRecoverAtBoot() { +// tournait avant — ce que l'optimisation aurait fait en finissant. Sans +// verrou, un simple stat : rien d'autre n'est lu. +func tuneRecoverStale() { if _, err := os.Stat(tuneLockPath()); err != nil { return } - if !tuneReapStale() || externalActive() || serviceIsActive() { + if tuneReapStale() { + tuneRecoverStart() + } +} + +// tuneRecoverStart relance le moteur après une optimisation interrompue, sauf +// preset externe, moteur déjà reparti ou configuration incomplète. Variable +// pour les tests. +var tuneRecoverStart = func() { + if externalActive() || serviceIsActive() { return } if err := preflightEngine(); err != nil { @@ -349,3 +359,19 @@ func tuneRecoverAtBoot() { fmt.Println(dim("[loki tune] optimisation interrompue : relance du moteur")) _ = serviceActionOS("start") } + +// tuneRecoverEvery : la période de la reprise dans le processus web. +const tuneRecoverEvery = 30 * time.Second + +// tuneRecoverWatch : au démarrage du processus web, puis à chaque tic. Le +// démarrage seul ne suffisait pas : un « loki tune » en ligne de commande tué +// sans ses defers (kill -9, terminal perdu) laissait le moteur arrêté jusqu'au +// prochain redémarrage de l'interface. Le tic ne coûte qu'un stat tant +// qu'aucun verrou n'existe ; pendant une optimisation vivante, une lecture du +// verrou et de l'identité de son propriétaire. +func tuneRecoverWatch(tick <-chan time.Time) { + tuneRecoverStale() + for range tick { + tuneRecoverStale() + } +} diff --git a/internal/loki/backend_tune_test.go b/internal/loki/backend_tune_test.go index 3524b51..3369e6f 100644 --- a/internal/loki/backend_tune_test.go +++ b/internal/loki/backend_tune_test.go @@ -832,3 +832,68 @@ func TestTuneUndoInterruptedApply(t *testing.T) { t.Error("configuration modifiée hors phase d'application") } } + +// Reprise dans le processus web : un « loki tune » tué sans ses defers ne +// laisse plus le moteur arrêté jusqu'au prochain démarrage de l'interface. Le +// tic suivant écarte le verrou périmé et relance le moteur — une fois, et +// seulement s'il tournait avant. Sans verrou, ni lecture ni relance. +func TestTuneRecoverWatch(t *testing.T) { + testHome(t) + alive := true + prevAlive, prevStart := tuneProcAlive, tuneRecoverStart + tuneProcAlive = func(p tuneProc) bool { return alive && p.PID == os.Getpid() } + starts := 0 + tuneRecoverStart = func() { starts++ } + t.Cleanup(func() { tuneProcAlive, tuneRecoverStart = prevAlive, prevStart }) + + tick := make(chan time.Time) + done := make(chan struct{}) + go func() { tuneRecoverWatch(tick); close(done) }() + // Un envoi est reçu quand le tic précédent a fini : deux de suite, et le + // premier est traité en entier. + step := func() { tick <- time.Now() } + step() + step() + if starts != 0 { + t.Fatal("relance sans verrou") + } + l, err := tuneLockAcquire("cli", true) + if err != nil { + t.Fatal(err) + } + step() + step() + if starts != 0 { + t.Fatal("relance pendant une optimisation vivante") + } + if _, err := os.Stat(tuneLockPath()); err != nil { + t.Fatal("verrou vivant retiré") + } + alive = false // kill -9 : le verrou reste, son propriétaire est mort + step() + step() + if starts != 1 { + t.Fatalf("relances : %d, attendu 1", starts) + } + if _, err := os.Stat(tuneLockPath()); !os.IsNotExist(err) { + t.Error("verrou périmé non retiré") + } + l.release() + + // Le moteur ne tournait pas avant : verrou retiré, pas de relance. + alive = true + if _, err := tuneLockAcquire("cli", false); err != nil { + t.Fatal(err) + } + alive = false + step() + step() + close(tick) + <-done + if starts != 1 { + t.Errorf("relance d'un moteur qui ne tournait pas : %d", starts) + } + if _, err := os.Stat(tuneLockPath()); !os.IsNotExist(err) { + t.Error("verrou périmé non retiré") + } +} diff --git a/internal/loki/web_server.go b/internal/loki/web_server.go index 90d1070..edd34e4 100644 --- a/internal/loki/web_server.go +++ b/internal/loki/web_server.go @@ -139,9 +139,10 @@ func newWebMux() *http.ServeMux { // Jeton de tentative du décodage spéculatif automatique : effacé dès que le // moteur répond, sans dépendre d'une page ouverte (voir backend_serve_spec.go). startSpecAttemptWatch() - // Optimisation interrompue (Loki tué en plein essai) : essai orphelin arrêté, - // moteur relancé s'il tournait avant (backend_tune_lock.go). - go tuneRecoverAtBoot() + // Optimisation interrompue (Loki ou « loki tune » tué en plein essai) : essai + // orphelin arrêté, moteur relancé s'il tournait avant (backend_tune_lock.go) + // — au démarrage, puis toutes les 30 s (un stat sans verrou). + go tuneRecoverWatch(time.NewTicker(tuneRecoverEvery).C) mux := http.NewServeMux() // Pages publiques : le HTML et le JS ne contiennent aucun secret. Toute la // donnée et toutes les actions passent par /api/* qui, lui, exige la clé.