mirror of
https://github.com/git-pkgs/proxy.git
synced 2026-11-09 23:59:16 -05:00
- Keep a gzip copy of each rewrite cache entry when it is smaller, made once
per rewrite and counted against metadata_rewrite_cache_size
- Serve it with Content-Encoding: gzip to clients that accept gzip, with an
ETag of its own ("<sha256>-gzip") and Vary: Accept-Encoding
- Rewrites made outside the cache (cache disabled, cooldown on) stay
uncompressed
- rewriteCache.get and rewrite return rewrittenMetadata
263 lines
8.5 KiB
Go
263 lines
8.5 KiB
Go
package handler
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"sync"
|
|
"sync/atomic"
|
|
"testing"
|
|
|
|
"github.com/git-pkgs/cooldown"
|
|
)
|
|
|
|
// countingRewrite upper-cases its input and counts calls. If entered is set it
|
|
// is closed when the first call starts, and the call then waits on release.
|
|
type countingRewrite struct {
|
|
calls atomic.Int64
|
|
entered chan struct{}
|
|
release chan struct{}
|
|
once sync.Once
|
|
}
|
|
|
|
func (r *countingRewrite) fn(in []byte) ([]byte, error) {
|
|
r.calls.Add(1)
|
|
if r.entered != nil {
|
|
r.once.Do(func() { close(r.entered) })
|
|
<-r.release
|
|
}
|
|
return []byte(strings.ToUpper(string(in))), nil
|
|
}
|
|
|
|
func (r *countingRewrite) keeping(in []byte) ([]byte, []string, error) {
|
|
out, err := r.fn(in)
|
|
return out, nil, err
|
|
}
|
|
|
|
func TestRewriteCache_RepeatServesCachedRewrite(t *testing.T) {
|
|
c := newRewriteCache(1 << 20)
|
|
var r countingRewrite
|
|
key := rewriteCacheKey("npm", "http://proxy", "left-pad", []byte("doc"))
|
|
|
|
for range 3 {
|
|
m, err := c.rewrite(context.Background(), key, []byte("doc"), r.fn)
|
|
if err != nil || string(m.body) != "DOC" {
|
|
t.Fatalf("rewrite = %q, %v", m.body, err)
|
|
}
|
|
}
|
|
if got := r.calls.Load(); got != 1 {
|
|
t.Errorf("rewrites = %d, want 1", got)
|
|
}
|
|
}
|
|
|
|
func TestRewriteCache_NewUpstreamBytesRewriteAgain(t *testing.T) {
|
|
c := newRewriteCache(1 << 20)
|
|
var r countingRewrite
|
|
|
|
for _, doc := range []string{"v1", "v2"} {
|
|
key := rewriteCacheKey("npm", "http://proxy", "left-pad", []byte(doc))
|
|
m, err := c.rewrite(context.Background(), key, []byte(doc), r.fn)
|
|
if err != nil || string(m.body) != strings.ToUpper(doc) {
|
|
t.Fatalf("rewrite(%s) = %q, %v", doc, m.body, err)
|
|
}
|
|
}
|
|
if got := r.calls.Load(); got != 2 {
|
|
t.Errorf("rewrites = %d, want 2", got)
|
|
}
|
|
}
|
|
|
|
// TestRewriteCache_ConcurrentRequestsShareRewrite asserts that requests
|
|
// arriving while a document is being rewritten wait for that rewrite instead
|
|
// of running their own. Callers that arrive after it finishes hit the cache,
|
|
// so the count holds however the goroutines are scheduled.
|
|
func TestRewriteCache_ConcurrentRequestsShareRewrite(t *testing.T) {
|
|
c := newRewriteCache(1 << 20)
|
|
r := countingRewrite{entered: make(chan struct{}), release: make(chan struct{})}
|
|
key := rewriteCacheKey("npm", "http://proxy", "typescript", []byte("doc"))
|
|
const n = 10
|
|
|
|
outs := make(chan string, n)
|
|
run := func() {
|
|
m, err := c.rewrite(context.Background(), key, []byte("doc"), r.fn)
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
outs <- string(m.body)
|
|
}
|
|
go run()
|
|
<-r.entered
|
|
for range n - 1 {
|
|
go run()
|
|
}
|
|
close(r.release)
|
|
|
|
for range n {
|
|
if out := <-outs; out != "DOC" {
|
|
t.Errorf("out = %q", out)
|
|
}
|
|
}
|
|
if got := r.calls.Load(); got != 1 {
|
|
t.Errorf("rewrites = %d, want 1", got)
|
|
}
|
|
}
|
|
|
|
// TestRewriteCache_WaiterLeavingKeepsRewrite asserts that a caller waiting on
|
|
// another's rewrite returns its own context error when its client leaves,
|
|
// while the rewrite completes and is cached for the next request.
|
|
func TestRewriteCache_WaiterLeavingKeepsRewrite(t *testing.T) {
|
|
c := newRewriteCache(1 << 20)
|
|
r := countingRewrite{entered: make(chan struct{}), release: make(chan struct{})}
|
|
key := rewriteCacheKey("npm", "http://proxy", "typescript", []byte("doc"))
|
|
|
|
first := make(chan error, 1)
|
|
go func() {
|
|
_, err := c.rewrite(context.Background(), key, []byte("doc"), r.fn)
|
|
first <- err
|
|
}()
|
|
<-r.entered
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
cancel()
|
|
if _, err := c.rewrite(ctx, key, []byte("doc"), r.fn); !errors.Is(err, context.Canceled) {
|
|
t.Errorf("waiter err = %v, want context.Canceled", err)
|
|
}
|
|
close(r.release)
|
|
if err := <-first; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if m, err := c.rewrite(context.Background(), key, []byte("doc"), r.fn); err != nil || string(m.body) != "DOC" {
|
|
t.Errorf("cached rewrite = %q, %v", m.body, err)
|
|
}
|
|
if got := r.calls.Load(); got != 1 {
|
|
t.Errorf("rewrites = %d, want 1", got)
|
|
}
|
|
}
|
|
|
|
func TestRewriteCache_EvictsLeastRecentlyUsed(t *testing.T) {
|
|
c := newRewriteCache(6) // room for two three-byte outputs
|
|
var r countingRewrite
|
|
keyFor := func(doc string) string { return rewriteCacheKey("npm", "http://proxy", doc, []byte(doc)) }
|
|
|
|
for _, doc := range []string{"aaa", "bbb"} {
|
|
_, _ = c.rewrite(context.Background(), keyFor(doc), []byte(doc), r.fn)
|
|
}
|
|
_, _ = c.rewrite(context.Background(), keyFor("aaa"), []byte("aaa"), r.fn) // aaa is now the most recent
|
|
_, _ = c.rewrite(context.Background(), keyFor("ccc"), []byte("ccc"), r.fn) // evicts bbb
|
|
|
|
before := r.calls.Load()
|
|
_, _ = c.rewrite(context.Background(), keyFor("aaa"), []byte("aaa"), r.fn)
|
|
_, _ = c.rewrite(context.Background(), keyFor("ccc"), []byte("ccc"), r.fn)
|
|
if got := r.calls.Load() - before; got != 0 {
|
|
t.Errorf("aaa and ccc rewritten %d times, want both still cached", got)
|
|
}
|
|
_, _ = c.rewrite(context.Background(), keyFor("bbb"), []byte("bbb"), r.fn)
|
|
if got := r.calls.Load() - before; got != 1 {
|
|
t.Errorf("bbb should have been evicted and rewritten")
|
|
}
|
|
}
|
|
|
|
func TestRewriteCache_OutputLargerThanCacheIsNotStored(t *testing.T) {
|
|
c := newRewriteCache(2)
|
|
var r countingRewrite
|
|
key := rewriteCacheKey("npm", "http://proxy", "big", []byte("big"))
|
|
|
|
_, _ = c.rewrite(context.Background(), key, []byte("big"), r.fn)
|
|
if c.len() != 0 {
|
|
t.Errorf("cached %d entries, want 0", c.len())
|
|
}
|
|
}
|
|
|
|
func TestRewriteCache_ErrorsAreNotCached(t *testing.T) {
|
|
c := newRewriteCache(1 << 20)
|
|
var calls int
|
|
failing := func([]byte) ([]byte, error) { calls++; return nil, errors.New("bad document") }
|
|
key := rewriteCacheKey("npm", "http://proxy", "broken", []byte("doc"))
|
|
|
|
for range 2 {
|
|
if _, err := c.rewrite(context.Background(), key, []byte("doc"), failing); err == nil {
|
|
t.Fatal("expected the rewrite error")
|
|
}
|
|
}
|
|
if calls != 2 {
|
|
t.Errorf("rewrites = %d, want 2", calls)
|
|
}
|
|
}
|
|
|
|
func TestCachedRewrite_DisabledRewritesEveryTime(t *testing.T) {
|
|
proxy, _, _, _ := setupTestProxy(t)
|
|
proxy.SetMetadataRewriteCacheSize(0)
|
|
var r countingRewrite
|
|
|
|
for range 2 {
|
|
_, _ = proxy.cachedRewrite(context.Background(), "npm", "http://proxy", "left-pad", []byte("doc"), "", r.keeping)
|
|
}
|
|
if got := r.calls.Load(); got != 2 {
|
|
t.Errorf("rewrites = %d, want 2", got)
|
|
}
|
|
}
|
|
|
|
// TestCachedRewrite_CooldownBypassesCache asserts that with cooldown on every
|
|
// request is rewritten: cooldown filtering depends on the current time, so a
|
|
// cached rewrite could keep hiding a version after its cooldown ends.
|
|
func TestCachedRewrite_CooldownBypassesCache(t *testing.T) {
|
|
proxy, _, _, _ := setupTestProxy(t)
|
|
proxy.SetMetadataRewriteCacheSize(1 << 20)
|
|
proxy.Cooldown = &cooldown.Config{Default: "3d"}
|
|
var r countingRewrite
|
|
|
|
for range 2 {
|
|
_, _ = proxy.cachedRewrite(context.Background(), "npm", "http://proxy", "left-pad", []byte("doc"), "", r.keeping)
|
|
}
|
|
if got := r.calls.Load(); got != 2 {
|
|
t.Errorf("rewrites = %d, want 2", got)
|
|
}
|
|
}
|
|
|
|
// TestMetadataHandlers_ServeCachedRewrite asserts that repeated requests for
|
|
// the same npm or Composer metadata reuse one rewrite and return the same
|
|
// bytes, with the download URLs pointing at the proxy.
|
|
func TestMetadataHandlers_ServeCachedRewrite(t *testing.T) {
|
|
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
switch r.URL.Path {
|
|
case "/left-pad":
|
|
_, _ = w.Write([]byte(`{"name":"left-pad","versions":{"1.3.0":{"dist":{"tarball":"https://registry.npmjs.org/left-pad/-/left-pad-1.3.0.tgz"}}}}`))
|
|
case "/p2/vendor/pkg.json":
|
|
_, _ = w.Write([]byte(`{"packages":{"vendor/pkg":[{"version":"1.0.0","dist":{"type":"zip","url":"https://example.com/pkg-1.0.0.zip"}}]}}`))
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
}))
|
|
defer upstream.Close()
|
|
|
|
proxy, _, _, _ := setupTestProxy(t)
|
|
proxy.HTTPClient = upstream.Client()
|
|
proxy.SetMetadataRewriteCacheSize(1 << 20)
|
|
|
|
cases := []struct {
|
|
name string
|
|
handler http.Handler
|
|
path string
|
|
}{
|
|
{"npm", NewNPMHandler(proxy, "http://proxy.example", upstream.URL).Routes(), "/left-pad"},
|
|
{"composer", NewComposerHandlerWithUpstreams(proxy, "http://proxy.example", upstream.URL, upstream.URL).Routes(), "/p2/vendor/pkg.json"},
|
|
}
|
|
for i, c := range cases {
|
|
var bodies []string
|
|
for range 2 {
|
|
rec := httptest.NewRecorder()
|
|
c.handler.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, c.path, nil))
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("%s: status %d", c.name, rec.Code)
|
|
}
|
|
bodies = append(bodies, rec.Body.String())
|
|
}
|
|
if bodies[0] != bodies[1] || !strings.Contains(bodies[0], "http://proxy.example") {
|
|
t.Errorf("%s: bodies %q and %q", c.name, bodies[0], bodies[1])
|
|
}
|
|
if got := proxy.rewrites.len(); got != i+1 {
|
|
t.Errorf("%s: cached rewrites = %d, want %d", c.name, got, i+1)
|
|
}
|
|
}
|
|
}
|