Commit ee55b8c0ca

ee55b8c0ca849c27d1c5afb6a57d210827cf3531

parent: 99c5044cba

Verified · cmc

cmc <hello@cleberg.net> · 2026-09-28 21:47 UTC

config: pack_concurrency, pack_per_principal, pack_queue, pack_queue_wait

Ref #262

Layout: unified · split

internal/config/config.go +45
@@ -24,6 +24,15 @@ import (
24// and webhook deliveries. 24// and webhook deliveries.
25const DefaultWriteRate = 60 25const DefaultWriteRate = 60
26 26
27// Pack generation defaults for a four-core host: a full clone of a large
28// repository runs git at about 1.5 cores (Performance wiki page).
29const (
30 DefaultPackConcurrency = 3
31 DefaultPackPerPrincipal = 2
32 DefaultPackQueue = 32
33 DefaultPackQueueWait = time.Minute
34)
35
27type Config struct { 36type Config struct {
28 Server Server `toml:"server"` 37 Server Server `toml:"server"`
29 SSH SSH `toml:"ssh"` 38 SSH SSH `toml:"ssh"`
@@ -214,6 +223,37 @@ type Limits struct {
214 // account. 223 // account.
215 MaxReposPerUser int `toml:"max_repos_per_user"` 224 MaxReposPerUser int `toml:"max_repos_per_user"`
216 MaxBytesPerUser int64 `toml:"max_bytes_per_user"` 225 MaxBytesPerUser int64 `toml:"max_bytes_per_user"`
226 // PackConcurrency caps git pack generation (upload-pack and
227 // upload-archive) running at once across SSH, smart HTTP and git://.
228 // PackPerPrincipal caps it per account, or per client address on the
229 // anonymous transports. PackQueue is how many may wait for a slot,
230 // for at most PackQueueWait ("60s"). For the three counts 0 takes the
231 // default and a negative value turns that bound off.
232 PackConcurrency int `toml:"pack_concurrency"`
233 PackPerPrincipal int `toml:"pack_per_principal"`
234 PackQueue int `toml:"pack_queue"`
235 PackQueueWait string `toml:"pack_queue_wait"`
236}
237
238// PackLimits resolves the pack_* settings for packlimit.New. A zero
239// count is no bound.
240func (l Limits) PackLimits() (max, per, queue int, wait time.Duration) {
241 pick := func(v, def int) int {
242 switch {
243 case v == 0:
244 return def
245 case v < 0:
246 return 0
247 }
248 return v
249 }
250 wait = DefaultPackQueueWait
251 if d, err := time.ParseDuration(l.PackQueueWait); err == nil && d > 0 {
252 wait = d
253 }
254 return pick(l.PackConcurrency, DefaultPackConcurrency),
255 pick(l.PackPerPrincipal, DefaultPackPerPrincipal),
256 pick(l.PackQueue, DefaultPackQueue), wait
217} 257}
218 258
219type Mail struct { 259type Mail struct {
@@ -444,6 +484,11 @@ func (c Config) Validate() error {
444 if c.Limits.MaxReposPerUser < 0 || c.Limits.MaxBytesPerUser < 0 || c.Limits.MaxSnippetsPerUser < 0 { 484 if c.Limits.MaxReposPerUser < 0 || c.Limits.MaxBytesPerUser < 0 || c.Limits.MaxSnippetsPerUser < 0 {
445 errs = append(errs, errors.New("limits.max_repos_per_user, max_bytes_per_user and max_snippets_per_user must not be negative")) 485 errs = append(errs, errors.New("limits.max_repos_per_user, max_bytes_per_user and max_snippets_per_user must not be negative"))
446 } 486 }
487 if w := c.Limits.PackQueueWait; w != "" {
488 if d, err := time.ParseDuration(w); err != nil || d <= 0 {
489 errs = append(errs, fmt.Errorf("limits.pack_queue_wait %q must be a positive duration such as 60s", w))
490 }
491 }
447 if c.Push.Enabled { 492 if c.Push.Enabled {
448 for _, f := range []struct{ name, val string }{ 493 for _, f := range []struct{ name, val string }{
449 {"push.key_file", c.Push.KeyFile}, 494 {"push.key_file", c.Push.KeyFile},
internal/config/config_test.go +21
@@ -12,6 +12,7 @@ import (
12 "testing" 12 "testing"
13 13
14 "filippo.io/age" 14 "filippo.io/age"
15 "time"
15) 16)
16 17
17func writeConfig(t *testing.T, body string) string { 18func writeConfig(t *testing.T, body string) string {
@@ -52,6 +53,11 @@ func TestContradictions(t *testing.T) {
52 body string 53 body string
53 wantErr string 54 wantErr string
54 }{ 55 }{
56 {
57 "bad pack_queue_wait",
58 minimal + "\n[limits]\npack_queue_wait = \"soon\"\n",
59 "limits.pack_queue_wait",
60 },
55 { 61 {
56 "registration open without smtp", 62 "registration open without smtp",
57 minimal + "\n[registration]\nmode = \"open\"\n", 63 minimal + "\n[registration]\nmode = \"open\"\n",
@@ -389,3 +395,18 @@ func TestBackupRecipients(t *testing.T) {
389 t.Fatalf("default: %v, %v", cfg.Backup, err) 395 t.Fatalf("default: %v, %v", cfg.Backup, err)
390 } 396 }
391} 397}
398
399func TestPackLimits(t *testing.T) {
400 max, per, queue, wait := Limits{}.PackLimits()
401 if max != DefaultPackConcurrency || per != DefaultPackPerPrincipal || queue != DefaultPackQueue || wait != DefaultPackQueueWait {
402 t.Fatalf("defaults: %d %d %d %s", max, per, queue, wait)
403 }
404 max, per, queue, wait = Limits{PackConcurrency: -1, PackPerPrincipal: -1, PackQueue: -1, PackQueueWait: "5s"}.PackLimits()
405 if max != 0 || per != 0 || queue != 0 || wait != 5*time.Second {
406 t.Fatalf("off: %d %d %d %s", max, per, queue, wait)
407 }
408 max, per, queue, _ = Limits{PackConcurrency: 8, PackPerPrincipal: 3, PackQueue: 64}.PackLimits()
409 if max != 8 || per != 3 || queue != 64 {
410 t.Fatalf("set: %d %d %d", max, per, queue)
411 }
412}