Skip to content

Commit 83f41d3

Browse files
committed
fix(git): stage parallel snapshot temp file on target filesystem
Parallel restore wrote the whole compressed snapshot to the default temp dir, which can fail with ENOSPC on hosts where /tmp is a small or separate tmpfs even when the target directory has room. Stage it under the target directory's parent (same filesystem, created if missing) instead. Add an end-to-end parallel restore test covering this.
1 parent c811d61 commit 83f41d3

5 files changed

Lines changed: 60 additions & 3 deletions

File tree

Procfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
cachewd cachew.hcl **/*.go !**/*_test.go !state/**/* debounce=2s ready=http:8080/_readiness=200: CACHEW_URL=http://localhost:8080 CACHEW_LOG_LEVEL=debug cachewd
1+
cachewd cachew.hcl **/*.go !**/*_test.go !state/**/* debounce=2s ready=http:8080/_readiness=200 timeout=600s: CACHEW_URL=http://localhost:8080 CACHEW_LOG_LEVEL=debug cachewd

bin/proctor

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
.proctor-0.9.3.pkg
1+
.proctor-0.10.0.pkg

cmd/cachew/git.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"io"
77
"os"
88
"os/exec"
9+
"path/filepath"
910
"strings"
1011
"time"
1112

@@ -162,7 +163,15 @@ func (c *GitRestoreCmd) streamFetchAndExtract(ctx context.Context, api *client.C
162163
// WriteAt so it cannot stream into extraction; the temp file is removed on
163164
// return.
164165
func (c *GitRestoreCmd) parallelFetchAndExtract(ctx context.Context, api *client.Client) (string, string, error) {
165-
tmp, err := os.CreateTemp("", "cachew-snapshot-*.tar.zst")
166+
// Stage the temp snapshot on the same filesystem as the restore target so a
167+
// small or separate /tmp can't fail a restore the target directory has room
168+
// for. The parent of c.Directory shares its filesystem and is created by
169+
// extraction anyway.
170+
tmpDir := filepath.Dir(c.Directory)
171+
if err := os.MkdirAll(tmpDir, 0o750); err != nil {
172+
return "", "", errors.Wrap(err, "create snapshot temp dir")
173+
}
174+
tmp, err := os.CreateTemp(tmpDir, ".cachew-snapshot-*.tar.zst")
166175
if err != nil {
167176
return "", "", errors.Wrap(err, "create snapshot temp file")
168177
}

cmd/cachew/git_test.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,54 @@ func TestGitRestoreSnapshot(t *testing.T) {
129129
assert.Equal(t, "nested content", string(content))
130130
}
131131

132+
func TestGitRestoreSnapshotParallel(t *testing.T) {
133+
srcDir := t.TempDir()
134+
initGitRepo(t, srcDir, map[string]string{
135+
"hello.txt": "hello world",
136+
"subdir/nested.txt": "nested content",
137+
})
138+
snapshotData := createTarZst(t, srcDir)
139+
140+
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
141+
if strings.HasSuffix(r.URL.Path, "/snapshot.tar.zst") {
142+
w.Header().Set("Content-Type", "application/zstd")
143+
w.Header().Set("ETag", `"snap-v1"`)
144+
// ServeContent honours Range/If-Range against the ETag, so ParallelGet
145+
// fetches the snapshot in concurrent chunks.
146+
http.ServeContent(w, r, "snapshot.tar.zst", time.Time{}, bytes.NewReader(snapshotData))
147+
return
148+
}
149+
http.NotFound(w, r)
150+
}))
151+
defer srv.Close()
152+
153+
// A nested, not-yet-existing target exercises temp-dir creation on the
154+
// target filesystem.
155+
dstDir := filepath.Join(t.TempDir(), "nested", "restored")
156+
cmd := &GitRestoreCmd{
157+
RepoURL: "https://github.com/test/repo",
158+
Directory: dstDir,
159+
DownloadConcurrency: 4,
160+
DownloadChunkSizeMB: 8,
161+
}
162+
api := client.NewWithHTTPClient(srv.URL, srv.Client())
163+
assert.NoError(t, cmd.Run(context.Background(), api))
164+
165+
content, err := os.ReadFile(filepath.Join(dstDir, "hello.txt"))
166+
assert.NoError(t, err)
167+
assert.Equal(t, "hello world", string(content))
168+
content, err = os.ReadFile(filepath.Join(dstDir, "subdir", "nested.txt"))
169+
assert.NoError(t, err)
170+
assert.Equal(t, "nested content", string(content))
171+
172+
// The temp snapshot is staged on the target filesystem and cleaned up.
173+
entries, err := os.ReadDir(filepath.Dir(dstDir))
174+
assert.NoError(t, err)
175+
for _, e := range entries {
176+
assert.False(t, strings.HasPrefix(e.Name(), ".cachew-snapshot-"), "temp snapshot left behind: %s", e.Name())
177+
}
178+
}
179+
132180
func TestGitRestoreWithBundle(t *testing.T) {
133181
srcDir := t.TempDir()
134182
initGitRepo(t, srcDir, map[string]string{"file.txt": "v1"})

0 commit comments

Comments
 (0)