From e348c4ef6e40e5a3fb61017c809d3d9a43880214 Mon Sep 17 00:00:00 2001 From: kisfenyo Date: Sat, 8 Aug 2026 17:55:18 +0200 Subject: [PATCH] hub: the Gitea client keeps its connections (MaxIdleConnsPerHost was 2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Third and last leg, found the same way as the second — by not accepting that the numbers matched the arithmetic when they did not. After the fan-out and the side-by-side resolve the page was ~11.9s mean where ~3s was predicted. Cause: the client used http.DefaultTransport, whose MaxIdleConnsPerHost is 2. Above that Go opens a connection per request and discards it after, so under a 16-way fan-out almost every call paid a fresh TCP setup AND a fresh authentication. Authentication is the expensive half: unauthenticated /api/v1/version answers in ~0.03s while an authenticated package call takes ~0.24s against the same Gitea instance. Transport sized to the fan-out: MaxIdleConnsPerHost 16, MaxConnsPerHost 16 as a ceiling so a large package list can never stampede Gitea harder than the fan-out needs, IdleConnTimeout 90s. --- hub/CHANGELOG.md | 8 +++++++- hub/internal/gitea/gitea.go | 16 +++++++++++++++- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/hub/CHANGELOG.md b/hub/CHANGELOG.md index cb6dceb..b649e93 100644 --- a/hub/CHANGELOG.md +++ b/hub/CHANGELOG.md @@ -1,4 +1,4 @@ -## v0.100.1 — the Configuration page took 26 seconds, and it was never hashing anything (2026-08-08) +## v0.100.2 — the Configuration page took 26 seconds, and it was never hashing anything (2026-08-08) **Measured, not guessed:** `GET /configuration` → **HTTP 200 in 26.2 s**. @@ -37,6 +37,12 @@ and that search is the slowest single call here: measured in-cluster at **1.1– **The cap stays at 20** and now bounds the fan-out too, not just the rendered list. +**And the client no longer uses `http.DefaultTransport`**, whose `MaxIdleConnsPerHost` is **2**. +Above that it opens a connection per request and discards it, so under a 16-way fan-out nearly every +call paid a fresh TCP setup *and* a fresh authentication — and the authentication is the expensive +half: an unauthenticated `/api/v1/version` answers in **~0.03 s** where an authenticated package call +takes **~0.24 s** against the same Gitea. The transport is now sized to the fan-out (16). + **MEASURED HONESTLY, because the first fix did less than the arithmetic predicted and that mattered.** Concurrency alone took 26.2 s to ~11–18 s, not the ~2 s expected. Chasing the gap is what found the package SEARCH — `/api/v1/packages/admin?type=generic&q=…` — which no amount of per-version fan-out diff --git a/hub/internal/gitea/gitea.go b/hub/internal/gitea/gitea.go index 671f75b..91eaaa7 100644 --- a/hub/internal/gitea/gitea.go +++ b/hub/internal/gitea/gitea.go @@ -26,13 +26,27 @@ type Client struct { } // New builds a client. baseURL/owner/user/token come from the hub's registry config. +// +// ⚠ IT DOES NOT USE http.DefaultTransport, and that is the point (v0.100.2). The Configuration page +// resolves up to 40 package-metadata calls against this one host, now concurrently — and the default +// transport allows **MaxIdleConnsPerHost: 2**. Above that it opens a connection per request and +// throws it away afterwards, so almost every call paid a fresh TCP setup AND a fresh +// authentication, which is the expensive part: an unauthenticated `/api/v1/version` answers in +// ~0.03 s while an authenticated package call takes ~0.24 s against the same Gitea. +// +// Sizing it to the fan-out (16 = 8 in flight × the 2 dropdowns resolved side by side) lets the +// connections be reused for the whole page instead of churned. func New(baseURL, owner, user, token string) *Client { + tr := http.DefaultTransport.(*http.Transport).Clone() + tr.MaxIdleConnsPerHost = 16 + tr.MaxConnsPerHost = 16 // a ceiling too: never stampede Gitea harder than the fan-out needs + tr.IdleConnTimeout = 90 * time.Second return &Client{ baseURL: strings.TrimRight(baseURL, "/"), owner: owner, user: user, token: token, - http: &http.Client{Timeout: 10 * time.Second}, + http: &http.Client{Timeout: 10 * time.Second, Transport: tr}, } }