feat: one Poll Lane per Site, replacing the shared pace (#100)
Each Site runs its own poll goroutine, paced by Rest and Gap from the registry (sites.go) instead of the shared cooldown/interval/stagger/batch config. Rest is enforced by the due query's WHERE clause; the Lane sleeps its effective gap between fetches (hour/eligible, floored at 1s). Lane-local failures: two refusals stop that Site for 15m, a lost browser stops only the round's remaining browser Lanes, and cover heals moved to background goroutines so a slow CDN cannot consume a Lane's gap. The five LATEST_CHAPTER_POLL_* pace env vars are gone; only the kill switch remains.
This commit is contained in:
+21
-117
@@ -9,31 +9,15 @@ import (
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"bookmarkmanager/backend/internal/latest"
|
||||
"bookmarkmanager/backend/internal/store"
|
||||
)
|
||||
|
||||
func TestLoadLatestPollDefaults(t *testing.T) {
|
||||
for _, k := range []string{
|
||||
"LATEST_CHAPTER_POLL_ENABLED", "LATEST_CHAPTER_POLL_COOLDOWN",
|
||||
"LATEST_CHAPTER_POLL_BROWSER_COOLDOWN", "LATEST_CHAPTER_POLL_INTERVAL",
|
||||
"LATEST_CHAPTER_POLL_STAGGER", "LATEST_CHAPTER_POLL_BATCH",
|
||||
} {
|
||||
t.Setenv(k, "")
|
||||
}
|
||||
|
||||
got := loadLatestPoll()
|
||||
want := LatestPoll{
|
||||
Enabled: true,
|
||||
Cooldown: time.Hour,
|
||||
BrowserCooldown: 6 * time.Hour,
|
||||
Interval: 10 * time.Minute,
|
||||
Stagger: 20 * time.Second,
|
||||
Batch: 14,
|
||||
}
|
||||
if got != want {
|
||||
t.Fatalf("loadLatestPoll() = %+v, want %+v", got, want)
|
||||
t.Setenv("LATEST_CHAPTER_POLL_ENABLED", "")
|
||||
if got := loadLatestPoll(); got != (LatestPoll{Enabled: true}) {
|
||||
t.Fatalf("loadLatestPoll() = %+v, want %+v", got, LatestPoll{Enabled: true})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,105 +47,25 @@ func TestLoadLatestPollEnabledParsing(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadLatestPollClampsAndFallsBack(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
env map[string]string
|
||||
wantFrom func(LatestPoll) any
|
||||
want any
|
||||
}{
|
||||
{
|
||||
name: "cooldown below the floor is clamped up",
|
||||
env: map[string]string{"LATEST_CHAPTER_POLL_COOLDOWN": "1m"},
|
||||
wantFrom: func(p LatestPoll) any { return p.Cooldown },
|
||||
want: 15 * time.Minute,
|
||||
},
|
||||
{
|
||||
name: "cooldown at the floor is kept",
|
||||
env: map[string]string{"LATEST_CHAPTER_POLL_COOLDOWN": "15m"},
|
||||
wantFrom: func(p LatestPoll) any { return p.Cooldown },
|
||||
want: 15 * time.Minute,
|
||||
},
|
||||
{
|
||||
name: "browser cooldown below the floor is clamped up",
|
||||
env: map[string]string{"LATEST_CHAPTER_POLL_BROWSER_COOLDOWN": "1m"},
|
||||
wantFrom: func(p LatestPoll) any { return p.BrowserCooldown },
|
||||
want: 15 * time.Minute,
|
||||
},
|
||||
{
|
||||
name: "browser cooldown at the floor is kept",
|
||||
env: map[string]string{"LATEST_CHAPTER_POLL_BROWSER_COOLDOWN": "15m"},
|
||||
wantFrom: func(p LatestPoll) any { return p.BrowserCooldown },
|
||||
want: 15 * time.Minute,
|
||||
},
|
||||
{
|
||||
name: "browser cooldown override is honoured",
|
||||
env: map[string]string{"LATEST_CHAPTER_POLL_BROWSER_COOLDOWN": "8h"},
|
||||
wantFrom: func(p LatestPoll) any { return p.BrowserCooldown },
|
||||
want: 8 * time.Hour,
|
||||
},
|
||||
{
|
||||
name: "browser cooldown unparseable value falls back",
|
||||
env: map[string]string{"LATEST_CHAPTER_POLL_BROWSER_COOLDOWN": "six hours"},
|
||||
wantFrom: func(p LatestPoll) any { return p.BrowserCooldown },
|
||||
want: 6 * time.Hour,
|
||||
},
|
||||
{
|
||||
name: "a valid override is honoured",
|
||||
env: map[string]string{"LATEST_CHAPTER_POLL_INTERVAL": "5m"},
|
||||
wantFrom: func(p LatestPoll) any { return p.Interval },
|
||||
want: 5 * time.Minute,
|
||||
},
|
||||
{
|
||||
name: "an unparseable duration falls back",
|
||||
env: map[string]string{"LATEST_CHAPTER_POLL_INTERVAL": "ten minutes"},
|
||||
wantFrom: func(p LatestPoll) any { return p.Interval },
|
||||
want: 10 * time.Minute,
|
||||
},
|
||||
{
|
||||
name: "a zero duration falls back",
|
||||
env: map[string]string{"LATEST_CHAPTER_POLL_STAGGER": "0s"},
|
||||
wantFrom: func(p LatestPoll) any { return p.Stagger },
|
||||
want: 20 * time.Second,
|
||||
},
|
||||
{
|
||||
name: "a valid batch is honoured",
|
||||
env: map[string]string{"LATEST_CHAPTER_POLL_BATCH": "30"},
|
||||
wantFrom: func(p LatestPoll) any { return p.Batch },
|
||||
want: 30,
|
||||
},
|
||||
{
|
||||
name: "a negative batch falls back",
|
||||
env: map[string]string{"LATEST_CHAPTER_POLL_BATCH": "-5"},
|
||||
wantFrom: func(p LatestPoll) any { return p.Batch },
|
||||
want: 14,
|
||||
},
|
||||
{
|
||||
name: "a non-numeric batch falls back",
|
||||
env: map[string]string{"LATEST_CHAPTER_POLL_BATCH": "lots"},
|
||||
wantFrom: func(p LatestPoll) any { return p.Batch },
|
||||
want: 14,
|
||||
},
|
||||
// newLatestPoller wires the fetcher seams; pace lives in the registry, so
|
||||
// nothing here sizes a cooldown any more.
|
||||
func TestNewLatestPollerWiresFetchers(t *testing.T) {
|
||||
tls := &latest.TLSFetcher{}
|
||||
p := newLatestPoller(nil, LatestPoll{Enabled: true}, tls, nil)
|
||||
if p.Fetch != tls {
|
||||
t.Fatalf("Fetch not wired")
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
for k, v := range tt.env {
|
||||
t.Setenv(k, v)
|
||||
}
|
||||
if got := tt.wantFrom(loadLatestPoll()); got != tt.want {
|
||||
t.Fatalf("got %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
if p.BrowserFetch != nil {
|
||||
t.Fatalf("BrowserFetch = %v, want nil for a browser-less deployment", p.BrowserFetch)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewLatestPollerWiresCooldowns(t *testing.T) {
|
||||
p := newLatestPoller(nil, LatestPoll{
|
||||
Cooldown: time.Hour,
|
||||
BrowserCooldown: 6 * time.Hour,
|
||||
}, nil, nil)
|
||||
if p.Cooldown != time.Hour || p.BrowserCooldown != 6*time.Hour {
|
||||
t.Fatalf("poller cooldowns = %s/%s, want 1h/6h", p.Cooldown, p.BrowserCooldown)
|
||||
if p.CoverFetch != nil {
|
||||
t.Fatalf("CoverFetch = %v, want nil when the browser is absent", p.CoverFetch)
|
||||
}
|
||||
if p.CoverBytesFetch == nil {
|
||||
t.Fatalf("CoverBytesFetch = nil, want the TLS cover fetcher")
|
||||
}
|
||||
if p.Now == nil {
|
||||
t.Fatalf("Now = nil, want the live clock")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user