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:
parent
f100342720
commit
b0ea022dc2
7 changed files with 147 additions and 4 deletions
|
|
@ -41,7 +41,7 @@ type Router struct {
|
|||
negativeTTL time.Duration
|
||||
client *http.Client
|
||||
mu sync.RWMutex
|
||||
upstreamKeys map[string]string // upstream URL → Nix public key string
|
||||
upstreamKeys map[string]string // upstream URL -> Nix public key string
|
||||
group singleflight.Group
|
||||
}
|
||||
|
||||
|
|
@ -73,6 +73,12 @@ func (r *Router) SetUpstreamKey(url, pubKeyStr string) error {
|
|||
// Returns the best upstream for the given store hash.
|
||||
// Checks the route cache first; on miss races the provided candidates.
|
||||
func (r *Router) Resolve(storeHash string, candidates []string) (*Result, error) {
|
||||
// Fast path: negative cache.
|
||||
if neg, err := r.db.IsNegative(storeHash); err == nil && neg {
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
|
||||
// Fast path: route cache hit.
|
||||
entry, err := r.db.GetRoute(storeHash)
|
||||
if err == nil && entry != nil && entry.IsValid() {
|
||||
h := r.prober.GetHealth(entry.UpstreamURL)
|
||||
|
|
@ -88,7 +94,14 @@ func (r *Router) Resolve(storeHash string, candidates []string) (*Result, error)
|
|||
metrics.NarinfoCacheMisses.Inc()
|
||||
|
||||
v, raceErr, _ := r.group.Do(storeHash, func() (interface{}, error) {
|
||||
return r.race(storeHash, candidates)
|
||||
result, err := r.race(storeHash, candidates)
|
||||
if errors.Is(err, ErrNotFound) {
|
||||
_ = r.db.SetNegative(storeHash, r.negativeTTL)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return result, nil
|
||||
})
|
||||
if raceErr != nil {
|
||||
return nil, raceErr
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue