7.5 KiB
7.5 KiB
Task-5-Report: Rate-Limit + Ruhezeiten
Status: DONE · Branch feat/v1 · Basis dc94f45 (Task 4)
Commits: b20e6dd (config) → 5385a4b (queue) → b56c8ab (api/talkd/cli) → 5979d45 (Prozessgrenzen-Tests) → 24ceff3 (README). Kein Push, .scratch/ unangetastet.
Timing-Semantik (verbindlich, wer wartet / wer verfällt / was now darf)
Rangordnung: mute (API, immer) > now > Ruhezeit > Rate-Limit.
| Situation | low | normal | high | now:true |
|---|---|---|---|---|
Rate-Fenster voll (Start der letzten Ansage < queue.rate_limit her) |
verworfen — schon beim Eintritt, 202 {id:"",discarded:"rate_limit"}; kam es bei freiem Fenster rein, verfällt es beim Worker-Durchlauf (Log-Zeile) |
parkt in der Queue, spielt zum Slot (rate_limit.next_slot in /status; Slot-Wecker weckt den Worker) |
mit allow_high_burst=true (Default): spielt sofort (bewusster Verstoß); false: parkt wie normal |
spielt sofort — umgeht auch das Rate-Limit (korrigierte Brief-Entscheidung umgesetzt) |
Ruhezeit aktiv (quiet_hours, lokal, über Mitternacht) |
verworfen, 202 {discarded:"quiet_hours"} |
dito | dito — Burst ist ein Rate-Recht, kein Ruhe-Recht | spielt sofort — bricht auch die Ruhezeit (dokumentierter Ausbruch) |
quiet_mode="toast" |
/ | / | / | / : statt Verwurf Toast-Ersetzung, 202 {discarded:"quiet_hours",toast:true} |
| Fenster frei + keine Ruhezeit | spielt | spielt | spielt | spielt + springt vor |
Weitere Regeln, dokumentiert und getestet:
- Jede gestartete Ansage bucht das Fenster neu (
Reservevor dem Abspielen) — auch eine per now oder high-burst ausgebrochene und auch eine fehlgeschlagene (kaputter Provider darf nicht zum Maschinengewehr werden). Fenster = Mindestabstand zwischen Ansage-Starts. - While parked bleibt ein normal/high-Item voll sichtbar (
queue_ids,--waitfunktioniert); neu eintreffende now-/burst-fähige Items werden vorgezogen (Rank-Ordnung unverändert: now > high > normal > low, FIFO je Level). --waitauf ein verworfenes Item endet sauber (ID verschwindet aus der Queue — dasselbe Muster wie bei cancel).
Umsetzung
internal/queue/quiet.go(neu):QuietHours— ParseHH:MM-HH:MM(leer = aus, Start==End = aus),Active(Tag-/Nachtfenster, Start inklusiv, End exklusiv),Until(Fenster-Ende für /status),String(Roundtrip).internal/queue/rate.go:RateLimiter(aus T4,Next()liefert jetzt Nullzeit bei nie gebuchtem Fenster laut Doku), dazuLaws+ reinedecide-Ordnung,Admission(Enqueue-Ergebnis mit Verwurfs-Grund),LawState(/status-Sicht), Grund-Konstantenrate_limit/quiet_hours.internal/queue/queue.go: Loop erweitert (nicht umgeschrieben — Rank/Serialisierung/Mute unangetastet):readyLocked/takeLocked(parken ohne Entnahme),armWakeLocked(Slot-Wecker broadcastet die Cond; Wait-Bedingung prüft die Gesetze neu → verlorene Signale harmlos),Enqueueentscheidet Ruhezeit/low-Budget schon beim Eintritt (damit die 202 den Grund nennen kann). Lock-Ordnung Queue → RateLimiter durchgehend gewahrt; Race-Detektor grün.internal/config:[queue] rate_limit = "30s"(Dauer-String,"0s"= unbegrenzt; TOML-Decode viatime.Duration-TextUnmarshaler, verifiziert),allow_high_burst(Default true; partielle[queue]-Tabellen behalten Defaults — getestet),quiet_hours/quiet_mode(Default aus/discard). Env:TALK_RATE_LIMIT,TALK_ALLOW_HIGH_BURST,TALK_QUIET_HOURS(off= aus),TALK_QUIET_MODE. Validierung gegenqueue.ParseQuietHours(eine Quelle; Import config→queue, kein Zyklus).internal/api:/speakantwortet bei Verwurf 202{id:"",discarded:…}(+toast:true) — analog zum Mute-Muster, kein Fehler;/statusadditivrate_limit {interval, allow_high_burst, next_slot}+quiet_hours {window, active, mode, until}(RFC3339 bzw. null). Kein Feld gebrochen.cmd/talkd: verwiredLawsaus der Config, Startup-Log nennt das Gesetz.- CLI:
saymeldetdiscarded: <grund>(+(toast)) plain auf stdout mit Exit 0 (wiemuted);talk statuszeigtrate-limit: … (high-burst …), nächster slot …undruhezeit: … (modus …, aktiv bis …/inaktiv). - README: kompletter Abschnitt zu Semantik + Config/Env + Rangordnung; Endpunkt-Tabelle aktualisiert.
Tests
- Prozessgrenze (
test/ratelimit_test.go, echte Binaries, Fake-Provider, sink=null, ephemeral Ports): kurzes Fenster (1 s) → 5 Ansagen: genau 1 spielt sofort, low verfällt beim Eintritt (HTTP und CLI-Output), normal parkt (queue_ids + next_slot sichtbar) und spielt frühestens zum Slot (Timing am Fake gemessen, ≥ 850 ms Abstand); high-burst an (spielt im Fenster) / aus (parkt, cancel räumt, Daemon gesund — das Warten-Aus mit langem Fenster deckt der Park-Test);nowbricht durch Rate-Limit und Ruhezeit; Ruhezeit-Fenster künstlich um die lokale Jetztzeit (±1 min) → discard (auch high, kein Provider-Kontakt, Log-Zeile, until in /status) und toast (Toast-Protokoll-Zeile);/status- +talk status-Felder (Defaults 30 s/Burst an/Slot frei, gebuchter Slot nach Ansage). - Unit (
internal/queue): QuietHours-Parse/Active/Until/String (tischgesteuert inkl. Mitternacht); Queue-Gesetze mit gesteuerter Spielerin (low verfällt, normal parkt + spielt, burst ein/aus, now-Ausbruch + Folgebuchung, Ruhezeit discard/toast auch für high, LawState). - Config (
internal/config): Defaults, volle/partielle TOML, Env-Overrides inkl.off, klare Fehler für Müll. - Harness: Basis-Env
TALK_RATE_LIMIT=0s— die Task-04-Semantik-Tests (Serialisierung, Priorität, …) erwarten dichtes Durchspielen und blieben unverändert; Gesetz-Tests überschreiben bewusst.
Verifikation: gofmt -l . leer · go vet ./... grün · go test ./... grün · go test -race -count=1 ./... grün (queue+test zusätzlich -count=2 zur Flakiness-Probe) · go build ./... grün · Cross-Compile-Smoke CGO_ENABLED=0 GOOS=darwin/windows GOARCH=amd64 für talk+talkd erfolgreich.
Entscheidungen & Hinweise
now:trueumgeht auch das Rate-Limit — die CORRECTION im Brief ist bindend umgesetzt und überall (Code-Kommentare, README, Tests) konsistent dokumentiert; ein now-Bruch startet trotzdem ein neues Fenster.- Ruhezeiten sind per Default aus (leer). Ein Default-Fenster „18:00–08:00" würde jeden Cron/Test in den Abendstunden stilllegen (CI läuft zu jeder Uhrzeit) — Opt-in wie Mute, im Config-Kommentar begründet.
quiet_mode="toast"ist derzeit eine Daemon-Protokoll-Zeile über den Notification-Pfad (Log-Writer), keine OS-Benachrichtigung — die Brief-Vorgabe „log line via notification path if trivial, else document limitation": der Notification-Pfad existiert noch nicht (internal/notify ist doc-only), also die dokumentierte Einschränkung. Die Ersetzung sitzt im Gesetz an genau einer Stelle (verdictToast), die OS-Anbindung ersetzt nur das Log-Ziel.- Degraded Mode respektiert weiterhin nur Mute, nicht Rate-Limit/Ruhezeit (Task-4-Umfang unverändert; der Brief fordert das Gesetz für den Daemon). Der Lock-File-Pfad bleibt sequenziell-best-effort. Für V1 akzeptiert; falls gewünscht, als Folge-Task.
- Verwurf beim Eintritt (low über Budget, Ruhezeit) vs. Worker-Durchlauf: Eintritt, damit die 202-Antwort den Grund nennen kann (so der Brief); was bei freiem Fenster eintritt und erst später verfällt, verfällt im Worker mit Log-Zeile — beides getestet bzw. in der Semantik-Tabelle dokumentiert.
- Stale Slot-Wecker nach Close sind harmlos (Broadcast ohne Waiter; Code-Kommentar erläutert es).