fix(metrics): prevent GitHub API calls from hanging forever - #274
Merged
Conversation
whywaita
force-pushed
the
fix/metrics-github-api-hang
branch
from
July 14, 2026 08:25
50a9903 to
25d5bfb
Compare
The /metrics collector performs live GitHub API calls (ListInstallations, ListRepositoryWorkflowRuns, installation-token refresh) synchronously and waits on them with wg.Wait(). The GitHub *http.Client had no Timeout, so a stalled connection to the GitHub API made GET /metrics block indefinitely. Because the calls never returned, responseCache was never populated, so every scrape (2 Prometheus scrapers + the starter loop) re-issued the same live API calls, sustaining the stall. - gh: give every GitHub *http.Client a 10s Timeout so no single request can hang forever (also protects the starter loop and runner deletion). - gh: fix listInstallations / listAppsInstalledRepo calling the underlying _list* function twice on a cache miss (the first result was cached then discarded), doubling GitHub API load. - cmd/server: import net/http/pprof so the already-running localhost:6060 server (SetBlockProfileRate / SetMutexProfileFraction are set) actually serves /debug/pprof for future investigations. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
whywaita
force-pushed
the
fix/metrics-github-api-hang
branch
from
July 14, 2026 08:34
25d5bfb to
6f6ddb6
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes
GET /metricshanging and never returning a response. On a production pod,/metricsreturned nothing after 60s (curl localhost:80/metrics→http=000 time_total=60s size=0).Root cause
The
/metricsPrometheus collector performs live GitHub API calls (Apps.ListInstallations,Actions.ListRepositoryWorkflowRuns, installation-token refresh) synchronously and waits on them withwg.Wait(). The GitHub*http.Clienthas noTimeout(andhttp.DefaultTransporthas noResponseHeaderTimeout), so when a connection to the GitHub API stalls, the request blocks indefinitely andGET /metricshangs.Worse,
responseCacheis only populated on a successful call, so while stalled the two Prometheus scrapers and the starter loop keep re-issuing the same live API calls, which sustains the stall (a self-reinforcing loop).Evidence from the running pod (last 15 min)
GET /metricsname: githubdatastore/memorycontext canceledThe
datastore/memoryscrapers had 0 failures,api.github.com/zenfrom the pod responded in ~190ms, and the pod had only 15 TCP sockets / 12 threads (no exhaustion). The only thing stuck was the github scraper waiting on the GitHub App API.Changes
pkg/gh/github.go: give every GitHub*http.ClientaTimeout(10s) via a newnewGitHubHTTPClienthelper, so no single request can hang forever. This also protects the starter loop and runner deletion (which were hanging the same way, e.g.unexpected EOF).pkg/gh/installation.go: fixlistInstallations/listAppsInstalledRepocalling the underlying_list*function twice on a cache miss (the first result was cached and then discarded), which doubled GitHub API load.Note: the
/metricshandler intentionally keepsr.Context()and does not add its own deadline — the scrape timeout is the scraper's (Prometheus) responsibility.cmd/server/cmd.go: importnet/http/pprof. The server already listens onlocalhost:6060and setsSetBlockProfileRate/SetMutexProfileFraction, but the pprof handlers were never registered, so every/debug/pprof/*endpoint returned 404. This makes pprof usable for future investigations (it was unavailable during this one).Follow-up (separate PR)
This PR stops the hang via timeouts, which is a mitigation. The longer-term fix is to stop doing synchronous live API calls inside the collector and instead refresh in the background and serve cached values (a Prometheus collector should not block on external I/O). That is a larger behavioral change, so it is kept separate.
Testing
go build ./...andgo vet ./pkg/gh/... ./pkg/web/... ./cmd/server/...pass.go test ./pkg/gh/...passes (pkg/webrequires Docker via dockertest and was not run in this environment).🤖 Generated with Claude Code