Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 42 additions & 6 deletions internal/handler/gradle.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ import (
"regexp"
"strconv"
"strings"
"time"

"github.com/git-pkgs/proxy/internal/metrics"
"github.com/git-pkgs/proxy/internal/storage"
)

Expand All @@ -34,33 +36,39 @@ func NewGradleBuildCacheHandler(proxy *Proxy) *GradleBuildCacheHandler {
// Routes returns the HTTP handler for Gradle HttpBuildCache requests.
func (h *GradleBuildCacheHandler) Routes() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
rw := &statusCapturingResponseWriter{ResponseWriter: w, status: http.StatusOK}
defer func() {
metrics.RecordRequest("gradle", rw.status, time.Since(start))
}()

switch r.Method {
case http.MethodGet, http.MethodHead, http.MethodPut:
default:
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
http.Error(rw, "method not allowed", http.StatusMethodNotAllowed)
return
}

key, statusCode := h.parseCacheKey(r.URL.Path)
if statusCode != http.StatusOK {
if statusCode == http.StatusNotFound {
http.NotFound(w, r)
http.NotFound(rw, r)
return
}
http.Error(w, "invalid cache key", statusCode)
http.Error(rw, "invalid cache key", statusCode)
return
}

if r.Method == http.MethodPut {
if h.proxy.GradleReadOnly {
http.Error(w, "gradle build cache is read-only", http.StatusMethodNotAllowed)
http.Error(rw, "gradle build cache is read-only", http.StatusMethodNotAllowed)
return
}
h.handlePut(w, r, key)
h.handlePut(rw, r, key)
return
}

h.handleGetOrHead(w, r, key)
h.handleGetOrHead(rw, r, key)
})
}

