Moteur : un refus de LOAD_GUARD n'est plus relancé en boucle par systemd

Le refus d'un chargement résident trop gros pour la RAM faisait sortir
« loki serve » en erreur (code 1) ; l'unité loki-engine (Restart=on-failure,
RestartSec=3) le relançait toutes les trois secondes, sans fin, pour un échec
qui ne dépend que du preset et de la machine.

- Le refus sort avec un code dédié, 78 (EX_CONFIG) ; mustExit respecte le
  code porté par l'erreur, toutes les autres gardent 1.
- L'unité écrite par « loki install » porte RestartPreventExitStatus=78 ;
  les autres échecs restent relancés.
- Unité d'une version précédente (« loki update » ne réécrit pas les unités,
  il tourne sans droits root) : reconnue à coup sûr (parent systemd,
  INVOCATION_ID, unité et compléments lus sans la directive), le refus y sort
  sans erreur pour ne pas boucler, avec la marche à suivre au journal.
  Même chose sous launchd, qui relance toute sortie non nulle.
- Le moteur d'essai de l'optimiseur garde toujours le vrai code. Le
  conteneur ne relance jamais le moteur de lui-même : rien à y changer, la
  raison reste au journal et l'interface la montre (modelLoadError).
- Tests : directive reconnue (et seulement elle), code de sortie du refus,
  enveloppé ou non, unité générée (Linux).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
MichaelandClaude Opus 5.5 committed 2026-10-04 14:29:13 +02:00
1 parent d50c8a56c6
commit a4db6d4da1
9 files changed
+194 -3

No files matched your search

+47
View File
@@ -1,6 +1,8 @@
package loki
import (
"errors"
"fmt"
"reflect"
"strings"
"testing"
@@ -283,3 +285,48 @@ func TestModelLoadErrorLoadGuard(t *testing.T) {
t.Errorf("refus ancien encore signalé : %q", got)
}
}
// Un refus de LOAD_GUARD ne doit pas être relancé en boucle : l'unité systemd
// écrite par « loki install » exclut son code de sortie, une unité d'avant ne
// le fait pas.
func TestUnitPreventsRefusalRestart(t *testing.T) {
for _, c := range []struct {
unit string
want bool
}{
{"[Service]\nRestart=on-failure\nRestartSec=3\n", false},
{"[Service]\nRestart=on-failure\nRestartPreventExitStatus=78\n", true},
{"[Service]\n RestartPreventExitStatus = 1 78 SIGKILL\n", true},
{"[Service]\nRestartPreventExitStatus=178\n", false},
{"[Service]\n# RestartPreventExitStatus=78\n", false},
} {
if got := unitPreventsRefusalRestart(c.unit); got != c.want {
t.Errorf("%q : %v, attendu %v", c.unit, got, c.want)
}
}
}
// Le refus sort avec son propre code (pas 1), y compris enveloppé ; une erreur
// ordinaire garde 1. Le moteur d'essai garde toujours le vrai code.
func TestLoadGuardRefusalExitStatus(t *testing.T) {
err := loadGuardRefusal("refus ; "+loadGuardMarker, true)
if got := exitStatusOf(err); got != loadGuardExitStatus {
t.Errorf("essai : code %d, attendu %d", got, loadGuardExitStatus)
}
if !strings.Contains(err.Error(), loadGuardMarker) {
t.Errorf("raison perdue : %q", err)
}
if got := exitStatusOf(fmt.Errorf("enveloppé : %w", err)); got != loadGuardExitStatus {
t.Errorf("enveloppé : code %d", got)
}
if got := exitStatusOf(errors.New("modèle introuvable")); got != 1 {
t.Errorf("erreur ordinaire : code %d, attendu 1", got)
}
// Hors superviseur (les tests ne tournent ni sous systemd ni sous launchd) :
// le vrai code aussi.
if restarts, _ := supervisorRestartsRefusal(); !restarts {
if got := exitStatusOf(loadGuardRefusal("refus", false)); got != loadGuardExitStatus {
t.Errorf("hors superviseur : code %d", got)
}
}
}