mirror of
https://github.com/AmanTahiliani/box-box.git
synced 2026-08-07 19:56:18 -04:00
Add OpenF1 request pacing, 429 retry, and hub completeness guard
Concurrent fan-outs could burst past the free-tier rate limit; callers swallowed the 429s as missing data, and the championship hub cached the resulting partial season for its full TTL (observed live: 8 of 22 rounds, zero wins/podiums/form). - requestPacer spaces live requests (350ms anonymous, 100ms with key); cache hits never wait - get() retries 429s up to 3 times honouring Retry-After (capped 10s) - hub tracks per-meeting fetch failures and caches incomplete aggregates for only 2 minutes instead of 15min/24h Verified against the live API: 2026 hub now reports all 22 rounds with wins/podiums/form populated. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -2,15 +2,52 @@ package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Minimum spacing between live OpenF1 requests. The free tier throttles
|
||||
// bursts of more than ~3 requests/second, so anonymous clients are paced
|
||||
// conservatively; authenticated (paid tier) clients get a higher rate.
|
||||
const (
|
||||
anonRequestInterval = 350 * time.Millisecond
|
||||
authRequestInterval = 100 * time.Millisecond
|
||||
)
|
||||
|
||||
// requestPacer spaces network requests evenly so concurrent callers
|
||||
// (e.g. the championship hub fan-out) cannot burst past the API rate limit.
|
||||
// Cache hits never touch the pacer.
|
||||
type requestPacer struct {
|
||||
mu sync.Mutex
|
||||
interval time.Duration
|
||||
next time.Time
|
||||
}
|
||||
|
||||
// wait blocks until this caller's reserved slot arrives.
|
||||
func (p *requestPacer) wait() {
|
||||
if p == nil || p.interval <= 0 {
|
||||
return
|
||||
}
|
||||
p.mu.Lock()
|
||||
now := time.Now()
|
||||
if p.next.Before(now) {
|
||||
p.next = now
|
||||
}
|
||||
sleep := p.next.Sub(now)
|
||||
p.next = p.next.Add(p.interval)
|
||||
p.mu.Unlock()
|
||||
if sleep > 0 {
|
||||
time.Sleep(sleep)
|
||||
}
|
||||
}
|
||||
|
||||
type OpenF1Client struct {
|
||||
url string
|
||||
apiKey string
|
||||
httpClient *http.Client
|
||||
cache *Cache
|
||||
pacer *requestPacer
|
||||
|
||||
// staleFlag is set to 1 atomically whenever a request falls back to stale
|
||||
// cached data (e.g. because the API is locked during a live session).
|
||||
@@ -24,6 +61,7 @@ func NewOpenF1Client(url string, timeout time.Duration) *OpenF1Client {
|
||||
url: url,
|
||||
httpClient: &http.Client{Timeout: timeout},
|
||||
cache: NewCache(),
|
||||
pacer: &requestPacer{interval: anonRequestInterval},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,6 +73,7 @@ func NewOpenF1ClientWithKey(url string, timeout time.Duration, apiKey string) *O
|
||||
apiKey: apiKey,
|
||||
httpClient: &http.Client{Timeout: timeout},
|
||||
cache: NewCache(),
|
||||
pacer: &requestPacer{interval: authRequestInterval},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user