Skip to content

tts: the unhealthy-verdict warn re-fires every probe cycle β€” ~8,600 stack traces/day on a piper-only boxΒ #338

Description

@genwave-radio

πŸ”Š Field observation (Pi 5, v2.9.0, --pinned --piper-only)

warn: GenWave.Tts.DependencyHealthProber[0]
      kokoro health probe failed (Name or service not known (kokoro:8880)) β€” 29 consecutive
      failures, cached verdict is now unhealthy
      System.Net.Http.HttpRequestException: Name or service not known (kokoro:8880)
       ---> System.Net.Sockets.SocketException (0xFFFDFFFF): Name or service not known
         [full stack trace]

Plus two info entries per probe from the KokoroHealthProbe typed client (LogicalHandler + ClientHandler), each carrying its own stack trace.

πŸ› Two defects

1. The warn re-announces a transition that already happened.

DependencyHealthProber.RecordFailure (DependencyHealthProber.cs:129) branches on verdict.Healthy. The Debug branch is careful and correct β€” "N of M consecutive failures needed before the verdict flips; cached verdict still healthy". The Warn branch is not: it fires on every subsequent failure, with the wording "cached verdict is now unhealthy".

The verdict flipped at failure 2 (F70.2 default threshold). Failure 29 changed nothing. The message reads as an incident every 30s when there was exactly one incident.

2. On piper-only the condition is permanent by construction, so it never stops.

compose.piper-only.yaml profile-disables kokoro and maps all six SegmentKinds to piper. Its header explains why Tts__Endpoint must keep pointing at the absent host (KokoroTtsSynthesizer speaks kokoro-fastapi's wire protocol, piper's server does not, and TtsOptions.Endpoint is [Required]), and describes the probe flipping unhealthy as the intended mechanism. That design is sound β€” but it means the probe fails forever, and nothing anticipated the log volume:

Per 30s probe Per day
1 Γ— warn + stack trace 2,880
2 Γ— info HttpClient + stack traces 5,760
~8,640 stack traces/day

For a condition that will never change and carries zero information after the first entry.

πŸ“ It violates the method's own stated principle

From RecordFailure's doc comment:

"This is what keeps a busy-but-alive dependency off the warning stream entirely: gh-#125's Kokoro blocks its own event loop for the length of a render, so it misses isolated probes forever, and paging on that is noise."

The reasoning applies with more force here. gh-#125's kokoro was busy-but-alive and might recover. This kokoro is deliberately absent and definitionally cannot. Same class as gh-#251's icecast warn spam, and on a Loki-shipping box it is real ingest volume.

πŸ”§ Proposed shape

  • Warn on the flip only. Steady-state repeats with an unchanged reason drop to Debug (which per current fleet config does not reach Loki), and drop the exception argument β€” the stack trace is identical every cycle and the reason string already carries the cause. Fleet volume goes from 2,880/day to ~1 per outage.
  • Consider skipping the probe entirely when Tts:EngineByKind routes no SegmentKind to the dependency. On piper-only that is all six, so the probe is answering a question nothing asks. This is knowable from config at registration time.
  • Quiet the typed client's info logging for probe clients β€” a health probe's request/response pair is not application-interesting at info, and it is 2 of the 3 stack traces.

Care needed on the first bullet: the flip β†’ Warn β†’ recover β†’ flip cycle must still produce a Warn each time a real dependency genuinely drops, so the suppression has to key on "verdict unchanged since last cycle", not on "verdict is unhealthy".

πŸ“‹ Notes

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions