mirror of
https://github.com/git-pkgs/proxy.git
synced 2026-11-09 07:39:19 -05:00
A cached rewrite made with cooldown on stays right until a version it held back leaves cooldown, so it now expires then instead of after a fixed time. Handlers ask cooldown through Proxy.cooldown, which reports each withheld version to the rewrites running at that moment. A rewrite that held nothing back is kept for the shortest cooldown, at most a day.
178 lines
6.2 KiB
Go
178 lines
6.2 KiB
Go
package handler
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/git-pkgs/cooldown"
|
|
)
|
|
|
|
// TestCachedRewrite_ExpiresWhenWithheldVersionIsAvailable checks that a
|
|
// rewrite which held a version back is cached until that version leaves
|
|
// cooldown, and no longer.
|
|
func TestCachedRewrite_ExpiresWhenWithheldVersionIsAvailable(t *testing.T) {
|
|
proxy, _, _, _ := setupTestProxy(t)
|
|
clock := &testClock{t: time.Now()}
|
|
proxy.now = clock.now
|
|
proxy.SetMetadataRewriteCacheSize(1 << 20)
|
|
proxy.Cooldown = &cooldown.Config{Default: "3d"}
|
|
purl := canonicalPackagePURL("npm", "left-pad")
|
|
// One version is 3d-5h old: it leaves cooldown in five hours.
|
|
published := clock.now().Add(-72*time.Hour + 5*time.Hour)
|
|
|
|
var calls int
|
|
var rewrite keepingRewrite = func(b []byte) ([]byte, []string, error) {
|
|
calls++
|
|
if proxy.cooldown().IsAllowed("npm", purl, published) {
|
|
return b, []string{"1.0.0", "1.1.0"}, nil
|
|
}
|
|
return b, []string{"1.0.0"}, nil
|
|
}
|
|
get := func() {
|
|
if _, err := proxy.cachedRewrite(context.Background(), "npm", "http://proxy", "left-pad", []byte("doc"), "", rewrite); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
get()
|
|
clock.advance(5*time.Hour - time.Minute)
|
|
get()
|
|
if calls != 1 {
|
|
t.Errorf("rewrites before the withheld version is available = %d, want 1", calls)
|
|
}
|
|
clock.advance(time.Minute)
|
|
get()
|
|
if calls != 2 {
|
|
t.Errorf("rewrites once the withheld version is available = %d, want 2", calls)
|
|
}
|
|
}
|
|
|
|
// TestCachedRewrite_NothingWithheldCachedForCap checks that a rewrite which
|
|
// held nothing back is cached for cooldownRewriteTTL.
|
|
func TestCachedRewrite_NothingWithheldCachedForCap(t *testing.T) {
|
|
proxy, _, _, _ := setupTestProxy(t)
|
|
clock := &testClock{t: time.Date(2026, 10, 10, 12, 0, 0, 0, time.UTC)}
|
|
proxy.now = clock.now
|
|
proxy.SetMetadataRewriteCacheSize(1 << 20)
|
|
proxy.Cooldown = &cooldown.Config{Default: "2h"}
|
|
proxy.ShortestCooldown = 2 * time.Hour
|
|
var r countingRewrite
|
|
|
|
get := func() {
|
|
_, _ = proxy.cachedRewrite(context.Background(), "npm", "http://proxy", "left-pad", []byte("doc"), "", r.keeping)
|
|
}
|
|
get()
|
|
clock.advance(2*time.Hour - time.Second)
|
|
get()
|
|
if got := r.calls.Load(); got != 1 {
|
|
t.Errorf("rewrites within the cap = %d, want 1", got)
|
|
}
|
|
clock.advance(time.Second)
|
|
get()
|
|
if got := r.calls.Load(); got != 2 {
|
|
t.Errorf("rewrites after the cap = %d, want 2", got)
|
|
}
|
|
}
|
|
|
|
// TestCooldownExpiry checks how one rewrite's recorded decisions set its
|
|
// expiry, including decisions made for other rewrites running at the same
|
|
// time, which may only make it expire earlier.
|
|
func TestCooldownExpiry(t *testing.T) {
|
|
p := &Proxy{Cooldown: &cooldown.Config{Default: "3d"}, ShortestCooldown: 72 * time.Hour}
|
|
// IsAllowed compares with the wall clock, so the times are relative to it.
|
|
start := time.Now()
|
|
limit := start.Add(24 * time.Hour)
|
|
purl := canonicalPackagePURL("npm", "left-pad")
|
|
|
|
rec := p.watchCooldown()
|
|
if got := p.cooldownExpiry(rec, start); !got.Equal(limit) {
|
|
t.Errorf("nothing withheld: expiry = %v, want the cap %v", got, limit)
|
|
}
|
|
|
|
other := p.watchCooldown()
|
|
p.cooldown().IsAllowed("npm", purl, start.Add(-60*time.Hour)) // available in 12h
|
|
p.unwatchCooldown(other)
|
|
p.cooldown().IsAllowed("npm", purl, start.Add(-66*time.Hour)) // available in 6h, after other ended
|
|
if got, want := p.cooldownExpiry(rec, start), start.Add(6*time.Hour); !got.Equal(want) {
|
|
t.Errorf("rec expiry = %v, want the earliest availability %v", got, want)
|
|
}
|
|
if got, want := p.cooldownExpiry(other, start), start.Add(12*time.Hour); !got.Equal(want) {
|
|
t.Errorf("other expiry = %v, want %v: decisions after it ended don't count", got, want)
|
|
}
|
|
|
|
p.cooldown().Evaluate("npm", purl, start.Add(-71*time.Hour), start) // available in 1h
|
|
if got, want := p.cooldownExpiry(rec, start), start.Add(time.Hour); !got.Equal(want) {
|
|
t.Errorf("after Evaluate: expiry = %v, want %v", got, want)
|
|
}
|
|
p.unwatchCooldown(rec)
|
|
if len(p.cooldownWatch.active) != 0 {
|
|
t.Errorf("%d recorders left after unwatch", len(p.cooldownWatch.active))
|
|
}
|
|
}
|
|
|
|
// TestHandlersAskCooldownThroughRecorder checks that handlers make cooldown
|
|
// decisions through Proxy.cooldown. A decision made on Proxy.Cooldown
|
|
// directly would not reach the rewrites running, so a cached rewrite could
|
|
// keep hiding a version past its cooldown.
|
|
func TestHandlersAskCooldownThroughRecorder(t *testing.T) {
|
|
direct := regexp.MustCompile(`\.Cooldown\.(IsAllowed|Evaluate)\(|Cooldown:\s+\w+(\.\w+)*\.Cooldown\b`)
|
|
files, err := filepath.Glob("*.go")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, f := range files {
|
|
if strings.HasSuffix(f, "_test.go") || f == "cooldown_expiry.go" {
|
|
continue
|
|
}
|
|
src, err := os.ReadFile(f)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for i, line := range strings.Split(string(src), "\n") {
|
|
if direct.MatchString(line) {
|
|
t.Errorf("%s:%d asks Proxy.Cooldown directly; use Proxy.cooldown(): %s", f, i+1, strings.TrimSpace(line))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestHandlerRewriteExpiresWithWithheldVersion checks, through the handlers,
|
|
// that a cached rewrite expires when the version cooldown held back from it
|
|
// becomes available.
|
|
func TestHandlerRewriteExpiresWithWithheldVersion(t *testing.T) {
|
|
for _, c := range validatorCases {
|
|
t.Run(c.ecosystem, func(t *testing.T) {
|
|
u := cooldownUpstream(t) // the newest version was published 2h ago
|
|
proxy, _, clock := validatorProxyWithDB(t, u, time.Hour)
|
|
// Cooldown compares with the wall clock, so the proxy's clock
|
|
// follows it here.
|
|
clock.t = time.Now()
|
|
proxy.Cooldown = &cooldown.Config{Default: "3h"}
|
|
proxy.ShortestCooldown = 3 * time.Hour
|
|
handler := c.handler(proxy, u)
|
|
|
|
if rec := serve(handler, c.path, nil); rec.Code != http.StatusOK {
|
|
t.Fatalf("status %d", rec.Code)
|
|
}
|
|
proxy.rewrites.mu.Lock()
|
|
defer proxy.rewrites.mu.Unlock()
|
|
if len(proxy.rewrites.entries) != 1 {
|
|
t.Fatalf("%d cached rewrites, want 1", len(proxy.rewrites.entries))
|
|
}
|
|
for _, el := range proxy.rewrites.entries {
|
|
expires := el.Value.(*rewriteEntry).expires
|
|
// Published 2h ago with a 3h cooldown: available in an hour,
|
|
// to the second the publish time was given in.
|
|
if want := time.Now().Add(time.Hour); expires.Before(want.Add(-2*time.Second)) || expires.After(want.Add(time.Second)) {
|
|
t.Errorf("rewrite expires %v, want about %v when the withheld version is available", expires, want)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|