From ce2fc62ef82ea0c041c6d2e55b6b6302230976a6 Mon Sep 17 00:00:00 2001 From: Mathias Date: Wed, 3 Jun 2026 22:54:24 +0200 Subject: [PATCH] feat(config): TAPIR_FETCH_BACKOFF for rate-limit retry window MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds FetchBackoff (Go duration, default 1h) controlling how long the run loop waits before re-fetching a transcript that returned HTTP 429. Zero means always retry. Not required by ValidateForRun — a zero/unset value is a valid policy. Co-Authored-By: Claude Opus 4.8 (1M context) --- .env.example | 5 +++++ internal/config/config.go | 13 +++++++++++++ internal/config/config_test.go | 7 +++++++ 3 files changed, 25 insertions(+) diff --git a/.env.example b/.env.example index 301cf65..130e83e 100644 --- a/.env.example +++ b/.env.example @@ -46,3 +46,8 @@ TAPIR_SECRETS_FILE= # --- run loop ------------------------------------------------------------- # Empty/0 = single pass. Set (e.g. 15m) to poll on that cadence. TAPIR_POLL_INTERVAL= +# How long to wait before re-fetching a transcript that returned HTTP 429 +# (rate_limited). Inside the window the video is skipped without hitting the +# caption endpoint; after it expires the video is retried. 0 = always retry. +# Go duration; default 1h. +TAPIR_FETCH_BACKOFF= diff --git a/internal/config/config.go b/internal/config/config.go index 527d433..8895bfe 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -59,6 +59,12 @@ type Config struct { // PollInterval, when > 0, makes `run` loop on that cadence; 0 means run once. PollInterval time.Duration + // FetchBackoff is how long the run loop waits before re-fetching a transcript + // that previously returned HTTP 429 (rate_limited). Inside the window the video + // is skipped without hitting the caption endpoint, saving requests; after it + // expires the video is retried. Zero means "always retry" (no backoff). + FetchBackoff time.Duration + // HTTPAddr is the listen address for `tapir serve` (the Stage-0 web UI). HTTPAddr string @@ -85,6 +91,7 @@ const ( defaultYTConnectRedirectURL = "https://tapir.d-ma.be/oauth/youtube/callback" defaultOAuthRedirectAddr = "localhost:8080" defaultHTTPAddr = ":8080" + defaultFetchBackoff = time.Hour ) // Load reads the environment into a Config, applying defaults. It does not @@ -124,6 +131,12 @@ func Load() (Config, error) { } c.PollInterval = interval + backoff, err := durationOr("TAPIR_FETCH_BACKOFF", defaultFetchBackoff) + if err != nil { + return Config{}, err + } + c.FetchBackoff = backoff + return c, nil } diff --git a/internal/config/config_test.go b/internal/config/config_test.go index c8fce0d..59e0849 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -45,6 +45,9 @@ func TestLoad_AppliesDefaults(t *testing.T) { if c.PollInterval != 0 { t.Errorf("PollInterval = %v, want 0 (run once)", c.PollInterval) } + if c.FetchBackoff != defaultFetchBackoff { + t.Errorf("FetchBackoff = %v, want default %v", c.FetchBackoff, defaultFetchBackoff) + } } func TestLoad_ParsesValues(t *testing.T) { @@ -56,6 +59,7 @@ func TestLoad_ParsesValues(t *testing.T) { "TAPIR_SUMMARIZER_TIMEOUT": "90s", "TAPIR_DB_DSN": "postgres://x", "TAPIR_POLL_INTERVAL": "10m", + "TAPIR_FETCH_BACKOFF": "30m", }) c, err := Load() @@ -77,6 +81,9 @@ func TestLoad_ParsesValues(t *testing.T) { if c.PollInterval != 10*time.Minute { t.Errorf("PollInterval = %v, want 10m", c.PollInterval) } + if c.FetchBackoff != 30*time.Minute { + t.Errorf("FetchBackoff = %v, want 30m", c.FetchBackoff) + } } func TestLoad_RejectsBadDuration(t *testing.T) {