Skip to content

fix(health): report a cold-starting AI service as starting, not failed - #88

Merged
Fluory merged 4 commits into
mainfrom
claude/fix-health-cold-start-81
Sep 28, 2026
Merged

Fluory merged 4 commits into
mainfrom
claude/fix-health-cold-start-81

Conversation

@Fluory

@Fluory Fluory commented Sep 27, 2026 •

Copy link
Copy Markdown
Owner

Warum

Fixes #81

Arbeitsstand

  • Ziel: Ein Kaltstart des KI-Containers liest sich in /api/health nicht mehr wie ein Ausfall.
  • Nicht-Ziele: längere Zeitlimits, Änderung von Status oder HTTP-Code für Datenbank und Speicher.
  • Erledigt: Zustand starting für informative Abhängigkeiten ohne Antwort in der Frist; nach 30 s ohne Antwort wieder failed (Review); Unit-Tests und Integrationsfall an der echten Netzgrenze; Doku (API, Betrieb, Runbook §8); CHANGELOG.
  • Offen: Merge.
  • Annahmen: keine.
  • Nächster kleinster Schritt: Merge.

Was ist passiert (Klartext)

Die Statusseite /api/health meldete den KI-Dienst nach einer Pause als „failed“, obwohl er wenige Sekunden später lief: Der Dienst schläft nach 5 Minuten ohne Aufrufe ein, und das Aufwachen dauert etwa 6 Sekunden – länger, als die Statusseite wartet. Jetzt unterscheidet sie: Antwortet der Dienst gar nicht rechtzeitig, heißt es „starting“ (er wacht gerade auf – der Aufruf selbst weckt ihn); antwortet er mit einem Fehler oder ist nicht erreichbar, bleibt es „failed“. Für Datenbank und Speicher ändert sich nichts.

Plan-Pflicht (SYSTEM.md §4)

  • Kein Auslöser – keine Modulgrenze, öffentliche API, Migration, Auth/Rechte, kein Zahlungs-/Daten-/Infrapfad, höchstens zwei Module, keine Architekturvarianten, umkehrbar
  • Auslöser zutreffend – Impact Manifest ausgefüllt (Plan vor Code)

Impact Manifest

  • Betroffene Module: observability (health.ts: DependencyResult, withStartupGrace), app (Route /api/health); Doku.
  • Schnittstellen / Datenänderungen: öffentliche Route /api/health – additiv: dependencies.aiService kann jetzt auch starting sein (Befund aus dem Review: öffentliche API ⇒ Auslöser). Status und HTTP-Code für checks unverändert. Im Repo wertet niemand aiService mit festen Werten aus (CI curl -f, Playwright nur die URL).
  • Akzeptanzkriterien: die aus fix(health): AI dependency reads as failed during a container cold start #81.
  • Testplan: Unit – starting bei hängender Abhängigkeit und bei TimeoutError; Pflicht-Check hängt → failed + 503; Karenz: länger als 30 s → failed, eine Antwort beendet sie, ein neuer Kaltstart bekommt eine neue; echter Fehler sofort failed. Integration – echter Server nimmt an und antwortet nie → starting.
  • Verifizierte Fakten: pingAiService bricht nach 2 s mit TimeoutError ab (vor der 3-s-Frist der Route); cachedFor cacht keine abgelehnten Promises; gemessener Kaltstart ≈ 5,6 s (ADR-0001 D11 amendment).
  • Offene Annahmen: keine.
  • Nicht-Ziele: längere Fristen. Begründung: Eine längere Frist hält jede /api/health-Antwort beim Kaltstart bis zu ~6 s auf und lässt trotzdem offen, ob der Dienst startet oder hängt; starting plus 30-s-Karenz antwortet sofort und meldet einen echten Ausfall nach 30 s.
  • Risiken und Rollback: Monitore, die nur ok/failed kennen, sehen einen neuen Wert – im Repo gibt es keine; Rollback per Revert.

