From 862badce297a51b9ab264eace9aa39e38761d5d0 Mon Sep 17 00:00:00 2001 From: Paul Buetow Date: Fri, 1 May 2026 12:54:32 +0300 Subject: Make rescan asynchronous in background goroutine TriggerRescan now returns immediately and runs the actual scan in a 30-minute timeout background goroutine. This prevents long scans from tying up or timing out the HTTP request. Update related tests to wait on a channel for the async scan to complete. --- internal/service/admin.go | 16 ++++++++++++---- internal/service/admin_test.go | 12 ++++++++++-- 2 files changed, 22 insertions(+), 6 deletions(-) (limited to 'internal') diff --git a/internal/service/admin.go b/internal/service/admin.go index a775bed..673bf37 100644 --- a/internal/service/admin.go +++ b/internal/service/admin.go @@ -3,6 +3,7 @@ package service import ( "context" "fmt" + "time" "codeberg.org/snonux/player/internal/auth" "codeberg.org/snonux/player/internal/clock" @@ -39,10 +40,17 @@ func (s *adminService) TriggerRescan(ctx context.Context) error { if s.scanner == nil { return fmt.Errorf("scanner not configured") } - // Use a background context so the scan isn't canceled when the HTTP request finishes. - if err := s.scanner.Scan(context.Background(), s.mediaRoot); err != nil { - return fmt.Errorf("scan failed: %w", err) - } + // Run the scan in a background goroutine so the HTTP request + // returns immediately and the scan continues asynchronously. + go func() { + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Minute) + defer cancel() + if err := s.scanner.Scan(ctx, s.mediaRoot); err != nil { + fmt.Printf("[rescan] scan failed: %v\n", err) + } else { + fmt.Printf("[rescan] scan completed\n") + } + }() return nil } diff --git a/internal/service/admin_test.go b/internal/service/admin_test.go index bef8ca2..ac73ce2 100644 --- a/internal/service/admin_test.go +++ b/internal/service/admin_test.go @@ -57,9 +57,11 @@ func TestAdminService_ListTrash(t *testing.T) { func TestAdminService_TriggerRescan(t *testing.T) { ctx := context.Background() var scannedRoot string + done := make(chan struct{}) sc := &fakeScanner{ scanFunc: func(_ context.Context, root string) error { scannedRoot = root + close(done) return nil }, } @@ -67,6 +69,8 @@ func TestAdminService_TriggerRescan(t *testing.T) { if err := svc.TriggerRescan(ctx); err != nil { t.Fatalf("unexpected error: %v", err) } + <-done + // Give the background goroutine a moment to write scannedRoot. if scannedRoot != "/media" { t.Fatalf("expected root %q, got %q", "/media", scannedRoot) } @@ -74,16 +78,20 @@ func TestAdminService_TriggerRescan(t *testing.T) { func TestAdminService_TriggerRescan_Error(t *testing.T) { ctx := context.Background() + done := make(chan struct{}) sc := &fakeScanner{ scanFunc: func(_ context.Context, _ string) error { + close(done) return errors.New("scan failed") }, } svc := NewAdminService(&repository.MockStore{}, newMockClock(), &fakeHasher{fixed: "hash"}, sc, "/media") err := svc.TriggerRescan(ctx) - if err == nil { - t.Fatal("expected error") + // TriggerRescan now always returns nil immediately; failure is logged in background. + if err != nil { + t.Fatalf("unexpected error: %v", err) } + <-done } func TestAdminService_TriggerRescan_NilScanner(t *testing.T) { -- cgit v1.2.3