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 (
14 "time" 14 "time"
15 15
16 "gitbay.org/gitbay/internal/config" 16 "gitbay.org/gitbay/internal/config"
17 "gitbay.org/gitbay/internal/packlimit"
17 "gitbay.org/gitbay/internal/protocol" 18 "gitbay.org/gitbay/internal/protocol"
18 "gitbay.org/gitbay/internal/store" 19 "gitbay.org/gitbay/internal/store"
19) 20)
@@ -67,6 +68,9 @@ type Ctx struct {
67 // restarting. It closes Done too; a command that ends on Done checks 68 // restarting. It closes Done too; a command that ends on Done checks
68 // it to say why. 69 // it to say why.
69 Stopping <-chan struct{} 70 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
70} 74}
71 75
72// SourceWeb is Ctx.Source for a request from a browser session. Its 76// 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 @@
1package control 1package control
2 2
3import ( 3import (
4 "errors"
4 "io" 5 "io"
6 "strconv"
5 "strings" 7 "strings"
6 8
7 "gitbay.org/gitbay/internal/gitutil" 9 "gitbay.org/gitbay/internal/gitutil"
10 "gitbay.org/gitbay/internal/packlimit"
8 "gitbay.org/gitbay/internal/policy" 11 "gitbay.org/gitbay/internal/policy"
9 "gitbay.org/gitbay/internal/protocol" 12 "gitbay.org/gitbay/internal/protocol"
10) 13)
@@ -117,6 +120,22 @@ func runRepoDownload(c *Ctx, args []string) int {
117 // The prefix git puts on every path inside the archive, so unpacking 120 // The prefix git puts on every path inside the archive, so unpacking
118 // lands in a named directory rather than the current one. 121 // lands in a named directory rather than the current one.
119 prefix := repo.Name + "-" + ref 122 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()
120 if err := gitutil.Archive(dir, ref, prefix, c.Stdout); err != nil { 139 if err := gitutil.Archive(dir, ref, prefix, c.Stdout); err != nil {
121 return c.fail(protocol.ExitFailure, "%v", err) 140 return c.fail(protocol.ExitFailure, "%v", err)
122 } 141 }
internal/httpd/api.go +1
@@ -77,6 +77,7 @@ func (s *Server) apiCmd(w http.ResponseWriter, r *http.Request) {
77 Expires: tok.ExpiresAt, 77 Expires: tok.ExpiresAt,
78 Done: s.until(r), 78 Done: s.until(r),
79 Stopping: s.stopping, 79 Stopping: s.stopping,
80 Packs: s.packs,
80 } 81 }
81 code := control.Dispatch(ctx, req.Argv) 82 code := control.Dispatch(ctx, req.Argv)
82 83
internal/httpd/apiread.go +1
@@ -64,6 +64,7 @@ func (s *Server) apiRead(w http.ResponseWriter, r *http.Request) {
64 ReadOnly: true, 64 ReadOnly: true,
65 Done: s.until(r), 65 Done: s.until(r),
66 Stopping: s.stopping, 66 Stopping: s.stopping,
67 Packs: s.packs,
67 } 68 }
68 code := control.Dispatch(ctx, argv) 69 code := control.Dispatch(ctx, argv)
69 70
internal/sshd/sshd.go +1
@@ -515,6 +515,7 @@ func Exec(cfg config.Config, st *store.Store, packs, pushes *packlimit.Limiter,
515 Done: done, 515 Done: done,
516 Stopping: stopping, 516 Stopping: stopping,
517 Expires: key.ExpiresAt, 517 Expires: key.ExpiresAt,
518 Packs: packs,
518 } 519 }
519 return control.Dispatch(ctx, argv) 520 return control.Dispatch(ctx, argv)
520} 521}