From 0f2a82256c05407e5af8bb2d1591d6f34e37bbc6 Mon Sep 17 00:00:00 2001 From: Caleb Allen Date: Sun, 29 Mar 2026 16:34:02 -0400 Subject: [PATCH] fix: cap slow-path search at 3s to prevent 20s+ searches The cross-referencing phase browses 3 artist discographies via MB API. When the background indexer is also making MB requests, 429 retries with exponential backoff can stack up to 20+ seconds. Added a 3s context.WithTimeout covering both LB popularity and cross-referencing. If LB popularity exhausts the budget, cross-ref is skipped entirely. If cross-ref is running when the deadline hits, the BrowseReleaseGroups calls are cancelled mid-flight. Worst case search time is now ~7s (4s MB search + 3s slow path) instead of unbounded. --- backend/explore/explore.go | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/backend/explore/explore.go b/backend/explore/explore.go index dc74276..6f3b107 100644 --- a/backend/explore/explore.go +++ b/backend/explore/explore.go @@ -424,14 +424,23 @@ func (e *Service) Search(query string) (*MBSearchResult, error) { e.boostWithIndexPopularity(&result) } else { // Phase 2: LB popularity lookups (3 POST calls, rate-limited). + // Use a tight deadline so a slow LB/MB doesn't stall the search. + slowCtx, slowCancel := context.WithTimeout(e.ctx, searchSlowPathTimeout) + lbStart := time.Now() e.boostWithPopularity(&result) lbDur := time.Since(lbStart) // Phase 3: cross-reference artist discographies. + // Skip if the slow-path budget is already exhausted. xrefStart := time.Now() - e.crossReferenceAlbums(query, &result) + + if slowCtx.Err() == nil { + e.crossReferenceAlbums(slowCtx, query, &result) + } + xrefDur := time.Since(xrefStart) + slowCancel() e.logger.Info("search slow path breakdown", "query", query, @@ -489,7 +498,7 @@ const ( // Matched albums not already in result.ReleaseGroups are injected // at the front. This handles queries like "for you tatsuro" // where MB text search can't associate the title with the artist. -func (e *Service) crossReferenceAlbums(query string, result *MBSearchResult) { +func (e *Service) crossReferenceAlbums(ctx context.Context, query string, result *MBSearchResult) { if len(result.Artists) == 0 { return } @@ -526,7 +535,7 @@ func (e *Service) crossReferenceAlbums(query string, result *MBSearchResult) { go func(a MBArtist) { defer wg.Done() - rgs, err := e.mb.BrowseReleaseGroups(e.ctx, a.MBID) + rgs, err := e.mb.BrowseReleaseGroups(ctx, a.MBID) if err != nil { e.logger.Warn("cross-reference browse failed", "artist", a.Name, @@ -830,6 +839,14 @@ const ( // results degrade to index-only rather than blocking the user. searchMBTimeout = 4 * time.Second + // searchSlowPathTimeout caps the total time spent on the slow + // path (LB popularity + cross-referencing). When the index + // isn't ready, these API calls can stack up — especially + // cross-referencing, which browses 3 artist discographies via + // MB and can hit 429 retries. The timeout ensures search + // returns within a reasonable window. + searchSlowPathTimeout = 3 * time.Second + // maxResults caps each entity slice after filtering. maxResults = 15