erplibre/script/qemu_cache/plafond.go
Mathieu Benoit f20e21b7d2 [ADD] cache qemu : nettoyage par âge et plafond de taille
Le cache ne diminuait jamais de lui-même et vit sur le disque de
l'orchestrateur : un parc qui déploie chaque jour finissait par le remplir,
sans que rien ne le signale. Deux règles existent désormais, éteintes par
défaut et réglées depuis le menu : l'âge du dernier usage, et un plafond de
taille qui efface le moins récemment servi jusqu'à tenir dessous, objets et
dépôts git confondus. Un minuteur systemd quotidien les applique, et une
réinstallation garde les valeurs choisies. Le menu montre d'abord ce qui
partirait.

--- EN ---

The cache never shrank by itself and lives on the orchestrator's disk: a site
deploying every day filled it up with nothing to say so. Two rules now exist,
off by default and set from the menu: the age of last use, and a size ceiling
that erases the least recently served until the cache fits under it, objects
and git mirrors alike. A daily systemd timer applies them, and a reinstall
keeps the chosen values. The menu first shows what would go.

Assisted-by: Claude Opus 5
2026-09-16 05:36:13 -04:00

142 lines
3.7 KiB
Go

// © 2026 TechnoLibre (http://www.technolibre.ca)
// License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
package main
import (
"fmt"
"os"
"path/filepath"
"sort"
"strconv"
"strings"
"time"
)
// LireTaille lit une taille : un nombre, suivi ou non d'une unité binaire.
//
// « 50G », « 50Gio », « 50GiB », « 1.5 G », « 500M », « 2T » ou un nombre
// d'octets. Les unités valent des puissances de 1024 : c'est ce qu'affiche
// HumanBytes, et un plafond lu autrement que la taille affichée ne se
// comparerait pas à elle.
func LireTaille(s string) (int64, error) {
brut := strings.TrimSpace(s)
if brut == "" {
return 0, fmt.Errorf("%s", T("taille vide"))
}
t := strings.ToUpper(strings.ReplaceAll(brut, " ", ""))
for _, suffixe := range []string{"IB", "IO", "B", "O"} {
if strings.HasSuffix(t, suffixe) && len(t) > len(suffixe) {
t = strings.TrimSuffix(t, suffixe)
break
}
}
facteur := int64(1)
if n := len(t); n > 0 {
switch t[n-1] {
case 'K':
facteur = 1 << 10
case 'M':
facteur = 1 << 20
case 'G':
facteur = 1 << 30
case 'T':
facteur = 1 << 40
}
if facteur > 1 {
t = t[:n-1]
}
}
v, err := strconv.ParseFloat(t, 64)
if err != nil || v <= 0 {
return 0, fmt.Errorf(T("taille illisible %q : essayer 50G, 500M, 2T"), brut)
}
return int64(v * float64(facteur)), nil
}
// BilanPlafond dit ce qu'une purge au plafond a effacé, ou effacerait.
type BilanPlafond struct {
Objets int
OctetsObjets int64
Depots int
OctetsDepots int64
Avant int64
Apres int64
}
// candidat est un objet du magasin ou un dépôt en miroir, avec son dernier
// usage : ce qui décide de l'ordre d'éviction.
type candidat struct {
chemin string
octets int64
usage time.Time
depot bool
}
// PurgerJusqua efface le moins récemment servi, objets et dépôts confondus,
// jusqu'à ce que l'ensemble tienne sous le plafond. aBlanc compte sans rien
// effacer.
//
// Confondus, parce que le plafond porte sur le disque et que les deux s'y
// disputent la place : un dépôt oublié depuis des mois pèse plus que mille
// paquets servis récemment. L'usage d'un objet est la date de son corps, remise à
// jour à chaque service ; celle d'un dépôt, son dernier rafraîchissement.
// Rien ne part quand le cache tient déjà sous le plafond.
func PurgerJusqua(store *Store, miroir *GitMirror, plafond int64, aBlanc bool) (BilanPlafond, error) {
var b BilanPlafond
var tous []candidat
if store != nil && store.Dir != "" {
filepath.Walk(store.Dir, func(p string, info os.FileInfo, err error) error {
if err != nil || info == nil {
return nil
}
if horsCasier(store.Dir, p, info) {
return filepath.SkipDir
}
if info.IsDir() || filepath.Ext(p) != ".body" {
return nil
}
tous = append(tous, candidat{p, info.Size(), info.ModTime(), false})
return nil
})
}
for _, d := range miroir.Depots() {
tous = append(tous, candidat{d.Chemin, d.Octets, d.Maj, true})
}
for _, c := range tous {
b.Avant += c.octets
}
b.Apres = b.Avant
if b.Avant <= plafond {
return b, nil
}
sort.SliceStable(tous, func(i, j int) bool { return tous[i].usage.Before(tous[j].usage) })
for _, c := range tous {
if b.Apres <= plafond {
break
}
if !aBlanc {
if c.depot {
if err := miroir.Retirer(c.chemin); err != nil {
return b, err
}
} else {
if err := os.Remove(c.chemin); err != nil {
continue
}
// Le méta part AVEC le corps : orphelin, il ferait croire à
// une copie présente.
os.Remove(strings.TrimSuffix(c.chemin, ".body") + ".meta")
}
}
b.Apres -= c.octets
if c.depot {
b.Depots++
b.OctetsDepots += c.octets
} else {
b.Objets++
b.OctetsObjets += c.octets
}
}
return b, nil
}