Gemeten: 709 seconden voor 318 images, terwijl de netwerktijd zo'n 48 seconden is. Twee oorzaken. - het aantal tegelijk gold in totaal in plaats van per registry. Registries knijpen per host af en tweederde staat op Docker Hub, dus ghcr, lscr en quay stonden daarachter te wachten zonder dat dat iets oploste. Nu vier per registry, registries naast elkaar - elke ronde controleerde alles opnieuw. Geslaagde controles blijven een week staan, met spreiding per image zodat niet alles op dezelfde dag verloopt. De sleutel is de naam met tag, dus een gewijzigde tag wordt altijd gecontroleerd; mislukkingen komen nooit in de cache Gemeten na de wijziging: koude cache 43s, warme cache 0s. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q9eqpADJSRs49SoGGr4NAy
112 lines
4.4 KiB
Python
112 lines
4.4 KiB
Python
"""De cache van de image-controle.
|
|
|
|
De volledige ronde kost elf à twaalf minuten omdat registries afknijpen en een
|
|
afgeknepen verzoek niet te onderscheiden is van een ontbrekend image. Die ronde
|
|
draaide bij élke push, ook als er geen enkel sjabloon was gewijzigd.
|
|
|
|
Wat hier vastligt is niet de snelheid maar de correctheid van het overslaan: een
|
|
gewijzigde tag mag nooit ongezien blijven, en een ontbrekend image mag nooit uit
|
|
beeld verdwijnen doordat het ooit goed was.
|
|
"""
|
|
import importlib
|
|
import json
|
|
import sys
|
|
import time
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
WORTEL = Path(__file__).resolve().parent.parent
|
|
|
|
|
|
@pytest.fixture
|
|
def ci(tmp_path, monkeypatch):
|
|
monkeypatch.setenv("SU_IMAGE_CACHE", str(tmp_path / "images.json"))
|
|
sys.path.insert(0, str(WORTEL / "tools"))
|
|
for naam in [n for n in sys.modules if n == "controleer_images"]:
|
|
del sys.modules[naam]
|
|
mod = importlib.import_module("controleer_images")
|
|
return mod
|
|
|
|
|
|
def test_een_verse_cache_controleert_alles(ci):
|
|
alles = {"nginx:alpine": ["a"], "redis:7": ["b"]}
|
|
assert sorted(ci.te_controleren(alles, {}, time.time())) == sorted(alles)
|
|
|
|
|
|
def test_recent_goed_bevonden_wordt_overgeslagen(ci):
|
|
nu = time.time()
|
|
alles = {"nginx:alpine": ["a"], "redis:7": ["b"]}
|
|
cache = {"nginx:alpine": nu, "redis:7": nu}
|
|
assert ci.te_controleren(alles, cache, nu) == []
|
|
|
|
|
|
def test_een_andere_tag_wordt_altijd_gecontroleerd(ci):
|
|
"""De sleutel is de naam mét tag. Wijzigt een sjabloon zijn tag, dan is dat
|
|
een nieuwe sleutel — anders zou je juist de wijziging ongezien laten."""
|
|
nu = time.time()
|
|
cache = {"nginx:alpine": nu}
|
|
assert ci.te_controleren({"nginx:1.27": ["a"]}, cache, nu) == ["nginx:1.27"]
|
|
|
|
|
|
def test_de_cache_verloopt(ci):
|
|
nu = time.time()
|
|
oud = nu - ci.HOUDBAAR - ci.SPREIDING - 10
|
|
assert ci.te_controleren({"nginx:alpine": ["a"]}, {"nginx:alpine": oud}, nu) \
|
|
== ["nginx:alpine"]
|
|
|
|
|
|
def test_niet_alles_verloopt_op_dezelfde_dag(ci):
|
|
"""Zonder spreiding heb je na een week alsnog één ronde van twaalf minuten."""
|
|
ts = 1_700_000_000.0
|
|
momenten = {ci.houdbaar_tot(f"voorbeeld/app-{i}:latest", ts) for i in range(200)}
|
|
spanne = max(momenten) - min(momenten)
|
|
assert spanne > 2 * 24 * 3600, f"vervaldata liggen te dicht op elkaar: {spanne}s"
|
|
|
|
|
|
def test_een_ontbrekend_image_blijft_gemeld(ci, monkeypatch, tmp_path):
|
|
"""Een mislukking mag nooit in de cache belanden: dan zou hij één keer
|
|
gemeld worden en daarna een week onzichtbaar zijn."""
|
|
monkeypatch.setattr(ci, "images",
|
|
lambda: {"bestaat/niet:1": ["proefapp"], "goed/wel:1": ["b"]})
|
|
monkeypatch.setattr(ci, "bestaat", lambda i: not i.startswith("bestaat/niet"))
|
|
|
|
assert ci.main() == 1 # eerste ronde meldt hem
|
|
cache = json.loads((tmp_path / "images.json").read_text())
|
|
assert "goed/wel:1" in cache
|
|
assert "bestaat/niet:1" not in cache, "de mislukking is onthouden"
|
|
|
|
assert ci.main() == 1, "tweede ronde zwijgt over het ontbrekende image"
|
|
|
|
|
|
def test_verdwenen_images_blijven_niet_in_de_cache_hangen(ci, monkeypatch, tmp_path):
|
|
(tmp_path / "images.json").write_text(json.dumps({
|
|
"oud/weg:1": time.time(), "blijft/staan:1": time.time()}))
|
|
monkeypatch.setattr(ci, "images", lambda: {"blijft/staan:1": ["a"]})
|
|
monkeypatch.setattr(ci, "bestaat", lambda i: True)
|
|
ci.main()
|
|
cache = json.loads((tmp_path / "images.json").read_text())
|
|
assert list(cache) == ["blijft/staan:1"], cache
|
|
|
|
|
|
def test_alles_negeert_de_cache(ci, monkeypatch, tmp_path):
|
|
(tmp_path / "images.json").write_text(json.dumps({"nginx:alpine": time.time()}))
|
|
gecontroleerd = []
|
|
monkeypatch.setattr(ci, "images", lambda: {"nginx:alpine": ["a"]})
|
|
monkeypatch.setattr(ci, "bestaat",
|
|
lambda i: (gecontroleerd.append(i), True)[1])
|
|
monkeypatch.setattr(sys, "argv", ["x", "--alles"])
|
|
ci.main()
|
|
assert gecontroleerd == ["nginx:alpine"]
|
|
|
|
|
|
def test_zonder_cachepad_werkt_het_gewoon(tmp_path, monkeypatch):
|
|
"""Lokaal draaien zonder volume mag niet stukgaan."""
|
|
monkeypatch.delenv("SU_IMAGE_CACHE", raising=False)
|
|
sys.path.insert(0, str(WORTEL / "tools"))
|
|
for naam in [n for n in sys.modules if n == "controleer_images"]:
|
|
del sys.modules[naam]
|
|
mod = importlib.import_module("controleer_images")
|
|
assert mod.CACHE_PAD is None
|
|
assert mod.lees_cache() == {}
|
|
mod.schrijf_cache({"a": 1}) # mag geen fout geven
|