summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-05-19 14:59:27 +0300
committerPaul Buetow <paul@buetow.org>2026-05-19 14:59:27 +0300
commit2967a0d2c860d3af16164e106b547256b7ebf7c7 (patch)
tree00d39faa8f77970f7b6a9fb5a8d8dba18cd0bd28
parente4d25b11b63234a4b99d2cffa85fa02aeb16c2f3 (diff)
Unify panic recovery into service.RecoverWorker
cmd/player/main.go had recoverBackgroundWorkerPanic and internal/service/recover.go had handleWorkerPanic implementing the same log-and-stack panic recovery. Export the service variant as RecoverWorker (unchanged signature), use it from main.go, and drop the duplicate helper plus the now-unused runtime/debug import. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
-rw-r--r--player-server/cmd/player/main.go14
-rw-r--r--player-server/internal/service/gc.go2
-rw-r--r--player-server/internal/service/podcast_checker.go2
-rw-r--r--player-server/internal/service/recover.go17
-rw-r--r--player-server/internal/service/scan.go2
5 files changed, 25 insertions, 12 deletions
diff --git a/player-server/cmd/player/main.go b/player-server/cmd/player/main.go
index 72bff60..8835a1a 100644
--- a/player-server/cmd/player/main.go
+++ b/player-server/cmd/player/main.go
@@ -8,7 +8,6 @@ import (
"net/http"
"os"
"os/signal"
- "runtime/debug"
"syscall"
"time"
@@ -82,12 +81,6 @@ func buildLogger(logLevel string) *slog.Logger {
return slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: level}))
}
-func recoverBackgroundWorkerPanic(logger *slog.Logger, worker string) {
- if r := recover(); r != nil && logger != nil {
- logger.Error("background worker panic", "worker", worker, "panic", r, "stack", string(debug.Stack()))
- }
-}
-
// wireDeps constructs the core service layer dependencies.
func wireDeps(cfg *internal.Config, store repository.Store, logger *slog.Logger, appCtx context.Context) *appDeps {
clk := clock.RealClock{}
@@ -144,7 +137,12 @@ func startBackgroundWorkers(deps *appDeps) {
select {
case <-ticker.C:
func() {
- defer recoverBackgroundWorkerPanic(deps.logger, "podcast checker")
+ // Use the unified service.RecoverWorker helper so this
+ // matches every other background-worker panic path in
+ // the codebase (gc, rescan, podcast feed check).
+ defer func() {
+ service.RecoverWorker(deps.logger, "podcast checker", recover())
+ }()
if err := deps.podcastSvc.CheckFeeds(context.Background()); err != nil {
deps.logger.Error("podcast feed check failed", "err", err)
}
diff --git a/player-server/internal/service/gc.go b/player-server/internal/service/gc.go
index 6eb485a..ab7f646 100644
--- a/player-server/internal/service/gc.go
+++ b/player-server/internal/service/gc.go
@@ -72,7 +72,7 @@ func (w *GCWorker) Start() {
case <-tickCh:
func() {
defer func() {
- handleWorkerPanic(w.logger, "gc", recover())
+ RecoverWorker(w.logger, "gc", recover())
}()
w.run(w.ctx)
}()
diff --git a/player-server/internal/service/podcast_checker.go b/player-server/internal/service/podcast_checker.go
index 1de920c..362fd0e 100644
--- a/player-server/internal/service/podcast_checker.go
+++ b/player-server/internal/service/podcast_checker.go
@@ -42,7 +42,7 @@ func (s *podcastFeedChecker) CheckFeeds(ctx context.Context) error {
go func(f model.PodcastFeed) {
defer wg.Done()
defer func() {
- handleWorkerPanic(s.logger, "podcast feed check", recover())
+ RecoverWorker(s.logger, "podcast feed check", recover())
}()
if err := s.checkFeed(ctx, f); err != nil {
s.logger.Warn("podcast feed check failed", "feed_id", f.ID, "feed_url", f.FeedURL, "err", err)
diff --git a/player-server/internal/service/recover.go b/player-server/internal/service/recover.go
index c89a78e..59a4716 100644
--- a/player-server/internal/service/recover.go
+++ b/player-server/internal/service/recover.go
@@ -6,7 +6,22 @@ import (
"runtime/debug"
)
-func handleWorkerPanic(logger *slog.Logger, worker string, r any) error {
+// RecoverWorker is the unified panic-recovery helper for background workers.
+// Pass the value returned by recover() in r; when non-nil it logs the panic
+// together with the stack trace and returns a formatted error so callers can
+// propagate the failure (e.g. to scan progress) if they care. When r is nil
+// the function is a no-op, which makes it safe to use unconditionally inside
+// a deferred wrapper.
+//
+// Typical usage:
+//
+// defer func() {
+// service.RecoverWorker(logger, "podcast checker", recover())
+// }()
+//
+// This consolidates what used to be duplicated as handleWorkerPanic (here)
+// and recoverBackgroundWorkerPanic (in cmd/player) into one exported variant.
+func RecoverWorker(logger *slog.Logger, worker string, r any) error {
if r == nil {
return nil
}
diff --git a/player-server/internal/service/scan.go b/player-server/internal/service/scan.go
index da5c54f..fe9513b 100644
--- a/player-server/internal/service/scan.go
+++ b/player-server/internal/service/scan.go
@@ -61,7 +61,7 @@ func (s *scanService) TriggerRescan(ctx context.Context) error {
defer cancel()
defer s.notifyDone()
defer func() {
- if err := handleWorkerPanic(s.logger, "rescan", recover()); err != nil {
+ if err := RecoverWorker(s.logger, "rescan", recover()); err != nil {
progress.Done(err)
}
}()