[0.2.7] - 2026-08-03
Behoben
-
Der DNS-Retry war nur gegen Mocks belegt. Die Unit-Tests faelschen
Aufloeser und Antwort; sie zeigen, dass die Schleife tut, was sie soll,
aber nicht, dass der Aufruf am Ende echte Daten bringt. Genau diese Luecke
hat den Fehler ueberhaupt erst durchgelassen — gemeldet hat ihn am
3. August 2026 ein Live-Lauf, nicht die Suite.Ein Live-Test faelscht jetzt nur noch den ersten Aufloesungsversuch und
laesst alles danach echt: echtes DNS beim zweiten Versuch, echte Verbindung,
echte BISTA-Antwort, echte Backoff-Wartezeit. Ein zweiter Live-Test prueft
die Egress-Blocklist (SEC-005) gegen die echte Antwort des echten Hosts
statt gegen eine erfundene.Zwei Nebenbefunde, beide behoben: Die DNS-Stub-Fixture in
tests/test_retry_policy.pysetzt fuer Live-Tests aus (ein «Live»-Test
gegen einen gestubbten Aufloeser prueft das Gegenteil seines Namens), und
der gepoolte HTTP-Client wird vor und nach jedem Live-Test frisch gesetzt:
Seine offenen Verbindungen gehoeren dem Event-Loop, in dem sie entstanden,
und pytest-asyncio gibt jedem Test einen eigenen. Der zweite Live-Test
scheiterte deshalb mit «Event loop is closed» — an einem Fehler des
Testaufbaus, der wie ein Ausfall der Quelle aussieht. Unit-Tests merken
davon nichts, weil respx die Transport-Schicht ersetzt; latent war es,
bis ein zweiter Live-Test dazukam. -
Ein DNS-Zucken beendete den Tool-Aufruf sofort — und wurde als
Egress-Verstoss gemeldet._resolve_and_validatewarf fuer zwei
grundverschiedene Lagen denselbenPermissionError: fuer den
Policy-Verstoss (Host loest auf eine interne IP auf, SEC-005) und fuer
das Scheitern vonsocket.getaddrinfo(«Temporary failure in name
resolution»). Die Retry-Schleife ueberspringtPermissionErrorbewusst —
richtig fuer die Politik, falsch fuer den Ausfall. Bei einem Live-Lauf am
3. August 2026 scheiterten drei Tool-Aufrufe hintereinander genau so; der
vierte ging durch.Die Lagen tragen jetzt eigene Typen,
EgressBlockedund
UpstreamUnresolvable, beide weiterhin aufPermissionErrorals
gemeinsamer Basis — bestehendeexcept-Klauseln und Tests behalten damit
ihre Bedeutung, statt still ins Leere zu laufen.UpstreamUnresolvablewird wiederholt, unter demselben Budget,
derselben Versuchszahl und derselben Backoff-Kurve wie jeder andere
Ausfall;EgressBlockedwird weiterhin nie wiederholt. Damit das Budget
fuer diesen Pfad auch haelt, laeuft die Aufloesung im Thread-Pool statt im
Event-Loop:getaddrinfoist synchron, und was den Loop blockiert, kann
die Wanduhr-Deadline nicht schneiden — aus vier Versuchen waeren sonst vier
Blockaden ueber das Budget hinaus geworden._handle_errortrennt die Meldungen: Ein Aufloeser-Ausfall verweist nicht
mehr auf die Egress-Konfiguration, wo dabei nichts zu finden ist, sondern
sagt, dass es voruebergehend ist und ein erneuter Versuch die richtige
Handlung. Die sanitisierte Form bleibt (OBS-002, keinestr(e)-Leaks). -
Vier von sechs Datensaetzen waren gegen die echte API kaputt: BISTA hat die
Schreibweise der Kopfzeile gewechselt. Der Code lasr["Schulgemeinde"],
die Quelle liefertschulgemeinde. Der Zugriff ergab keinen Treffer,
sondern ein leeres Ergebnis mit der Meldung «Schulgemeinde nicht gefunden» —
ein Ausfall, der wie eine Antwort aussieht.Stand 3. August 2026 liefern vier der sechs genutzten Datensaetze klein,
zwei gross, und zwei mischen innerhalb einer Kopfzeile
(gebiet_Bezeichnung,staatsangehoerigkeit_ISO2_Code). Die Schluesselnamen
werden deshalb beim Parsen normalisiert, statt eine Schreibweise zu waehlen,
die schon zweimal gewechselt hat.Die Unit-Tests haben davon nichts gesehen: Ihre Fixtures pinnen die alte
Kopfzeile, also blieben sie gruen. Gemeldet hat es allein der Live-Test — den
CI per-m "not live"ausschliesst. -
Ein Fuenftel der Zeilen liess die Tools abstuerzen. BISTA unterdrueckt
kleine Fallzahlen aus Datenschutzgruenden und schreibt statt einer Zahl
1 bis 5; dazu kommenNULLund leere Zellen. Betroffen waren 18.6 % der
Sek-I- und 18.1 % der Staatsangehoerigkeits-Zeilen.int("1 bis 5")wirft,
und der Aufrufer sah davon nur «unerwarteter interner Fehler».Solche Werte als 0 zu zaehlen waere die schlechtere Antwort gewesen: Die
Summe bliebe plausibel, waere still zu tief und durch nichts als falsch
erkennbar. Sie werden jetzt aus den Summen ausgenommen, in der Tabelle als
1 bis 5gezeigt, und jede betroffene Ausgabe traegt einen Hinweis, wie
viele Zeilen fehlen und dass die echten Werte hoeher liegen (FID-003). -
_latest_yearsuchte inJahrstattjahrund lieferte darum «Keine
Jahresdaten verfuegbar» — derselbe Drift, eine Ebene tiefer. -
Staatsangehoerigkeit_ISO2_Codewurde nie gefunden, weil die Spalte
gemischt geschrieben ist. Die ISO2-Spalte blieb dauerhaft auf «—». -
Die
_no_sleep-Fixture griff weiter, als sie durfte. Sie patchte
http_client.asyncio.sleep— das sieht lokal aus, trifft aber das Modul
asynciound damit jeden Import im Prozess. Jeder Test, der
asyncio.sleep(0)benutzt, um dem Event-Loop das Wort zu geben, haette
danach still nichts mehr geprueft: Er laeuft weiter und misst nichts.In diesem Repo gibt es derzeit keinen solchen Test, der Schaden war also
latent. Insrgssr-mcpist derselbe Griff in derselben Kampagne
zugeschnappt und hat eine Parallelitaets-Pruefung entschaerft.Der Backoff laeuft jetzt ueber den Modul-Alias
http_client._sleep, und ein
Test haelt fest, dassasyncio.sleepintakt bleibt.
Hinzugefuegt
-
Retry-Politik gegenueber BISTA (ARCH-014). Bisher gab es keine: Ein
einzelner Netzwerkfehler, ein Timeout oder ein 503 beendete den Tool-Aufruf,
obwohl der naechste Versuch Sekunden spaeter geklappt haette. Genau so fielen
am 1. August inswiss-efv-mcpvier Live-Tests wegen eines voruebergehenden
Ausfalls der Quelle.Wiederholt werden Netzwerkfehler, Timeouts, 5xx und 429 — vier Versuche. Ein
4xx ausser 429 ist eine Aussage ueber die Anfrage und keine ueber den Moment
und scheitert weiterhin sofort.PermissionErroraus dem Egress-Guard ist
eine Policy-Entscheidung und wird nie wiederholt: Vier Mal dieselbe verbotene
Anfrage zu stellen macht sie nicht erlaubter. -
Retry-Afterwird gelesen und schlaegt die eigene Backoff-Kurve. Bei 429
und 503 sagt die Quelle im Header, wann sie wieder mag — als Sekundenzahl
oder als HTTP-Datum; beide Formen kommen vor, beide werden gelesen
(RFC 9110 §10.2.3). Ein unbrauchbarer Header fuehrt zurueck auf die Kurve
statt zum Absturz — auf dem Fehlerpfad ist das der Unterschied zwischen einer
Verzoegerung und einem zweiten Fehler. -
Backoff ist gestreut (Jitter). Eine reine
2**attempt-Kurve ist
deterministisch: Faellt BISTA aus, waehrend mehrere Clients es abfragen,
laufen deren Retries im Gleichtakt, und die Last kommt als Welle zurueck —
genau wenn die Quelle sich erholt. Exponentielle Wartezeiten landen in
[0.5x, 1.5x]; auf einemRetry-Afterist die Streuung einseitig
([1.0x, 1.25x]), weil frueher wiederzukommen die Missachtung derselben
Angabe waere, die man gerade liest. Deckel von 20 s auf jede Einzelwartezeit,
angewandt nach dem Jittern — die andere Reihenfolge macht den Deckel zu
gar keiner Schranke. -
Gesamtbudget von 25 s ueber den ganzen Aufruf. Eine Versuchszahl ist
keine Grenze: Vier Versuche a 30 s Timeout plus Backoff sind ueber zwei
Minuten, und die Zahl4sagt das nirgends. Entscheidender ist, dass die
massgebliche Grenze gar nicht uns gehoert — der Aufrufer hat sein eigenes
Timeout, und jenseits davon hoert niemand mehr zu. Der Anker ist gemessen:
Das Python-MCP-SDK setztMCP_DEFAULT_TIMEOUT = 30.0.Das Budget haengt an einer
asyncio.timeout-Deadline, nicht am
httpx-Timeout: httpx begrenzt pro Operation, und sein Read-Timeout beginnt
mit jedem Chunk von vorn — eine langsam troepfelnde Antwort wuerde das Budget
sonst ueberdauern, ohne dass ein einzelner Read ablaeuft.
Behoben
- Ein aufgebrauchtes Gesamtbudget las sich als «unerwarteter interner
Fehler». Es wirft den builtinTimeoutError,_handle_errorkannte aber
nurhttpx.TimeoutException. Fuer den Aufrufer ist beides dasselbe: Es hat
zu lange gedauert.