fix: shared MB rate limiter prevents search/indexer 429 collisions

Previously, the MusicBrainzClient had no proactive rate limiter —
it relied on the musicbrainzws2 library's retry-on-429 backoff.
When the background indexer was resolving artist images (hitting MB
at 1.5 req/sec) and a user search fired 3+ concurrent MB calls,
the combined burst triggered 429s with cascading retries up to 60s.

Now a single shared RateLimiter (1 req/sec) gates all MB API calls:
- MusicBrainzClient search/lookup/browse methods
- ArtistImageProvider fetchMBRels (was on a separate 1.5 req/sec limiter)

The limiter serializes access proactively, preventing 429s entirely.
The musicbrainzws2 retry logic remains as a safety net.

Also split the old shared limiter into separate lbLimiter (for
ListenBrainz + CoverArt) and mbLimiter (for MusicBrainz) so the
two APIs don't block each other.
This commit is contained in:
2026-03-29 16:37:51 -04:00
parent 0f2a82256c
commit a93a83c4d2
4 changed files with 56 additions and 16 deletions
+6 -5
View File
@@ -35,12 +35,13 @@ type Service struct {
// client, and ListenBrainz client internally.
func NewExploreService(logger *slog.Logger, db *database.DB) *Service {
cache := NewCache(db, logger.WithGroup("cache"))
limiter := NewRateLimiter()
mb := NewMusicBrainzClient(cache, logger.WithGroup("musicbrainz"))
lb := NewListenBrainzClient(limiter, cache, logger.WithGroup("listenbrainz"))
artProxy := NewCoverArtProxy(db, limiter)
lbLimiter := NewRateLimiter()
mbLimiter := NewRateLimiter() // 1 req/sec, shared across all MB consumers
mb := NewMusicBrainzClient(cache, mbLimiter, logger.WithGroup("musicbrainz"))
lb := NewListenBrainzClient(lbLimiter, cache, logger.WithGroup("listenbrainz"))
artProxy := NewCoverArtProxy(db, lbLimiter)
artistImg := NewArtistImageProvider(
db, cache, NewRateLimiterF(1.5), logger.WithGroup("artist-image"),
db, cache, mbLimiter, logger.WithGroup("artist-image"),
)
index := NewSearchIndex(db, lb, artistImg, logger.WithGroup("search-index"))
libMBID := NewLibraryMBIDIndex(db)