[ADD] cache qemu : revalider les index, ranger chaque représentation
Sur une installation complète, les index pip, les métadonnées npm et le bundle de repo pesaient environ 110 Mo par VM, repris en entier à chaque fois. Un index déjà gardé part désormais avec son ETag : l'amont juge chaque requête, et un « 304 » sert le corps depuis le disque. Sous « Vary: Accept », chaque représentation est rangée à part, faute de quoi deux clients qui alternent les retéléchargent tour à tour. Les métadonnées PEP 658 de pip, nommées « .whl.metadata », sont figées comme l'archive qu'elles décrivent. --- EN --- On a full install the pip indexes, npm metadata and repo bundle weighed about 110 MB per VM, taken whole every time. An index already kept now leaves with its ETag: upstream judges every request, and a « 304 » serves the body from disk. Under « Vary: Accept » each representation is stored apart, without which two clients alternating between them re-download each in turn. The PEP 658 metadata of pip, named « .whl.metadata », are as immutable as the archive they describe. Assisted-by: Claude Opus 5
This commit is contained in:
parent
7df58956ea
commit
90ef1d24e7
13 changed files with 595 additions and 4 deletions
|
|
@ -127,6 +127,8 @@ au [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|||
- The long-test menu asks before creating real machines, and asks again, in words of its own, before `--detruire` removes machines with their disks. The command is shown first, which is what makes the question answerable; a dry run or a performance report creates nothing and asks nothing
|
||||
- Every entry of the Proxmox VE menu carries an icon, the same picture meaning the same action as in the other menus of the tool
|
||||
- The QEMU cache binary speaks English or French: service journal, `--status`, `--age`, option help and the error served to a VM. The language comes from `--lang`, then `EL_LANG`, then French; the installer writes `EL_LANG` to the service settings and the TODO menu passes its own. Rules, verdict codes and JSON keys are never translated
|
||||
- A repository index the cache already holds is revalidated with its ETag rather than downloaded again: upstream still judges every request, and a « 304 » serves the stored body from disk. On a full ERPLibre install the pip indexes, npm metadata and repo bundle had been about 110 MB per VM, taken whole each time. An index stored without its host — shared by every mirror of a rotating list — and an answer carrying no ETag are taken whole as before; the access log names the new outcome `revalidated`
|
||||
- A registry page served under `Vary: Accept` keeps one copy per representation. npm asks for the same `/npm` page abridged, then complete, then abridged again; kept under one key they replaced each other and the 31 MB were fetched on every install. Each representation is now revalidated and, offline, served on its own; `--detient` still reads the page under its URL alone
|
||||
|
||||
<!-- [fr] -->
|
||||
|
||||
|
|
@ -138,6 +140,8 @@ au [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|||
- Le menu des tests longs demande avant de créer de vraies machines, et redemande, avec des mots à lui, avant que `--detruire` efface des machines avec leurs disques. La commande est montrée d'abord, c'est elle qui rend la question répondable ; un plan à blanc ou un rapport de performance ne crée rien et ne demande rien
|
||||
- Chaque entrée du menu Proxmox VE porte une icône, la même image voulant dire la même action que dans les autres menus de l'outil
|
||||
- Le binaire du cache QEMU parle anglais ou français : journal du service, `--status`, `--age`, aide des options et erreur servie à une VM. La langue vient de `--lang`, puis d'`EL_LANG`, puis du français ; l'installateur écrit `EL_LANG` dans les réglages du service et le menu TODO passe la sienne. Règles, codes de verdict et clés JSON ne se traduisent jamais
|
||||
- Un index de dépôt que le cache détient déjà est revalidé par son ETag au lieu d'être retéléchargé : l'amont juge toujours chaque requête, et un « 304 » sert le corps gardé depuis le disque. Sur une installation complète d'ERPLibre, les index pip, les métadonnées npm et le bundle de repo pesaient environ 110 Mo par VM, repris en entier à chaque fois. Un index rangé sans son hôte — partagé par tous les miroirs d'une liste qui tourne — et une réponse sans ETag sont repris en entier comme avant ; le journal d'accès nomme la nouvelle issue `revalidated`
|
||||
- Une page de registre servie sous `Vary: Accept` garde une copie par représentation. npm demande la même page `/npm` abrégée, puis complète, puis de nouveau abrégée ; rangées sous une seule clé, elles se remplaçaient et les 31 Mo repartaient à chaque installation. Chaque représentation est désormais revalidée et, hors ligne, servie à part ; `--detient` lit toujours la page sous sa seule URL
|
||||
|
||||
<!-- [en] -->
|
||||
## Fixed
|
||||
|
|
@ -182,6 +186,7 @@ au [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|||
- The mirror warning counts only the repositories of the Odoo version being deployed. It used to add up every manifest in the repository — the deprecated one included — and announced 170 missing mirrors where a real Odoo 18 deployment meets four: an alarm that fires for nothing is one that stops being read. Filling the mirrors still takes every version ahead, which is its purpose
|
||||
- A host banned from decryption on a burst of transport errors gets another chance. Three failed handshakes in a row put it in an opaque tunnel, and a tunnel never consults the store: a distribution mirror condemned by a few corrupted records sent all its traffic back upstream, including the hundreds of objects already held for it, until the service was restarted. A TLS alert still bans for good — the client looked at our certificate and refused it — but a repeated cut is only a suspicion, and it reopens after ten minutes
|
||||
- **Deployment › QEMU cache › Git mirrors** fills the base of the active Odoo version, or its extra modules, on their own — beside the full fill of every manifest, which takes hours. Each list shows how many repositories it declares and how many still lack a mirror. What a deployment clones is now read with the manifest merge's own rule and lists, so the extra modules, installed only on request, and the mobile project no longer count: the offline warning announced four missing mirrors that a default Odoo 18 install never clones
|
||||
- pip's PEP 658 metadata — the `.whl.metadata` file fetched before each wheel — is served from disk. The name ends in `.metadata`, which no rule knew, so each file was taken again on every install: 178 of them on a full ERPLibre install. Copies stored before this change are not reached again; the next online install refills them
|
||||
|
||||
<!-- [fr] -->
|
||||
|
||||
|
|
@ -222,6 +227,7 @@ au [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|||
- L'avertissement sur les miroirs ne compte que les dépôts de la version d'Odoo déployée. Il additionnait tous les manifestes du dépôt — le déprécié compris — et annonçait 170 miroirs manquants là où un vrai déploiement Odoo 18 en rencontre quatre : une alarme qui sonne pour rien est une alarme qu'on cesse de lire. Le remplissage des miroirs, lui, prend toujours de l'avance pour toutes les versions, ce qui est son rôle
|
||||
- Un hôte banni du déchiffrement sur une rafale d'erreurs de transport retrouve sa chance. Trois poignées de main manquées d'affilée le passaient en tunnel opaque, et un tunnel ne consulte jamais le magasin : un miroir de distribution condamné par quelques enregistrements corrompus renvoyait tout son trafic à l'amont — y compris les centaines d'objets déjà détenus pour lui — jusqu'au redémarrage du service. Une alerte TLS bannit toujours définitivement, le client ayant regardé notre certificat et l'ayant refusé ; mais une coupure répétée n'est qu'un soupçon, et elle se rouvre au bout de dix minutes
|
||||
- **Déploiement › Cache QEMU › Miroirs git** remplit à part la base de la version d'Odoo active, ou ses modules extra — à côté du remplissage de tous les manifestes, qui prend des heures. Chaque liste dit combien de dépôts elle déclare et combien n'ont pas encore de miroir. Ce qu'un déploiement clone se lit désormais avec la règle et les listes de la fusion des manifestes elle-même : les modules extra, installés seulement sur demande, et le projet mobile ne comptent plus, là où l'avertissement hors ligne annonçait quatre miroirs manquants qu'une installation Odoo 18 par défaut ne clone jamais
|
||||
- Les métadonnées PEP 658 de pip — le fichier `.whl.metadata` récupéré avant chaque roue — sont servies du disque. Le nom finit par `.metadata`, qu'aucune règle ne connaissait : chaque fichier repartait à chaque installation, 178 sur une installation complète d'ERPLibre. Les copies gardées avant ce changement ne sont plus atteintes ; la prochaine installation en ligne les remplit
|
||||
|
||||
<!-- [en] -->
|
||||
## Removed
|
||||
|
|
|
|||
|
|
@ -61,6 +61,8 @@ au [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|||
- Le menu des tests longs demande avant de créer de vraies machines, et redemande, avec des mots à lui, avant que `--detruire` efface des machines avec leurs disques. La commande est montrée d'abord, c'est elle qui rend la question répondable ; un plan à blanc ou un rapport de performance ne crée rien et ne demande rien
|
||||
- Chaque entrée du menu Proxmox VE porte une icône, la même image voulant dire la même action que dans les autres menus de l'outil
|
||||
- Le binaire du cache QEMU parle anglais ou français : journal du service, `--status`, `--age`, aide des options et erreur servie à une VM. La langue vient de `--lang`, puis d'`EL_LANG`, puis du français ; l'installateur écrit `EL_LANG` dans les réglages du service et le menu TODO passe la sienne. Règles, codes de verdict et clés JSON ne se traduisent jamais
|
||||
- Un index de dépôt que le cache détient déjà est revalidé par son ETag au lieu d'être retéléchargé : l'amont juge toujours chaque requête, et un « 304 » sert le corps gardé depuis le disque. Sur une installation complète d'ERPLibre, les index pip, les métadonnées npm et le bundle de repo pesaient environ 110 Mo par VM, repris en entier à chaque fois. Un index rangé sans son hôte — partagé par tous les miroirs d'une liste qui tourne — et une réponse sans ETag sont repris en entier comme avant ; le journal d'accès nomme la nouvelle issue `revalidated`
|
||||
- Une page de registre servie sous `Vary: Accept` garde une copie par représentation. npm demande la même page `/npm` abrégée, puis complète, puis de nouveau abrégée ; rangées sous une seule clé, elles se remplaçaient et les 31 Mo repartaient à chaque installation. Chaque représentation est désormais revalidée et, hors ligne, servie à part ; `--detient` lit toujours la page sous sa seule URL
|
||||
|
||||
## Corrigé
|
||||
|
||||
|
|
@ -101,6 +103,7 @@ au [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|||
- L'avertissement sur les miroirs ne compte que les dépôts de la version d'Odoo déployée. Il additionnait tous les manifestes du dépôt — le déprécié compris — et annonçait 170 miroirs manquants là où un vrai déploiement Odoo 18 en rencontre quatre : une alarme qui sonne pour rien est une alarme qu'on cesse de lire. Le remplissage des miroirs, lui, prend toujours de l'avance pour toutes les versions, ce qui est son rôle
|
||||
- Un hôte banni du déchiffrement sur une rafale d'erreurs de transport retrouve sa chance. Trois poignées de main manquées d'affilée le passaient en tunnel opaque, et un tunnel ne consulte jamais le magasin : un miroir de distribution condamné par quelques enregistrements corrompus renvoyait tout son trafic à l'amont — y compris les centaines d'objets déjà détenus pour lui — jusqu'au redémarrage du service. Une alerte TLS bannit toujours définitivement, le client ayant regardé notre certificat et l'ayant refusé ; mais une coupure répétée n'est qu'un soupçon, et elle se rouvre au bout de dix minutes
|
||||
- **Déploiement › Cache QEMU › Miroirs git** remplit à part la base de la version d'Odoo active, ou ses modules extra — à côté du remplissage de tous les manifestes, qui prend des heures. Chaque liste dit combien de dépôts elle déclare et combien n'ont pas encore de miroir. Ce qu'un déploiement clone se lit désormais avec la règle et les listes de la fusion des manifestes elle-même : les modules extra, installés seulement sur demande, et le projet mobile ne comptent plus, là où l'avertissement hors ligne annonçait quatre miroirs manquants qu'une installation Odoo 18 par défaut ne clone jamais
|
||||
- Les métadonnées PEP 658 de pip — le fichier `.whl.metadata` récupéré avant chaque roue — sont servies du disque. Le nom finit par `.metadata`, qu'aucune règle ne connaissait : chaque fichier repartait à chaque installation, 178 sur une installation complète d'ERPLibre. Les copies gardées avant ce changement ne sont plus atteintes ; la prochaine installation en ligne les remplit
|
||||
|
||||
## Retiré
|
||||
|
||||
|
|
|
|||
|
|
@ -61,6 +61,8 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|||
- The long-test menu asks before creating real machines, and asks again, in words of its own, before `--detruire` removes machines with their disks. The command is shown first, which is what makes the question answerable; a dry run or a performance report creates nothing and asks nothing
|
||||
- Every entry of the Proxmox VE menu carries an icon, the same picture meaning the same action as in the other menus of the tool
|
||||
- The QEMU cache binary speaks English or French: service journal, `--status`, `--age`, option help and the error served to a VM. The language comes from `--lang`, then `EL_LANG`, then French; the installer writes `EL_LANG` to the service settings and the TODO menu passes its own. Rules, verdict codes and JSON keys are never translated
|
||||
- A repository index the cache already holds is revalidated with its ETag rather than downloaded again: upstream still judges every request, and a « 304 » serves the stored body from disk. On a full ERPLibre install the pip indexes, npm metadata and repo bundle had been about 110 MB per VM, taken whole each time. An index stored without its host — shared by every mirror of a rotating list — and an answer carrying no ETag are taken whole as before; the access log names the new outcome `revalidated`
|
||||
- A registry page served under `Vary: Accept` keeps one copy per representation. npm asks for the same `/npm` page abridged, then complete, then abridged again; kept under one key they replaced each other and the 31 MB were fetched on every install. Each representation is now revalidated and, offline, served on its own; `--detient` still reads the page under its URL alone
|
||||
|
||||
## Fixed
|
||||
|
||||
|
|
@ -101,6 +103,7 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|||
- The mirror warning counts only the repositories of the Odoo version being deployed. It used to add up every manifest in the repository — the deprecated one included — and announced 170 missing mirrors where a real Odoo 18 deployment meets four: an alarm that fires for nothing is one that stops being read. Filling the mirrors still takes every version ahead, which is its purpose
|
||||
- A host banned from decryption on a burst of transport errors gets another chance. Three failed handshakes in a row put it in an opaque tunnel, and a tunnel never consults the store: a distribution mirror condemned by a few corrupted records sent all its traffic back upstream, including the hundreds of objects already held for it, until the service was restarted. A TLS alert still bans for good — the client looked at our certificate and refused it — but a repeated cut is only a suspicion, and it reopens after ten minutes
|
||||
- **Deployment › QEMU cache › Git mirrors** fills the base of the active Odoo version, or its extra modules, on their own — beside the full fill of every manifest, which takes hours. Each list shows how many repositories it declares and how many still lack a mirror. What a deployment clones is now read with the manifest merge's own rule and lists, so the extra modules, installed only on request, and the mobile project no longer count: the offline warning announced four missing mirrors that a default Odoo 18 install never clones
|
||||
- pip's PEP 658 metadata — the `.whl.metadata` file fetched before each wheel — is served from disk. The name ends in `.metadata`, which no rule knew, so each file was taken again on every install: 178 of them on a full ERPLibre install. Copies stored before this change are not reached again; the next online install refills them
|
||||
|
||||
## Removed
|
||||
|
||||
|
|
|
|||
|
|
@ -422,12 +422,14 @@ def table_posee_cmd(table: str = TABLE) -> str:
|
|||
# chercher là plutôt que de recopier son chemin, qui est réglable.
|
||||
CONF = "/etc/erplibre_go_qemu_cache/env"
|
||||
|
||||
# Les issues qui prouvent qu'un CORPS est en réserve. « fetched » n'en est
|
||||
# pas : elle couvre aussi le « 304 » d'une revalidation, qui n'a pas de corps.
|
||||
# Les issues qui prouvent qu'un CORPS est en réserve. « revalidated » en est :
|
||||
# l'amont a confirmé la copie gardée, et c'est elle qui est servie. « fetched »
|
||||
# n'en est pas : elle couvre aussi le « 304 » rendu à un client qui posait sa
|
||||
# propre condition, sans corps.
|
||||
# « stored-status » et « stale-status » n'en sont pas davantage : elles
|
||||
# portent un objet de STATUT seul — une redirection ou un refus gardés, sans
|
||||
# corps — et l'index d'une suite gardé en 404 ne la rend pas lisible.
|
||||
ISSUES_EN_RESERVE = ("stored", "hit", "stale")
|
||||
ISSUES_EN_RESERVE = ("stored", "hit", "stale", "revalidated")
|
||||
|
||||
|
||||
def journal(conf: str = CONF) -> str:
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ package main
|
|||
// qu'aucun T(...) n'emploie, et une traduction qui ne garde pas les mêmes
|
||||
// verbes de format que son original.
|
||||
var anglais = map[string]string{
|
||||
"cache : variante de %s non gardée : %v": "cache: variant of %s not kept: %v",
|
||||
"langue des messages : fr ou en ; à défaut, EL_LANG": "language of messages: fr or en; otherwise EL_LANG",
|
||||
"granularité inconnue %q : jour, semaine ou mois": "unknown granularity %q: day, week or month",
|
||||
"durée vide": "empty duration",
|
||||
|
|
|
|||
|
|
@ -55,6 +55,11 @@ var immutableSuffixes = []string{
|
|||
".pkg.tar.zst.sig", ".pkg.tar.xz.sig",
|
||||
// écosystèmes Python et Node
|
||||
".whl", ".tgz",
|
||||
// métadonnées d'une distribution Python servies à part (PEP 658) : le nom
|
||||
// est celui de l'archive suivi de « .metadata », sous le même chemin
|
||||
// d'empreinte. Sans ces suffixes, « .metadata » ne correspond à rien et le
|
||||
// fichier tombe dans le volatil par défaut, repris à chaque installation.
|
||||
".whl.metadata", ".tar.gz.metadata", ".zip.metadata",
|
||||
// images et supports d'installation
|
||||
".qcow2", ".iso", ".img", ".raw", ".vmdk",
|
||||
// archives amont
|
||||
|
|
|
|||
|
|
@ -24,6 +24,10 @@ func TestClassify(t *testing.T) {
|
|||
ClassImmutable, "paquet Fedora"},
|
||||
{"https://pypi.example/packages/ab/cd/requests-2.33.0-py3-none-any.whl",
|
||||
ClassImmutable, "roue Python"},
|
||||
{"https://pypi.example/packages/ab/cd/requests-2.33.0-py3-none-any.whl.metadata",
|
||||
ClassImmutable, "métadonnées d'une roue (PEP 658)"},
|
||||
{"https://pypi.example/packages/ab/cd/requests-2.33.0.tar.gz.metadata",
|
||||
ClassImmutable, "métadonnées d'une archive source (PEP 658)"},
|
||||
{"https://images.example/arch/Arch-Linux-x86_64-cloudimg.qcow2",
|
||||
ClassImmutable, "image cloud"},
|
||||
|
||||
|
|
@ -247,3 +251,20 @@ func TestUnPointeurDeDerniereVersionEstVolatile(t *testing.T) {
|
|||
t.Errorf("une version nommée classée « %s », attendu « immutable »", got)
|
||||
}
|
||||
}
|
||||
|
||||
// Les métadonnées PEP 658 se rangent sans leur hôte, comme l'archive qu'elles
|
||||
// décrivent : leur chemin porte déjà l'empreinte du contenu.
|
||||
func TestLesMetadonneesPythonSontPortables(t *testing.T) {
|
||||
for _, brut := range []string{
|
||||
"https://pypi.example/packages/ab/cd/requests-2.33.0-py3-none-any.whl.metadata",
|
||||
"https://pypi.example/packages/ab/cd/requests-2.33.0.zip.metadata",
|
||||
} {
|
||||
u, err := url.Parse(brut)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !PortableParChemin(u) {
|
||||
t.Errorf("%s n'est pas portable", brut)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ import (
|
|||
"time"
|
||||
)
|
||||
|
||||
const version = "0.2.4"
|
||||
const version = "0.2.8"
|
||||
|
||||
func main() {
|
||||
var (
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ const (
|
|||
// en réserve, et un refus gardé ne l'est pas.
|
||||
OutcomeStoredStatus = "stored-status" // pris à l'amont, statut seul gardé
|
||||
OutcomeStaleStatus = "stale-status" // amont muet, statut seul rejoué
|
||||
OutcomeRevalidated = "revalidated" // l'amont confirme la copie (304), corps servi du disque
|
||||
)
|
||||
|
||||
// AccessLog écrit une ligne JSON par requête. Un format à une ligne par
|
||||
|
|
@ -287,6 +288,42 @@ func (p *Proxy) serve(w http.ResponseWriter, r *http.Request, scheme string) {
|
|||
amont = sansCondition(r)
|
||||
}
|
||||
|
||||
// Un index déjà gardé part avec SON validateur. L'amont reste juge de
|
||||
// chaque requête — un index n'est jamais servi du disque sans son accord
|
||||
// tant qu'il répond — mais une copie qu'il déclare à jour n'est pas
|
||||
// retéléchargée : « 304 » ne porte aucun corps, et le nôtre sort du
|
||||
// disque. Sans cela, chaque installation reprenait en entier des index
|
||||
// de dizaines de mégaoctets qui n'avaient pas changé.
|
||||
//
|
||||
// L'ETag seul, jamais la date : sous « Vary: Accept », deux
|
||||
// représentations d'une même URL partagent leur Last-Modified, et un
|
||||
// « 304 » accordé sur la date validerait celle qui n'est pas gardée. Pas
|
||||
// sous une clé portable non plus : partagée par tous les miroirs, elle
|
||||
// présenterait à l'un le validateur d'un autre. La condition du client,
|
||||
// quand il en pose une, reste la sienne.
|
||||
//
|
||||
// Sous « Vary: Accept », la représentation que CE client accepte a pu être
|
||||
// rangée à part (voir CleVariante) : c'est son validateur qui part, et
|
||||
// elle qui sort sur « 304 ».
|
||||
cleVariante := ""
|
||||
if cacheable && class == ClassVolatile && r.Method == "GET" && !partial {
|
||||
cleVariante = CleVariante(key, r.Header.Get("Accept"))
|
||||
}
|
||||
varianteTenue := cleVariante != "" && p.Store.Detient(cleVariante)
|
||||
cleRevalidee := key
|
||||
if varianteTenue {
|
||||
cleRevalidee = cleVariante
|
||||
}
|
||||
revalide := false
|
||||
if cacheable && (detient || varianteTenue) && class == ClassVolatile &&
|
||||
r.Method == "GET" && !partial && !conditionnelle && !PortableParChemin(u) {
|
||||
if etag := p.etagGarde(cleRevalidee); etag != "" {
|
||||
amont = r.Clone(r.Context())
|
||||
amont.Header.Set("If-None-Match", etag)
|
||||
revalide = true
|
||||
}
|
||||
}
|
||||
|
||||
// Un statut seul vit sous sa propre clé, que les lecteurs de corps ne
|
||||
// calculent pas : voir CleStatut.
|
||||
cleStatut := CleStatut(r.Method, u)
|
||||
|
|
@ -300,6 +337,19 @@ func (p *Proxy) serve(w http.ResponseWriter, r *http.Request, scheme string) {
|
|||
(detient || conditionnelle || p.Store.TientStatut(cleStatut))
|
||||
|
||||
resp, upErr := p.fetch(amont, u, repli)
|
||||
if upErr == nil && revalide && resp.StatusCode == http.StatusNotModified {
|
||||
resp.Body.Close()
|
||||
// Effacée entre-temps — par un nettoyage —, la copie ne sort plus :
|
||||
// la requête repart sans condition, et le client reçoit le corps de
|
||||
// l'amont plutôt qu'un « 304 » qu'il n'a pas demandé.
|
||||
if p.serveFromStore(w, r, u, cleRevalidee, class, OutcomeRevalidated) {
|
||||
if cleRevalidee != key {
|
||||
p.Store.Toucher(key)
|
||||
}
|
||||
return
|
||||
}
|
||||
resp, upErr = p.fetch(r, u, repli)
|
||||
}
|
||||
// Une redirection est SUIVIE quand le nom du fichier demandé porte déjà
|
||||
// son identité, et le contenu est gardé sous l'URL DEMANDÉE.
|
||||
//
|
||||
|
|
@ -322,6 +372,13 @@ func (p *Proxy) serve(w http.ResponseWriter, r *http.Request, scheme string) {
|
|||
// Un index plus récent que la signature qui l'annonce n'est PAS
|
||||
// servi : voir indexIncoherent. Le client tombe alors sur le « 304 »
|
||||
// qui le laisse garder ses listes, ou sur le refus qui suit.
|
||||
// La représentation de CE client d'abord : la base porte la dernière
|
||||
// rangée, qui peut être l'autre.
|
||||
if cacheable && varianteTenue && !p.indexIncoherent(u, key) &&
|
||||
p.serveFromStore(w, r, u, cleVariante, class, OutcomeStale) {
|
||||
p.Store.Toucher(key)
|
||||
return
|
||||
}
|
||||
if cacheable && !p.indexIncoherent(u, key) &&
|
||||
p.serveFromStore(w, r, u, key, class, OutcomeStale) {
|
||||
return
|
||||
|
|
@ -450,6 +507,13 @@ func (p *Proxy) serve(w http.ResponseWriter, r *http.Request, scheme string) {
|
|||
outcome = OutcomeStoredStatus
|
||||
} else {
|
||||
outcome = OutcomeStored
|
||||
// La base est publiée telle qu'avant ; sa copie sous la variante
|
||||
// garde cette représentation quand l'autre la remplacera.
|
||||
if cleVariante != "" && varieSurAccept(resp.Header) {
|
||||
if verr := p.Store.Copier(key, cleVariante); verr != nil {
|
||||
log.Printf(T("cache : variante de %s non gardée : %v"), u, verr)
|
||||
}
|
||||
}
|
||||
}
|
||||
case !cacheable:
|
||||
outcome = OutcomePassthrough
|
||||
|
|
@ -462,6 +526,30 @@ func (p *Proxy) serve(w http.ResponseWriter, r *http.Request, scheme string) {
|
|||
})
|
||||
}
|
||||
|
||||
// varieSurAccept dit si la réponse annonce varier selon l'en-tête Accept.
|
||||
// « Vary » est une liste de noms d'en-têtes, insensible à la casse, et peut
|
||||
// être répété.
|
||||
func varieSurAccept(h http.Header) bool {
|
||||
for _, v := range h.Values("Vary") {
|
||||
for _, nom := range strings.Split(v, ",") {
|
||||
if strings.EqualFold(strings.TrimSpace(nom), "Accept") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// etagGarde rend l'ETag du corps gardé sous la clé, ou "" : rien de gardé,
|
||||
// un statut seul, ou une réponse d'amont qui n'en portait pas.
|
||||
func (p *Proxy) etagGarde(key string) string {
|
||||
m, err := p.Store.LireMeta(key)
|
||||
if err != nil || m.StatutSeul() {
|
||||
return ""
|
||||
}
|
||||
return m.Header.Get("ETag")
|
||||
}
|
||||
|
||||
// serveFromStore rend vrai quand la réponse est partie du disque.
|
||||
func (p *Proxy) serveFromStore(
|
||||
w http.ResponseWriter, r *http.Request, u *url.URL, key string,
|
||||
|
|
|
|||
207
script/qemu_cache/revalidation_test.go
Normal file
207
script/qemu_cache/revalidation_test.go
Normal file
|
|
@ -0,0 +1,207 @@
|
|||
// © 2026 TechnoLibre (http://www.technolibre.ca)
|
||||
// License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"sync"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// amontValidant sert un corps sous un ETag et rend « 304 » à qui présente le
|
||||
// bon. Il compte les CORPS envoyés, qui sont ce que la revalidation épargne,
|
||||
// et note la condition reçue à chaque requête.
|
||||
type amontValidant struct {
|
||||
srv *httptest.Server
|
||||
mu sync.Mutex
|
||||
etag string // vide : aucune ETag, seulement une date
|
||||
corps string
|
||||
corpsParti int
|
||||
requetes int
|
||||
conditions []string
|
||||
}
|
||||
|
||||
func nouvelAmontValidant(t *testing.T, etag, corps string) *amontValidant {
|
||||
t.Helper()
|
||||
a := &amontValidant{etag: etag, corps: corps}
|
||||
a.srv = httptest.NewServer(http.HandlerFunc(
|
||||
func(w http.ResponseWriter, r *http.Request) {
|
||||
a.mu.Lock()
|
||||
defer a.mu.Unlock()
|
||||
a.requetes++
|
||||
cond := r.Header.Get("If-None-Match")
|
||||
if cond == "" && r.Header.Get("If-Modified-Since") != "" {
|
||||
cond = "date:" + r.Header.Get("If-Modified-Since")
|
||||
}
|
||||
a.conditions = append(a.conditions, cond)
|
||||
if a.etag == "" {
|
||||
w.Header().Set("Last-Modified", "Mon, 14 Sep 2026 14:53:55 GMT")
|
||||
} else {
|
||||
w.Header().Set("ETag", a.etag)
|
||||
if cond == a.etag {
|
||||
w.WriteHeader(http.StatusNotModified)
|
||||
return
|
||||
}
|
||||
}
|
||||
a.corpsParti++
|
||||
io.WriteString(w, a.corps)
|
||||
}))
|
||||
t.Cleanup(a.srv.Close)
|
||||
return a
|
||||
}
|
||||
|
||||
func (a *amontValidant) changer(etag, corps string) {
|
||||
a.mu.Lock()
|
||||
defer a.mu.Unlock()
|
||||
a.etag, a.corps = etag, corps
|
||||
}
|
||||
|
||||
func (a *amontValidant) releve() (requetes, corps int, conditions []string) {
|
||||
a.mu.Lock()
|
||||
defer a.mu.Unlock()
|
||||
return a.requetes, a.corpsParti, append([]string(nil), a.conditions...)
|
||||
}
|
||||
|
||||
func (a *amontValidant) hote() string {
|
||||
u, _ := url.Parse(a.srv.URL)
|
||||
return u.Host
|
||||
}
|
||||
|
||||
// indexPip est un index volatil rangé avec son hôte : la revalidation lui est
|
||||
// ouverte. Le test l'éprouve avant de s'en servir, pour ne pas mesurer une
|
||||
// règle d'exclusion à la place de celle qu'il nomme.
|
||||
const indexPip = "/simple/requests/"
|
||||
|
||||
func exigerIndexRevalidable(t *testing.T, hote string) {
|
||||
t.Helper()
|
||||
u := &url.URL{Scheme: "http", Host: hote, Path: indexPip}
|
||||
if Classify(u) != ClassVolatile {
|
||||
t.Fatalf("%s n'est plus volatil : le test mesurerait autre chose", indexPip)
|
||||
}
|
||||
}
|
||||
|
||||
// Un index que l'amont déclare inchangé sort du disque, et son corps ne
|
||||
// repart pas : c'est tout le gain. L'amont est pourtant interrogé à CHAQUE
|
||||
// demande — un index n'est jamais servi sans son accord tant qu'il répond.
|
||||
func TestUnIndexConfirmeParLAmontSortDuDisque(t *testing.T) {
|
||||
a := nouvelAmontValidant(t, `"v1"`, "index pip, version 1")
|
||||
exigerIndexRevalidable(t, a.hote())
|
||||
p := proxyDeTest(t)
|
||||
|
||||
for i := 1; i <= 3; i++ {
|
||||
w := demande(t, p, a.hote(), indexPip)
|
||||
if w.Code != http.StatusOK || w.Body.String() != "index pip, version 1" {
|
||||
t.Fatalf("demande %d : %d %q", i, w.Code, w.Body.String())
|
||||
}
|
||||
veut := OutcomeRevalidated
|
||||
if i == 1 {
|
||||
veut = "miss"
|
||||
}
|
||||
if got := w.Header().Get("X-ERPLibre-Cache"); got != veut {
|
||||
t.Errorf("demande %d servie « %s », attendu « %s »", i, got, veut)
|
||||
}
|
||||
}
|
||||
requetes, corps, conditions := a.releve()
|
||||
if requetes != 3 {
|
||||
t.Errorf("l'amont a reçu %d requêtes, attendu 3 : il doit juger chacune", requetes)
|
||||
}
|
||||
if corps != 1 {
|
||||
t.Errorf("%d corps envoyés, attendu 1 : la copie confirmée a été retéléchargée", corps)
|
||||
}
|
||||
if conditions[0] != "" || conditions[1] != `"v1"` || conditions[2] != `"v1"` {
|
||||
t.Errorf("conditions reçues %q : la première sans, les suivantes avec l'ETag gardé", conditions)
|
||||
}
|
||||
}
|
||||
|
||||
// Un index qui a changé est repris en entier, remplace la copie, et c'est son
|
||||
// NOUVEL ETag qui part ensuite.
|
||||
func TestUnIndexChangeEstRepris(t *testing.T) {
|
||||
a := nouvelAmontValidant(t, `"v1"`, "version 1")
|
||||
exigerIndexRevalidable(t, a.hote())
|
||||
p := proxyDeTest(t)
|
||||
|
||||
demande(t, p, a.hote(), indexPip)
|
||||
a.changer(`"v2"`, "version 2")
|
||||
if w := demande(t, p, a.hote(), indexPip); w.Body.String() != "version 2" {
|
||||
t.Fatalf("après changement, corps %q", w.Body.String())
|
||||
}
|
||||
w := demande(t, p, a.hote(), indexPip)
|
||||
if w.Body.String() != "version 2" || w.Header().Get("X-ERPLibre-Cache") != OutcomeRevalidated {
|
||||
t.Errorf("troisième demande : %q servie « %s »", w.Body.String(), w.Header().Get("X-ERPLibre-Cache"))
|
||||
}
|
||||
_, corps, conditions := a.releve()
|
||||
if corps != 2 {
|
||||
t.Errorf("%d corps envoyés, attendu 2", corps)
|
||||
}
|
||||
if conditions[2] != `"v2"` {
|
||||
t.Errorf("condition de la troisième demande %q, attendu l'ETag de la copie remplacée", conditions[2])
|
||||
}
|
||||
}
|
||||
|
||||
// Sans ETag, rien ne part : une date seule validerait, sous « Vary: Accept »,
|
||||
// une représentation qui n'est pas celle gardée.
|
||||
func TestSansETagLIndexRepartEntier(t *testing.T) {
|
||||
a := nouvelAmontValidant(t, "", "index sans ETag")
|
||||
exigerIndexRevalidable(t, a.hote())
|
||||
p := proxyDeTest(t)
|
||||
|
||||
demande(t, p, a.hote(), indexPip)
|
||||
w := demande(t, p, a.hote(), indexPip)
|
||||
if got := w.Header().Get("X-ERPLibre-Cache"); got != "miss" {
|
||||
t.Errorf("seconde demande servie « %s », attendu « miss »", got)
|
||||
}
|
||||
_, corps, conditions := a.releve()
|
||||
if corps != 2 || conditions[1] != "" {
|
||||
t.Errorf("%d corps, condition %q : attendu 2 corps et aucune condition", corps, conditions[1])
|
||||
}
|
||||
}
|
||||
|
||||
// La condition du client reste la sienne : il demande si SA copie a changé,
|
||||
// et lui substituer l'ETag du cache répondrait à une autre question.
|
||||
func TestLaConditionDuClientNEstPasRemplacee(t *testing.T) {
|
||||
a := nouvelAmontValidant(t, `"v1"`, "version 1")
|
||||
exigerIndexRevalidable(t, a.hote())
|
||||
p := proxyDeTest(t)
|
||||
|
||||
demande(t, p, a.hote(), indexPip)
|
||||
r := httptest.NewRequest("GET", indexPip, nil)
|
||||
r.Host = a.hote()
|
||||
r.Header.Set("If-None-Match", `"copie-du-client"`)
|
||||
w := httptest.NewRecorder()
|
||||
p.serve(w, r, "http")
|
||||
|
||||
if w.Code != http.StatusOK || w.Body.String() != "version 1" {
|
||||
t.Errorf("client dont la copie diffère : %d %q", w.Code, w.Body.String())
|
||||
}
|
||||
_, _, conditions := a.releve()
|
||||
if conditions[1] != `"copie-du-client"` {
|
||||
t.Errorf("condition envoyée %q, attendu celle du client", conditions[1])
|
||||
}
|
||||
}
|
||||
|
||||
// Une clé sans hôte est partagée par tous les miroirs d'une liste qui tourne :
|
||||
// présenter à l'un l'ETag gardé depuis un autre ne se fait pas, et l'index
|
||||
// repart entier comme avant.
|
||||
func TestUneCleSansHoteNeRevalidePas(t *testing.T) {
|
||||
a := nouvelAmontValidant(t, `"v1"`, "base de paquets")
|
||||
const chemin = "/arch/core/os/x86_64/core.db"
|
||||
u := &url.URL{Scheme: "http", Host: a.hote(), Path: chemin}
|
||||
if !PortableParChemin(u) || Classify(u) != ClassVolatile {
|
||||
t.Fatalf("%s n'est plus un index volatil rangé sans hôte", chemin)
|
||||
}
|
||||
p := proxyDeTest(t)
|
||||
|
||||
demande(t, p, a.hote(), chemin)
|
||||
w := demande(t, p, a.hote(), chemin)
|
||||
if got := w.Header().Get("X-ERPLibre-Cache"); got != "miss" {
|
||||
t.Errorf("seconde demande servie « %s », attendu « miss »", got)
|
||||
}
|
||||
_, corps, conditions := a.releve()
|
||||
if corps != 2 || conditions[1] != "" {
|
||||
t.Errorf("%d corps, condition %q : attendu 2 corps et aucune condition", corps, conditions[1])
|
||||
}
|
||||
}
|
||||
|
|
@ -118,6 +118,60 @@ func CleDe(method string, u *url.URL) string {
|
|||
return Key(method, u.String())
|
||||
}
|
||||
|
||||
// CleVariante rend la clé d'une REPRÉSENTATION de l'objet rangé sous key,
|
||||
// celle que choisit l'en-tête Accept de la requête.
|
||||
//
|
||||
// Un amont qui répond « Vary: Accept » sert sous une même URL plusieurs corps
|
||||
// selon ce que le client accepte : une fiche de paquet complète ou abrégée.
|
||||
// Rangés sous la seule clé de l'URL, ils se remplacent l'un l'autre, si bien
|
||||
// qu'une installation qui demande les deux tour à tour les retélécharge
|
||||
// chaque fois. La clé de base reste écrite comme avant ; la variante s'y
|
||||
// ajoute. Rend "" pour un Accept vide : la requête n'en choisit aucune.
|
||||
func CleVariante(key, accept string) string {
|
||||
accept = strings.Join(strings.Fields(strings.ToLower(accept)), " ")
|
||||
if accept == "" {
|
||||
return ""
|
||||
}
|
||||
sum := sha256.Sum256([]byte(key + " accept " + accept))
|
||||
return hex.EncodeToString(sum[:])
|
||||
}
|
||||
|
||||
// Copier publie sous dst le corps et les métadonnées rangés sous src, par le
|
||||
// même temporaire et le même renommage qu'une écriture ordinaire : une copie
|
||||
// interrompue ne laisse rien de visible. La date d'usage de src n'est pas
|
||||
// touchée.
|
||||
func (s *Store) Copier(src, dst string) error {
|
||||
m, err := s.LireMeta(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, bodyPath := s.paths(src)
|
||||
f, err := os.Open(bodyPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
w, err := s.NewWriter(dst, *m)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := io.Copy(w, f); err != nil {
|
||||
w.Abort()
|
||||
return err
|
||||
}
|
||||
return w.Commit(m.Size)
|
||||
}
|
||||
|
||||
// Toucher remet à maintenant la date d'usage du corps rangé sous key, s'il
|
||||
// existe. Servir une variante sert aussi l'objet de base : sans cela, un
|
||||
// nettoyage par âge retirerait la base qu'on sert encore par sa variante, et
|
||||
// --detient la dirait absente.
|
||||
func (s *Store) Toucher(key string) {
|
||||
_, bodyPath := s.paths(key)
|
||||
maintenant := time.Now()
|
||||
_ = os.Chtimes(bodyPath, maintenant, maintenant)
|
||||
}
|
||||
|
||||
// CleStatut rend la clé sous laquelle un STATUT SEUL est rangé et cherché.
|
||||
//
|
||||
// Un espace de clés à part, et non la clé du corps : un lecteur qui ne
|
||||
|
|
|
|||
200
script/qemu_cache/variante_test.go
Normal file
200
script/qemu_cache/variante_test.go
Normal file
|
|
@ -0,0 +1,200 @@
|
|||
// © 2026 TechnoLibre (http://www.technolibre.ca)
|
||||
// License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Les deux en-têtes Accept d'un client de registre : la fiche abrégée, et la
|
||||
// fiche complète.
|
||||
const (
|
||||
acceptAbrege = "application/vnd.npm.install-v1+json; q=1.0, application/json; q=0.8, */*"
|
||||
acceptComplet = "application/json"
|
||||
)
|
||||
|
||||
// amontAVariantes sert deux représentations d'une même URL selon Accept, chacune
|
||||
// sous son ETag, avec « Vary: Accept » ; « 304 » à qui présente l'ETag de la
|
||||
// représentation qu'il demande. Il compte les corps envoyés par représentation.
|
||||
type amontAVariantes struct {
|
||||
srv *httptest.Server
|
||||
mu sync.Mutex
|
||||
vary bool
|
||||
corps map[string]int
|
||||
}
|
||||
|
||||
func nouvelAmontAVariantes(t *testing.T, vary bool) *amontAVariantes {
|
||||
t.Helper()
|
||||
a := &amontAVariantes{vary: vary, corps: map[string]int{}}
|
||||
a.srv = httptest.NewServer(http.HandlerFunc(
|
||||
func(w http.ResponseWriter, r *http.Request) {
|
||||
a.mu.Lock()
|
||||
defer a.mu.Unlock()
|
||||
rep, etag, corps := "complète", `"complet"`, "fiche complète, très longue"
|
||||
if strings.Contains(r.Header.Get("Accept"), "install-v1") {
|
||||
rep, etag, corps = "abrégée", `"abrege"`, "fiche abrégée"
|
||||
}
|
||||
if a.vary {
|
||||
w.Header().Set("Vary", "accept-encoding, accept")
|
||||
}
|
||||
w.Header().Set("ETag", etag)
|
||||
if r.Header.Get("If-None-Match") == etag {
|
||||
w.WriteHeader(http.StatusNotModified)
|
||||
return
|
||||
}
|
||||
a.corps[rep]++
|
||||
io.WriteString(w, corps)
|
||||
}))
|
||||
t.Cleanup(a.srv.Close)
|
||||
return a
|
||||
}
|
||||
|
||||
func (a *amontAVariantes) envoyes(rep string) int {
|
||||
a.mu.Lock()
|
||||
defer a.mu.Unlock()
|
||||
return a.corps[rep]
|
||||
}
|
||||
|
||||
func (a *amontAVariantes) hote() string {
|
||||
u, _ := url.Parse(a.srv.URL)
|
||||
return u.Host
|
||||
}
|
||||
|
||||
const ficheNpm = "/npm"
|
||||
|
||||
func demandeAccept(t *testing.T, p *Proxy, hote, accept string) *httptest.ResponseRecorder {
|
||||
t.Helper()
|
||||
r := httptest.NewRequest("GET", ficheNpm, nil)
|
||||
r.Host = hote
|
||||
if accept != "" {
|
||||
r.Header.Set("Accept", accept)
|
||||
}
|
||||
w := httptest.NewRecorder()
|
||||
p.serve(w, r, "http")
|
||||
return w
|
||||
}
|
||||
|
||||
// Deux représentations demandées tour à tour ne repartent qu'une fois chacune :
|
||||
// rangée à part, l'une ne remplace plus l'autre.
|
||||
func TestDeuxRepresentationsQuiAlternentNeRepartentQuUneFois(t *testing.T) {
|
||||
a := nouvelAmontAVariantes(t, true)
|
||||
u := &url.URL{Scheme: "http", Host: a.hote(), Path: ficheNpm}
|
||||
if Classify(u) != ClassVolatile || PortableParChemin(u) {
|
||||
t.Fatalf("%s n'est plus un volatil rangé avec son hôte", ficheNpm)
|
||||
}
|
||||
p := proxyDeTest(t)
|
||||
|
||||
ordre := []string{acceptAbrege, acceptComplet, acceptAbrege, acceptComplet, acceptAbrege}
|
||||
for i, accept := range ordre {
|
||||
w := demandeAccept(t, p, a.hote(), accept)
|
||||
veut := "fiche complète, très longue"
|
||||
if accept == acceptAbrege {
|
||||
veut = "fiche abrégée"
|
||||
}
|
||||
if w.Body.String() != veut {
|
||||
t.Fatalf("demande %d : corps %q, attendu %q", i+1, w.Body.String(), veut)
|
||||
}
|
||||
}
|
||||
if n := a.envoyes("abrégée"); n != 1 {
|
||||
t.Errorf("fiche abrégée envoyée %d fois, attendu 1", n)
|
||||
}
|
||||
if n := a.envoyes("complète"); n != 1 {
|
||||
t.Errorf("fiche complète envoyée %d fois, attendu 1", n)
|
||||
}
|
||||
}
|
||||
|
||||
// La clé de base reste tenue : --detient, le pré-vol et le rattrapage hors
|
||||
// ligne ne connaissent pas l'en-tête Accept.
|
||||
func TestLaCleDeBaseResteTenuePourDetient(t *testing.T) {
|
||||
a := nouvelAmontAVariantes(t, true)
|
||||
p := proxyDeTest(t)
|
||||
demandeAccept(t, p, a.hote(), acceptAbrege)
|
||||
|
||||
d := p.Store.Detenir("GET", "http://"+a.hote()+ficheNpm)
|
||||
if d.Verdict != VerdictGarde {
|
||||
t.Errorf("--detient dit %q pour une fiche gardée", d.Verdict)
|
||||
}
|
||||
}
|
||||
|
||||
// Sans « Vary: Accept », rien n'est rangé à part : la réponse ne varie pas, et
|
||||
// la dupliquer ne ferait que doubler le disque.
|
||||
func TestSansVaryAcceptAucuneVariante(t *testing.T) {
|
||||
a := nouvelAmontAVariantes(t, false)
|
||||
p := proxyDeTest(t)
|
||||
demandeAccept(t, p, a.hote(), acceptAbrege)
|
||||
|
||||
base := CleDe("GET", &url.URL{Scheme: "http", Host: a.hote(), Path: ficheNpm})
|
||||
if p.Store.Detient(CleVariante(base, acceptAbrege)) {
|
||||
t.Error("une variante a été rangée pour une réponse sans Vary: Accept")
|
||||
}
|
||||
if !p.Store.Detient(base) {
|
||||
t.Error("la clé de base n'est plus écrite")
|
||||
}
|
||||
}
|
||||
|
||||
// Amont muet : le client reçoit SA représentation, pas la dernière rangée sous
|
||||
// la base, qui peut être l'autre.
|
||||
func TestHorsLigneLaRepresentationDuClientSort(t *testing.T) {
|
||||
a := nouvelAmontAVariantes(t, true)
|
||||
p := proxyDeTest(t)
|
||||
hote := a.hote()
|
||||
demandeAccept(t, p, hote, acceptAbrege)
|
||||
demandeAccept(t, p, hote, acceptComplet) // la base porte désormais la complète
|
||||
a.srv.Close()
|
||||
|
||||
w := demandeAccept(t, p, hote, acceptAbrege)
|
||||
if w.Code != http.StatusOK || w.Body.String() != "fiche abrégée" {
|
||||
t.Errorf("hors ligne : %d %q, attendu la fiche abrégée", w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
// Une requête sans Accept ne choisit aucune représentation : elle suit la clé
|
||||
// de base, comme avant.
|
||||
func TestSansAcceptLaBaseSeule(t *testing.T) {
|
||||
if CleVariante("cle", "") != "" || CleVariante("cle", " ") != "" {
|
||||
t.Error("un Accept vide produit une clé de variante")
|
||||
}
|
||||
if CleVariante("cle", "Application/JSON") != CleVariante("cle", "application/json") {
|
||||
t.Error("la casse d'Accept change la variante")
|
||||
}
|
||||
if !varieSurAccept(http.Header{"Vary": {"accept-encoding, Accept"}}) ||
|
||||
varieSurAccept(http.Header{"Vary": {"accept-encoding"}}) {
|
||||
t.Error("lecture de Vary erronée")
|
||||
}
|
||||
}
|
||||
|
||||
// Servir une variante sert aussi l'objet de base : sa date d'usage suit, sans
|
||||
// quoi un nettoyage par âge retirerait la base qu'on sert encore, et --detient
|
||||
// la dirait absente.
|
||||
func TestServirUneVarianteRajeunitLaBase(t *testing.T) {
|
||||
a := nouvelAmontAVariantes(t, true)
|
||||
p := proxyDeTest(t)
|
||||
demandeAccept(t, p, a.hote(), acceptAbrege)
|
||||
|
||||
base := CleDe("GET", &url.URL{Scheme: "http", Host: a.hote(), Path: ficheNpm})
|
||||
_, corpsBase := p.Store.paths(base)
|
||||
vieux := time.Now().Add(-90 * 24 * time.Hour)
|
||||
if err := os.Chtimes(corpsBase, vieux, vieux); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
w := demandeAccept(t, p, a.hote(), acceptAbrege)
|
||||
if got := w.Header().Get("X-ERPLibre-Cache"); got != OutcomeRevalidated {
|
||||
t.Fatalf("seconde demande servie « %s », attendu « %s »", got, OutcomeRevalidated)
|
||||
}
|
||||
fi, err := os.Stat(corpsBase)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if time.Since(fi.ModTime()) > time.Hour {
|
||||
t.Errorf("la base date encore du %s : son usage n'a pas été noté", fi.ModTime())
|
||||
}
|
||||
}
|
||||
|
|
@ -113,6 +113,7 @@ MITM_GO = os.path.join("script", "qemu_cache", "mitm.go")
|
|||
# nombreuses, faute d'avoir été relue quand elles sont apparues.
|
||||
ORDRE_ISSUES = (
|
||||
"hit",
|
||||
"revalidated",
|
||||
"mirror",
|
||||
"stored",
|
||||
"stale",
|
||||
|
|
|
|||
Loading…
Reference in a new issue