An RPM repository names each metadata file after the hash of its content; only repomd.xml is current. Classified volatile, a primary.xml.gz of tens of megabytes was taken whole on every install, about 110 MB per VM on a RHEL family system. dnf fetches .zck metadata by ranges, and a range is never stored: upstream cut, the next VM got 504 and dnf stopped. Such files are now immutable, and a range on a missing immutable file makes the cache fetch the whole file in the background, once per key. Checked: a range served offline from disk, each rule mutation-tested, race detector clean. --- FR --- Un dépôt RPM nomme chaque métadonnée d'après l'empreinte de son contenu ; seul repomd.xml est courant. Classé volatile, un primary.xml.gz de dizaines de mégaoctets était repris en entier à chaque installation, environ 110 Mo par VM sur un système de la famille RHEL. dnf prend les métadonnées .zck par plages, et une plage ne se garde pas : amont coupé, la VM suivante recevait 504 et dnf s'arrêtait. Ces fichiers sont désormais figés, et une plage sur un fichier figé absent fait prendre au cache le fichier entier en arrière-plan, une fois par clé. Vérifié : plage servie hors ligne du disque, chaque règle éprouvée par mutation, détecteur de courses muet. Assisted-by: Claude Opus 5
139 lines
4.4 KiB
Go
139 lines
4.4 KiB
Go
// © 2026 TechnoLibre (http://www.technolibre.ca)
|
|
// License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
|
|
|
|
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"strings"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// amontAPlages sert un corps fixe et honore « Range » comme un miroir : 206
|
|
// et le seul fragment demandé. Il compte séparément les plages et les corps
|
|
// entiers envoyés.
|
|
type amontAPlages struct {
|
|
srv *httptest.Server
|
|
plages int64
|
|
entiers int64
|
|
}
|
|
|
|
func nouvelAmontAPlages(t *testing.T, corps string) *amontAPlages {
|
|
t.Helper()
|
|
a := &amontAPlages{}
|
|
a.srv = httptest.NewServer(http.HandlerFunc(
|
|
func(w http.ResponseWriter, r *http.Request) {
|
|
if r.Header.Get("Range") != "" {
|
|
atomic.AddInt64(&a.plages, 1)
|
|
} else {
|
|
atomic.AddInt64(&a.entiers, 1)
|
|
}
|
|
http.ServeContent(w, r, "", time.Time{}, bytes.NewReader([]byte(corps)))
|
|
}))
|
|
t.Cleanup(a.srv.Close)
|
|
return a
|
|
}
|
|
|
|
func (a *amontAPlages) hote() string {
|
|
return strings.TrimPrefix(a.srv.URL, "http://")
|
|
}
|
|
|
|
func demandePlage(t *testing.T, p *Proxy, hote, chemin, plage string) *httptest.ResponseRecorder {
|
|
t.Helper()
|
|
r := httptest.NewRequest("GET", chemin, nil)
|
|
r.Host = hote
|
|
r.Header.Set("Range", plage)
|
|
w := httptest.NewRecorder()
|
|
p.serve(w, r, "http")
|
|
return w
|
|
}
|
|
|
|
const metadonneeZck = "/fedora/updates/repodata/" +
|
|
"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" +
|
|
"-primary.xml.zck"
|
|
|
|
// Une plage sur une métadonnée figée absente fait prendre le fichier entier,
|
|
// une fois ; les plages suivantes sortent du disque sans rien demander à
|
|
// l'amont.
|
|
func TestUnePlageFaitGarderLeFichierEntier(t *testing.T) {
|
|
a := nouvelAmontAPlages(t, "en-tete|morceau-1|morceau-2")
|
|
p := proxyDeTest(t)
|
|
|
|
w := demandePlage(t, p, a.hote(), metadonneeZck, "bytes=0-6")
|
|
if w.Code != http.StatusPartialContent || w.Body.String() != "en-tete" {
|
|
t.Fatalf("première plage : %d %q", w.Code, w.Body.String())
|
|
}
|
|
p.attendreCompletions()
|
|
|
|
w = demandePlage(t, p, a.hote(), metadonneeZck, "bytes=8-16")
|
|
if w.Code != http.StatusPartialContent || w.Body.String() != "morceau-1" {
|
|
t.Errorf("seconde plage : %d %q", w.Code, w.Body.String())
|
|
}
|
|
if got := w.Header().Get("X-ERPLibre-Cache"); got != OutcomeHit {
|
|
t.Errorf("seconde plage servie « %s », attendu « %s »", got, OutcomeHit)
|
|
}
|
|
if n := atomic.LoadInt64(&a.entiers); n != 1 {
|
|
t.Errorf("%d corps entiers pris à l'amont, attendu 1", n)
|
|
}
|
|
if n := atomic.LoadInt64(&a.plages); n != 1 {
|
|
t.Errorf("%d plages envoyées à l'amont, attendu 1", n)
|
|
}
|
|
}
|
|
|
|
// Amont coupé, la plage d'une VM suivante sort du disque : c'est ce qui manquait
|
|
// à une installation hors ligne.
|
|
func TestHorsLigneLaPlageSortDuDisque(t *testing.T) {
|
|
a := nouvelAmontAPlages(t, "en-tete|morceau-1|morceau-2")
|
|
p := proxyDeTest(t)
|
|
hote := a.hote()
|
|
demandePlage(t, p, hote, metadonneeZck, "bytes=0-6")
|
|
p.attendreCompletions()
|
|
a.srv.Close()
|
|
|
|
w := demandePlage(t, p, hote, metadonneeZck, "bytes=18-26")
|
|
if w.Code != http.StatusPartialContent || w.Body.String() != "morceau-2" {
|
|
t.Errorf("hors ligne : %d %q", w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
// Un index volatile demandé par plage ne déclenche aucune prise : il n'est
|
|
// jamais servi du disque tant que l'amont répond, le garder n'épargnerait rien.
|
|
func TestUnePlageSurUnIndexNePrendRien(t *testing.T) {
|
|
a := nouvelAmontAPlages(t, "index de dépôt")
|
|
p := proxyDeTest(t)
|
|
|
|
demandePlage(t, p, a.hote(), "/fedora/repodata/repomd.xml", "bytes=0-4")
|
|
p.attendreCompletions()
|
|
if n := atomic.LoadInt64(&a.entiers); n != 0 {
|
|
t.Errorf("%d corps entiers pris pour un index volatile, attendu 0", n)
|
|
}
|
|
}
|
|
|
|
// Plusieurs plages simultanées sur le même fichier ne prennent le corps entier
|
|
// qu'une fois.
|
|
func TestDesPlagesSimultaneesNePrennentQuUneFois(t *testing.T) {
|
|
a := nouvelAmontAPlages(t, strings.Repeat("x", 1<<16))
|
|
p := proxyDeTest(t)
|
|
u, _ := url.Parse("http://" + a.hote() + metadonneeZck)
|
|
key := CleDe("GET", u)
|
|
p.completions.Store(key, true) // une prise est déjà en cours
|
|
|
|
for i := 0; i < 5; i++ {
|
|
demandePlage(t, p, a.hote(), metadonneeZck, "bytes=0-9")
|
|
}
|
|
p.attendreCompletions()
|
|
if n := atomic.LoadInt64(&a.entiers); n != 0 {
|
|
t.Errorf("%d prises lancées alors qu'une était en cours, attendu 0", n)
|
|
}
|
|
p.completions.Delete(key)
|
|
demandePlage(t, p, a.hote(), metadonneeZck, "bytes=0-9")
|
|
p.attendreCompletions()
|
|
if n := atomic.LoadInt64(&a.entiers); n != 1 {
|
|
t.Errorf("%d prises après la fin de la première, attendu 1", n)
|
|
}
|
|
}
|