fix(queue): fertige Ansagen nicht mehr als „abgebrochen" protokollieren

Der Worker rief cancel() auf seinem eigenen Kontext, BEVOR er ctx.Err()
prüfte — damit war jede normal beendete Ansage im Protokoll „abgebrochen",
auch bei Erfolg. Der Abbruch wird jetzt vor dem eigenen cancel() abgelesen.
Regressionstest: fertig heißt „fertig nach", ein echter Cancel heißt
„abgebrochen".

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
m3ta-chironandClaude Fable 5.1 committed 2026-10-24 09:50:08 +02:00
1 parent bc26a73aeb
commit 79314dfb0f
2 files changed
+60 -1

No files matched your search

+4 -1
View File
@@ -317,6 +317,9 @@ func (q *Queue) loop() {
start := time.Now() start := time.Now()
err := q.speak(ctx, item) err := q.speak(ctx, item)
dur := time.Since(start) dur := time.Since(start)
// Abbruch VOR dem eigenen cancel() ablesen — danach wäre ctx.Err()
// immer gesetzt und jede fertige Ansage hieße „abgebrochen".
cancelled := ctx.Err() != nil
cancel() cancel()
q.mu.Lock() q.mu.Lock()
@@ -325,7 +328,7 @@ func (q *Queue) loop() {
q.mu.Unlock() q.mu.Unlock()
switch { switch {
case ctx.Err() != nil: case cancelled:
q.logf("queue: ansage %s abgebrochen", item.ID) q.logf("queue: ansage %s abgebrochen", item.ID)
case err != nil: case err != nil:
q.logf("queue: ansage %s fehlgeschlagen nach %s: %v", item.ID, dur.Round(time.Millisecond), err) q.logf("queue: ansage %s fehlgeschlagen nach %s: %v", item.ID, dur.Round(time.Millisecond), err)
+56
View File
@@ -5,6 +5,8 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"slices" "slices"
"strings"
"sync"
"testing" "testing"
"time" "time"
) )
@@ -552,3 +554,57 @@ func TestInactiveQuietWindow(t *testing.T) {
} }
} }
} }
// syncLog ist ein nebenläufigkeitssicherer Log-Puffer für Worker-Zeilen.
type syncLog struct {
mu sync.Mutex
b strings.Builder
}
func (l *syncLog) Write(p []byte) (int, error) {
l.mu.Lock()
defer l.mu.Unlock()
return l.b.Write(p)
}
func (l *syncLog) String() string {
l.mu.Lock()
defer l.mu.Unlock()
return l.b.String()
}
// TestProtokollFertigNichtAbgebrochen: Eine normal beendete Ansage wird als
// „fertig" protokolliert, nicht als „abgebrochen" — der Worker darf seinen
// eigenen cancel() nicht als Abbruch deuten. Ein echter Cancel heißt
// dagegen „abgebrochen".
func TestProtokollFertigNichtAbgebrochen(t *testing.T) {
logb := &syncLog{}
g := neueSpielerin()
q := New(Laws{}, g.speak, logb)
defer q.Close()
q.Enqueue(Item{Text: "normal zu ende"})
g.warteBis(t, 1)
g.step <- struct{}{}
waitFor(t, func() bool { return strings.Contains(logb.String(), "fertig nach") })
if strings.Contains(logb.String(), "abgebrochen") {
t.Errorf("fertige Ansage als abgebrochen protokolliert:\n%s", logb.String())
}
q.Enqueue(Item{Text: "wird abgebrochen"})
g.warteBis(t, 2)
q.Cancel()
waitFor(t, func() bool { return strings.Contains(logb.String(), "abgebrochen") })
}
// waitFor pollt cond bis zu zwei Sekunden.
func waitFor(t *testing.T, cond func() bool) {
t.Helper()
deadline := time.Now().Add(2 * time.Second)
for !cond() {
if time.Now().After(deadline) {
t.Fatal("Bedingung nicht binnen 2 s erfüllt")
}
time.Sleep(5 * time.Millisecond)
}
}