Expand Down Expand Up @@ -94,36 +102,51 @@ func (h *GradleBuildCacheHandler) handleGetOrHead(w http.ResponseWriter, r *http
w.Header().Set("Content-Type", gradleBuildCacheContentType)

if r.Method == http.MethodHead {
existsStart := time.Now()
exists, err := h.proxy.Storage.Exists(r.Context(), storagePath)
metrics.RecordStorageOperation("read", time.Since(existsStart))
if err != nil {
metrics.RecordStorageError("read")
h.proxy.Logger.Error("failed to check gradle build cache entry", "key", key, "error", err)
http.Error(w, "failed to read cache entry", http.StatusInternalServerError)
return
}
if !exists {
metrics.RecordCacheMiss("gradle")
http.NotFound(w, r)
return
}
metrics.RecordCacheHit("gradle")

sizeStart := time.Now()
if size, err := h.proxy.Storage.Size(r.Context(), storagePath); err == nil && size >= 0 {
metrics.RecordStorageOperation("read", time.Since(sizeStart))
w.Header().Set("Content-Length", strconv.FormatInt(size, 10))
} else if err != nil {
metrics.RecordStorageOperation("read", time.Since(sizeStart))
metrics.RecordStorageError("read")
}

w.WriteHeader(http.StatusOK)
return
}

readStart := time.Now()
reader, err := h.proxy.Storage.Open(r.Context(), storagePath)
metrics.RecordStorageOperation("read", time.Since(readStart))
if err != nil {
if errors.Is(err, storage.ErrNotFound) {
metrics.RecordCacheMiss("gradle")
http.NotFound(w, r)
return
}
metrics.RecordStorageError("read")
h.proxy.Logger.Error("failed to open gradle build cache entry", "key", key, "error", err)
http.Error(w, "failed to read cache entry", http.StatusInternalServerError)
return
}
defer func() { _ = reader.Close() }()
metrics.RecordCacheHit("gradle")

w.WriteHeader(http.StatusOK)
_, _ = io.Copy(w, reader)
Expand All @@ -138,14 +161,17 @@ func (h *GradleBuildCacheHandler) handlePut(w http.ResponseWriter, r *http.Reque

r.Body = http.MaxBytesReader(w, r.Body, maxUploadSize)

storeStart := time.Now()
_, hash, err := h.proxy.Storage.Store(r.Context(), storagePath, r.Body)
metrics.RecordStorageOperation("write", time.Since(storeStart))
if err != nil {
var maxBytesErr *http.MaxBytesError
if errors.As(err, &maxBytesErr) {
http.Error(w, "cache entry too large", http.StatusRequestEntityTooLarge)
return
}

metrics.RecordStorageError("write")
h.proxy.Logger.Error("failed to store gradle build cache entry", "key", key, "error", err)
http.Error(w, "failed to write cache entry", http.StatusInternalServerError)
return
Expand All @@ -156,3 +182,13 @@ func (h *GradleBuildCacheHandler) handlePut(w http.ResponseWriter, r *http.Reque

w.WriteHeader(http.StatusCreated)
}

type statusCapturingResponseWriter struct {
http.ResponseWriter
status int
}

func (rw *statusCapturingResponseWriter) WriteHeader(code int) {
rw.status = code
rw.ResponseWriter.WriteHeader(code)
}
72 changes: 72 additions & 0 deletions internal/handler/gradle_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,12 @@ import (
"io"
"net/http"
"net/http/httptest"
"strconv"
"strings"
"testing"

"github.com/git-pkgs/proxy/internal/metrics"
"github.com/prometheus/client_golang/prometheus/testutil"
)

func TestGradleBuildCacheHandler_PutGetHead(t *testing.T) {
Expand Down Expand Up @@ -227,3 +231,71 @@ func TestGradleBuildCacheHandler_PutTooLarge(t *testing.T) {
t.Fatalf("PUT status = %d, want %d", resp.StatusCode, http.StatusRequestEntityTooLarge)
}
}

func TestGradleBuildCacheHandler_RecordsMetrics(t *testing.T) {
proxy, _, _, _ := setupTestProxy(t)
h := NewGradleBuildCacheHandler(proxy)
srv := httptest.NewServer(h.Routes())
defer srv.Close()

okBefore := testutil.ToFloat64(metrics.RequestsTotal.WithLabelValues("gradle", strconv.Itoa(http.StatusOK)))
createdBefore := testutil.ToFloat64(metrics.RequestsTotal.WithLabelValues("gradle", strconv.Itoa(http.StatusCreated)))
notFoundBefore := testutil.ToFloat64(metrics.RequestsTotal.WithLabelValues("gradle", strconv.Itoa(http.StatusNotFound)))
hitsBefore := testutil.ToFloat64(metrics.CacheHits.WithLabelValues("gradle"))
missesBefore := testutil.ToFloat64(metrics.CacheMisses.WithLabelValues("gradle"))

key := "metrics-key"
putReq, err := http.NewRequest(http.MethodPut, srv.URL+"/"+key, strings.NewReader("payload"))
if err != nil {
t.Fatalf("failed to create PUT request: %v", err)
}
putResp, err := http.DefaultClient.Do(putReq)
if err != nil {
t.Fatalf("PUT request failed: %v", err)
}
_ = putResp.Body.Close()

getResp, err := http.Get(srv.URL + "/" + key)
if err != nil {
t.Fatalf("GET request failed: %v", err)
}
_ = getResp.Body.Close()

headReq, err := http.NewRequest(http.MethodHead, srv.URL+"/"+key, nil)
if err != nil {
t.Fatalf("failed to create HEAD request: %v", err)
}
headResp, err := http.DefaultClient.Do(headReq)
if err != nil {
t.Fatalf("HEAD request failed: %v", err)
}
_ = headResp.Body.Close()

missResp, err := http.Get(srv.URL + "/missing-key")
if err != nil {
t.Fatalf("GET miss request failed: %v", err)
}
_ = missResp.Body.Close()

okAfter := testutil.ToFloat64(metrics.RequestsTotal.WithLabelValues("gradle", strconv.Itoa(http.StatusOK)))
createdAfter := testutil.ToFloat64(metrics.RequestsTotal.WithLabelValues("gradle", strconv.Itoa(http.StatusCreated)))
notFoundAfter := testutil.ToFloat64(metrics.RequestsTotal.WithLabelValues("gradle", strconv.Itoa(http.StatusNotFound)))
hitsAfter := testutil.ToFloat64(metrics.CacheHits.WithLabelValues("gradle"))
missesAfter := testutil.ToFloat64(metrics.CacheMisses.WithLabelValues("gradle"))

if diff := createdAfter - createdBefore; diff != 1 {
t.Fatalf("created requests delta = %.0f, want 1", diff)
}
if diff := okAfter - okBefore; diff != 2 {
t.Fatalf("ok requests delta = %.0f, want 2", diff)
}
if diff := notFoundAfter - notFoundBefore; diff != 1 {
t.Fatalf("not found requests delta = %.0f, want 1", diff)
}
if diff := hitsAfter - hitsBefore; diff != 2 {
t.Fatalf("cache hits delta = %.0f, want 2", diff)
}
if diff := missesAfter - missesBefore; diff != 1 {
t.Fatalf("cache misses delta = %.0f, want 1", diff)
}
}
Loading