cache: add negative cache; router: skip race for cached 404s

Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: Ibeb44b313850395898bb20f2d947b0b76a6a6964
This commit is contained in:
raf 2026-03-06 21:30:24 +03:00
commit b0ea022dc2
Signed by: NotAShelf
GPG key ID: 29D95B64378DB4BF
7 changed files with 147 additions and 4 deletions

View file

@ -177,6 +177,40 @@ func TestResolveWithDownUpstream(t *testing.T) {
}
}
func TestNegativeCaching(t *testing.T) {
var raceCount int32
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
atomic.AddInt32(&raceCount, 1)
w.WriteHeader(http.StatusNotFound)
}))
defer ts.Close()
db, err := cache.Open(":memory:", 1000)
if err != nil {
t.Fatal(err)
}
defer db.Close()
p := prober.New(0.3)
p.InitUpstreams([]config.UpstreamConfig{{URL: ts.URL}})
r := router.New(db, p, time.Hour, 5*time.Second, 10*time.Minute)
_, err = r.Resolve("not-on-any-upstream", []string{ts.URL})
if !errors.Is(err, router.ErrNotFound) {
t.Fatalf("first resolve: expected ErrNotFound, got %v", err)
}
count1 := atomic.LoadInt32(&raceCount)
_, err = r.Resolve("not-on-any-upstream", []string{ts.URL})
if !errors.Is(err, router.ErrNotFound) {
t.Fatalf("second resolve: expected ErrNotFound, got %v", err)
}
count2 := atomic.LoadInt32(&raceCount)
if count2 != count1 {
t.Errorf("second resolve hit upstream %d extra times, want 0 (should be negatively cached)", count2-count1)
}
}
func TestSingleflightDedup(t *testing.T) {
var headCount int32
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {