Skip to content

Commit 78b29e5

Browse files
joyheroesdindin
andauthored
fix: cache PyPI metadata for filtered versions (#258)
Co-authored-by: dindin <dindin@DMBA.local>
1 parent 3e53469 commit 78b29e5

2 files changed

Lines changed: 60 additions & 12 deletions

File tree

internal/handler/pypi.go

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -110,24 +110,14 @@ func (h *PyPIHandler) handleSimplePackage(w http.ResponseWriter, r *http.Request
110110
// that should be filtered out due to cooldown.
111111
func (h *PyPIHandler) fetchFilteredVersions(r *http.Request, name string) map[string]bool {
112112
jsonURL := fmt.Sprintf("%s/pypi/%s/json", h.upstreamURL, name)
113-
req, err := http.NewRequestWithContext(r.Context(), http.MethodGet, jsonURL, nil)
114-
if err != nil {
115-
return nil
116-
}
117-
req.Header.Set("Accept", "application/json")
118113

119-
resp, err := h.proxy.HTTPClient.Do(req)
114+
body, _, err := h.proxy.FetchOrCacheMetadata(r.Context(), "pypi", name+"/json", jsonURL)
120115
if err != nil {
121116
return nil
122117
}
123-
defer func() { _ = resp.Body.Close() }()
124-
125-
if resp.StatusCode != http.StatusOK {
126-
return nil
127-
}
128118

129119
var metadata map[string]any
130-
if err := json.NewDecoder(resp.Body).Decode(&metadata); err != nil {
120+
if err := json.Unmarshal(body, &metadata); err != nil {
131121
return nil
132122
}
133123

internal/handler/pypi_test.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"net/http"
88
"net/http/httptest"
99
"strings"
10+
"sync/atomic"
1011
"testing"
1112
"time"
1213

@@ -292,3 +293,60 @@ func TestPyPIDownloadCooldown(t *testing.T) {
292293
})
293294
}
294295
}
296+
297+
// TestPyPIDownloadCooldownMetadataCache ensures that repeated downloads that
298+
// trigger cooldown filtering reuse the cached PyPI JSON metadata instead of
299+
// fetching it from upstream once per download.
300+
func TestPyPIDownloadCooldownMetadataCache(t *testing.T) {
301+
now := time.Now()
302+
releases := `{"releases": {
303+
"1.0.0": [{"upload_time_iso_8601": "` + now.Add(-30*24*time.Hour).Format(time.RFC3339) + `"}],
304+
"2.0.0": [{"upload_time_iso_8601": "` + now.Add(-1*time.Hour).Format(time.RFC3339) + `"}]
305+
}}`
306+
307+
var metadataRequests atomic.Int64
308+
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
309+
if r.URL.Path == "/pypi/newpkg/json" {
310+
metadataRequests.Add(1)
311+
w.Header().Set("Content-Type", "application/json")
312+
_, _ = io.WriteString(w, releases)
313+
return
314+
}
315+
w.Header().Set("Content-Type", "application/octet-stream")
316+
_, _ = io.WriteString(w, "package data")
317+
}))
318+
defer upstream.Close()
319+
320+
proxy, _, _, fetcher := setupTestProxy(t)
321+
proxy.HTTPClient = upstream.Client()
322+
proxy.CacheMetadata = true
323+
proxy.MetadataTTL = time.Hour
324+
proxy.Cooldown = &cooldown.Config{Default: "7d"}
325+
fetcher.artifact = &fetch.Artifact{
326+
Body: io.NopCloser(strings.NewReader("package data")),
327+
ContentType: "application/octet-stream",
328+
}
329+
330+
h := &PyPIHandler{
331+
proxy: proxy,
332+
upstreamURL: upstream.URL,
333+
proxyURL: "http://localhost",
334+
}
335+
srv := httptest.NewServer(h.Routes())
336+
defer srv.Close()
337+
338+
// Two downloads of the same package: one outside the cooldown window
339+
// (served) and one inside (withheld). Both go through the download path
340+
// that resolves filtered versions.
341+
for _, filename := range []string{"newpkg-1.0.0.tar.gz", "newpkg-2.0.0.tar.gz"} {
342+
resp, err := http.Get(srv.URL + "/packages/packages/ab/cd/ef0123456789/" + filename)
343+
if err != nil {
344+
t.Fatalf("request failed: %v", err)
345+
}
346+
_ = resp.Body.Close()
347+
}
348+
349+
if got := metadataRequests.Load(); got != 1 {
350+
t.Errorf("upstream metadata JSON requests = %d, want 1 (repeated downloads should reuse the cached metadata)", got)
351+
}
352+
}

0 commit comments

Comments
 (0)