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.
This commit is contained in:
2026-03-29 16:34:02 -04:00
parent fb3a340fa2
commit 0f2a82256c
+20 -3
View File
@@ -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