From aa080f3a242f0ee941e7533df9023ea42c56ec5d Mon Sep 17 00:00:00 2001 From: dvcdsys Date: Mon, 17 Aug 2026 13:03:25 +0100 Subject: [PATCH 1/3] fix(server): stop unbounded .git growth in repo checkouts, self-heal broken clones MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Context: production hosts ~70 actively-pushed external repos; the data dir grew to ~76 GB against ~2.3 GB of database. Root cause: the update path in repocloner is fetch(Depth:1, Force) + hard reset, go-git writes ONE NEW PACKFILE per fetch, the reset makes the previously fetched tree unreachable, and nothing ever runs gc (go-git has none; the distroless runtime has no git binary to shell out to). So every push of every repo permanently added a pack, forever. Full brief with measurements: loadtests/GIT_STORAGE_CONTEXT.md (local, gitignored). Changes in repocloner.CloneOrFetch, reuse path restructured into cloneFresh/updateExisting/reclone: 1. Packfile budget (maxFetchPacks=20): before reusing a checkout, count .git/objects/pack/*.pack; at/over budget, discard the directory and shallow-clone fresh. A fresh clone is one pack; real git auto-gcs at 50. This both bounds future growth and automatically reclaims the existing bloat in production: each over-budget repo re-clones on its next webhook/poll fetch. After a re-clone PrevIndexedSHA is unreachable, so Changes=nil and the caller lands in reconcile mode (hash-gated, cheap). 2. Self-healing reuse path: failures rooted in on-disk state (PlainOpen, pre-fetch Head — the signature of a SIGKILL-mid-clone half-write that previously wedged the repo in a forever-failing error loop, remote URL mismatch after a github_url change, post-fetch ref resolve, worktree, reset) are wrapped with an errLocalState sentinel and recovered by nuke + re-clone. Fetch/transport errors deliberately stay fatal-but- preserving: a network blip must not cost a healthy clone and force a reindex. A cancelled context also never triggers the nuke. 3. Result.RecloneReason (informational) + a log line in repojobs.handleClone so operators can see why a fetch turned into a full clone. 4. maintenance.DirSizeBytes now skips unreadable subtrees and keeps counting instead of returning (0,false) on any walk error — previously one bad directory made the whole "Cloned repositories" row vanish from the Resources screen. (0,false) is still returned for a missing/unreadable root (keeps "unreadable" vs "empty" distinguishable) and on context cancellation (partial numbers from an aborted request are not cached). Not addressed here (possible follow-ups): a maintenance category for bloat inside live checkouts (the packfile budget makes it mostly redundant for actively-pushed repos, but idle bloated repos only shrink on their next fetch); per-branch checkout duplication (no shared object store across branches of the same repo); accounting for a moved CIX_REPOS_DIR stranding the old tree. Tests: 4 new repocloner tests (half-written clone self-heals; changed remote URL re-clones; packfile budget re-clones and resets the count; fetch failure against a dead upstream preserves the local clone) + 4 new DirSizeBytes tests (sum, missing root, unreadable subtree partial, cancelled context). Full server suite green. Co-Authored-By: Claude Fable 5 --- server/internal/maintenance/dirsize_test.go | 78 +++++++++ server/internal/maintenance/maintenance.go | 23 ++- server/internal/repocloner/repocloner.go | 159 ++++++++++++++---- server/internal/repocloner/repocloner_test.go | 133 +++++++++++++++ server/internal/repojobs/repojobs.go | 4 + 5 files changed, 357 insertions(+), 40 deletions(-) create mode 100644 server/internal/maintenance/dirsize_test.go diff --git a/server/internal/maintenance/dirsize_test.go b/server/internal/maintenance/dirsize_test.go new file mode 100644 index 00000000..2eb8f5da --- /dev/null +++ b/server/internal/maintenance/dirsize_test.go @@ -0,0 +1,78 @@ +package maintenance + +import ( + "context" + "fmt" + "os" + "path/filepath" + "testing" +) + +func TestDirSizeBytes_SumsRegularFiles(t *testing.T) { + dir := t.TempDir() + writeFileOfSize(t, filepath.Join(dir, "a"), 100) + writeFileOfSize(t, filepath.Join(dir, "sub", "b"), 50) + + n, ok := DirSizeBytes(context.Background(), dir) + if !ok { + t.Fatal("ok = false on a readable tree") + } + if n != 150 { + t.Errorf("total = %d, want 150", n) + } +} + +func TestDirSizeBytes_MissingRoot_ReportsNotOK(t *testing.T) { + n, ok := DirSizeBytes(context.Background(), filepath.Join(t.TempDir(), "nope")) + if ok { + t.Error("ok = true on a missing directory — 'unreadable' and 'empty' must stay distinguishable") + } + if n != 0 { + t.Errorf("total = %d, want 0", n) + } +} + +func TestDirSizeBytes_UnreadableSubtree_ReturnsPartial(t *testing.T) { + if os.Getuid() == 0 { + t.Skip("root ignores directory permissions") + } + dir := t.TempDir() + writeFileOfSize(t, filepath.Join(dir, "a"), 100) + locked := filepath.Join(dir, "locked") + writeFileOfSize(t, filepath.Join(locked, "hidden"), 999) + if err := os.Chmod(locked, 0o000); err != nil { + t.Fatalf("chmod: %v", err) + } + t.Cleanup(func() { _ = os.Chmod(locked, 0o755) }) + + n, ok := DirSizeBytes(context.Background(), dir) + if !ok { + t.Fatal("ok = false — one unreadable subtree must not throw the whole number away") + } + if n != 100 { + t.Errorf("total = %d, want 100 (the readable part)", n) + } +} + +func TestDirSizeBytes_CancelledContext_ReportsNotOK(t *testing.T) { + dir := t.TempDir() + // Enough entries to guarantee the every-512-entries context check fires. + for i := range 600 { + writeFileOfSize(t, filepath.Join(dir, fmt.Sprintf("f%04d", i)), 1) + } + ctx, cancel := context.WithCancel(context.Background()) + cancel() + if _, ok := DirSizeBytes(ctx, dir); ok { + t.Error("ok = true on a cancelled context, want false so callers omit the number") + } +} + +func writeFileOfSize(t *testing.T, path string, size int) { + t.Helper() + if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { + t.Fatalf("mkdir %s: %v", filepath.Dir(path), err) + } + if err := os.WriteFile(path, make([]byte, size), 0o644); err != nil { + t.Fatalf("write %s: %v", path, err) + } +} diff --git a/server/internal/maintenance/maintenance.go b/server/internal/maintenance/maintenance.go index f82c1220..0afcdb1a 100644 --- a/server/internal/maintenance/maintenance.go +++ b/server/internal/maintenance/maintenance.go @@ -217,9 +217,13 @@ type Analysis struct { Warnings []string `json:"warnings,omitempty"` } -// DirSizeBytes walks dir and sums regular-file sizes. Returns (0,false) on any -// error (missing dir, permission, cancelled context) so callers can omit the -// number rather than report a misleading 0. +// DirSizeBytes walks dir and sums regular-file sizes. An unreadable entry +// inside the tree is skipped and the rest still counts — a partial number +// beats no number on a tree of hundreds of thousands of git objects, where a +// single bad directory used to make the whole "Cloned repositories" row +// vanish. Returns (partial, false) only when nothing trustworthy could be +// produced: the root itself is missing/unreadable (so "unreadable" and +// "empty" stay distinguishable) or the context was cancelled mid-walk. // // The context is checked every so many entries: on a vector store that is one // file per document these walks visit hundreds of thousands of entries, and a @@ -231,9 +235,16 @@ type Analysis struct { func DirSizeBytes(ctx context.Context, dir string) (int64, bool) { var total int64 var seen int - walkErr := filepath.WalkDir(dir, func(_ string, d fs.DirEntry, err error) error { + walkErr := filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error { if err != nil { - return err + // Root failure means there is nothing to report; anything + // deeper is one bad subtree — skip it and keep counting. + // (WalkDir already skips the children of a directory it + // could not read.) + if path == dir { + return err + } + return nil } // Checking every entry would make ctx.Err() a meaningful share of the // walk's cost; every 512 keeps cancellation prompt for free. @@ -253,7 +264,7 @@ func DirSizeBytes(ctx context.Context, dir string) (int64, bool) { return nil }) if walkErr != nil { - return 0, false + return total, false } return total, true } diff --git a/server/internal/repocloner/repocloner.go b/server/internal/repocloner/repocloner.go index 870e9bea..3bdbee57 100644 --- a/server/internal/repocloner/repocloner.go +++ b/server/internal/repocloner/repocloner.go @@ -14,6 +14,10 @@ // What this package does: // - Clone a branch (public OR PAT-authenticated) // - Fetch + reset to remote HEAD on subsequent runs +// - Discard and re-clone a checkout whose local state is unusable +// (half-written clone, changed remote URL) or whose object store has +// accumulated too many fetch packfiles — go-git never runs gc, so +// re-cloning is the only way the store ever shrinks // - Report the current HEAD SHA (for last_sha bookkeeping) // - Resolve a "github.com/owner/repo" + branch to a deterministic local // directory under DataDir/repos/{path_hash}/ @@ -42,6 +46,23 @@ import ( // short-circuit reindex on this. var ErrAlreadyUpToDate = errors.New("repo already up to date") +// errLocalState marks a reuse-path failure caused by the checkout on disk +// itself (half-written clone, missing refs, mismatched remote) rather than by +// the network or the remote. CloneOrFetch recovers from these by discarding +// the directory and cloning fresh; anything NOT wrapped with this sentinel +// (fetch/transport failures) is returned as-is so a network blip never costs +// an otherwise healthy clone. +var errLocalState = errors.New("local clone state unusable") + +// maxFetchPacks bounds packfile accumulation in a reused checkout. Every +// fetch persists one new .pack/.idx pair, the subsequent hard reset makes the +// previously fetched tree unreachable, and go-git has no gc — so without a +// bound the object store grows with every upstream push, forever. Real git +// self-triggers gc at 50 packs; we re-clone earlier because a shallow +// re-clone is cheap (one pack, worktree-sized) while the accumulated packs +// are pure dead weight. +const maxFetchPacks = 20 + // CloneOptions parameterises a clone or fetch. type CloneOptions struct { // GitHubURL is the canonical HTTPS URL — "https://github.com/owner/repo" @@ -107,12 +128,25 @@ type Result struct { // matches the local HEAD before the fetch (i.e. nothing new). The // caller can skip enqueueing an index_repo job entirely. NoChanges bool + // RecloneReason is non-empty when an existing checkout was discarded + // and cloned fresh (unusable local state, changed remote URL, or the + // packfile budget was exceeded). Purely informational — callers log it + // so the operator can see why a fetch turned into a full clone. + RecloneReason string } // CloneOrFetch clones the repo when LocalDir is empty, otherwise fetches // + resets the local checkout to origin/{branch}. Returns the HEAD SHA // after the operation completes. // +// An existing checkout is discarded and cloned fresh (Result.RecloneReason +// says why) in three situations: its .git state is unusable — the half-clone +// a SIGKILL mid-clone leaves behind used to fail every retry forever; its +// origin URL no longer matches the requested one (github_url changed); or its +// object store has accumulated maxFetchPacks fetch packfiles. Fetch/transport +// failures are NOT grounds for a re-clone — a network blip must not cost a +// healthy clone (and force the full reindex that follows one). +// // The caller is responsible for choosing a LocalDir that won't collide // across repos — typically `/repos//` keyed by // projects.path_hash (NOT the github URL, which can change with @@ -132,46 +166,91 @@ func CloneOrFetch(ctx context.Context, opts CloneOptions) (Result, error) { // First-time clone path: LocalDir is missing or empty. if needsClone(opts.LocalDir) { - if err := os.MkdirAll(opts.LocalDir, 0o755); err != nil { - return Result{}, fmt.Errorf("mkdir clone target: %w", err) - } - repo, err := git.PlainCloneContext(ctx, opts.LocalDir, false, &git.CloneOptions{ - URL: url, - Auth: auth, - ReferenceName: plumbing.NewBranchReferenceName(opts.Branch), - SingleBranch: true, - Depth: 1, // shallow — minimises bandwidth + disk - }) - if err != nil { - // Cleanup so the next retry isn't stuck with a half-clone. - _ = os.RemoveAll(opts.LocalDir) - return Result{}, fmt.Errorf("clone: %w", err) - } - head, err := repo.Head() - if err != nil { - return Result{}, fmt.Errorf("resolve HEAD: %w", err) - } - return Result{HeadSHA: head.Hash().String()}, nil + return cloneFresh(ctx, opts, url, auth) + } + + // Packfile budget: every fetch below adds a pack and nothing ever + // removes one, so past the budget the store is mostly unreachable + // dead weight. Cheaper to start over than to keep carrying it. + if n := packfileCount(opts.LocalDir); n >= maxFetchPacks { + return reclone(ctx, opts, url, auth, fmt.Sprintf("object store has %d fetch packfiles (budget %d)", n, maxFetchPacks)) + } + + res, err := updateExisting(ctx, opts, url, auth) + if err == nil { + return res, nil + } + // Only local-state failures are recoverable by re-cloning, and never on + // a dead context — a cancelled shutdown fetch is not evidence the + // checkout is bad. + if !errors.Is(err, errLocalState) || ctx.Err() != nil { + return Result{}, err + } + return reclone(ctx, opts, url, auth, err.Error()) +} + +// cloneFresh is the first-time clone into an empty or missing LocalDir. +func cloneFresh(ctx context.Context, opts CloneOptions, url string, auth *http.BasicAuth) (Result, error) { + if err := os.MkdirAll(opts.LocalDir, 0o755); err != nil { + return Result{}, fmt.Errorf("mkdir clone target: %w", err) + } + repo, err := git.PlainCloneContext(ctx, opts.LocalDir, false, &git.CloneOptions{ + URL: url, + Auth: auth, + ReferenceName: plumbing.NewBranchReferenceName(opts.Branch), + SingleBranch: true, + Depth: 1, // shallow — minimises bandwidth + disk + }) + if err != nil { + // Cleanup so the next retry isn't stuck with a half-clone. + _ = os.RemoveAll(opts.LocalDir) + return Result{}, fmt.Errorf("clone: %w", err) + } + head, err := repo.Head() + if err != nil { + return Result{}, fmt.Errorf("resolve HEAD: %w", err) + } + return Result{HeadSHA: head.Hash().String()}, nil +} + +// reclone discards the existing checkout and clones fresh. The re-clone loses +// the old object store, so PrevIndexedSHA becomes unreachable and Changes +// stays nil — the caller lands in its reconcile path, which is the correct +// (and hash-gated, so cheap) recovery. +func reclone(ctx context.Context, opts CloneOptions, url string, auth *http.BasicAuth, reason string) (Result, error) { + if err := os.RemoveAll(opts.LocalDir); err != nil { + return Result{}, fmt.Errorf("remove stale clone at %s (%s): %w", opts.LocalDir, reason, err) } + res, err := cloneFresh(ctx, opts, url, auth) + if err != nil { + return Result{}, fmt.Errorf("reclone (%s): %w", reason, err) + } + res.RecloneReason = reason + return res, nil +} - // Reuse path: open the existing repo, ensure the remote matches, fetch, - // (optionally compute change set,) reset to origin/{branch}. +// updateExisting is the reuse path: open the existing repo, ensure the remote +// matches, fetch, (optionally compute change set,) reset to origin/{branch}. +// Failures rooted in the on-disk state are wrapped with errLocalState so the +// caller can recover by re-cloning; fetch failures are returned bare. +func updateExisting(ctx context.Context, opts CloneOptions, url string, auth *http.BasicAuth) (Result, error) { repo, err := git.PlainOpen(opts.LocalDir) if err != nil { - return Result{}, fmt.Errorf("open existing repo at %s: %w", opts.LocalDir, err) + return Result{}, fmt.Errorf("%w: open existing repo at %s: %v", errLocalState, opts.LocalDir, err) } if err := ensureRemote(repo, url); err != nil { - return Result{}, err + return Result{}, fmt.Errorf("%w: %v", errLocalState, err) } // Snapshot the pre-fetch HEAD so we can short-circuit on NoChanges // when the fetch reveals no new commits. This is the commit currently // on disk; it may or may not match opts.PrevIndexedSHA (mismatch // means a previous index job failed mid-way — the caller decides - // how to recover). + // how to recover). A failure here is the signature of a half-written + // clone (SIGKILL mid-PlainClone leaves .git without refs). prevHead, err := repo.Head() if err != nil { - return Result{}, fmt.Errorf("resolve pre-fetch HEAD: %w", err) + return Result{}, fmt.Errorf("%w: resolve pre-fetch HEAD: %v", errLocalState, err) } err = repo.FetchContext(ctx, &git.FetchOptions{ @@ -186,7 +265,7 @@ func CloneOrFetch(ctx context.Context, opts CloneOptions) (Result, error) { remoteRef, err := repo.Reference(plumbing.NewRemoteReferenceName("origin", opts.Branch), true) if err != nil { - return Result{}, fmt.Errorf("resolve remote ref: %w", err) + return Result{}, fmt.Errorf("%w: resolve remote ref: %v", errLocalState, err) } newSHA := remoteRef.Hash() @@ -217,20 +296,21 @@ func CloneOrFetch(ctx context.Context, opts CloneOptions) (Result, error) { wt, err := repo.Worktree() if err != nil { - return Result{}, fmt.Errorf("worktree: %w", err) + return Result{}, fmt.Errorf("%w: worktree: %v", errLocalState, err) } // Hard reset — discards any local mutation that crept in. Worker-managed - // checkouts have no human edits we'd want to preserve. + // checkouts have no human edits we'd want to preserve. The commit was + // just fetched, so a failure here means the local store is broken. if err := wt.Reset(&git.ResetOptions{ Commit: newSHA, Mode: git.HardReset, }); err != nil { - return Result{}, fmt.Errorf("reset: %w", err) + return Result{}, fmt.Errorf("%w: reset: %v", errLocalState, err) } head, err := repo.Head() if err != nil { - return Result{}, fmt.Errorf("resolve HEAD post-reset: %w", err) + return Result{}, fmt.Errorf("%w: resolve HEAD post-reset: %v", errLocalState, err) } return Result{HeadSHA: head.Hash().String(), Changes: changes}, nil } @@ -332,6 +412,17 @@ func normaliseURL(u string) string { return u } +// packfileCount counts the .pack files in the checkout's object store. A +// fresh shallow clone has exactly one; each subsequent fetch adds one more. +// Best effort — 0 on any error keeps the caller on the ordinary reuse path. +func packfileCount(dir string) int { + matches, err := filepath.Glob(filepath.Join(dir, ".git", "objects", "pack", "*.pack")) + if err != nil { + return 0 + } + return len(matches) +} + func needsClone(dir string) bool { gitDir := filepath.Join(dir, ".git") if _, err := os.Stat(gitDir); err != nil { @@ -349,9 +440,9 @@ func ensureRemote(repo *git.Repository, wantURL string) error { urls := remote.Config().URLs if len(urls) == 0 || urls[0] != wantURL { // Repo on disk points at a different URL — likely the workspace - // admin changed the github_url. Easiest fix: nuke + reclone, but - // the caller can't see that from here. Surface as an error so the - // operator at least sees the mismatch in the failed job. + // admin changed the github_url. The old checkout is dead weight; + // the errLocalState wrap this gets in updateExisting is what turns + // it into a nuke + re-clone. return fmt.Errorf("local repo remote %v does not match expected %s", urls, wantURL) } return nil diff --git a/server/internal/repocloner/repocloner_test.go b/server/internal/repocloner/repocloner_test.go index 4a9f0ffd..3c18caa0 100644 --- a/server/internal/repocloner/repocloner_test.go +++ b/server/internal/repocloner/repocloner_test.go @@ -2,6 +2,7 @@ package repocloner import ( "context" + "fmt" "os" "path/filepath" "sort" @@ -338,6 +339,138 @@ func TestCloneOrFetch_EmptyPrevSHA_ReturnsNilChangeSet(t *testing.T) { } } +func TestCloneOrFetch_HalfWrittenClone_SelfHeals(t *testing.T) { + upstream, w := makeBareUpstream(t, "main") + headSHA := w.CommitFiles(t, "init", map[string]string{ + "a.go": "package a\n", + }) + + // Simulate what a SIGKILL mid-PlainClone leaves behind: .git exists + // (so needsClone says "reuse") but holds no usable repository state. + local := filepath.Join(t.TempDir(), "clone") + if err := os.MkdirAll(filepath.Join(local, ".git"), 0o755); err != nil { + t.Fatalf("mkdir fake .git: %v", err) + } + if err := os.WriteFile(filepath.Join(local, ".git", "HEAD"), []byte("ref: refs/heads/main\n"), 0o644); err != nil { + t.Fatalf("write fake HEAD: %v", err) + } + + res, err := CloneOrFetch(context.Background(), CloneOptions{ + GitHubURL: "file://" + upstream, + Branch: "main", + LocalDir: local, + }) + if err != nil { + t.Fatalf("CloneOrFetch on half-written clone: %v (want self-heal, got permanent failure)", err) + } + if res.HeadSHA != headSHA { + t.Errorf("HeadSHA = %s, want %s", res.HeadSHA, headSHA) + } + if res.RecloneReason == "" { + t.Error("RecloneReason empty, want the local-state failure that forced the re-clone") + } +} + +func TestCloneOrFetch_RemoteURLChanged_Reclones(t *testing.T) { + upstreamA, wa := makeBareUpstream(t, "main") + wa.CommitFiles(t, "init A", map[string]string{"a.go": "package a\n"}) + + local := filepath.Join(t.TempDir(), "clone") + initialCloneFor(t, upstreamA, local, "main") + + upstreamB, wb := makeBareUpstream(t, "main") + headB := wb.CommitFiles(t, "init B", map[string]string{"b.go": "package b\n"}) + + res, err := CloneOrFetch(context.Background(), CloneOptions{ + GitHubURL: "file://" + upstreamB, + Branch: "main", + LocalDir: local, + }) + if err != nil { + t.Fatalf("CloneOrFetch with changed URL: %v (want re-clone, got error)", err) + } + if res.HeadSHA != headB { + t.Errorf("HeadSHA = %s, want %s (upstream B)", res.HeadSHA, headB) + } + if res.RecloneReason == "" { + t.Error("RecloneReason empty, want the remote-mismatch reason") + } +} + +func TestCloneOrFetch_PackfileBudget_Reclones(t *testing.T) { + upstream, w := makeBareUpstream(t, "main") + w.CommitFiles(t, "init", map[string]string{"a.go": "package a\n"}) + + local := filepath.Join(t.TempDir(), "clone") + initialCloneFor(t, upstream, local, "main") + + // Pad the object store up to the budget with dummy packs — the count is + // what matters, not the content. + packDir := filepath.Join(local, ".git", "objects", "pack") + for i := packfileCount(local); i < maxFetchPacks; i++ { + name := filepath.Join(packDir, fmt.Sprintf("pack-%040d.pack", i)) + if err := os.WriteFile(name, []byte("dummy"), 0o644); err != nil { + t.Fatalf("write dummy pack: %v", err) + } + } + + headSHA := w.CommitFiles(t, "v2", map[string]string{"b.go": "package b\n"}) + + res, err := CloneOrFetch(context.Background(), CloneOptions{ + GitHubURL: "file://" + upstream, + Branch: "main", + LocalDir: local, + }) + if err != nil { + t.Fatalf("CloneOrFetch over pack budget: %v", err) + } + if res.RecloneReason == "" { + t.Error("RecloneReason empty, want the packfile-budget reason") + } + if res.HeadSHA != headSHA { + t.Errorf("HeadSHA = %s, want %s", res.HeadSHA, headSHA) + } + if n := packfileCount(local); n >= maxFetchPacks { + t.Errorf("packfileCount = %d after re-clone, want it back to a fresh clone's worth", n) + } +} + +func TestCloneOrFetch_FetchFailure_KeepsClone(t *testing.T) { + upstream, w := makeBareUpstream(t, "main") + headSHA := w.CommitFiles(t, "init", map[string]string{"a.go": "package a\n"}) + + local := filepath.Join(t.TempDir(), "clone") + initialCloneFor(t, upstream, local, "main") + + // Kill the upstream. The URL still matches the checkout's origin, so + // this is indistinguishable from a network outage — the fetch must + // fail WITHOUT costing us the healthy local clone. + if err := os.RemoveAll(upstream); err != nil { + t.Fatalf("remove upstream: %v", err) + } + + _, err := CloneOrFetch(context.Background(), CloneOptions{ + GitHubURL: "file://" + upstream, + Branch: "main", + LocalDir: local, + }) + if err == nil { + t.Fatal("CloneOrFetch succeeded against a dead upstream, want error") + } + + repo, oerr := git.PlainOpen(local) + if oerr != nil { + t.Fatalf("local clone destroyed by a fetch failure: %v", oerr) + } + head, herr := repo.Head() + if herr != nil { + t.Fatalf("local clone HEAD unreadable after fetch failure: %v", herr) + } + if head.Hash().String() != headSHA { + t.Errorf("local HEAD = %s, want untouched %s", head.Hash().String(), headSHA) + } +} + func TestChangeSet_IsEmpty(t *testing.T) { if !(*ChangeSet)(nil).IsEmpty() { t.Error("nil ChangeSet should report IsEmpty=true") diff --git a/server/internal/repojobs/repojobs.go b/server/internal/repojobs/repojobs.go index 721fd586..8c236440 100644 --- a/server/internal/repojobs/repojobs.go +++ b/server/internal/repojobs/repojobs.go @@ -241,6 +241,10 @@ func handleClone(ctx context.Context, d Deps, job jobs.Job) error { d.recordFailure(ctx, g, fmt.Errorf("clone: %w", err)) return err } + if result.RecloneReason != "" { + d.Logger.Info("repojobs: checkout discarded and re-cloned", + "project", g.ProjectPath, "reason", result.RecloneReason) + } if err := d.GitRepos.SetClone(ctx, g.ProjectPath, result.HeadSHA, ""); err != nil { d.Logger.Warn("repojobs: set last_sha failed", "project", g.ProjectPath, "err", err) From 9c224821d59078fd44e2babe330a79e93744e00d Mon Sep 17 00:00:00 2001 From: dvcdsys Date: Tue, 18 Aug 2026 13:49:11 +0100 Subject: [PATCH 2/3] feat(server): in-place object-store compaction + NoTags clones, replacing budget-reclone MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces the maxFetchPacks nuke-and-reclone bound (previous commit) with the approach validated by the PoC on branch poc/gc-compaction (server/cmd/gc-poc, run against all 45 loadtests fixtures — spring-boot, grafana, etc — with byte-level identity vs canonical full clones, git fsck --strict, a 12-round leak test, and O() scaling measurements; see that branch's commit messages for the numbers). Two production changes: 1. Tags:NoTags on both clone and fetch (repocloner.go). go-git's clone default is AllTags, and on a shallow clone every tag arrives as a FULL tree snapshot: real checkouts measured 2.6-5x the worktree on day zero (spring-boot: 391 tags -> 102MB store / 39MB worktree; worst fixture: 1240 tags, 503MB / 278MB). cix indexes exactly one branch and never reads tags. This kills the second bloat driver at the source. 2. In-place compaction (compact.go), run from CloneOrFetch after every successful update when needsCompaction() says so: packfileCount >= compactPackThreshold (4) — the fetch+reset path persists one snapshot-sized pack per fetch and go-git has no gc — OR tag refs present. The tag trigger IS the upgrade migration: the first ordinary update of every pre-NoTags checkout drops refs/tags/*, collapses the store to one branch-snapshot pack and rewrites .git/shallow, with no separate migration code. Runs on the NoChanges path too, so cleanup does not wait for the repo's next commit. Mechanism: reachability walk from non-tag refs + explicitly protected commits (PrevIndexedSHA — the base of the next incremental tree-diff, unreferenced once the branch moves, kept alive across compactions), honouring .git/shallow graft points exactly like git; encode the set into one pack via storer.PackfileWriter (window 0 — measured -17% size for 2.9x CPU with deltas, not worth it); delete old packs only after the new pack is durable (crash mid-compaction leaves extra packs, never a broken store); drop all loose objects; keep only still-present shallow entries. The walker is go-git's objectWalker with three fixes real repos hit immediately: shallow grafts terminate parent walks, submodule gitlinks are skipped, symlink-reached blobs are leaves. Costs (measured): linear time ~0.2-1.4ms CPU per object + ~0.2s/GB emitted; heap ~3x the uncompressed snapshot (go-git's encoder materialises content) — package-level compactMu serialises compactions across concurrent clone jobs so those peaks never stack on the 8GB prod hosts. Failed compaction falls back to nuke+reclone (store state unknown; shallow reclone is always correct). Result gains Compaction *CompactStats; repojobs logs it (bytes before/ after, packs deleted, tag refs dropped, duration) next to the existing RecloneReason log. Tests: fresh clone carries zero tag refs; the upgrade scenario end-to-end (legacy AllTags clone + 2 legacy fetch cycles -> first new-code update compacts: 2 tag refs dropped, packs 3+ -> 1, objects dir shrinks, the SAME update still returns the v4->v5 incremental ChangeSet, a later update still diffs from the protected-but-unreferenced indexed_sha, and a no-op cycle stays quiet); pack count stays <= threshold across 8 push/fetch cycles with at least one compaction. Budget-reclone test removed with the mechanism. Full server suite + vet green. Co-Authored-By: Claude Fable 5 --- server/internal/repocloner/compact.go | 360 ++++++++++++++++++ server/internal/repocloner/repocloner.go | 87 +++-- server/internal/repocloner/repocloner_test.go | 262 ++++++++++++- server/internal/repojobs/repojobs.go | 7 + 4 files changed, 666 insertions(+), 50 deletions(-) create mode 100644 server/internal/repocloner/compact.go diff --git a/server/internal/repocloner/compact.go b/server/internal/repocloner/compact.go new file mode 100644 index 00000000..f074a807 --- /dev/null +++ b/server/internal/repocloner/compact.go @@ -0,0 +1,360 @@ +package repocloner + +// In-process object-store compaction for shallow checkouts. +// +// Why it exists: the update path is fetch(Depth:1) + hard reset. go-git +// persists ONE NEW PACKFILE per fetch — and each of those packs is a +// near-full snapshot of the tree, not a delta — while the reset makes the +// previously fetched snapshot unreachable. go-git has no gc and the +// distroless runtime has no git binary, so without intervention the object +// store grows with every upstream push, forever (this is what took a ~4.5 GB +// production fleet of checkouts to 76 GB). On top of that, go-git's clone +// default is Tags:AllTags, so a day-zero clone of a tag-rich repo carries a +// full shallow snapshot PER TAG (spring-boot: 391 tags → a 102 MB store for +// a 39 MB worktree) that cix, which indexes exactly one branch, never reads. +// +// Compaction rewrites the store down to what the server actually uses: +// it drops refs/tags/*, walks the objects reachable from the remaining refs +// (honouring .git/shallow graft points exactly like git does) plus any +// explicitly protected commits, encodes that set into one new pack, deletes +// the old packs and loose objects, and rewrites .git/shallow to the entries +// that still exist. `git fsck --strict` is clean afterwards and the worktree +// is untouched — validated byte-for-byte against full-history canonical +// clones on 45 real checkouts (spring-boot, grafana, …) by the PoC on branch +// poc/gc-compaction (server/cmd/gc-poc). +// +// Cost model, measured on those 45 checkouts: time is linear — +// ~0.2–1.4 ms CPU per reachable object plus ~0.2 s per emitted GB (zlib); +// memory is linear in the SNAPSHOT size (not the store size) at roughly 3× +// the uncompressed content, because go-git's packfile encoder materialises +// object data. A typical 60 MB checkout compacts in single-digit seconds +// within a few hundred MB of transient heap; compactMu keeps concurrent +// clone jobs from stacking those peaks. +// +// The delta window is 0 on purpose: after tags are dropped the reachable set +// is essentially a single snapshot, and the PoC measured window=10 at −17% +// pack size for 2.9× the CPU. + +import ( + "fmt" + "os" + "path/filepath" + "strings" + "sync" + "time" + + "github.com/go-git/go-git/v5" + "github.com/go-git/go-git/v5/plumbing" + "github.com/go-git/go-git/v5/plumbing/filemode" + "github.com/go-git/go-git/v5/plumbing/format/packfile" + "github.com/go-git/go-git/v5/plumbing/object" + "github.com/go-git/go-git/v5/plumbing/storer" + "github.com/go-git/go-git/v5/storage" +) + +// compactPackThreshold is how many packfiles a checkout may accumulate +// before the next CloneOrFetch rewrites the store. Each fetch adds one +// snapshot-sized pack, so the steady-state disk overhead between compactions +// is bounded by (threshold-1) worktree-sized packs, and the compaction cost +// is amortised over that many pushes. +const compactPackThreshold = 4 + +// compactMu serialises compactions across concurrent clone jobs. The +// transient heap of one compaction is ~3× the repo's uncompressed snapshot; +// letting several worker goroutines pay that simultaneously is how an 8 GB +// host gets OOM-killed. +var compactMu sync.Mutex + +const tagRefPrefix = "refs/tags/" + +// CompactStats reports what one compaction did. Purely informational — +// callers log it. +type CompactStats struct { + ObjectsBefore int64 // .git/objects bytes before + ObjectsAfter int64 // .git/objects bytes after + Reachable int // objects written to the new pack + PacksDeleted int + LoosePruned int + TagRefsDropped int + Duration time.Duration +} + +// needsCompaction reports whether the checkout's object store warrants a +// rewrite: enough accumulated fetch packs, or tag refs left behind by +// pre-NoTags server versions (their snapshots dominate the store, and with +// Tags:NoTags on every fetch they will not come back). The tag check makes +// the first post-upgrade update of every existing checkout clean it — there +// is deliberately no separate migration. +func needsCompaction(dir string) bool { + if packfileCount(dir) >= compactPackThreshold { + return true + } + repo, err := git.PlainOpen(dir) + if err != nil { + return false + } + refs, err := repo.References() + if err != nil { + return false + } + defer refs.Close() + found := false + _ = refs.ForEach(func(ref *plumbing.Reference) error { + if strings.HasPrefix(ref.Name().String(), tagRefPrefix) { + found = true + return storer.ErrStop + } + return nil + }) + return found +} + +// compactCheckout rewrites dir's object store down to the objects reachable +// from its non-tag references plus the protected commits. protect carries +// commits no ref points at that must survive — in practice +// git_repos.indexed_sha, the base of the next incremental tree-diff; entries +// that are zero or absent from the store are skipped. +// +// Failure modes are safe by construction: the new pack is durable before any +// old pack is deleted, so a crash mid-compaction leaves extra packs for the +// next run, never a store missing objects. The caller handles a returned +// error by discarding the checkout and re-cloning. +func compactCheckout(dir string, protect ...plumbing.Hash) (CompactStats, error) { + compactMu.Lock() + defer compactMu.Unlock() + + started := time.Now() + st := CompactStats{ObjectsBefore: objectsDirSize(dir)} + + repo, err := git.PlainOpen(dir) + if err != nil { + return st, fmt.Errorf("open: %w", err) + } + + // 1. Drop tag refs. cix serves exactly one branch; every tag is a whole + // retained snapshot the server never reads. + refs, err := repo.References() + if err != nil { + return st, err + } + var tagRefs []plumbing.ReferenceName + err = refs.ForEach(func(ref *plumbing.Reference) error { + if strings.HasPrefix(ref.Name().String(), tagRefPrefix) { + tagRefs = append(tagRefs, ref.Name()) + } + return nil + }) + if err != nil { + return st, err + } + for _, name := range tagRefs { + if err := repo.Storer.RemoveReference(name); err != nil { + return st, fmt.Errorf("remove tag ref %s: %w", name, err) + } + st.TagRefsDropped++ + } + + // 2. Reachability walk from the remaining refs + protected commits, + // with git's shallow semantics. + shallowList, err := repo.Storer.Shallow() + if err != nil { + return st, fmt.Errorf("read shallow: %w", err) + } + shallowSet := make(map[plumbing.Hash]struct{}, len(shallowList)) + for _, h := range shallowList { + shallowSet[h] = struct{}{} + } + w := &objectWalker{storer: repo.Storer, shallow: shallowSet, seen: map[plumbing.Hash]struct{}{}} + refs, err = repo.References() + if err != nil { + return st, err + } + err = refs.ForEach(func(ref *plumbing.Reference) error { + if ref.Type() != plumbing.HashReference { + return nil + } + return w.walk(ref.Hash()) + }) + if err != nil { + return st, fmt.Errorf("reachability walk: %w", err) + } + for _, h := range protect { + if h.IsZero() { + continue + } + if _, gerr := object.GetObject(repo.Storer, h); gerr != nil { + // Not in the store (already gc'd away, or a bogus SHA) — + // nothing to protect. + continue + } + if err := w.walk(h); err != nil { + return st, fmt.Errorf("walk protected %s: %w", h, err) + } + } + st.Reachable = len(w.seen) + objs := make([]plumbing.Hash, 0, len(w.seen)) + for h := range w.seen { + objs = append(objs, h) + } + + // 3. Write the reachable set as one new pack. PackfileWriter lands it in + // objects/pack with a proper idx before we touch anything old. + pos, ok := repo.Storer.(storer.PackedObjectStorer) + if !ok { + return st, fmt.Errorf("storage does not support packed objects") + } + oldPacks, err := pos.ObjectPacks() + if err != nil { + return st, err + } + pfw, ok := repo.Storer.(storer.PackfileWriter) + if !ok { + return st, fmt.Errorf("storage does not support packfile writing") + } + wc, err := pfw.PackfileWriter() + if err != nil { + return st, err + } + enc := packfile.NewEncoder(wc, repo.Storer, false) + // Window 0: no delta search — see the package comment. + newPack, err := enc.Encode(objs, 0) + if cerr := wc.Close(); err == nil { + err = cerr + } + if err != nil { + return st, fmt.Errorf("encode pack: %w", err) + } + + // 4. Only now that the new pack is durable: delete the old ones. + for _, h := range oldPacks { + if h == newPack { + continue + } + if err := pos.DeleteOldObjectPackAndIndex(h, time.Time{}); err != nil { + return st, fmt.Errorf("delete pack %s: %w", h, err) + } + st.PacksDeleted++ + } + + // 5. Loose objects: everything reachable is in the new pack, so every + // loose object is redundant regardless of reachability. + if los, ok := repo.Storer.(storer.LooseObjectStorer); ok { + err = los.ForEachObjectHash(func(h plumbing.Hash) error { + if derr := los.DeleteLooseObject(h); derr != nil { + return derr + } + st.LoosePruned++ + return nil + }) + if err != nil { + return st, fmt.Errorf("prune loose: %w", err) + } + } + + // 6. .git/shallow gains one graft entry per fetch; entries whose commit + // was just dropped would make real git tooling error out ("did not + // find object for shallow …"), so keep only entries still present. + kept := shallowList[:0] + for _, h := range shallowList { + if _, reachable := w.seen[h]; reachable { + kept = append(kept, h) + } + } + if len(kept) != len(shallowList) { + if err := repo.Storer.SetShallow(kept); err != nil { + return st, fmt.Errorf("rewrite shallow: %w", err) + } + } + + st.ObjectsAfter = objectsDirSize(dir) + st.Duration = time.Since(started) + return st, nil +} + +// objectWalker collects the reachable object set. It is go-git's own +// objectWalker (repository.go uses it for RepackObjects) with three +// behavioural fixes, each of which real checkouts hit immediately: +// +// - commits listed in .git/shallow are graft points whose parents are +// never walked — git's own semantics. (Stock go-git follows ParentHashes +// unconditionally: it crashes on any multi-commit push fetched at +// Depth:1, and where the chain happens to be complete it retains every +// previously fetched snapshot forever.) +// - submodule (gitlink) tree entries are skipped — the hash is a commit in +// a different repository. (Stock go-git crashes.) +// - blobs reached as objects (via symlink and other non-regular-file tree +// entries) are accepted leaves. (Stock go-git errors "unknown object".) +type objectWalker struct { + storer storage.Storer + shallow map[plumbing.Hash]struct{} + seen map[plumbing.Hash]struct{} +} + +func (w *objectWalker) walk(hash plumbing.Hash) error { + if _, ok := w.seen[hash]; ok { + return nil + } + obj, err := object.GetObject(w.storer, hash) + if err != nil { + return fmt.Errorf("get object %s: %w", hash, err) + } + w.seen[hash] = struct{}{} + switch obj := obj.(type) { + case *object.Commit: + if err := w.walk(obj.TreeHash); err != nil { + return err + } + if _, grafted := w.shallow[obj.Hash]; grafted { + break + } + for _, p := range obj.ParentHashes { + if _, ok := w.seen[p]; ok { + continue + } + // A parent this shallow store never fetched: boundary, not error. + if _, gerr := object.GetObject(w.storer, p); gerr == plumbing.ErrObjectNotFound { + continue + } + if err := w.walk(p); err != nil { + return err + } + } + case *object.Tree: + for i := range obj.Entries { + e := obj.Entries[i] + if e.Mode == filemode.Submodule { + continue + } + if e.Mode|0o755 == filemode.Executable { // plain blob, any file mode + w.seen[e.Hash] = struct{}{} + continue + } + if err := w.walk(e.Hash); err != nil { + return err + } + } + case *object.Blob: + // Leaf. + case *object.Tag: + return w.walk(obj.Target) + default: + return fmt.Errorf("unknown object type %T at %s", obj, hash) + } + return nil +} + +// objectsDirSize sums .git/objects — a few packs plus loose files, so the +// walk is cheap. Best effort; 0 on error. +func objectsDirSize(dir string) int64 { + var total int64 + _ = filepath.WalkDir(filepath.Join(dir, ".git", "objects"), func(_ string, d os.DirEntry, err error) error { + if err != nil || d.IsDir() { + return nil + } + if info, ierr := d.Info(); ierr == nil { + total += info.Size() + } + return nil + }) + return total +} diff --git a/server/internal/repocloner/repocloner.go b/server/internal/repocloner/repocloner.go index 3bdbee57..a15f9667 100644 --- a/server/internal/repocloner/repocloner.go +++ b/server/internal/repocloner/repocloner.go @@ -12,12 +12,13 @@ // go-git into the binary keeps the runtime image untouched. // // What this package does: -// - Clone a branch (public OR PAT-authenticated) +// - Clone a branch (public OR PAT-authenticated), shallow and tag-free // - Fetch + reset to remote HEAD on subsequent runs +// - Compact the object store in place when fetch packs pile up or legacy +// tag snapshots are present (see compact.go) — go-git has no gc of its +// own and the distroless runtime has no git binary // - Discard and re-clone a checkout whose local state is unusable -// (half-written clone, changed remote URL) or whose object store has -// accumulated too many fetch packfiles — go-git never runs gc, so -// re-cloning is the only way the store ever shrinks +// (half-written clone, changed remote URL, failed compaction) // - Report the current HEAD SHA (for last_sha bookkeeping) // - Resolve a "github.com/owner/repo" + branch to a deterministic local // directory under DataDir/repos/{path_hash}/ @@ -54,15 +55,6 @@ var ErrAlreadyUpToDate = errors.New("repo already up to date") // an otherwise healthy clone. var errLocalState = errors.New("local clone state unusable") -// maxFetchPacks bounds packfile accumulation in a reused checkout. Every -// fetch persists one new .pack/.idx pair, the subsequent hard reset makes the -// previously fetched tree unreachable, and go-git has no gc — so without a -// bound the object store grows with every upstream push, forever. Real git -// self-triggers gc at 50 packs; we re-clone earlier because a shallow -// re-clone is cheap (one pack, worktree-sized) while the accumulated packs -// are pure dead weight. -const maxFetchPacks = 20 - // CloneOptions parameterises a clone or fetch. type CloneOptions struct { // GitHubURL is the canonical HTTPS URL — "https://github.com/owner/repo" @@ -129,10 +121,13 @@ type Result struct { // caller can skip enqueueing an index_repo job entirely. NoChanges bool // RecloneReason is non-empty when an existing checkout was discarded - // and cloned fresh (unusable local state, changed remote URL, or the - // packfile budget was exceeded). Purely informational — callers log it - // so the operator can see why a fetch turned into a full clone. + // and cloned fresh (unusable local state, changed remote URL, or a + // failed compaction). Purely informational — callers log it so the + // operator can see why a fetch turned into a full clone. RecloneReason string + // Compaction is set when this call rewrote the checkout's object store + // (see compact.go). Purely informational — callers log it. + Compaction *CompactStats } // CloneOrFetch clones the repo when LocalDir is empty, otherwise fetches @@ -142,10 +137,16 @@ type Result struct { // An existing checkout is discarded and cloned fresh (Result.RecloneReason // says why) in three situations: its .git state is unusable — the half-clone // a SIGKILL mid-clone leaves behind used to fail every retry forever; its -// origin URL no longer matches the requested one (github_url changed); or its -// object store has accumulated maxFetchPacks fetch packfiles. Fetch/transport -// failures are NOT grounds for a re-clone — a network blip must not cost a -// healthy clone (and force the full reindex that follows one). +// origin URL no longer matches the requested one (github_url changed); or a +// compaction of its object store failed. Fetch/transport failures are NOT +// grounds for a re-clone — a network blip must not cost a healthy clone (and +// force the full reindex that follows one). +// +// After a successful update the checkout's object store is compacted when it +// needs it (accumulated fetch packs, or tag snapshots left behind by +// pre-NoTags server versions — which makes the first update after a server +// upgrade clean every existing checkout, with no separate migration). See +// compact.go for the mechanism and its measured costs. // // The caller is responsible for choosing a LocalDir that won't collide // across repos — typically `/repos//` keyed by @@ -164,21 +165,15 @@ func CloneOrFetch(ctx context.Context, opts CloneOptions) (Result, error) { url := normaliseURL(opts.GitHubURL) auth := authFor(opts.PAT) - // First-time clone path: LocalDir is missing or empty. + // First-time clone path: LocalDir is missing or empty. A fresh NoTags + // clone is one branch-snapshot pack — nothing to compact. if needsClone(opts.LocalDir) { return cloneFresh(ctx, opts, url, auth) } - // Packfile budget: every fetch below adds a pack and nothing ever - // removes one, so past the budget the store is mostly unreachable - // dead weight. Cheaper to start over than to keep carrying it. - if n := packfileCount(opts.LocalDir); n >= maxFetchPacks { - return reclone(ctx, opts, url, auth, fmt.Sprintf("object store has %d fetch packfiles (budget %d)", n, maxFetchPacks)) - } - res, err := updateExisting(ctx, opts, url, auth) if err == nil { - return res, nil + return maybeCompact(ctx, opts, url, auth, res) } // Only local-state failures are recoverable by re-cloning, and never on // a dead context — a cancelled shutdown fetch is not evidence the @@ -189,6 +184,32 @@ func CloneOrFetch(ctx context.Context, opts CloneOptions) (Result, error) { return reclone(ctx, opts, url, auth, err.Error()) } +// maybeCompact runs the object-store compaction after a successful update +// when the checkout warrants it. It runs on the NoChanges path too: the +// post-upgrade cleanup of a tag-carrying checkout must not wait for the +// repo's next actual commit. A failed compaction falls back to nuke + +// re-clone — the store's state is unknown at that point, and a shallow +// re-clone is always correct (Changes degrade to nil, so the caller +// reconciles instead of diffing). +func maybeCompact(ctx context.Context, opts CloneOptions, url string, auth *http.BasicAuth, res Result) (Result, error) { + if ctx.Err() != nil || !needsCompaction(opts.LocalDir) { + return res, nil + } + var protect []plumbing.Hash + if s := strings.TrimSpace(opts.PrevIndexedSHA); s != "" { + // The indexed commit is the base of the NEXT incremental diff; no + // ref points at it once the branch has moved on, so it must be + // protected explicitly. + protect = append(protect, plumbing.NewHash(s)) + } + st, err := compactCheckout(opts.LocalDir, protect...) + if err != nil { + return reclone(ctx, opts, url, auth, fmt.Sprintf("compaction failed: %v", err)) + } + res.Compaction = &st + return res, nil +} + // cloneFresh is the first-time clone into an empty or missing LocalDir. func cloneFresh(ctx context.Context, opts CloneOptions, url string, auth *http.BasicAuth) (Result, error) { if err := os.MkdirAll(opts.LocalDir, 0o755); err != nil { @@ -200,6 +221,11 @@ func cloneFresh(ctx context.Context, opts CloneOptions, url string, auth *http.B ReferenceName: plumbing.NewBranchReferenceName(opts.Branch), SingleBranch: true, Depth: 1, // shallow — minimises bandwidth + disk + // go-git's clone default is AllTags, and on a shallow clone every + // tag arrives as a FULL tree snapshot (spring-boot's 391 tags cost + // a 102 MB store for a 39 MB worktree). cix indexes one branch and + // never reads tags. + Tags: git.NoTags, }) if err != nil { // Cleanup so the next retry isn't stuck with a half-clone. @@ -258,6 +284,9 @@ func updateExisting(ctx context.Context, opts CloneOptions, url string, auth *ht RefSpecs: []config.RefSpec{config.RefSpec(fmt.Sprintf("+refs/heads/%s:refs/remotes/origin/%s", opts.Branch, opts.Branch))}, Depth: 1, Force: true, + // Default is TagFollowing; without NoTags every fetch can drag in + // new tag snapshots. See the matching option in cloneFresh. + Tags: git.NoTags, }) if err != nil && !errors.Is(err, git.NoErrAlreadyUpToDate) { return Result{}, fmt.Errorf("fetch: %w", err) diff --git a/server/internal/repocloner/repocloner_test.go b/server/internal/repocloner/repocloner_test.go index 3c18caa0..eeff6a87 100644 --- a/server/internal/repocloner/repocloner_test.go +++ b/server/internal/repocloner/repocloner_test.go @@ -6,6 +6,7 @@ import ( "os" "path/filepath" "sort" + "strings" "testing" "time" @@ -147,6 +148,98 @@ func (w *commitWriter) CommitFiles(t *testing.T, message string, files map[strin return sha.String() } +// Tag creates a lightweight tag at the current worktree HEAD and pushes it. +func (w *commitWriter) Tag(t *testing.T, name string) { + t.Helper() + w.ensureWorktree(t) + repo, err := git.PlainOpen(w.worktree) + if err != nil { + t.Fatalf("open worktree: %v", err) + } + head, err := repo.Head() + if err != nil { + t.Fatalf("head: %v", err) + } + if _, err := repo.CreateTag(name, head.Hash(), nil); err != nil { + t.Fatalf("create tag %s: %v", name, err) + } + if err := repo.Push(&git.PushOptions{ + RemoteName: "origin", + RefSpecs: []config.RefSpec{ + config.RefSpec("refs/tags/" + name + ":refs/tags/" + name), + }, + }); err != nil { + t.Fatalf("push tag %s: %v", name, err) + } +} + +// legacyClone reproduces what pre-NoTags server versions wrote to disk: +// go-git's clone default was Tags:AllTags, so a shallow clone carried a full +// snapshot per tag. +func legacyClone(t *testing.T, upstream, dir, branch string) { + t.Helper() + _, err := git.PlainClone(dir, false, &git.CloneOptions{ + URL: "file://" + upstream, + ReferenceName: plumbing.NewBranchReferenceName(branch), + SingleBranch: true, + Depth: 1, + Tags: git.AllTags, + }) + if err != nil { + t.Fatalf("legacy clone: %v", err) + } +} + +// legacyFetchReset reproduces the old update path: fetch(Depth:1)+hard reset +// without NoTags, persisting one more snapshot pack per call. +func legacyFetchReset(t *testing.T, dir, branch string) { + t.Helper() + repo, err := git.PlainOpen(dir) + if err != nil { + t.Fatalf("open %s: %v", dir, err) + } + err = repo.Fetch(&git.FetchOptions{ + RefSpecs: []config.RefSpec{config.RefSpec("+refs/heads/" + branch + ":refs/remotes/origin/" + branch)}, + Depth: 1, + Force: true, + }) + if err != nil && err != git.NoErrAlreadyUpToDate { + t.Fatalf("legacy fetch: %v", err) + } + ref, err := repo.Reference(plumbing.NewRemoteReferenceName("origin", branch), true) + if err != nil { + t.Fatalf("legacy resolve remote ref: %v", err) + } + wt, err := repo.Worktree() + if err != nil { + t.Fatalf("legacy worktree: %v", err) + } + if err := wt.Reset(&git.ResetOptions{Commit: ref.Hash(), Mode: git.HardReset}); err != nil { + t.Fatalf("legacy reset: %v", err) + } +} + +func tagRefCount(t *testing.T, dir string) int { + t.Helper() + repo, err := git.PlainOpen(dir) + if err != nil { + t.Fatalf("open %s: %v", dir, err) + } + refs, err := repo.References() + if err != nil { + t.Fatalf("references: %v", err) + } + defer refs.Close() + n := 0 + _ = refs.ForEach(func(ref *plumbing.Reference) error { + if strings.HasPrefix(ref.Name().String(), tagRefPrefix) { + n++ + } + return nil + }) + return n +} + // initialCloneFor runs a full CloneOrFetch (first-time clone path) so // subsequent calls go through the reuse/fetch branch. func initialCloneFor(t *testing.T, upstream, localDir, branch string) Result { @@ -397,41 +490,168 @@ func TestCloneOrFetch_RemoteURLChanged_Reclones(t *testing.T) { } } -func TestCloneOrFetch_PackfileBudget_Reclones(t *testing.T) { +func TestCloneOrFetch_FreshClone_HasNoTags(t *testing.T) { upstream, w := makeBareUpstream(t, "main") - w.CommitFiles(t, "init", map[string]string{"a.go": "package a\n"}) + w.CommitFiles(t, "v1", map[string]string{"a.go": "package a\n"}) + w.Tag(t, "v1.0.0") + w.CommitFiles(t, "v2", map[string]string{"a.go": "package a // v2\n"}) + w.Tag(t, "v2.0.0") local := filepath.Join(t.TempDir(), "clone") initialCloneFor(t, upstream, local, "main") - // Pad the object store up to the budget with dummy packs — the count is - // what matters, not the content. - packDir := filepath.Join(local, ".git", "objects", "pack") - for i := packfileCount(local); i < maxFetchPacks; i++ { - name := filepath.Join(packDir, fmt.Sprintf("pack-%040d.pack", i)) - if err := os.WriteFile(name, []byte("dummy"), 0o644); err != nil { - t.Fatalf("write dummy pack: %v", err) - } + if n := tagRefCount(t, local); n != 0 { + t.Errorf("fresh clone carries %d tag refs, want 0 (Tags:NoTags)", n) } +} + +// TestCloneOrFetch_UpgradeCompactsLegacyCheckout is the no-explicit-migration +// upgrade path: a checkout produced by a PRE-NoTags server (AllTags clone, +// accumulated fetch packs) must be cleaned by the FIRST CloneOrFetch the +// upgraded server runs on it — tags dropped, packs collapsed to one, disk +// reclaimed — while the incremental diff for that same update still computes. +func TestCloneOrFetch_UpgradeCompactsLegacyCheckout(t *testing.T) { + upstream, w := makeBareUpstream(t, "main") + w.CommitFiles(t, "v1", map[string]string{ + "a.go": "package a\n", + "keep.go": "package keep\n", + "assets/big": strings.Repeat("payload ", 4096), + }) + w.Tag(t, "r1") + w.CommitFiles(t, "v2", map[string]string{"a.go": "package a // v2\n"}) + w.Tag(t, "r2") - headSHA := w.CommitFiles(t, "v2", map[string]string{"b.go": "package b\n"}) + // What the OLD server left on disk: AllTags shallow clone plus two + // fetch+reset cycles, each of which persisted another snapshot pack. + local := filepath.Join(t.TempDir(), "clone") + legacyClone(t, upstream, local, "main") + w.CommitFiles(t, "v3", map[string]string{"b.go": "package b\n"}) + legacyFetchReset(t, local, "main") + indexedSHA := w.CommitFiles(t, "v4", map[string]string{"c.go": "package c\n"}) + legacyFetchReset(t, local, "main") + + if n := tagRefCount(t, local); n != 2 { + t.Fatalf("legacy checkout has %d tag refs, want 2 — test setup broken", n) + } + if n := packfileCount(local); n < 3 { + t.Fatalf("legacy checkout has %d packs, want >=3 — test setup broken", n) + } + objectsBefore := objectsDirSize(local) + // Server upgrades. The next upstream push triggers an ordinary update — + // and that first update must clean the store. + newSHA := w.CommitFiles(t, "v5", map[string]string{"c.go": "package c // v5\n"}) res, err := CloneOrFetch(context.Background(), CloneOptions{ - GitHubURL: "file://" + upstream, - Branch: "main", - LocalDir: local, + GitHubURL: "file://" + upstream, + Branch: "main", + LocalDir: local, + PrevIndexedSHA: indexedSHA, }) if err != nil { - t.Fatalf("CloneOrFetch over pack budget: %v", err) + t.Fatalf("first post-upgrade CloneOrFetch: %v", err) } - if res.RecloneReason == "" { - t.Error("RecloneReason empty, want the packfile-budget reason") + if res.HeadSHA != newSHA { + t.Errorf("HeadSHA = %s, want %s", res.HeadSHA, newSHA) } - if res.HeadSHA != headSHA { - t.Errorf("HeadSHA = %s, want %s", res.HeadSHA, headSHA) + if res.RecloneReason != "" { + t.Errorf("RecloneReason = %q — the upgrade path must compact in place, not re-clone", res.RecloneReason) + } + if res.Compaction == nil { + t.Fatal("Compaction stats nil — legacy checkout was not compacted on first update") + } + if res.Compaction.TagRefsDropped != 2 { + t.Errorf("TagRefsDropped = %d, want 2", res.Compaction.TagRefsDropped) + } + if n := tagRefCount(t, local); n != 0 { + t.Errorf("%d tag refs survive the upgrade compaction, want 0", n) + } + if n := packfileCount(local); n != 1 { + t.Errorf("packfileCount = %d after compaction, want 1", n) + } + if after := objectsDirSize(local); after >= objectsBefore { + t.Errorf("objects dir did not shrink: %d -> %d bytes", objectsBefore, after) + } + // The very update that compacted must still deliver the incremental + // change set (v4 -> v5, computed before the reset). + if res.Changes == nil { + t.Fatal("Changes nil across the compacting update, want incremental diff") + } + if got := sortedCopy(res.Changes.Modified); !equalSlices(got, []string{"c.go"}) { + t.Errorf("Modified = %v, want [c.go]", got) + } + + // The protected diff base must survive compaction: pretend the index + // job after the upgrade never completed (indexed_sha still v4), push + // again, and demand a v4-based diff. + newestSHA := w.CommitFiles(t, "v6", map[string]string{"a.go": "package a // v6\n"}) + res2, err := CloneOrFetch(context.Background(), CloneOptions{ + GitHubURL: "file://" + upstream, + Branch: "main", + LocalDir: local, + PrevIndexedSHA: indexedSHA, + }) + if err != nil { + t.Fatalf("second post-upgrade CloneOrFetch: %v", err) + } + if res2.HeadSHA != newestSHA { + t.Errorf("HeadSHA = %s, want %s", res2.HeadSHA, newestSHA) + } + if res2.Changes == nil { + t.Error("Changes nil — protected indexed_sha did not survive compaction") + } + + // And a quiet no-op cycle afterwards: nothing left to clean. + res3, err := CloneOrFetch(context.Background(), CloneOptions{ + GitHubURL: "file://" + upstream, + Branch: "main", + LocalDir: local, + PrevIndexedSHA: newestSHA, + }) + if err != nil { + t.Fatalf("no-op CloneOrFetch: %v", err) + } + if !res3.NoChanges { + t.Error("NoChanges = false on an unchanged upstream") + } + if res3.Compaction != nil { + t.Error("Compaction ran on a clean checkout below the pack threshold") + } +} + +func TestCloneOrFetch_PackAccumulationStaysBounded(t *testing.T) { + upstream, w := makeBareUpstream(t, "main") + prev := w.CommitFiles(t, "init", map[string]string{"a.go": "package a\n"}) + + local := filepath.Join(t.TempDir(), "clone") + initialCloneFor(t, upstream, local, "main") + + compactions := 0 + for i := 1; i <= 2*compactPackThreshold; i++ { + sha := w.CommitFiles(t, fmt.Sprintf("push %d", i), map[string]string{ + "a.go": fmt.Sprintf("package a // rev %d\n", i), + }) + res, err := CloneOrFetch(context.Background(), CloneOptions{ + GitHubURL: "file://" + upstream, + Branch: "main", + LocalDir: local, + PrevIndexedSHA: prev, + }) + if err != nil { + t.Fatalf("cycle %d: %v", i, err) + } + if res.HeadSHA != sha { + t.Fatalf("cycle %d: HeadSHA = %s, want %s", i, res.HeadSHA, sha) + } + if res.Compaction != nil { + compactions++ + } + if n := packfileCount(local); n > compactPackThreshold { + t.Fatalf("cycle %d: %d packs on disk, bound is %d", i, n, compactPackThreshold) + } + prev = sha } - if n := packfileCount(local); n >= maxFetchPacks { - t.Errorf("packfileCount = %d after re-clone, want it back to a fresh clone's worth", n) + if compactions == 0 { + t.Errorf("no compaction ran across %d fetch cycles", 2*compactPackThreshold) } } diff --git a/server/internal/repojobs/repojobs.go b/server/internal/repojobs/repojobs.go index 8c236440..9e682b83 100644 --- a/server/internal/repojobs/repojobs.go +++ b/server/internal/repojobs/repojobs.go @@ -245,6 +245,13 @@ func handleClone(ctx context.Context, d Deps, job jobs.Job) error { d.Logger.Info("repojobs: checkout discarded and re-cloned", "project", g.ProjectPath, "reason", result.RecloneReason) } + if c := result.Compaction; c != nil { + d.Logger.Info("repojobs: checkout object store compacted", + "project", g.ProjectPath, + "bytes_before", c.ObjectsBefore, "bytes_after", c.ObjectsAfter, + "packs_deleted", c.PacksDeleted, "tag_refs_dropped", c.TagRefsDropped, + "objects", c.Reachable, "ms", c.Duration.Milliseconds()) + } if err := d.GitRepos.SetClone(ctx, g.ProjectPath, result.HeadSHA, ""); err != nil { d.Logger.Warn("repojobs: set last_sha failed", "project", g.ProjectPath, "err", err) From 942d6511cedb24566982d55f4ee2a39af32cfb5f Mon Sep 17 00:00:00 2001 From: dvcdsys Date: Tue, 18 Aug 2026 18:38:20 +0100 Subject: [PATCH 3/3] fix(server): address review findings 1-10 on compaction PR MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review (adversarial, 8 CONFIRMED + 2 PLAUSIBLE) accepted in full; every finding fixed, plus the runner-ups. Mapping: 1. Failed compaction no longer nukes the checkout. Compaction moved out of CloneOrFetch entirely into exported MaybeCompact; repojobs calls it after the clone section, logs a Warn on error and moves on — the store is exactly what the update left (valid), and the trigger re-fires next cycle. Nuke+reclone is reserved for the errLocalState taxonomy. 2. No more global-mutex-inside-repo-lock convoy. MaybeCompact acquires the global compaction gate FIRST, then takes the per-repo write lock (passed in by repojobs as a closure) only around the store mutation. A job queued behind another repo's compaction now holds no locks at all, so readers of its repo proceed; upgrade-day fleets serialise on the gate without stalling each other's reads. 3. NoChanges no longer trusts HEAD: updateExisting performs the hard reset on the NoChanges path too (a no-op write on a clean tree), repairing the torn-worktree state a crash mid-reset leaves behind (go-git writes HEAD before touching files). Test: NoChangesStillRepairsWorktree. 4. Crash-safe trigger ordering: tag refs are now dropped AFTER the new pack is durable and BEFORE old packs are deleted (a ref never dangles over a missing object). The remaining window — tags dropped, old packs still present — is re-armed by a new needsCompaction backstop: store >= 2x worktree (>=2 packs, 1MB floor). Test: RatioBackstopRearms simulates exactly the crashed state. 5. compactCheckout takes ctx: checked between phases and every 1024 objects inside the walk; cancellation aborts cleanly before any deletion. MaybeCompact also re-checks after waiting on the gate. Test: CancelledContext. 6. Pending index target protected: Result gains PrevHeadSHA (the on-disk HEAD before the fetch — the TargetSHA of a possibly still-queued index job) and repojobs passes {IndexedSHA, PrevHeadSHA} to the protect set. Test: ProtectsPendingIndexTarget reproduces the two-cycle race. 7. Protect probe distinguishes not-found from store errors: errors.Is(ErrObjectNotFound) -> skip; anything else -> loud failure. Same fix applied to the walker's parent probe. 8. errLocalState gets one retry before the nuke: transient EMFILE/EIO-class failures heal on the second attempt, a genuinely broken checkout fails identically and still recloses. (Existing half-written-clone test covers the broken path through the retry.) 9. DirSizeBytes undercounts are now visible: dirSizeDetail returns a skipped-entry count, DiskUsage gains partial:true (openapi.yaml + openapi-gen regenerated; maintenance.Usage serialises straight to the wire) and computeUsage logs a Warn naming the disk and skipped count. 10. Reachability walk converted from recursion to an iterative worklist — stack depth no longer scales with commit-chain length, so a full-history clone seeded into the repos dir cannot blow the goroutine stack. Runner-ups: the seven errLocalState wraps use double-%w so the cause chain survives errors.Is/As; refs are read in ONE pass (tags collected and roots seeded together); needsCompaction's ratio walk is gated on pack count >= 2 so the steady state never pays it. Not taken (documented deliberately): clone-into-temp+rename for reclone — reclone is now confined to genuinely-unusable-state paths where the old checkout has no value; the review marked it optional. Full server suite, vet, and openapi-gen sync green. Co-Authored-By: Claude Fable 5 --- doc/openapi.yaml | 6 + .../internal/httpapi/openapi/openapi.gen.go | 1384 +++++++++-------- server/internal/maintenance/dirsize_test.go | 5 +- server/internal/maintenance/maintenance.go | 19 +- server/internal/maintenance/usage.go | 24 +- server/internal/repocloner/compact.go | 370 +++-- server/internal/repocloner/repocloner.go | 115 +- server/internal/repocloner/repocloner_test.go | 329 +++- server/internal/repojobs/repojobs.go | 23 +- 9 files changed, 1317 insertions(+), 958 deletions(-) diff --git a/doc/openapi.yaml b/doc/openapi.yaml index f915431d..4a509ebb 100644 --- a/doc/openapi.yaml +++ b/doc/openapi.yaml @@ -4137,6 +4137,12 @@ components: Size of the tree. Absent when it could not be walked, which keeps "unreadable" distinguishable from "empty". The SQLite entry includes the -wal and -shm sidecars. + partial: + type: boolean + description: | + True when used_bytes undercounts because some entries inside the + tree were unreadable and skipped. Absent means the sum is + complete. fs_total_bytes: { type: integer, format: int64 } fs_free_bytes: { type: integer, format: int64 } diff --git a/server/internal/httpapi/openapi/openapi.gen.go b/server/internal/httpapi/openapi/openapi.gen.go index 639d9e72..0767a364 100644 --- a/server/internal/httpapi/openapi/openapi.gen.go +++ b/server/internal/httpapi/openapi/openapi.gen.go @@ -1528,7 +1528,12 @@ type DiskUsage struct { FsTotalBytes *int64 `json:"fs_total_bytes,omitempty"` Id DiskUsageId `json:"id"` Label string `json:"label"` - Path string `json:"path"` + + // Partial True when used_bytes undercounts because some entries inside the + // tree were unreadable and skipped. Absent means the sum is + // complete. + Partial *bool `json:"partial,omitempty"` + Path string `json:"path"` // UsedBytes Size of the tree. Absent when it could not be walked, which keeps // "unreadable" distinguishable from "empty". The SQLite entry @@ -8042,694 +8047,695 @@ func HandlerWithOptions(si ServerInterface, options ChiServerOptions) http.Handl // const string: with thousands of chunks the chained `+` fold is several // times slower for the Go compiler than parsing a slice literal. var swaggerSpec = []string{ - "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", } // decodeSpec returns the embedded OpenAPI spec as raw JSON bytes, diff --git a/server/internal/maintenance/dirsize_test.go b/server/internal/maintenance/dirsize_test.go index 2eb8f5da..e5e34e3a 100644 --- a/server/internal/maintenance/dirsize_test.go +++ b/server/internal/maintenance/dirsize_test.go @@ -45,13 +45,16 @@ func TestDirSizeBytes_UnreadableSubtree_ReturnsPartial(t *testing.T) { } t.Cleanup(func() { _ = os.Chmod(locked, 0o755) }) - n, ok := DirSizeBytes(context.Background(), dir) + n, skipped, ok := dirSizeDetail(context.Background(), dir) if !ok { t.Fatal("ok = false — one unreadable subtree must not throw the whole number away") } if n != 100 { t.Errorf("total = %d, want 100 (the readable part)", n) } + if skipped == 0 { + t.Error("skipped = 0 — the undercount must be visible so DiskUsage can flag it as partial") + } } func TestDirSizeBytes_CancelledContext_ReportsNotOK(t *testing.T) { diff --git a/server/internal/maintenance/maintenance.go b/server/internal/maintenance/maintenance.go index 0afcdb1a..26a8948a 100644 --- a/server/internal/maintenance/maintenance.go +++ b/server/internal/maintenance/maintenance.go @@ -224,6 +224,8 @@ type Analysis struct { // vanish. Returns (partial, false) only when nothing trustworthy could be // produced: the root itself is missing/unreadable (so "unreadable" and // "empty" stay distinguishable) or the context was cancelled mid-walk. +// Callers that need to tell a complete sum from an undercount use +// dirSizeDetail, which also reports how many entries were skipped. // // The context is checked every so many entries: on a vector store that is one // file per document these walks visit hundreds of thousands of entries, and a @@ -233,17 +235,22 @@ type Analysis struct { // Lives here rather than in httpapi because both the resource endpoints and // the project-detail card need it and there must be exactly one copy. func DirSizeBytes(ctx context.Context, dir string) (int64, bool) { - var total int64 + n, _, ok := dirSizeDetail(ctx, dir) + return n, ok +} + +func dirSizeDetail(ctx context.Context, dir string) (total int64, skipped int, ok bool) { var seen int walkErr := filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error { if err != nil { // Root failure means there is nothing to report; anything - // deeper is one bad subtree — skip it and keep counting. - // (WalkDir already skips the children of a directory it - // could not read.) + // deeper is one bad subtree — count it as skipped and keep + // walking. (WalkDir already skips the children of a directory + // it could not read.) if path == dir { return err } + skipped++ return nil } // Checking every entry would make ctx.Err() a meaningful share of the @@ -264,9 +271,9 @@ func DirSizeBytes(ctx context.Context, dir string) (int64, bool) { return nil }) if walkErr != nil { - return total, false + return total, skipped, false } - return total, true + return total, skipped, true } // dirSizeOrZero is the convenience form for places that already know the diff --git a/server/internal/maintenance/usage.go b/server/internal/maintenance/usage.go index 07a621b0..af1b86ea 100644 --- a/server/internal/maintenance/usage.go +++ b/server/internal/maintenance/usage.go @@ -23,7 +23,12 @@ type DiskUsage struct { Exists bool `json:"exists"` // UsedBytes is omitted rather than zeroed when the tree could not be // walked, so "unreadable" and "empty" stay distinguishable. - UsedBytes *int64 `json:"used_bytes,omitempty"` + UsedBytes *int64 `json:"used_bytes,omitempty"` + // Partial marks a UsedBytes that undercounts: some entries could not be + // read and were skipped. Without this flag a single root-owned checkout + // makes the row show a confident wrong number — exactly what an + // operator chasing disk growth must not rule out. + Partial bool `json:"partial,omitempty"` FSTotalBytes *int64 `json:"fs_total_bytes,omitempty"` FSFreeBytes *int64 `json:"fs_free_bytes,omitempty"` } @@ -125,27 +130,32 @@ func (s *Service) computeUsage(ctx context.Context) Usage { // pre-migration gob files are reported (and reclaimed) through the // abandoned-namespace category instead. if cfg.VectorsDir != "" { - out.Disks = append(out.Disks, walkedDisk(ctx, DiskChroma, "Vector store", cfg.VectorsDir)) + out.Disks = append(out.Disks, s.walkedDisk(ctx, DiskChroma, "Vector store", cfg.VectorsDir)) } else if cfg.ChromaPersistDir != "" { - out.Disks = append(out.Disks, walkedDisk(ctx, DiskChroma, "Vector store", cfg.ChromaPersistDir)) + out.Disks = append(out.Disks, s.walkedDisk(ctx, DiskChroma, "Vector store", cfg.ChromaPersistDir)) } if root := s.reposRoot(); root != "" { - out.Disks = append(out.Disks, walkedDisk(ctx, DiskRepos, "Cloned repositories", root)) + out.Disks = append(out.Disks, s.walkedDisk(ctx, DiskRepos, "Cloned repositories", root)) } if dir := s.activeGGUFCacheDir(); dir != "" { - out.Disks = append(out.Disks, walkedDisk(ctx, DiskGGUF, "Model cache", dir)) + out.Disks = append(out.Disks, s.walkedDisk(ctx, DiskGGUF, "Model cache", dir)) } return out } -func walkedDisk(ctx context.Context, id, label, path string) DiskUsage { +func (s *Service) walkedDisk(ctx context.Context, id, label, path string) DiskUsage { d := DiskUsage{ID: id, Label: label, Path: path} if _, err := os.Stat(path); err == nil { d.Exists = true if !ctxDone(ctx) { - if n, ok := DirSizeBytes(ctx, path); ok { + if n, skipped, ok := dirSizeDetail(ctx, path); ok { d.UsedBytes = &n + if skipped > 0 { + d.Partial = true + s.d.Logger.Warn("maintenance: disk usage undercounts — entries were unreadable", + "disk", id, "path", path, "skipped_entries", skipped) + } } } } diff --git a/server/internal/repocloner/compact.go b/server/internal/repocloner/compact.go index f074a807..c2357ca0 100644 --- a/server/internal/repocloner/compact.go +++ b/server/internal/repocloner/compact.go @@ -14,30 +14,39 @@ package repocloner // a 39 MB worktree) that cix, which indexes exactly one branch, never reads. // // Compaction rewrites the store down to what the server actually uses: -// it drops refs/tags/*, walks the objects reachable from the remaining refs -// (honouring .git/shallow graft points exactly like git does) plus any -// explicitly protected commits, encodes that set into one new pack, deletes -// the old packs and loose objects, and rewrites .git/shallow to the entries -// that still exist. `git fsck --strict` is clean afterwards and the worktree -// is untouched — validated byte-for-byte against full-history canonical -// clones on 45 real checkouts (spring-boot, grafana, …) by the PoC on branch -// poc/gc-compaction (server/cmd/gc-poc). +// it walks the objects reachable from the non-tag refs (honouring +// .git/shallow graft points exactly like git does) plus any explicitly +// protected commits, encodes that set into one new pack, drops refs/tags/*, +// deletes the old packs and loose objects, and rewrites .git/shallow to the +// entries that still exist. `git fsck --strict` is clean afterwards and the +// worktree is untouched — validated byte-for-byte against full-history +// canonical clones on 45 real checkouts (spring-boot, grafana, …) by the PoC +// on branch poc/gc-compaction (server/cmd/gc-poc). // // Cost model, measured on those 45 checkouts: time is linear — // ~0.2–1.4 ms CPU per reachable object plus ~0.2 s per emitted GB (zlib); // memory is linear in the SNAPSHOT size (not the store size) at roughly 3× // the uncompressed content, because go-git's packfile encoder materialises // object data. A typical 60 MB checkout compacts in single-digit seconds -// within a few hundred MB of transient heap; compactMu keeps concurrent -// clone jobs from stacking those peaks. +// within a few hundred MB of transient heap; MaybeCompact's global gate +// keeps concurrent clone jobs from stacking those peaks. +// +// Crash-safety ordering inside compactCheckout: the new pack is durable +// before anything is deleted, and the tag refs are removed before the old +// packs go away (so a ref never dangles over a missing object). A crash at +// any point leaves either extra packs or dropped-tags-with-bloat — both +// states re-trigger needsCompaction (pack count and store/worktree ratio +// respectively) and heal on the next update. // // The delta window is 0 on purpose: after tags are dropped the reachable set // is essentially a single snapshot, and the PoC measured window=10 at −17% // pack size for 2.9× the CPU. import ( + "context" + "errors" "fmt" - "os" + "io/fs" "path/filepath" "strings" "sync" @@ -53,16 +62,30 @@ import ( ) // compactPackThreshold is how many packfiles a checkout may accumulate -// before the next CloneOrFetch rewrites the store. Each fetch adds one +// before the next update compacts the store. Each fetch adds one // snapshot-sized pack, so the steady-state disk overhead between compactions // is bounded by (threshold-1) worktree-sized packs, and the compaction cost // is amortised over that many pushes. const compactPackThreshold = 4 +// Ratio backstop: a store this many times larger than the worktree it +// serves (and above the floor) is carrying dead weight regardless of pack +// count. This is what re-arms cleanup for a checkout whose tag refs were +// dropped by a compaction that crashed before deleting the old packs — pack +// count alone would never fire again on a quiet repo. A healthy compacted +// store is zlib-compressed and smaller than its worktree, so legitimate +// checkouts sit far below 2×. +const ( + compactRatioTrigger = 2 + compactRatioFloor = 1 << 20 // ignore ratio noise on tiny stores +) + // compactMu serialises compactions across concurrent clone jobs. The // transient heap of one compaction is ~3× the repo's uncompressed snapshot; // letting several worker goroutines pay that simultaneously is how an 8 GB -// host gets OOM-killed. +// host gets OOM-killed. MaybeCompact acquires it BEFORE the caller-supplied +// per-repo write lock, so a job queued on this mutex never stalls another +// repo's readers. var compactMu sync.Mutex const tagRefPrefix = "refs/tags/" @@ -79,14 +102,73 @@ type CompactStats struct { Duration time.Duration } +// MaybeCompact compacts dir's object store when it needs it (see +// needsCompaction) and reports what it did; (nil, nil) means "nothing to +// do". withWrite, when non-nil, must serialise the on-disk mutation against +// concurrent readers of this checkout (repojobs passes RepoLocks.WithWrite); +// it is acquired AFTER the global compaction gate, so waiting for another +// repo's compaction never happens while holding this repo's lock. +// +// A compaction error leaves the checkout exactly as the preceding update +// left it — valid — so callers must NOT treat it as reason to discard the +// checkout: log it and move on; the trigger re-fires on the next update. +// +// protect lists commit SHAs that must survive even though no ref points at +// them: git_repos.indexed_sha (the base of the next incremental tree-diff) +// and the pre-fetch HEAD (the target of a possibly still-queued index job). +// Empty strings and SHAs absent from the store are skipped. +func MaybeCompact(ctx context.Context, dir string, withWrite func(func() error) error, protect ...string) (*CompactStats, error) { + if ctx.Err() != nil || !needsCompaction(dir) { + return nil, nil + } + var hashes []plumbing.Hash + for _, s := range protect { + if s = strings.TrimSpace(s); s != "" { + hashes = append(hashes, plumbing.NewHash(s)) + } + } + + compactMu.Lock() + defer compactMu.Unlock() + // The queue on compactMu can be long on upgrade day; don't start work + // for a request that is already gone. + if err := ctx.Err(); err != nil { + return nil, err + } + + var st CompactStats + run := func() error { + var err error + st, err = compactCheckout(ctx, dir, hashes...) + return err + } + var err error + if withWrite != nil { + err = withWrite(run) + } else { + err = run() + } + if err != nil { + return nil, err + } + return &st, nil +} + // needsCompaction reports whether the checkout's object store warrants a -// rewrite: enough accumulated fetch packs, or tag refs left behind by -// pre-NoTags server versions (their snapshots dominate the store, and with -// Tags:NoTags on every fetch they will not come back). The tag check makes -// the first post-upgrade update of every existing checkout clean it — there -// is deliberately no separate migration. +// rewrite. Three triggers: +// +// - packfileCount ≥ compactPackThreshold: accumulated fetch packs. +// - tag refs present: snapshots left behind by pre-NoTags server versions. +// Their objects dominate the store, and with Tags:NoTags on every fetch +// they will not come back — this makes the first post-upgrade update of +// every existing checkout clean it, with no separate migration. +// - store ≥ compactRatioTrigger × worktree (packs ≥ 2 only): the backstop +// that re-arms cleanup after a crash mid-compaction dropped the tag refs +// without reclaiming their objects. Gated on pack count so the worktree +// walk is not paid on the common single-pack steady state. func needsCompaction(dir string) bool { - if packfileCount(dir) >= compactPackThreshold { + packs := packfileCount(dir) + if packs >= compactPackThreshold { return true } repo, err := git.PlainOpen(dir) @@ -97,7 +179,6 @@ func needsCompaction(dir string) bool { if err != nil { return false } - defer refs.Close() found := false _ = refs.ForEach(func(ref *plumbing.Reference) error { if strings.HasPrefix(ref.Name().String(), tagRefPrefix) { @@ -106,23 +187,24 @@ func needsCompaction(dir string) bool { } return nil }) - return found + refs.Close() + if found { + return true + } + if packs >= 2 { + if objects := objectsDirSize(dir); objects > compactRatioFloor { + return objects >= compactRatioTrigger*worktreeSize(dir) + } + } + return false } // compactCheckout rewrites dir's object store down to the objects reachable -// from its non-tag references plus the protected commits. protect carries -// commits no ref points at that must survive — in practice -// git_repos.indexed_sha, the base of the next incremental tree-diff; entries -// that are zero or absent from the store are skipped. -// -// Failure modes are safe by construction: the new pack is durable before any -// old pack is deleted, so a crash mid-compaction leaves extra packs for the -// next run, never a store missing objects. The caller handles a returned -// error by discarding the checkout and re-cloning. -func compactCheckout(dir string, protect ...plumbing.Hash) (CompactStats, error) { - compactMu.Lock() - defer compactMu.Unlock() - +// from its non-tag references plus the protected commits, then drops the tag +// refs. See the package comment for the crash-safety ordering. The context +// is honoured between phases and inside the walk; cancellation before the +// deletion phase leaves the store untouched (bar an extra pack). +func compactCheckout(ctx context.Context, dir string, protect ...plumbing.Hash) (CompactStats, error) { started := time.Now() st := CompactStats{ObjectsBefore: objectsDirSize(dir)} @@ -131,31 +213,46 @@ func compactCheckout(dir string, protect ...plumbing.Hash) (CompactStats, error) return st, fmt.Errorf("open: %w", err) } - // 1. Drop tag refs. cix serves exactly one branch; every tag is a whole - // retained snapshot the server never reads. + // 1. One pass over the refs: non-tag hash refs seed the walk, tag refs + // are remembered for deletion later. cix serves exactly one branch; + // every tag is a whole retained snapshot the server never reads. + var roots []plumbing.Hash + var tagRefs []plumbing.ReferenceName refs, err := repo.References() if err != nil { return st, err } - var tagRefs []plumbing.ReferenceName err = refs.ForEach(func(ref *plumbing.Reference) error { if strings.HasPrefix(ref.Name().String(), tagRefPrefix) { tagRefs = append(tagRefs, ref.Name()) + return nil + } + if ref.Type() == plumbing.HashReference { + roots = append(roots, ref.Hash()) } return nil }) + refs.Close() if err != nil { return st, err } - for _, name := range tagRefs { - if err := repo.Storer.RemoveReference(name); err != nil { - return st, fmt.Errorf("remove tag ref %s: %w", name, err) + for _, h := range protect { + if h.IsZero() { + continue } - st.TagRefsDropped++ + // Absent is fine (already gc'd away, or a bogus SHA — nothing to + // protect); any OTHER failure is a store problem the caller must + // hear about, not a silent loss of the diff base. + if _, gerr := object.GetObject(repo.Storer, h); gerr != nil { + if errors.Is(gerr, plumbing.ErrObjectNotFound) { + continue + } + return st, fmt.Errorf("probe protected %s: %w", h, gerr) + } + roots = append(roots, h) } - // 2. Reachability walk from the remaining refs + protected commits, - // with git's shallow semantics. + // 2. Reachability walk with git's shallow semantics. shallowList, err := repo.Storer.Shallow() if err != nil { return st, fmt.Errorf("read shallow: %w", err) @@ -164,38 +261,18 @@ func compactCheckout(dir string, protect ...plumbing.Hash) (CompactStats, error) for _, h := range shallowList { shallowSet[h] = struct{}{} } - w := &objectWalker{storer: repo.Storer, shallow: shallowSet, seen: map[plumbing.Hash]struct{}{}} - refs, err = repo.References() - if err != nil { - return st, err - } - err = refs.ForEach(func(ref *plumbing.Reference) error { - if ref.Type() != plumbing.HashReference { - return nil - } - return w.walk(ref.Hash()) - }) + seen, err := walkReachable(ctx, repo.Storer, roots, shallowSet) if err != nil { return st, fmt.Errorf("reachability walk: %w", err) } - for _, h := range protect { - if h.IsZero() { - continue - } - if _, gerr := object.GetObject(repo.Storer, h); gerr != nil { - // Not in the store (already gc'd away, or a bogus SHA) — - // nothing to protect. - continue - } - if err := w.walk(h); err != nil { - return st, fmt.Errorf("walk protected %s: %w", h, err) - } - } - st.Reachable = len(w.seen) - objs := make([]plumbing.Hash, 0, len(w.seen)) - for h := range w.seen { + st.Reachable = len(seen) + objs := make([]plumbing.Hash, 0, len(seen)) + for h := range seen { objs = append(objs, h) } + if err := ctx.Err(); err != nil { + return st, err + } // 3. Write the reachable set as one new pack. PackfileWriter lands it in // objects/pack with a proper idx before we touch anything old. @@ -224,8 +301,22 @@ func compactCheckout(dir string, protect ...plumbing.Hash) (CompactStats, error) if err != nil { return st, fmt.Errorf("encode pack: %w", err) } + if err := ctx.Err(); err != nil { + return st, err + } + + // 4. The new pack is durable — now drop the tag refs, BEFORE the old + // packs: a tag ref must never outlive its objects (fetch negotiation + // advertises refs as haves), and the reverse crash window — tags + // gone, bloat still on disk — is re-armed by the ratio trigger. + for _, name := range tagRefs { + if err := repo.Storer.RemoveReference(name); err != nil { + return st, fmt.Errorf("remove tag ref %s: %w", name, err) + } + st.TagRefsDropped++ + } - // 4. Only now that the new pack is durable: delete the old ones. + // 5. Delete the old packs. for _, h := range oldPacks { if h == newPack { continue @@ -236,7 +327,7 @@ func compactCheckout(dir string, protect ...plumbing.Hash) (CompactStats, error) st.PacksDeleted++ } - // 5. Loose objects: everything reachable is in the new pack, so every + // 6. Loose objects: everything reachable is in the new pack, so every // loose object is redundant regardless of reachability. if los, ok := repo.Storer.(storer.LooseObjectStorer); ok { err = los.ForEachObjectHash(func(h plumbing.Hash) error { @@ -251,12 +342,12 @@ func compactCheckout(dir string, protect ...plumbing.Hash) (CompactStats, error) } } - // 6. .git/shallow gains one graft entry per fetch; entries whose commit + // 7. .git/shallow gains one graft entry per fetch; entries whose commit // was just dropped would make real git tooling error out ("did not // find object for shallow …"), so keep only entries still present. kept := shallowList[:0] for _, h := range shallowList { - if _, reachable := w.seen[h]; reachable { + if _, reachable := seen[h]; reachable { kept = append(kept, h) } } @@ -271,8 +362,11 @@ func compactCheckout(dir string, protect ...plumbing.Hash) (CompactStats, error) return st, nil } -// objectWalker collects the reachable object set. It is go-git's own -// objectWalker (repository.go uses it for RepackObjects) with three +// walkReachable collects every object reachable from roots. It is go-git's +// objectWalker (repository.go uses it for RepackObjects) reworked into an +// iterative worklist — recursion depth would otherwise equal the contiguous +// commit-chain length, and a full-history clone manually seeded into the +// repos dir must not be able to blow the goroutine stack — with three // behavioural fixes, each of which real checkouts hit immediately: // // - commits listed in .git/shallow are graft points whose parents are @@ -284,71 +378,93 @@ func compactCheckout(dir string, protect ...plumbing.Hash) (CompactStats, error) // a different repository. (Stock go-git crashes.) // - blobs reached as objects (via symlink and other non-regular-file tree // entries) are accepted leaves. (Stock go-git errors "unknown object".) -type objectWalker struct { - storer storage.Storer - shallow map[plumbing.Hash]struct{} - seen map[plumbing.Hash]struct{} -} - -func (w *objectWalker) walk(hash plumbing.Hash) error { - if _, ok := w.seen[hash]; ok { - return nil - } - obj, err := object.GetObject(w.storer, hash) - if err != nil { - return fmt.Errorf("get object %s: %w", hash, err) - } - w.seen[hash] = struct{}{} - switch obj := obj.(type) { - case *object.Commit: - if err := w.walk(obj.TreeHash); err != nil { - return err - } - if _, grafted := w.shallow[obj.Hash]; grafted { - break +func walkReachable(ctx context.Context, s storage.Storer, roots []plumbing.Hash, shallow map[plumbing.Hash]struct{}) (map[plumbing.Hash]struct{}, error) { + seen := make(map[plumbing.Hash]struct{}) + stack := make([]plumbing.Hash, len(roots)) + copy(stack, roots) + visited := 0 + for len(stack) > 0 { + hash := stack[len(stack)-1] + stack = stack[:len(stack)-1] + if _, ok := seen[hash]; ok { + continue } - for _, p := range obj.ParentHashes { - if _, ok := w.seen[p]; ok { - continue - } - // A parent this shallow store never fetched: boundary, not error. - if _, gerr := object.GetObject(w.storer, p); gerr == plumbing.ErrObjectNotFound { - continue - } - if err := w.walk(p); err != nil { - return err + // Keep cancellation prompt without paying ctx.Err() per object. + if visited++; visited%1024 == 0 { + if err := ctx.Err(); err != nil { + return nil, err } } - case *object.Tree: - for i := range obj.Entries { - e := obj.Entries[i] - if e.Mode == filemode.Submodule { + obj, err := object.GetObject(s, hash) + if err != nil { + return nil, fmt.Errorf("get object %s: %w", hash, err) + } + seen[hash] = struct{}{} + switch obj := obj.(type) { + case *object.Commit: + stack = append(stack, obj.TreeHash) + if _, grafted := shallow[obj.Hash]; grafted { continue } - if e.Mode|0o755 == filemode.Executable { // plain blob, any file mode - w.seen[e.Hash] = struct{}{} - continue + for _, p := range obj.ParentHashes { + if _, ok := seen[p]; ok { + continue + } + // A parent this shallow store never fetched: boundary, + // not error. + if _, gerr := object.GetObject(s, p); gerr != nil { + if errors.Is(gerr, plumbing.ErrObjectNotFound) { + continue + } + return nil, fmt.Errorf("probe parent %s: %w", p, gerr) + } + stack = append(stack, p) } - if err := w.walk(e.Hash); err != nil { - return err + case *object.Tree: + for i := range obj.Entries { + e := obj.Entries[i] + if e.Mode == filemode.Submodule { + continue + } + if e.Mode|0o755 == filemode.Executable { // plain blob, any file mode + seen[e.Hash] = struct{}{} + continue + } + stack = append(stack, e.Hash) } + case *object.Blob: + // Leaf. + case *object.Tag: + stack = append(stack, obj.Target) + default: + return nil, fmt.Errorf("unknown object type %T at %s", obj, hash) } - case *object.Blob: - // Leaf. - case *object.Tag: - return w.walk(obj.Target) - default: - return fmt.Errorf("unknown object type %T at %s", obj, hash) - } - return nil + } + return seen, nil } // objectsDirSize sums .git/objects — a few packs plus loose files, so the // walk is cheap. Best effort; 0 on error. func objectsDirSize(dir string) int64 { + return treeSize(filepath.Join(dir, ".git", "objects"), false) +} + +// worktreeSize sums the checkout's payload, excluding .git. Only consulted +// by the ratio backstop, which is gated on pack count ≥ 2. +func worktreeSize(dir string) int64 { + return treeSize(dir, true) +} + +func treeSize(dir string, skipDotGit bool) int64 { var total int64 - _ = filepath.WalkDir(filepath.Join(dir, ".git", "objects"), func(_ string, d os.DirEntry, err error) error { - if err != nil || d.IsDir() { + _ = filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error { + if err != nil { + return nil + } + if d.IsDir() { + if skipDotGit && d.Name() == ".git" { + return filepath.SkipDir + } return nil } if info, ierr := d.Info(); ierr == nil { diff --git a/server/internal/repocloner/repocloner.go b/server/internal/repocloner/repocloner.go index a15f9667..bd35625e 100644 --- a/server/internal/repocloner/repocloner.go +++ b/server/internal/repocloner/repocloner.go @@ -121,13 +121,16 @@ type Result struct { // caller can skip enqueueing an index_repo job entirely. NoChanges bool // RecloneReason is non-empty when an existing checkout was discarded - // and cloned fresh (unusable local state, changed remote URL, or a - // failed compaction). Purely informational — callers log it so the - // operator can see why a fetch turned into a full clone. + // and cloned fresh (unusable local state or a changed remote URL). + // Purely informational — callers log it so the operator can see why a + // fetch turned into a full clone. RecloneReason string - // Compaction is set when this call rewrote the checkout's object store - // (see compact.go). Purely informational — callers log it. - Compaction *CompactStats + // PrevHeadSHA is the commit that was on disk BEFORE this update moved + // the checkout (empty on a fresh clone). Callers pass it to + // MaybeCompact's protect list: it is the target of a possibly + // still-queued index job, and nothing else keeps it alive once the + // branch ref has moved on. + PrevHeadSHA string } // CloneOrFetch clones the repo when LocalDir is empty, otherwise fetches @@ -135,18 +138,19 @@ type Result struct { // after the operation completes. // // An existing checkout is discarded and cloned fresh (Result.RecloneReason -// says why) in three situations: its .git state is unusable — the half-clone -// a SIGKILL mid-clone leaves behind used to fail every retry forever; its -// origin URL no longer matches the requested one (github_url changed); or a -// compaction of its object store failed. Fetch/transport failures are NOT -// grounds for a re-clone — a network blip must not cost a healthy clone (and -// force the full reindex that follows one). +// says why) in two situations: its .git state is unusable — the half-clone +// a SIGKILL mid-clone leaves behind used to fail every retry forever — or +// its origin URL no longer matches the requested one (github_url changed). +// The local-state check is retried once first: transient filesystem pressure +// (EMFILE under concurrent jobs, a momentary EACCES) must not cost a +// multi-GB checkout, while a genuinely broken one fails the retry the same +// way. Fetch/transport failures are never grounds for a re-clone — a network +// blip must not cost a healthy clone (and force the full reindex that +// follows one). // -// After a successful update the checkout's object store is compacted when it -// needs it (accumulated fetch packs, or tag snapshots left behind by -// pre-NoTags server versions — which makes the first update after a server -// upgrade clean every existing checkout, with no separate migration). See -// compact.go for the mechanism and its measured costs. +// Compaction of the object store is NOT part of this call: callers run +// MaybeCompact afterwards, outside their per-repo write lock — see that +// function for why the locking is layered that way. // // The caller is responsible for choosing a LocalDir that won't collide // across repos — typically `/repos//` keyed by @@ -165,15 +169,19 @@ func CloneOrFetch(ctx context.Context, opts CloneOptions) (Result, error) { url := normaliseURL(opts.GitHubURL) auth := authFor(opts.PAT) - // First-time clone path: LocalDir is missing or empty. A fresh NoTags - // clone is one branch-snapshot pack — nothing to compact. + // First-time clone path: LocalDir is missing or empty. if needsClone(opts.LocalDir) { return cloneFresh(ctx, opts, url, auth) } res, err := updateExisting(ctx, opts, url, auth) + if err != nil && errors.Is(err, errLocalState) && ctx.Err() == nil { + // One retry before concluding the state is structural: EMFILE/EIO + // class failures heal, a half-written clone fails identically. + res, err = updateExisting(ctx, opts, url, auth) + } if err == nil { - return maybeCompact(ctx, opts, url, auth, res) + return res, nil } // Only local-state failures are recoverable by re-cloning, and never on // a dead context — a cancelled shutdown fetch is not evidence the @@ -184,32 +192,6 @@ func CloneOrFetch(ctx context.Context, opts CloneOptions) (Result, error) { return reclone(ctx, opts, url, auth, err.Error()) } -// maybeCompact runs the object-store compaction after a successful update -// when the checkout warrants it. It runs on the NoChanges path too: the -// post-upgrade cleanup of a tag-carrying checkout must not wait for the -// repo's next actual commit. A failed compaction falls back to nuke + -// re-clone — the store's state is unknown at that point, and a shallow -// re-clone is always correct (Changes degrade to nil, so the caller -// reconciles instead of diffing). -func maybeCompact(ctx context.Context, opts CloneOptions, url string, auth *http.BasicAuth, res Result) (Result, error) { - if ctx.Err() != nil || !needsCompaction(opts.LocalDir) { - return res, nil - } - var protect []plumbing.Hash - if s := strings.TrimSpace(opts.PrevIndexedSHA); s != "" { - // The indexed commit is the base of the NEXT incremental diff; no - // ref points at it once the branch has moved on, so it must be - // protected explicitly. - protect = append(protect, plumbing.NewHash(s)) - } - st, err := compactCheckout(opts.LocalDir, protect...) - if err != nil { - return reclone(ctx, opts, url, auth, fmt.Sprintf("compaction failed: %v", err)) - } - res.Compaction = &st - return res, nil -} - // cloneFresh is the first-time clone into an empty or missing LocalDir. func cloneFresh(ctx context.Context, opts CloneOptions, url string, auth *http.BasicAuth) (Result, error) { if err := os.MkdirAll(opts.LocalDir, 0o755); err != nil { @@ -262,10 +244,10 @@ func reclone(ctx context.Context, opts CloneOptions, url string, auth *http.Basi func updateExisting(ctx context.Context, opts CloneOptions, url string, auth *http.BasicAuth) (Result, error) { repo, err := git.PlainOpen(opts.LocalDir) if err != nil { - return Result{}, fmt.Errorf("%w: open existing repo at %s: %v", errLocalState, opts.LocalDir, err) + return Result{}, fmt.Errorf("%w: open existing repo at %s: %w", errLocalState, opts.LocalDir, err) } if err := ensureRemote(repo, url); err != nil { - return Result{}, fmt.Errorf("%w: %v", errLocalState, err) + return Result{}, fmt.Errorf("%w: %w", errLocalState, err) } // Snapshot the pre-fetch HEAD so we can short-circuit on NoChanges @@ -276,7 +258,7 @@ func updateExisting(ctx context.Context, opts CloneOptions, url string, auth *ht // clone (SIGKILL mid-PlainClone leaves .git without refs). prevHead, err := repo.Head() if err != nil { - return Result{}, fmt.Errorf("%w: resolve pre-fetch HEAD: %v", errLocalState, err) + return Result{}, fmt.Errorf("%w: resolve pre-fetch HEAD: %w", errLocalState, err) } err = repo.FetchContext(ctx, &git.FetchOptions{ @@ -294,17 +276,19 @@ func updateExisting(ctx context.Context, opts CloneOptions, url string, auth *ht remoteRef, err := repo.Reference(plumbing.NewRemoteReferenceName("origin", opts.Branch), true) if err != nil { - return Result{}, fmt.Errorf("%w: resolve remote ref: %v", errLocalState, err) + return Result{}, fmt.Errorf("%w: resolve remote ref: %w", errLocalState, err) } newSHA := remoteRef.Hash() - // No-op fetch: remote HEAD already matches what's on disk. Skip the - // reset (it would be a no-op anyway) and tell the caller there is - // nothing to reindex. NoChanges supersedes Changes — the caller - // should not enqueue an index job at all. - if prevHead.Hash() == newSHA { - return Result{HeadSHA: newSHA.String(), NoChanges: true}, nil - } + // No-op fetch: remote HEAD already matches what's on disk. Tell the + // caller there is nothing to reindex (NoChanges supersedes Changes — + // no index job should be enqueued) but still run the hard reset below: + // a crash mid-reset on a previous run leaves HEAD already pointing at + // newSHA over half-rewritten files, and this path is the only chance + // to repair that — go-git writes HEAD before it touches the worktree, + // so the torn state looks exactly like a completed update. On a clean + // worktree the reset writes nothing. + noChanges := prevHead.Hash() == newSHA // Best-effort change-set computation. Runs BEFORE the reset so // tree.Diff still sees both commits via their stored tree objects. @@ -312,7 +296,7 @@ func updateExisting(ctx context.Context, opts CloneOptions, url string, auth *ht // falls back to a full reindex. var changes *ChangeSet diffBase := strings.TrimSpace(opts.PrevIndexedSHA) - if diffBase != "" { + if diffBase != "" && !noChanges { cs, derr := computeChangeSet(repo, diffBase, newSHA.String()) if derr == nil { changes = cs @@ -325,7 +309,7 @@ func updateExisting(ctx context.Context, opts CloneOptions, url string, auth *ht wt, err := repo.Worktree() if err != nil { - return Result{}, fmt.Errorf("%w: worktree: %v", errLocalState, err) + return Result{}, fmt.Errorf("%w: worktree: %w", errLocalState, err) } // Hard reset — discards any local mutation that crept in. Worker-managed // checkouts have no human edits we'd want to preserve. The commit was @@ -334,14 +318,19 @@ func updateExisting(ctx context.Context, opts CloneOptions, url string, auth *ht Commit: newSHA, Mode: git.HardReset, }); err != nil { - return Result{}, fmt.Errorf("%w: reset: %v", errLocalState, err) + return Result{}, fmt.Errorf("%w: reset: %w", errLocalState, err) } head, err := repo.Head() if err != nil { - return Result{}, fmt.Errorf("%w: resolve HEAD post-reset: %v", errLocalState, err) - } - return Result{HeadSHA: head.Hash().String(), Changes: changes}, nil + return Result{}, fmt.Errorf("%w: resolve HEAD post-reset: %w", errLocalState, err) + } + return Result{ + HeadSHA: head.Hash().String(), + Changes: changes, + NoChanges: noChanges, + PrevHeadSHA: prevHead.Hash().String(), + }, nil } // computeChangeSet diffs the tree of oldSHA against the tree of newSHA diff --git a/server/internal/repocloner/repocloner_test.go b/server/internal/repocloner/repocloner_test.go index eeff6a87..2b5f5ee9 100644 --- a/server/internal/repocloner/repocloner_test.go +++ b/server/internal/repocloner/repocloner_test.go @@ -3,6 +3,7 @@ package repocloner import ( "context" "fmt" + "math/rand" "os" "path/filepath" "sort" @@ -505,12 +506,33 @@ func TestCloneOrFetch_FreshClone_HasNoTags(t *testing.T) { } } -// TestCloneOrFetch_UpgradeCompactsLegacyCheckout is the no-explicit-migration -// upgrade path: a checkout produced by a PRE-NoTags server (AllTags clone, -// accumulated fetch packs) must be cleaned by the FIRST CloneOrFetch the -// upgraded server runs on it — tags dropped, packs collapsed to one, disk -// reclaimed — while the incremental diff for that same update still computes. -func TestCloneOrFetch_UpgradeCompactsLegacyCheckout(t *testing.T) { +// updateAndCompact drives one update the way repojobs.handleClone does: +// CloneOrFetch, then MaybeCompact with the indexed SHA and the pre-fetch +// HEAD in the protect set. +func updateAndCompact(t *testing.T, upstream, local, indexedSHA string) (Result, *CompactStats) { + t.Helper() + res, err := CloneOrFetch(context.Background(), CloneOptions{ + GitHubURL: "file://" + upstream, + Branch: "main", + LocalDir: local, + PrevIndexedSHA: indexedSHA, + }) + if err != nil { + t.Fatalf("CloneOrFetch: %v", err) + } + st, err := MaybeCompact(context.Background(), local, nil, indexedSHA, res.PrevHeadSHA) + if err != nil { + t.Fatalf("MaybeCompact: %v", err) + } + return res, st +} + +// TestUpgradeCompactsLegacyCheckout is the no-explicit-migration upgrade +// path: a checkout produced by a PRE-NoTags server (AllTags clone, +// accumulated fetch packs) must be cleaned by the FIRST update cycle the +// upgraded server runs on it — even one where the upstream has nothing new — +// and later updates must still get their incremental diffs. +func TestUpgradeCompactsLegacyCheckout(t *testing.T) { upstream, w := makeBareUpstream(t, "main") w.CommitFiles(t, "v1", map[string]string{ "a.go": "package a\n", @@ -538,29 +560,20 @@ func TestCloneOrFetch_UpgradeCompactsLegacyCheckout(t *testing.T) { } objectsBefore := objectsDirSize(local) - // Server upgrades. The next upstream push triggers an ordinary update — - // and that first update must clean the store. - newSHA := w.CommitFiles(t, "v5", map[string]string{"c.go": "package c // v5\n"}) - res, err := CloneOrFetch(context.Background(), CloneOptions{ - GitHubURL: "file://" + upstream, - Branch: "main", - LocalDir: local, - PrevIndexedSHA: indexedSHA, - }) - if err != nil { - t.Fatalf("first post-upgrade CloneOrFetch: %v", err) - } - if res.HeadSHA != newSHA { - t.Errorf("HeadSHA = %s, want %s", res.HeadSHA, newSHA) + // Server upgrades. The first update cycle sees NOTHING new upstream — + // cleanup must not wait for the repo's next commit. + res, st := updateAndCompact(t, upstream, local, indexedSHA) + if !res.NoChanges { + t.Errorf("NoChanges = false on an unchanged upstream") } if res.RecloneReason != "" { t.Errorf("RecloneReason = %q — the upgrade path must compact in place, not re-clone", res.RecloneReason) } - if res.Compaction == nil { - t.Fatal("Compaction stats nil — legacy checkout was not compacted on first update") + if st == nil { + t.Fatal("no compaction on the first post-upgrade update of a legacy checkout") } - if res.Compaction.TagRefsDropped != 2 { - t.Errorf("TagRefsDropped = %d, want 2", res.Compaction.TagRefsDropped) + if st.TagRefsDropped != 2 { + t.Errorf("TagRefsDropped = %d, want 2", st.TagRefsDropped) } if n := tagRefCount(t, local); n != 0 { t.Errorf("%d tag refs survive the upgrade compaction, want 0", n) @@ -571,54 +584,250 @@ func TestCloneOrFetch_UpgradeCompactsLegacyCheckout(t *testing.T) { if after := objectsDirSize(local); after >= objectsBefore { t.Errorf("objects dir did not shrink: %d -> %d bytes", objectsBefore, after) } - // The very update that compacted must still deliver the incremental - // change set (v4 -> v5, computed before the reset). - if res.Changes == nil { - t.Fatal("Changes nil across the compacting update, want incremental diff") + + // The indexed commit survived (it is HEAD here) — the next real update + // must deliver its incremental diff across the compacted store. + newSHA := w.CommitFiles(t, "v5", map[string]string{"c.go": "package c // v5\n"}) + res2, st2 := updateAndCompact(t, upstream, local, indexedSHA) + if res2.HeadSHA != newSHA { + t.Errorf("HeadSHA = %s, want %s", res2.HeadSHA, newSHA) + } + if res2.Changes == nil { + t.Fatal("Changes nil after compaction, want incremental diff") } - if got := sortedCopy(res.Changes.Modified); !equalSlices(got, []string{"c.go"}) { + if got := sortedCopy(res2.Changes.Modified); !equalSlices(got, []string{"c.go"}) { t.Errorf("Modified = %v, want [c.go]", got) } + if st2 != nil { + t.Errorf("compaction ran again on a clean two-pack checkout: %+v", st2) + } - // The protected diff base must survive compaction: pretend the index - // job after the upgrade never completed (indexed_sha still v4), push - // again, and demand a v4-based diff. + // The protected diff base must survive future compactions too: pretend + // the index job never completed (indexed_sha still v4), push again, and + // demand a v4-based diff. newestSHA := w.CommitFiles(t, "v6", map[string]string{"a.go": "package a // v6\n"}) - res2, err := CloneOrFetch(context.Background(), CloneOptions{ - GitHubURL: "file://" + upstream, - Branch: "main", - LocalDir: local, - PrevIndexedSHA: indexedSHA, + res3, _ := updateAndCompact(t, upstream, local, indexedSHA) + if res3.HeadSHA != newestSHA { + t.Errorf("HeadSHA = %s, want %s", res3.HeadSHA, newestSHA) + } + if res3.Changes == nil { + t.Error("Changes nil — protected indexed_sha did not survive") + } +} + +// TestMaybeCompact_ProtectsPendingIndexTarget covers the race the protect +// list exists for: clone cycle A fetched v2 and queued an index job for it, +// but before that job ran, cycle B fetched v3 and compacted. v2 is +// unreferenced by then — only PrevHeadSHA keeps it, and the diff from it +// must still compute afterwards. +func TestMaybeCompact_ProtectsPendingIndexTarget(t *testing.T) { + upstream, w := makeBareUpstream(t, "main") + w.CommitFiles(t, "v1", map[string]string{"a.go": "package a\n"}) + local := filepath.Join(t.TempDir(), "clone") + initialCloneFor(t, upstream, local, "main") + + pendingSHA := w.CommitFiles(t, "v2", map[string]string{"a.go": "package a // v2\n"}) + resA, err := CloneOrFetch(context.Background(), CloneOptions{ + GitHubURL: "file://" + upstream, Branch: "main", LocalDir: local, }) if err != nil { - t.Fatalf("second post-upgrade CloneOrFetch: %v", err) + t.Fatalf("cycle A: %v", err) } - if res2.HeadSHA != newestSHA { - t.Errorf("HeadSHA = %s, want %s", res2.HeadSHA, newestSHA) + if resA.HeadSHA != pendingSHA { + t.Fatalf("cycle A HeadSHA = %s, want %s", resA.HeadSHA, pendingSHA) } - if res2.Changes == nil { - t.Error("Changes nil — protected indexed_sha did not survive compaction") + + w.CommitFiles(t, "v3", map[string]string{"b.go": "package b\n"}) + resB, err := CloneOrFetch(context.Background(), CloneOptions{ + GitHubURL: "file://" + upstream, Branch: "main", LocalDir: local, + }) + if err != nil { + t.Fatalf("cycle B: %v", err) + } + if resB.PrevHeadSHA != pendingSHA { + t.Fatalf("PrevHeadSHA = %s, want the pending index target %s", resB.PrevHeadSHA, pendingSHA) + } + // Compact below threshold on purpose — call the internals directly the + // way MaybeCompact would once the trigger fires. + if _, err := compactCheckout(context.Background(), local, plumbing.NewHash(resB.PrevHeadSHA)); err != nil { + t.Fatalf("compactCheckout: %v", err) } - // And a quiet no-op cycle afterwards: nothing left to clean. - res3, err := CloneOrFetch(context.Background(), CloneOptions{ - GitHubURL: "file://" + upstream, - Branch: "main", - LocalDir: local, - PrevIndexedSHA: newestSHA, + // The pending index job's diff base (v2) must still be usable. + finalSHA := w.CommitFiles(t, "v4", map[string]string{"c.go": "package c\n"}) + resC, err := CloneOrFetch(context.Background(), CloneOptions{ + GitHubURL: "file://" + upstream, Branch: "main", LocalDir: local, + PrevIndexedSHA: pendingSHA, }) if err != nil { - t.Fatalf("no-op CloneOrFetch: %v", err) + t.Fatalf("cycle C: %v", err) } - if !res3.NoChanges { - t.Error("NoChanges = false on an unchanged upstream") + if resC.HeadSHA != finalSHA { + t.Errorf("HeadSHA = %s, want %s", resC.HeadSHA, finalSHA) } - if res3.Compaction != nil { - t.Error("Compaction ran on a clean checkout below the pack threshold") + if resC.Changes == nil { + t.Error("Changes nil — the pending index target was not protected across compaction") } } -func TestCloneOrFetch_PackAccumulationStaysBounded(t *testing.T) { +// TestCloneOrFetch_NoChangesStillRepairsWorktree: a crash mid-hard-reset +// leaves HEAD already moved over half-rewritten files, which a later cycle +// sees as "nothing to do". The NoChanges path must still reset so torn +// content cannot survive indefinitely. +func TestCloneOrFetch_NoChangesStillRepairsWorktree(t *testing.T) { + upstream, w := makeBareUpstream(t, "main") + headSHA := w.CommitFiles(t, "v1", map[string]string{"a.go": "package a\n"}) + local := filepath.Join(t.TempDir(), "clone") + initialCloneFor(t, upstream, local, "main") + + // Simulate the torn state: HEAD is right, a worktree file is not. + torn := filepath.Join(local, "a.go") + if err := os.WriteFile(torn, []byte("package torn\n"), 0o644); err != nil { + t.Fatalf("write torn file: %v", err) + } + + res, err := CloneOrFetch(context.Background(), CloneOptions{ + GitHubURL: "file://" + upstream, Branch: "main", LocalDir: local, + PrevIndexedSHA: headSHA, + }) + if err != nil { + t.Fatalf("CloneOrFetch: %v", err) + } + if !res.NoChanges { + t.Errorf("NoChanges = false, want true (upstream unchanged)") + } + got, err := os.ReadFile(torn) + if err != nil { + t.Fatalf("read repaired file: %v", err) + } + if string(got) != "package a\n" { + t.Errorf("worktree file = %q after NoChanges cycle, want the committed content", got) + } +} + +// TestMaybeCompact_ErrorKeepsCheckout: a compaction failure must leave the +// checkout exactly as the update left it — valid, tags intact (so the +// trigger re-fires) — and must not cascade into a delete or re-clone. +func TestMaybeCompact_ErrorKeepsCheckout(t *testing.T) { + if os.Getuid() == 0 { + t.Skip("root ignores directory permissions") + } + upstream, w := makeBareUpstream(t, "main") + headSHA := w.CommitFiles(t, "v1", map[string]string{"a.go": "package a\n"}) + w.Tag(t, "r1") + local := filepath.Join(t.TempDir(), "clone") + legacyClone(t, upstream, local, "main") + + // Make the pack directory unwritable: the encoder cannot land the new + // pack, which is the earliest (and per the crash-ordering, the safest) + // failure point. + packDir := filepath.Join(local, ".git", "objects", "pack") + if err := os.Chmod(packDir, 0o555); err != nil { + t.Fatalf("chmod: %v", err) + } + t.Cleanup(func() { _ = os.Chmod(packDir, 0o755) }) + + st, err := MaybeCompact(context.Background(), local, nil) + if err == nil { + t.Fatalf("MaybeCompact succeeded against a read-only pack dir, stats=%+v", st) + } + + // Checkout must be untouched and fully usable. + repo, oerr := git.PlainOpen(local) + if oerr != nil { + t.Fatalf("checkout destroyed by failed compaction: %v", oerr) + } + head, herr := repo.Head() + if herr != nil || head.Hash().String() != headSHA { + t.Fatalf("HEAD broken after failed compaction: %v (%v)", head, herr) + } + if n := tagRefCount(t, local); n != 1 { + t.Errorf("tag refs = %d after failed compaction, want 1 — the re-trigger must stay armed", n) + } +} + +// TestMaybeCompact_CancelledContext: shutdown must be able to skip +// compaction entirely; the store stays as-is and nothing is deleted. +func TestMaybeCompact_CancelledContext(t *testing.T) { + upstream, w := makeBareUpstream(t, "main") + w.CommitFiles(t, "v1", map[string]string{"a.go": "package a\n"}) + w.Tag(t, "r1") + local := filepath.Join(t.TempDir(), "clone") + legacyClone(t, upstream, local, "main") + + ctx, cancel := context.WithCancel(context.Background()) + cancel() + st, err := MaybeCompact(ctx, local, nil) + if st != nil || err != nil { + t.Fatalf("MaybeCompact(cancelled) = (%+v, %v), want (nil, nil)", st, err) + } + if n := tagRefCount(t, local); n != 1 { + t.Errorf("tag refs = %d, want 1 — cancelled compaction must not touch the store", n) + } +} + +// TestNeedsCompaction_RatioBackstopRearms covers the crash window where a +// previous compaction dropped the tag refs but died before deleting the old +// packs: no tags, below the pack threshold, yet the store dwarfs the +// worktree. The size-ratio trigger must re-arm cleanup. +func TestNeedsCompaction_RatioBackstopRearms(t *testing.T) { + upstream, w := makeBareUpstream(t, "main") + // Incompressible payloads so zlib cannot hide the retained snapshots. + w.CommitFiles(t, "v1", map[string]string{"blob.bin": randomContent(t, 1, 1<<20)}) + w.Tag(t, "r1") + w.CommitFiles(t, "v2", map[string]string{"blob.bin": randomContent(t, 2, 1<<20)}) + w.Tag(t, "r2") + + local := filepath.Join(t.TempDir(), "clone") + legacyClone(t, upstream, local, "main") + w.CommitFiles(t, "v3", map[string]string{"blob.bin": randomContent(t, 3, 1<<20)}) + legacyFetchReset(t, local, "main") + + // Simulate the crashed compaction: tags durably gone, bloat still here. + repo, err := git.PlainOpen(local) + if err != nil { + t.Fatalf("open: %v", err) + } + for _, name := range []string{"r1", "r2"} { + if err := repo.Storer.RemoveReference(plumbing.ReferenceName("refs/tags/" + name)); err != nil { + t.Fatalf("drop tag %s: %v", name, err) + } + } + if n := packfileCount(local); n < 2 || n >= compactPackThreshold { + t.Fatalf("packfileCount = %d, want in [2, %d) — test setup broken", n, compactPackThreshold) + } + + if !needsCompaction(local) { + t.Fatal("needsCompaction = false on a tagless bloated checkout — the ratio backstop is dead") + } + before := objectsDirSize(local) + st, err := MaybeCompact(context.Background(), local, nil) + if err != nil { + t.Fatalf("MaybeCompact: %v", err) + } + if st == nil { + t.Fatal("MaybeCompact did nothing") + } + if after := objectsDirSize(local); after >= before/2 { + t.Errorf("objects %d -> %d, want the retained snapshots reclaimed", before, after) + } + if needsCompaction(local) { + t.Error("needsCompaction still true after compaction — would loop every update") + } +} + +// randomContent builds deterministic incompressible bytes. +func randomContent(t *testing.T, seed int64, n int) string { + t.Helper() + rnd := rand.New(rand.NewSource(seed)) + b := make([]byte, n) + if _, err := rnd.Read(b); err != nil { + t.Fatalf("rand: %v", err) + } + return string(b) +} + +func TestPackAccumulationStaysBounded(t *testing.T) { upstream, w := makeBareUpstream(t, "main") prev := w.CommitFiles(t, "init", map[string]string{"a.go": "package a\n"}) @@ -630,19 +839,11 @@ func TestCloneOrFetch_PackAccumulationStaysBounded(t *testing.T) { sha := w.CommitFiles(t, fmt.Sprintf("push %d", i), map[string]string{ "a.go": fmt.Sprintf("package a // rev %d\n", i), }) - res, err := CloneOrFetch(context.Background(), CloneOptions{ - GitHubURL: "file://" + upstream, - Branch: "main", - LocalDir: local, - PrevIndexedSHA: prev, - }) - if err != nil { - t.Fatalf("cycle %d: %v", i, err) - } + res, st := updateAndCompact(t, upstream, local, prev) if res.HeadSHA != sha { t.Fatalf("cycle %d: HeadSHA = %s, want %s", i, res.HeadSHA, sha) } - if res.Compaction != nil { + if st != nil { compactions++ } if n := packfileCount(local); n > compactPackThreshold { diff --git a/server/internal/repojobs/repojobs.go b/server/internal/repojobs/repojobs.go index 9e682b83..153e07ac 100644 --- a/server/internal/repojobs/repojobs.go +++ b/server/internal/repojobs/repojobs.go @@ -245,7 +245,28 @@ func handleClone(ctx context.Context, d Deps, job jobs.Job) error { d.Logger.Info("repojobs: checkout discarded and re-cloned", "project", g.ProjectPath, "reason", result.RecloneReason) } - if c := result.Compaction; c != nil { + + // Compaction runs OUTSIDE the write-locked clone section above: + // MaybeCompact serialises all compactions on a global gate (their + // transient heap is ~3× the repo snapshot), and taking that gate while + // holding this repo's write lock would stall this repo's readers behind + // every other repo's compaction. MaybeCompact re-takes the write lock + // itself just for the store mutation. A compaction failure never fails + // the job — the checkout is still exactly what the update left behind, + // and the trigger re-fires on the next update. + // + // The protect list keeps two unreferenced commits alive across the + // rewrite: the indexed diff base, and the pre-fetch HEAD — the latter is + // the TargetSHA of an index job that may still be queued from a previous + // clone cycle. + compactLock := func(f func() error) error { return f() } + if d.RepoLocks != nil { + compactLock = func(f func() error) error { return d.RepoLocks.WithWrite(hash, f) } + } + if c, cerr := repocloner.MaybeCompact(ctx, cloneDir, compactLock, g.IndexedSHA, result.PrevHeadSHA); cerr != nil { + d.Logger.Warn("repojobs: compaction failed; checkout kept as-is", + "project", g.ProjectPath, "err", cerr) + } else if c != nil { d.Logger.Info("repojobs: checkout object store compacted", "project", g.ProjectPath, "bytes_before", c.ObjectsBefore, "bytes_after", c.ObjectsAfter,