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
136 lines
3.9 KiB
Go
136 lines
3.9 KiB
Go
// © 2026 TechnoLibre (http://www.technolibre.ca)
|
|
// License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
|
|
|
|
package main
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestLireTaille(t *testing.T) {
|
|
cas := map[string]int64{
|
|
"50G": 50 << 30,
|
|
"50Gio": 50 << 30,
|
|
"50GiB": 50 << 30,
|
|
"1.5 G": 3 << 29,
|
|
"500M": 500 << 20,
|
|
"2t": 2 << 40,
|
|
"1024": 1024,
|
|
"64K": 64 << 10,
|
|
}
|
|
for brut, veut := range cas {
|
|
if got, err := LireTaille(brut); err != nil || got != veut {
|
|
t.Errorf("%q : %d (%v), attendu %d", brut, got, err, veut)
|
|
}
|
|
}
|
|
for _, brut := range []string{"", "abc", "5X", "-1G", "0"} {
|
|
if _, err := LireTaille(brut); err == nil {
|
|
t.Errorf("%q accepté", brut)
|
|
}
|
|
}
|
|
}
|
|
|
|
// objetGarde range un corps de n octets, daté d'il y a `age`, et rend sa clé.
|
|
func objetGarde(t *testing.T, s *Store, nom string, n int, age time.Duration) string {
|
|
t.Helper()
|
|
cle := Key("GET", "http://miroir.example/"+nom)
|
|
w, err := s.NewWriter(cle, Meta{URL: "http://miroir.example/" + nom, Method: "GET", Status: 200})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
w.Write([]byte(strings.Repeat("x", n)))
|
|
if err := w.Commit(int64(n)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
_, corps := s.paths(cle)
|
|
quand := time.Now().Add(-age)
|
|
os.Chtimes(corps, quand, quand)
|
|
return cle
|
|
}
|
|
|
|
// Le moins récemment servi part d'abord, et la purge s'arrête dès que le
|
|
// cache tient sous le plafond : effacer plus rendrait de la place qu'on ne
|
|
// demandait pas, au prix de téléchargements repayés.
|
|
func TestLePlafondEffaceLePlusAncienDAbord(t *testing.T) {
|
|
s := &Store{Dir: t.TempDir()}
|
|
vieux := objetGarde(t, s, "vieux", 100, 90*24*time.Hour)
|
|
moyen := objetGarde(t, s, "moyen", 100, 30*24*time.Hour)
|
|
recent := objetGarde(t, s, "recent", 100, time.Hour)
|
|
|
|
b, err := PurgerJusqua(s, &GitMirror{}, 250, false)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if b.Objets != 1 || b.Avant != 300 || b.Apres != 200 {
|
|
t.Errorf("bilan %+v, attendu 1 objet, 300 → 200", b)
|
|
}
|
|
if s.Detient(vieux) {
|
|
t.Error("le plus ancien est resté")
|
|
}
|
|
if !s.Detient(moyen) || !s.Detient(recent) {
|
|
t.Error("un objet plus récent est parti")
|
|
}
|
|
if metaPath, _ := s.paths(vieux); fichierExiste(metaPath) {
|
|
t.Error("le méta de l'objet effacé est resté")
|
|
}
|
|
}
|
|
|
|
func fichierExiste(p string) bool {
|
|
_, err := os.Stat(p)
|
|
return err == nil
|
|
}
|
|
|
|
func TestSousLePlafondRienNePart(t *testing.T) {
|
|
s := &Store{Dir: t.TempDir()}
|
|
cle := objetGarde(t, s, "seul", 100, 90*24*time.Hour)
|
|
b, _ := PurgerJusqua(s, &GitMirror{}, 1000, false)
|
|
if b.Objets != 0 || !s.Detient(cle) {
|
|
t.Errorf("sous le plafond, %d objet(s) effacé(s)", b.Objets)
|
|
}
|
|
}
|
|
|
|
func TestABlancRienNEstEfface(t *testing.T) {
|
|
s := &Store{Dir: t.TempDir()}
|
|
cle := objetGarde(t, s, "vieux", 100, 90*24*time.Hour)
|
|
objetGarde(t, s, "recent", 100, time.Hour)
|
|
b, _ := PurgerJusqua(s, &GitMirror{}, 150, true)
|
|
if b.Objets != 1 || b.Apres != 100 {
|
|
t.Errorf("bilan à blanc %+v, attendu 1 objet compté", b)
|
|
}
|
|
if !s.Detient(cle) {
|
|
t.Error("une purge à blanc a effacé un objet")
|
|
}
|
|
}
|
|
|
|
// Objets et dépôts se disputent le même disque : un dépôt oublié depuis des
|
|
// mois part avant des paquets servis récemment.
|
|
func TestUnDepotOublieCedeSaPlaceAvantLesObjets(t *testing.T) {
|
|
s := &Store{Dir: t.TempDir()}
|
|
objet := objetGarde(t, s, "paquet", 100, time.Hour)
|
|
m := &GitMirror{Dir: t.TempDir()}
|
|
depot := filepath.Join(m.Dir, "forge.example", "vieux.git")
|
|
if err := os.MkdirAll(depot, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
os.WriteFile(filepath.Join(depot, "pack"), []byte(strings.Repeat("y", 500)), 0o644)
|
|
quand := time.Now().Add(-120 * 24 * time.Hour)
|
|
os.Chtimes(depot, quand, quand)
|
|
|
|
b, err := PurgerJusqua(s, m, 200, false)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if b.Depots != 1 || b.Objets != 0 {
|
|
t.Errorf("bilan %+v, attendu le seul dépôt", b)
|
|
}
|
|
if fichierExiste(depot) {
|
|
t.Error("le dépôt oublié est resté")
|
|
}
|
|
if !s.Detient(objet) {
|
|
t.Error("l'objet récent est parti avant le dépôt oublié")
|
|
}
|
|
}
|