feat(backend): implement session coverage, resumable ingestion, deep year backfill, backoff rate-limit, and dynamic cache TTLs

This commit is contained in:
2026-05-25 13:52:43 -04:00
parent bc1c551a82
commit 937674f808
9 changed files with 731 additions and 50 deletions

View File

@@ -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") ||

View File

@@ -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