diff options
| author | Bobby <[email protected]> | 2026-02-24 14:48:50 +0530 |
|---|---|---|
| committer | Bobby <[email protected]> | 2026-02-24 14:48:50 +0530 |
| commit | 17b77153a862ad1eb3babe1e34e748363ac9916c (patch) | |
| tree | 33871abe60da895112ebfffd5802f396005f7c79 | |
| parent | efb4f68869a9e712c4016ef286d3f16b0bba3110 (diff) | |
| download | metachan-17b77153a862ad1eb3babe1e34e748363ac9916c.tar.xz metachan-17b77153a862ad1eb3babe1e34e748363ac9916c.zip | |
Refactor rate limiter: simplify implementation and improve token management
| -rw-r--r-- | repositories/anime.go | 3 | ||||
| -rw-r--r-- | services/anime.go | 1 | ||||
| -rw-r--r-- | utils/api/jikan/jikan.go | 4 | ||||
| -rw-r--r-- | utils/ratelimit/limiter.go | 96 | ||||
| -rw-r--r-- | utils/ratelimit/types.go | 10 |
5 files changed, 37 insertions, 77 deletions
diff --git a/repositories/anime.go b/repositories/anime.go index ac10189..9a17542 100644 --- a/repositories/anime.go +++ b/repositories/anime.go @@ -63,6 +63,9 @@ func GetAnime[T idType](maptype enums.MappingType, id T) (entities.Anime, error) First(&anime) if result.Error != nil { + if errors.Is(result.Error, gorm.ErrRecordNotFound) { + return entities.Anime{}, result.Error + } logger.Errorf("Anime", "Failed to get anime details: %v", result.Error) return entities.Anime{}, errors.New("anime not found") } diff --git a/services/anime.go b/services/anime.go index 6efd81e..de19aa5 100644 --- a/services/anime.go +++ b/services/anime.go @@ -86,7 +86,6 @@ func fetchAnime(mapping *entities.Mapping, existing *entities.Anime) (*entities. logger.Warnf("AnimeService", "Failed to fetch characters from Jikan: %v", err) } - // Reset all slice fields so re-fetching doesn't double-append onto existing DB data anime.Episodes = nil anime.Characters = nil anime.Genres = nil diff --git a/utils/api/jikan/jikan.go b/utils/api/jikan/jikan.go index 244253d..8812c5a 100644 --- a/utils/api/jikan/jikan.go +++ b/utils/api/jikan/jikan.go @@ -63,7 +63,7 @@ func (c *client) handleRetry(retries *int, url string, reason string, retryAfter backoffDuration = retryAfter } - logger.Warnf("JikanClient", "%s for %s (attempt %d/%d)", reason, url, *retries, c.maxRetries) + logger.Debugf("JikanClient", "%s for %s (attempt %d/%d)", reason, url, *retries, c.maxRetries) time.Sleep(backoffDuration) return true } @@ -288,7 +288,7 @@ func GetAnimeProducers() (*types.JikanProducersResponse, error) { response.Pagination = pageResponse.Pagination } - logger.Debugf("JikanClient", "Fetched page (%d/%d) - %d producers", page, response.Pagination.LastVisiblePage, len(pageResponse.Data)) + logger.Infof("JikanClient", "Fetched page (%d/%d) - %d producers", page, response.Pagination.LastVisiblePage, len(pageResponse.Data)) response.Data = append(response.Data, pageResponse.Data...) hasNextPage = pageResponse.Pagination.HasNextPage diff --git a/utils/ratelimit/limiter.go b/utils/ratelimit/limiter.go index 6450c8b..c99a935 100644 --- a/utils/ratelimit/limiter.go +++ b/utils/ratelimit/limiter.go @@ -5,80 +5,40 @@ import ( ) func NewRateLimiter(maxRequests int, window time.Duration) *RateLimiter { - minDelay := window / time.Duration(maxRequests) - return &RateLimiter{ - lastRequests: make([]time.Time, 0, maxRequests), - maxRequests: maxRequests, - window: window, - minDelay: minDelay, - } + interval := window / time.Duration(maxRequests) + rl := &RateLimiter{ + tokens: make(chan struct{}, maxRequests), + done: make(chan struct{}), + } + rl.tokens <- struct{}{} + go func() { + ticker := time.NewTicker(interval) + defer ticker.Stop() + for { + select { + case <-ticker.C: + select { + case rl.tokens <- struct{}{}: + default: + } + case <-rl.done: + return + } + } + }() + return rl } func (r *RateLimiter) Wait() { - r.mu.Lock() - defer r.mu.Unlock() - - now := time.Now() - - if !r.lastRequest.IsZero() { - elapsed := now.Sub(r.lastRequest) - if elapsed < r.minDelay { - waitTime := r.minDelay - elapsed - r.mu.Unlock() - time.Sleep(waitTime) - r.mu.Lock() - now = time.Now() - } - } - - cutoff := now.Add(-r.window) - i := 0 - for i < len(r.lastRequests) && r.lastRequests[i].Before(cutoff) { - i++ - } - if i > 0 { - r.lastRequests = r.lastRequests[i:] - } - - if len(r.lastRequests) >= r.maxRequests { - oldestInWindow := r.lastRequests[0] - waitDuration := r.window - now.Sub(oldestInWindow) - - r.mu.Unlock() - time.Sleep(waitDuration + time.Millisecond) - r.mu.Lock() - - now = time.Now() - cutoff = now.Add(-r.window) - i = 0 - for i < len(r.lastRequests) && r.lastRequests[i].Before(cutoff) { - i++ - } - if i > 0 { - r.lastRequests = r.lastRequests[i:] - } - } + <-r.tokens +} - r.lastRequest = now - r.lastRequests = append(r.lastRequests, now) +func (r *RateLimiter) Stop() { + close(r.done) } func (r *RateLimiter) RemainingRequests() int { - r.mu.Lock() - defer r.mu.Unlock() - - now := time.Now() - cutoff := now.Add(-r.window) - - i := 0 - for i < len(r.lastRequests) && r.lastRequests[i].Before(cutoff) { - i++ - } - if i > 0 { - r.lastRequests = r.lastRequests[i:] - } - - return r.maxRequests - len(r.lastRequests) + return len(r.tokens) } func NewMultiLimiter(limiters ...*RateLimiter) *MultiLimiter { @@ -88,6 +48,8 @@ func NewMultiLimiter(limiters ...*RateLimiter) *MultiLimiter { } func (m *MultiLimiter) Wait() { + m.mu.Lock() + defer m.mu.Unlock() for _, limiter := range m.limiters { limiter.Wait() } diff --git a/utils/ratelimit/types.go b/utils/ratelimit/types.go index f962f22..17b7900 100644 --- a/utils/ratelimit/types.go +++ b/utils/ratelimit/types.go @@ -2,18 +2,14 @@ package ratelimit import ( "sync" - "time" ) type RateLimiter struct { - mu sync.Mutex - lastRequest time.Time - lastRequests []time.Time - maxRequests int - window time.Duration - minDelay time.Duration + tokens chan struct{} + done chan struct{} } type MultiLimiter struct { + mu sync.Mutex limiters []*RateLimiter } |