Geändert

  • src/features/observability/health.ts: DependencyResult = CheckResult | "starting"; withStartupGrace(check, graceMs) (länger als 30 s ohne Antwort ⇒ failed), von der Route /api/health mit 30 s genutzt; settle() unterscheidet Zeitüberschreitung (eigene Frist oder AbortSignal.timeout = TimeoutError) von Fehler; Pflicht-Checks werten eine Zeitüberschreitung weiter als failed
  • src/features/observability/index.ts: Typen CheckResult, DependencyResult exportiert
  • src/features/observability/health.test.ts: 5 neue Fälle (hängende Abhängigkeit → starting; TimeoutError → starting; hängender Pflicht-Check → failed + 503; Karenz über 30 s → failed und Neustart; echter Fehler sofort failed)
  • tests/integration/health.test.ts: echter Server, der annimmt und nie antwortet → starting
  • docs/technical/api.md, docs/technical/operations.md, docs/technical/deployment-vercel.md §8: Zustand starting
  • CHANGELOG.md: Fixed-Eintrag fix(health): AI dependency reads as failed during a container cold start #81 im neuen [Unreleased] (nach dem Release v0.1.0)

Nachweis (SYSTEM.md §11)

  • verify:changed: grün – vitest health.test.ts 12/12; zuerst rot: die 3 ersten neuen Fälle (failed statt starting), nach dem Review die 2 Karenz-Fälle (withStartupGrace is not a function)
  • verify: grün – CI am PR (2 min 10 s): Unit 237/237, Integration 19 Dateien grün, darunter tests/integration/health.test.ts 6/6 mit dem neuen Grenzfall; lokal typecheck, lint grün (Docker lokal aus)
  • verify:full / E2E-Spec: nicht betroffen
  • Manueller Prüfnachweis: Ausgangsbefund live belegt (Issue fix(health): AI dependency reads as failed during a container cold start #81); Sichtprüfung nach dem Deploy beim nächsten Kaltstart
  • Frischer Review (P1 vor Ready-for-review; Architektur/API/DB immer): siehe PR-Kommentar – changes requested, alle 6 Befunde bearbeitet (208eeca)

Doku-Entscheidung (genau eine)

  • Keine langlebige Doku betroffen – Begründung: –
  • Doku betroffen und im selben PR aktualisiert:

Entferntes oder Umbenanntes: docs/ + README gegrept, Treffer bereinigt: nichts entfernt

Dateigrößen und neue Bausteine (SYSTEM.md §7)

Dateien über 500 Zeilen im Diff (Ausnahmen: generierter Code, Lockfiles, Fixtures, Migrationen, Schemas, Ressourcen, Doku, Konfiguration):

  • keine
  • bewusst belassen – Begründung: –
  • im selben PR nach fachlicher Verantwortung geteilt
  • Folge-Issue –

Über 800 Zeilen mit neuer Fachlogik oder über 1000 Zeilen (P1/P2): nicht betroffen

Neue Shared-Komponente, Utility-Datei, Adapter oder fachlicher Service:

  • nein
  • ja – gesucht nach: –

Subagent-Einsätze

Keine.

Risiken / offene Punkte

  • Öffentliche API (additiver Wert starting): menschliche Freigabe laut api-Regel – allgemein erteilt am 2026-09-28 („merge getestetes“).

🤖 Generated with Claude Code

…ting

On the showcase the AI service is a scaled-to-zero container; its cold
start (~6 s) exceeded the probe's time limit, so /api/health read
"aiService: failed" although the service was fine a few seconds later.
An informational dependency without an answer in time is now
"starting" (the probe itself wakes the container); an answered error or
a refused connection stays "failed". Required checks (database,
storage) keep failing on a timeout with 503.

Fixes #81

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented Sep 27, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
requestflow Ready Ready Preview Sep 28, 2026 11:52am UTC
requestflow-ai Ready Ready Preview Sep 28, 2026 11:52am UTC

@Fluory

Fluory commented Sep 27, 2026

Copy link
Copy Markdown
Owner Author

Übergabe (2026-09-28, Sitzungsende)

  • Erledigt: Zustand starting für eine informative Abhängigkeit ohne Antwort in der Frist (test-first, 10/10), Pflicht-Checks unverändert; Doku (API, Betrieb, Runbook §8), CHANGELOG; CI check (verify inkl. Integration) grün.
  • Offen: frischer Review; nach dem Merge von chore(release): v0.1.0 – pilot scope #86 den CHANGELOG-Eintrag in den neuen [Unreleased] verschieben (Doku-Guard warnt bis dahin: 103 Zeilen).
  • Nächster Schritt: /review-pr 88, Befunde einarbeiten, dann ready.
  • Risiko: gering – rein informativer Teil von /api/health, Status und HTTP-Code für Datenbank und Speicher unverändert (getestet).

… the real boundary

The fresh review of #88 showed that in production the probe's own 2 s
abort always fires first, so a wrong address, dropped packets or a
hanging service would read `starting` for ever. withStartupGrace now
turns timeouts that last longer than 30 s into `failed` (a cold start
takes about 6 s); an answer or a real error ends the period. An
integration test lets a real server accept and never answer, so the
undici abort path is covered, not only a hand-made DOMException. Docs
now state the 2 s probe limit and that a failed probe is not cached.

Refs #81

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@Fluory

Fluory commented Sep 28, 2026

Copy link
Copy Markdown
Owner Author

Frischer Review (/review-pr) – unabhängiger Reviewer-Agent ohne Umsetzungskontext, 2026-09-28

Geprüft: Diff, #81, Route, pingAiService, Doku, Regeln api/testing. Unit lokal 10/10, CI grün. AK 2 erfüllt (Pflicht-Checks bei Zeitüberschreitung failed/503, Test Z. 72–77). AK 3 (Runbook §8) erfüllt; Klartext verständlich. Im Repo liest niemand aiService mit festen Werten (CI curl -f, Playwright nur die URL, Integrationstests nur ok/failed).

  1. [Important] – ai-client.ts:191 / health.ts:61 – In Produktion greift nie der eigene 3-s-Timer, sondern immer vorher AbortSignal.timeout(2_000). Mit Node 24 geprüft: hängender Server und verworfene SYN-Pakete (10.255.255.1) ergeben beide DOMException TimeoutError → starting; nur ECONNREFUSED/ENOTFOUND → failed. – Falsche IP, Firewall-Drop oder ein hängender Dienst zeigen dauerhaft starting. – Runbook ergänzen; optional anhaltende Zeitüberschreitung (z. B. > 30 s) als failed melden.
  2. [Important] – tests/integration/health.test.ts – Der Produktionspfad (undici-Abbruch, name === "TimeoutError") ist nur mit einer selbstgebauten DOMException getestet. – Verpackt jemand den Fetch-Fehler, bleiben die Unit-Tests grün, Produktion zeigt wieder failed. – Integrationsfall: Server antwortet nie → aiService: "starting" (~2 s).
  3. [Important] – PR, Plan-Pflicht – „keine öffentliche API“ angekreuzt, /api/health ist aber öffentlich dokumentiert und bekommt einen neuen Wert → Auslöser (SYSTEM.md §4); die api-Regel verlangt menschliche Freigabe. – Auslöser ankreuzen, kurzes Impact Manifest.
  4. [Note] – PR / AK 1 – starting statt längerer Frist ohne Begründung; das AK verlangt „decided and justified“.
  5. [Note] – operations.md Z. 102, PR-Risiken – „next probe after the 10 s cache“ stimmt nicht: cachedFor verwirft abgelehnte Promises, die nächste Abfrage pingt sofort neu.
  6. [Note] – health.ts:12,23, api.md „3 s time limit“ – Kommentare und Fristangabe veraltet; der KI-Ping bricht real nach 2 s ab.

Urteil: changes requested – Test an der echten Netzgrenze fehlt, Plan-Pflicht falsch beantwortet; die Änderung der öffentlichen API braucht danach eine menschliche Freigabe.


Bearbeitung (208eeca): 1 → withStartupGrace: länger als 30 s keine Antwort ⇒ failed (Kaltstart ≈ 6 s), eine Antwort oder ein echter Fehler beendet die Karenz; Unit-Tests mit Uhr; Runbook §8 nennt es. 2 → Integrationsfall mit echtem Server, der annimmt und nie antwortet. 3 → Plan-Pflicht korrigiert, Impact Manifest ergänzt. 4 → Begründung im PR. 5, 6 → Doku und Kommentare korrigiert.

@Fluory

Fluory commented Sep 28, 2026

Copy link
Copy Markdown
Owner Author

Merge auf Freigabe des Orchestrators (2026-09-28: „merge getestetes“) – CI grün, Grenzfall-Integrationstest 6/6.

@Fluory
Fluory merged commit 275950f into main Sep 28, 2026
7 of 9 checks passed

This branch was successfully deployed

2 active deployments
Preview – requestflow — 678e9c4d Deployed Sep 28, 2026 by vercel[bot]
Preview – requestflow-ai — 678e9c4d Deployed Sep 28, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(health): AI dependency reads as failed during a container cold start

1 participant