Watch
1
0
Fork
You've already forked pkg-proxy
1
mirror of https://github.com/git-pkgs/proxy.git synced 2026-11-09 07:39:19 -05:00
pkg-proxy/internal/gitcache/relay_test.go
Simon Chrzanowski 3fdc53d39a Stop relaying for a fetch once it has ended
exec.CommandContext kills only the git parent. Its git-remote-http helper
kept its relay connection and went on downloading for up to
http.lowSpeedTime after a client disconnect, Close or fetchTimeout. Each
read it relayed called cover, which reserved the relayed total again on a
reservation already settled or released, so c.reserved grew for good and
could evict repositories, and the helpers outlived the fetch cap.

Tie each relay route to the fetch's context and cancel it when the fetch
returns, so the upstream request ends and the helper's connection is cut.
Make cover refuse a reservation that has been given back.
2026-11-08 08:02:30 +01:00

117 lines
3.1 KiB
Go

package gitcache
import (
"io"
"log/slog"
"net/http"
"net/http/httptest"
"strings"
"sync"
"sync/atomic"
"testing"
)
func newTestRelay(t *testing.T) *relay {
t.Helper()
rl, err := newRelay(&http.Client{}, slog.New(slog.DiscardHandler))
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = rl.close() })
return rl
}
func TestRelayOnlyForwardsFetchRequests(t *testing.T) {
var forwarded atomic.Int32
upstream := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {
forwarded.Add(1)
}))
defer upstream.Close()
rl := newTestRelay(t)
base, _, unregister := rl.register(t.Context(), upstream.URL+"/repo", 0, nil)
refused := []struct{ method, url string }{
{http.MethodGet, rl.base + "/unknown/info/refs?service=git-upload-pack"},
{http.MethodGet, base + "/info/refs?service=git-receive-pack"},
{http.MethodPost, base + "/git-receive-pack"},
{http.MethodGet, base + "/HEAD"},
{http.MethodGet, base + "/git-upload-pack"},
}
for _, r := range refused {
req, _ := http.NewRequest(r.method, r.url, nil)
resp, err := http.DefaultClient.Do(req)
if err != nil {
t.Fatal(err)
}
_ = resp.Body.Close()
if resp.StatusCode != http.StatusNotFound {
t.Errorf("%s %s: status %d, want 404", r.method, r.url, resp.StatusCode)
}
}
if got := forwarded.Load(); got != 0 {
t.Errorf("forwarded %d refused requests", got)
}
unregister()
resp, err := http.Get(base + "/info/refs?service=git-upload-pack")
if err != nil {
t.Fatal(err)
}
_ = resp.Body.Close()
if resp.StatusCode != http.StatusNotFound || forwarded.Load() != 0 {
t.Errorf("unregistered route: status %d, forwarded %d", resp.StatusCode, forwarded.Load())
}
}
// TestRelayStopsAtLimit checks a fetch over git.max_repo_size is cut off
// rather than downloaded in full and thrown away.
func TestRelayStopsAtLimit(t *testing.T) {
const size, limit = 4 << 20, 64 << 10
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, _ = io.Copy(io.Discard, r.Body)
_, _ = io.Copy(w, io.LimitReader(zeros{}, size))
}))
defer upstream.Close()
var logs strings.Builder
var logMu sync.Mutex
rl, err := newRelay(&http.Client{}, slog.New(slog.NewTextHandler(lockedWriter{&logMu, &logs}, nil)))
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = rl.close() })
base, route, unregister := rl.register(t.Context(), upstream.URL+"/repo", limit, nil)
defer unregister()
resp, err := http.Post(base+"/git-upload-pack", requestType, strings.NewReader(pktFlush))
if err != nil {
t.Fatal(err)
}
n, err := io.Copy(io.Discard, resp.Body)
_ = resp.Body.Close()
if err == nil {
t.Errorf("read %d bytes without an error", n)
}
if !route.exceeded.Load() || route.relayed.Load() > size/2 {
logMu.Lock()
defer logMu.Unlock()
t.Errorf("exceeded %v after relaying %d of %d bytes; relay log:\n%s", route.exceeded.Load(), route.relayed.Load(), size, logs.String())
}
}
type lockedWriter struct {
mu *sync.Mutex
w io.Writer
}
func (l lockedWriter) Write(p []byte) (int, error) {
l.mu.Lock()
defer l.mu.Unlock()
return l.w.Write(p)
}
type zeros struct{}
func (zeros) Read(p []byte) (int, error) {
clear(p)
return len(p), nil
}