mirror of
https://github.com/AmanTahiliani/box-box.git
synced 2026-08-07 19:56:18 -04:00
feat(backend): implement session coverage, resumable ingestion, deep year backfill, backoff rate-limit, and dynamic cache TTLs
This commit is contained in:
@@ -5,6 +5,7 @@ import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
@@ -96,11 +97,30 @@ func cacheDBPath() string {
|
||||
// ttlForURL determines the appropriate TTL based on the URL pattern.
|
||||
// Returns 0 (CacheTTLForever) for historical data that will never change.
|
||||
func ttlForURL(url string) time.Duration {
|
||||
var year int
|
||||
if idx := strings.Index(url, "year="); idx != -1 && len(url) >= idx+9 {
|
||||
yearStr := url[idx+5 : idx+9]
|
||||
if y, err := strconv.Atoi(yearStr); err == nil {
|
||||
year = y
|
||||
}
|
||||
}
|
||||
|
||||
currentYear := time.Now().Year()
|
||||
|
||||
// Historical data — completed past seasons never change.
|
||||
if strings.Contains(url, "year=2023") || strings.Contains(url, "year=2024") {
|
||||
if year > 0 && year < currentYear {
|
||||
return CacheTTLForever
|
||||
}
|
||||
|
||||
// For current year or unspecified year (e.g. meeting/session list endpoint that includes a session key query):
|
||||
if year == currentYear || year == 0 {
|
||||
// Cache current year meetings and sessions metadata for 24h
|
||||
if (strings.Contains(url, "/meetings") || strings.Contains(url, "/sessions")) &&
|
||||
!strings.Contains(url, "/session_result") {
|
||||
return CacheTTLLong
|
||||
}
|
||||
}
|
||||
|
||||
// Live telemetry endpoints — change every few seconds during a session.
|
||||
if strings.Contains(url, "/position") ||
|
||||
strings.Contains(url, "/intervals") ||
|
||||
|
||||
@@ -20,6 +20,15 @@ import (
|
||||
// from ~30 min before a session starts until ~30 min after it ends.
|
||||
var ErrLiveSessionLocked = errors.New("live F1 session in progress — API access is restricted to authenticated users until the session ends")
|
||||
|
||||
// RateLimitError is returned when the OpenF1 API rate limit is reached (HTTP 429).
|
||||
type RateLimitError struct {
|
||||
RetryAfter time.Duration
|
||||
}
|
||||
|
||||
func (e *RateLimitError) Error() string {
|
||||
return fmt.Sprintf("openf1 API rate limit hit: retry after %v", e.RetryAfter)
|
||||
}
|
||||
|
||||
// IsLiveSessionError reports whether err (or any error in its chain) is the
|
||||
// live-session lockout error from the OpenF1 API.
|
||||
func IsLiveSessionError(err error) bool {
|
||||
@@ -111,6 +120,16 @@ func (c *OpenF1Client) FetchStrict(url string) ([]byte, error) {
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == http.StatusTooManyRequests {
|
||||
retryAfterDur := 500 * time.Millisecond
|
||||
if retryAfterHeader := resp.Header.Get("Retry-After"); retryAfterHeader != "" {
|
||||
if seconds, err := strconv.Atoi(retryAfterHeader); err == nil {
|
||||
retryAfterDur = time.Duration(seconds) * time.Second
|
||||
}
|
||||
}
|
||||
return nil, &RateLimitError{RetryAfter: retryAfterDur}
|
||||
}
|
||||
|
||||
data, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
Reference in New Issue
Block a user