feat: implement rate limiting and caching layer for MusicBrainz provider
- Add golang.org/x/time/rate dependency for token-bucket rate limiting - Replace custom channel-based rate limiter with rate.NewLimiter(1, 1) - Add context.Context support to doGet for cancellation - Add cached_at column to external_releases via migration 005 - Implement cache hit/miss queries with TTL-based filtering - Add CacheStats type for tracking cached RGIDs - Update ExternalRelease struct with CachedAt field - Add rate limiting tests (1 req/sec enforcement, burst behavior) - Add cache tests (hit, miss, expired, mixed, empty artist) - Update migration count test for new migration
This commit is contained in:
53
internal/musicbrainz/cache.go
Normal file
53
internal/musicbrainz/cache.go
Normal file
@@ -0,0 +1,53 @@
|
||||
package musicbrainz
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"naviwatcher/internal/database"
|
||||
)
|
||||
|
||||
// CacheStats holds the result of a cache lookup for a given artist.
|
||||
type CacheStats struct {
|
||||
// CachedRGIDs is the list of RGIDs that are currently cached (within TTL).
|
||||
CachedRGIDs []string
|
||||
// CacheHitCount is the number of entries found in cache.
|
||||
CacheHitCount int
|
||||
}
|
||||
|
||||
// IsCached returns true if the given RGID is in the cached set.
|
||||
func (cs *CacheStats) IsCached(rgid string) bool {
|
||||
for _, id := range cs.CachedRGIDs {
|
||||
if id == rgid {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// GetCachedReleases queries the external_releases table for entries
|
||||
// belonging to the given artist that were cached within the specified TTL.
|
||||
// It returns a CacheStats with the list of valid RGIDs already in cache.
|
||||
func GetCachedReleases(db *database.DB, artistID string, ttl time.Duration) (*CacheStats, error) {
|
||||
releases, err := database.GetExternalReleasesByArtistWithCache(db, artistID, ttl)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get cached releases: %w", err)
|
||||
}
|
||||
|
||||
stats := &CacheStats{}
|
||||
for _, r := range releases {
|
||||
stats.CachedRGIDs = append(stats.CachedRGIDs, r.RGID)
|
||||
stats.CacheHitCount++
|
||||
}
|
||||
return stats, nil
|
||||
}
|
||||
|
||||
// IsArtistCacheValid checks whether the cache for an artist is still valid.
|
||||
// Returns true if any entries exist within the TTL for this artist.
|
||||
func IsArtistCacheValid(db *database.DB, artistID string, ttl time.Duration) (bool, error) {
|
||||
stats, err := GetCachedReleases(db, artistID, ttl)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return stats.CacheHitCount > 0, nil
|
||||
}
|
||||
Reference in New Issue
Block a user