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:
@@ -424,14 +424,23 @@ func (e *Service) Search(query string) (*MBSearchResult, error) {
|
|||||||
e.boostWithIndexPopularity(&result)
|
e.boostWithIndexPopularity(&result)
|
||||||
} else {
|
} else {
|
||||||
// Phase 2: LB popularity lookups (3 POST calls, rate-limited).
|
// 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()
|
lbStart := time.Now()
|
||||||
e.boostWithPopularity(&result)
|
e.boostWithPopularity(&result)
|
||||||
lbDur := time.Since(lbStart)
|
lbDur := time.Since(lbStart)
|
||||||
|
|
||||||
// Phase 3: cross-reference artist discographies.
|
// Phase 3: cross-reference artist discographies.
|
||||||
|
// Skip if the slow-path budget is already exhausted.
|
||||||
xrefStart := time.Now()
|
xrefStart := time.Now()
|
||||||
e.crossReferenceAlbums(query, &result)
|
|
||||||
|
if slowCtx.Err() == nil {
|
||||||
|
e.crossReferenceAlbums(slowCtx, query, &result)
|
||||||
|
}
|
||||||
|
|
||||||
xrefDur := time.Since(xrefStart)
|
xrefDur := time.Since(xrefStart)
|
||||||
|
slowCancel()
|
||||||
|
|
||||||
e.logger.Info("search slow path breakdown",
|
e.logger.Info("search slow path breakdown",
|
||||||
"query", query,
|
"query", query,
|
||||||
@@ -489,7 +498,7 @@ const (
|
|||||||
// Matched albums not already in result.ReleaseGroups are injected
|
// Matched albums not already in result.ReleaseGroups are injected
|
||||||
// at the front. This handles queries like "for you tatsuro"
|
// at the front. This handles queries like "for you tatsuro"
|
||||||
// where MB text search can't associate the title with the artist.
|
// 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 {
|
if len(result.Artists) == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -526,7 +535,7 @@ func (e *Service) crossReferenceAlbums(query string, result *MBSearchResult) {
|
|||||||
go func(a MBArtist) {
|
go func(a MBArtist) {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
|
|
||||||
rgs, err := e.mb.BrowseReleaseGroups(e.ctx, a.MBID)
|
rgs, err := e.mb.BrowseReleaseGroups(ctx, a.MBID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
e.logger.Warn("cross-reference browse failed",
|
e.logger.Warn("cross-reference browse failed",
|
||||||
"artist", a.Name,
|
"artist", a.Name,
|
||||||
@@ -830,6 +839,14 @@ const (
|
|||||||
// results degrade to index-only rather than blocking the user.
|
// results degrade to index-only rather than blocking the user.
|
||||||
searchMBTimeout = 4 * time.Second
|
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 caps each entity slice after filtering.
|
||||||
maxResults = 15
|
maxResults = 15
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user