Commit 1ac5d04bdb

1ac5d04bdb7e9d1f53285d58d9eb9b4b1beeca58

parent: 568b879143

Verified · cmc

cmc <hello@cleberg.net> · 2026-09-29 16:32 UTC

control: repo download takes a pack slot

Ref #308

Layout: unified · split

internal/control/control.go +4
@@ -14,6 +14,7 @@ import (
1414 "time"
1515
1616 "gitbay.org/gitbay/internal/config"
17 "gitbay.org/gitbay/internal/packlimit"
1718 "gitbay.org/gitbay/internal/protocol"
1819 "gitbay.org/gitbay/internal/store"
1920)
@@ -67,6 +68,9 @@ type Ctx struct {
6768 // restarting. It closes Done too; a command that ends on Done checks
6869 // it to say why.
6970 Stopping <-chan struct{}
71 // Packs is the pack-generation limiter a command that runs git to
72 // produce an archive takes a slot from; nil is no limit.
73 Packs *packlimit.Limiter
7074}
7175
7276// SourceWeb is Ctx.Source for a request from a browser session. Its
internal/control/download_test.go added +71
@@ -0,0 +1,71 @@
1package control
2
3import (
4 "bytes"
5 "strings"
6 "testing"
7 "time"
8
9 "gitbay.org/gitbay/internal/packlimit"
10 "gitbay.org/gitbay/internal/protocol"
11 "gitbay.org/gitbay/internal/store"
12)
13
14// repo download takes a pack slot: busy while the limiter is full, and
15// the slot is free again once git archive has exited.
16func TestRepoDownloadTakesAPackSlot(t *testing.T) {
17 st, repo, root, _ := prunedRepo(t)
18 alice, err := st.UserByUsername("alice")
19 if err != nil {
20 t.Fatal(err)
21 }
22 packs := packlimit.New(1, 0, 0, time.Second)
23 hold, err := packs.Acquire(nil, "ip:elsewhere")
24 if err != nil {
25 t.Fatal(err)
26 }
27 c, errOut := pruneCtx(st, root, alice)
28 c.Packs = packs
29 if code := Dispatch(c, []string{"repo", "download", repo.Path()}); code != protocol.ExitFailure ||
30 !strings.Contains(errOut.String(), "limit of concurrent clones") {
31 t.Fatalf("busy: exit %d: %q", code, errOut.String())
32 }
33 hold()
34
35 c, errOut = pruneCtx(st, root, alice)
36 c.Packs = packs
37 if code := Dispatch(c, []string{"repo", "download", repo.Path()}); code != protocol.ExitOK {
38 t.Fatalf("free: exit %d: %s", code, errOut.String())
39 }
40 if c.Stdout.(*bytes.Buffer).Len() == 0 {
41 t.Fatal("no archive written")
42 }
43 hold, err = packs.Acquire(nil, "ip:elsewhere")
44 if err != nil {
45 t.Fatalf("slot not released after the download: %v", err)
46 }
47 hold()
48}
49
50// A download the caller may not make is refused before the limiter.
51func TestRepoDownloadRefusalStaysOffLimiter(t *testing.T) {
52 st, repo, root, _ := prunedRepo(t)
53 if err := st.SetRepoVisibility(repo.ID, "private"); err != nil {
54 t.Fatal(err)
55 }
56 bobID, err := st.CreateUser("bob", false)
57 if err != nil {
58 t.Fatal(err)
59 }
60 packs := packlimit.New(1, 0, 0, time.Second)
61 hold, err := packs.Acquire(nil, "ip:elsewhere")
62 if err != nil {
63 t.Fatal(err)
64 }
65 defer hold()
66 c, errOut := pruneCtx(st, root, store.User{ID: bobID, Username: "bob"})
67 c.Packs = packs
68 if code := Dispatch(c, []string{"repo", "download", repo.Path()}); code != protocol.ExitNotFound {
69 t.Fatalf("exit %d: %q", code, errOut.String())
70 }
71}
internal/control/explore.go +19
@@ -1,10 +1,13 @@
11package control
22
33import (
4 "errors"
45 "io"
6 "strconv"
57 "strings"
68
79 "gitbay.org/gitbay/internal/gitutil"
10 "gitbay.org/gitbay/internal/packlimit"
811 "gitbay.org/gitbay/internal/policy"
912 "gitbay.org/gitbay/internal/protocol"
1013)
@@ -117,6 +120,22 @@ func runRepoDownload(c *Ctx, args []string) int {
117120 // The prefix git puts on every path inside the archive, so unpacking
118121 // lands in a named directory rather than the current one.
119122 prefix := repo.Name + "-" + ref
123 // git archive draws on the same budget as clones and web archives,
124 // counted against the account the way upload-pack is.
125 principal := "user:" + strconv.FormatInt(c.User.ID, 10)
126 release, err := c.Packs.Acquire(c.Done, principal)
127 if err != nil {
128 transport := "ssh"
129 if c.ViaAPI {
130 transport = "api"
131 }
132 c.Packs.Refused(transport, principal, err)
133 if errors.Is(err, packlimit.ErrBusy) {
134 return c.fail(protocol.ExitFailure, "the server is busy: it is at its limit of concurrent clones and fetches; try again in a minute")
135 }
136 return c.fail(protocol.ExitFailure, "the server is restarting; try again in a minute")
137 }
138 defer release()
120139 if err := gitutil.Archive(dir, ref, prefix, c.Stdout); err != nil {
121140 return c.fail(protocol.ExitFailure, "%v", err)
122141 }
internal/httpd/api.go +1
@@ -77,6 +77,7 @@ func (s *Server) apiCmd(w http.ResponseWriter, r *http.Request) {
7777 Expires: tok.ExpiresAt,
7878 Done: s.until(r),
7979 Stopping: s.stopping,
80 Packs: s.packs,
8081 }
8182 code := control.Dispatch(ctx, req.Argv)
8283
internal/httpd/apiread.go +1
@@ -64,6 +64,7 @@ func (s *Server) apiRead(w http.ResponseWriter, r *http.Request) {
6464 ReadOnly: true,
6565 Done: s.until(r),
6666 Stopping: s.stopping,
67 Packs: s.packs,
6768 }
6869 code := control.Dispatch(ctx, argv)
6970
internal/sshd/sshd.go +1
@@ -515,6 +515,7 @@ func Exec(cfg config.Config, st *store.Store, packs, pushes *packlimit.Limiter,
515515 Done: done,
516516 Stopping: stopping,
517517 Expires: key.ExpiresAt,
518 Packs: packs,
518519 }
519520 return control.Dispatch(ctx, argv)
520521}