Skip to content

Commit f94d404

Browse files
authored
fix: cloning large repo's failed with fatal: early EOF (#33)
This only manifested with `--bare --mirror` because the clones are so large, due to including all refs. Also switched to using `httputil.ReverseProxy`, which is more robust. ``` ~/dev/cachew $ rm -rf git-source ; time git clone --bare --mirror http://127.0.0.1:8080/git/github.com/git/git.git git-source Cloning into bare repository 'git-source'... remote: Enumerating objects: 807474, done. remote: Counting objects: 100% (8533/8533), done. remote: Compressing objects: 100% (7947/7947), done. error: RPC failed; curl 18 transfer closed with outstanding read data remaining error: 297 bytes of body are still expected fetch-pack: unexpected disconnect while reading sideband packet fatal: early EOF fatal: fetch-pack: invalid index-pack output git clone --bare --mirror http://127.0.0.1:8080/git/github.com/git/git.git 5.26s user 1.17s system 21% cpu 30.509 total ``` Once the cache is populated, cloning git takes half the time: ``` ~/dev/cachew $ rm -rf git-source ; time git clone http://127.0.0.1:8080/git/github.com/git/git.git git-source Cloning into 'git-source'... remote: Enumerating objects: 403536, done. remote: Counting objects: 100% (756/756), done. remote: Compressing objects: 100% (363/363), done. remote: Total 403536 (delta 532), reused 498 (delta 393), pack-reused 402780 (from 4) Receiving objects: 100% (403536/403536), 282.29 MiB | 19.06 MiB/s, done. Resolving deltas: 100% (305003/305003), done. git clone http://127.0.0.1:8080/git/github.com/git/git.git git-source 18.61s user 2.65s system 96% cpu 21.940 total ~/dev/cachew $ rm -rf git-source ; time git clone http://127.0.0.1:8080/git/github.com/git/git.git git-source Cloning into 'git-source'... remote: Enumerating objects: 403536, done. remote: Counting objects: 100% (403536/403536), done. remote: Compressing objects: 100% (95986/95986), done. remote: Total 403536 (delta 305001), reused 403485 (delta 304954), pack-reused 0 (from 0) Receiving objects: 100% (403536/403536), 282.15 MiB | 117.21 MiB/s, done. Resolving deltas: 100% (305001/305001), done. git clone http://127.0.0.1:8080/git/github.com/git/git.git git-source 18.11s user 2.34s system 206% cpu 9.916 total ```
1 parent 2a551be commit f94d404

3 files changed

Lines changed: 34 additions & 46 deletions

File tree

internal/strategy/git/backend.go

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,13 @@ func (s *Strategy) executeClone(ctx context.Context, c *clone) error {
6969
}
7070

