fix(bg): Hintergrundarbeit reisst den Prozess nicht mehr mit

Das Vorschaubild entstand in einer nackten Goroutine. Chis Recoverer haengt
am Anfragebehandler und sieht davon nichts -- eine Panik im Bilddekodierer
nahm den ganzen Go-Prozess mit, ausgeloest durch einen gewoehnlichen
Upload. Die Bibliothek ist seit ef60497 aktuell; das schloss **diese**
Luecke. Dies hier schliesst die **Gattung**.

pkg/bg mit zwei Funktionen, und der Unterschied ist keine Feinheit:

* bg.Go(name, fn)  -- Goroutine, die eine Panik ueberlebt
* bg.Run(name, fn) -- dasselbe ohne eigene Goroutine, fuer Schleifen

Faengt man eine Panik um den **ganzen** Arbeiter ab, ueberlebt der Prozess
-- aber der Arbeiter ist tot, und von da an wird still keine Datei mehr
erkannt. Der OCR-Arbeiter sichert deshalb jeden **Auftrag** einzeln ab.

Umgestellt: Vorschaubild, Bestand nachtragen, Freigabe-Zaehler,
Speicherbereinigung, OCR-Arbeiter. Eine nackte Goroutine bleibt mit
Begruendung im Code -- die Signalbehandlung; ein recover braechte dort
nichts, weil main ohnehin wartete.

Aufgefangene Paniken werden mit Stapel und Herkunft protokolliert. Vier
Tests, darunter der wichtige: Eine Schleife laeuft nach einer Panik im
dritten Durchgang weiter.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
marcandClaude Opus 5 committed 2026-09-12 18:29:02 +02:00
1 parent 89f0817dcb
commit c26bcba012
7 files changed
+156 -17

No files matched your search

+20 -6
View File
@@ -38,6 +38,7 @@ import (
"aether/internal/tags"
"aether/internal/thumbnails"
appwebdav "aether/internal/webdav"
"aether/pkg/bg"
"aether/pkg/config"
appdb "aether/pkg/db"
"aether/pkg/middleware"
@@ -176,13 +177,16 @@ func main() {
Timeout: cfg.OCRTimeout,
TmpDir: cfg.TmpPath,
}, log.Logger)
go worker.Run(ctx)
// Die Schleife darin sichert jeden Auftrag einzeln ab (siehe
// worker.Run). Diese Hülle fängt, was **davor** passiert – das
// Aufräumen der Zwischendateien beim Start.
bg.Go("OCR-Arbeiter", func() { worker.Run(ctx) })
// Bestand nachtragen: Beim ersten Start nach dem Einbau hat noch
// keine einzige Datei einen Auftrag. Ohne das bliebe alles
// unauffindbar, was vorher hochgeladen wurde – und das ist am
// Anfang der gesamte Bestand.
go func() {
bg.Go("Bestand nachtragen", func() {
n, err := docStore.EnqueueMissing(ctx, 5000)
if err != nil {
log.Warn().Err(err).Msg("Bestand nachtragen fehlgeschlagen")
@@ -205,7 +209,7 @@ func main() {
log.Info().Int("files", m).Int("version", documents.ExtractVersion).
Msg("veraltete Texteintraege zur erneuten Erkennung eingeplant")
}
}()
})
}
}
@@ -633,6 +637,10 @@ func main() {
done := make(chan struct{})
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
// Die einzige nackte Goroutine, die bleibt – und das mit Absicht: Sie
// wartet auf ein Signal und schließt `done`. Ein recover brächte hier
// nichts, denn stürbe sie vor dem close, wartete main ohnehin für immer.
// Sie berührt außerdem keine fremden Daten.
go func() {
<-quit
log.Info().Msg("shutting down...")
@@ -698,8 +706,14 @@ func afterUpload(fh *files.Handler, fileStore *files.Store, st storage.Storage,
return
}
// Generate thumbnail in background – non-fatal if it fails.
go func() {
// Vorschaubild im Hintergrund – ein Fehlschlag ist nicht schlimm.
//
// **Über bg.Go, nicht als nackte Goroutine.** Hier wird eine
// hochgeladene Datei dekodiert. Chis Recoverer hängt am
// Anfragebehandler und sieht davon nichts; eine Panik im Dekodierer
// nähme sonst den ganzen Prozess mit – ausgelöst durch einen
// gewöhnlichen Upload.
bg.Go("Vorschaubild", func() {
ctx := context.Background()
f, err := fileStore.GetFileByID(ctx, created.ID)
if err != nil {
@@ -715,7 +729,7 @@ func afterUpload(fh *files.Handler, fileStore *files.Store, st storage.Storage,
return
}
_ = fileStore.UpdateThumbnail(ctx, created.ID, true)
}()
})
}
}