7171
// #nosec G204 - c.upstreamURL and c.path are controlled by us
72-
cmd := exec.CommandContext(ctx, "git", "clone", "--bare", "--mirror", c.upstreamURL, c.path)
72+
// Configure git for large repositories to avoid network buffer issues
73+
cmd := exec.CommandContext(ctx, "git", "clone",
74+
"--bare", "--mirror",
75+
"-c", "http.postBuffer=524288000", // 500MB buffer
76+
"-c", "http.lowSpeedLimit=1000", // 1KB/s minimum speed
77+
"-c", "http.lowSpeedTime=600", // 10 minute timeout at low speed
78+
c.upstreamURL, c.path)
7379
output, err := cmd.CombinedOutput()
7480
if err != nil {
7581
logger.ErrorContext(ctx, "git clone failed",
@@ -87,7 +93,12 @@ func (s *Strategy) executeFetch(ctx context.Context, c *clone) error {
8793
logger := logging.FromContext(ctx)
8894

8995
// #nosec G204 - c.path is controlled by us
90-
cmd := exec.CommandContext(ctx, "git", "-C", c.path, "fetch", "--all")
96+
// Configure git for large repositories to avoid network buffer issues
97+
cmd := exec.CommandContext(ctx, "git", "-C", c.path,
98+
"-c", "http.postBuffer=524288000", // 500MB buffer
99+
"-c", "http.lowSpeedLimit=1000", // 1KB/s minimum speed
100+
"-c", "http.lowSpeedTime=600", // 10 minute timeout at low speed
101+
"fetch", "--all")
91102
output, err := cmd.CombinedOutput()
92103
if err != nil {
93104
logger.ErrorContext(ctx, "git fetch failed",

internal/strategy/git/git.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"context"
66
"log/slog"
77
"net/http"
8+
"net/http/httputil"
89
"net/url"
910
"os"
1011
"path/filepath"
@@ -54,6 +55,7 @@ type Strategy struct {
5455
clones map[string]*clone
5556
clonesMu sync.RWMutex
5657
httpClient *http.Client
58+
proxy *httputil.ReverseProxy
5759
}
5860

5961
// New creates a new Git caching strategy.
@@ -79,6 +81,20 @@ func New(ctx context.Context, config Config, cache cache.Cache, mux strategy.Mux
7981
httpClient: http.DefaultClient,
8082
}
8183

84+
s.proxy = &httputil.ReverseProxy{
85+
Director: func(req *http.Request) {
86+
req.URL.Scheme = "https"
87+
req.URL.Host = req.PathValue("host")
88+
req.URL.Path = "/" + req.PathValue("path")
89+
req.Host = req.URL.Host
90+
},
91+
Transport: s.httpClient.Transport,
92+
ErrorHandler: func(w http.ResponseWriter, r *http.Request, err error) {
93+
logging.FromContext(r.Context()).ErrorContext(r.Context(), "Upstream request failed", slog.String("error", err.Error()))
94+
w.WriteHeader(http.StatusBadGateway)
95+
},
96+
}
97+
8298
mux.Handle("GET /git/{host}/{path...}", http.HandlerFunc(s.handleRequest))
8399
mux.Handle("POST /git/{host}/{path...}", http.HandlerFunc(s.handleRequest))
84100

internal/strategy/git/proxy.go

Lines changed: 5 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,59 +1,20 @@
11
package git
22

33
import (
4-
"io"
54
"log/slog"
65
"net/http"
76

8-
"github.com/block/cachew/internal/httputil"
97
"github.com/block/cachew/internal/logging"
108
)
119

1210
// forwardToUpstream forwards a request to the upstream Git server.
1311
func (s *Strategy) forwardToUpstream(w http.ResponseWriter, r *http.Request, host, pathValue string) {
14-
ctx := r.Context()
15-
logger := logging.FromContext(ctx)
12+
logger := logging.FromContext(r.Context())
1613

17-
upstreamURL := "https://" + host + "/" + pathValue
18-
if r.URL.RawQuery != "" {
19-
upstreamURL += "?" + r.URL.RawQuery
20-
}
21-
22-
logger.DebugContext(ctx, "Forwarding to upstream",
14+
logger.DebugContext(r.Context(), "Forwarding to upstream",
2315
slog.String("method", r.Method),
24-
slog.String("upstream_url", upstreamURL))
25-
26-
upstreamReq, err := http.NewRequestWithContext(ctx, r.Method, upstreamURL, r.Body)
27-
if err != nil {
28-
httputil.ErrorResponse(w, r, http.StatusInternalServerError, "failed to create upstream request")
29-
return
30-
}
31-
32-
// Copy relevant headers
33-
for _, header := range []string{"Content-Type", "Content-Length", "Content-Encoding", "Accept", "Accept-Encoding", "Git-Protocol"} {
34-
if v := r.Header.Get(header); v != "" {
35-
upstreamReq.Header.Set(header, v)
36-
}
37-
}
38-
39-
resp, err := s.httpClient.Do(upstreamReq)
40-
if err != nil {
41-
logger.ErrorContext(ctx, "Upstream request failed", slog.String("error", err.Error()))
42-
httputil.ErrorResponse(w, r, http.StatusBadGateway, "upstream request failed")
43-
return
44-
}
45-
defer resp.Body.Close()
46-
47-
// Copy response headers
48-
for key, values := range resp.Header {
49-
for _, value := range values {
50-
w.Header().Add(key, value)
51-
}
52-
}
53-
54-
w.WriteHeader(resp.StatusCode)
16+
slog.String("host", host),
17+
slog.String("path", pathValue))
5518

56-
if _, err := io.Copy(w, resp.Body); err != nil {
57-
logger.ErrorContext(ctx, "Failed to stream upstream response", slog.String("error", err.Error()))
58-
}
19+
s.proxy.ServeHTTP(w, r)
5920
}

0 commit comments

Comments
 (0)