Range-diff !542
back to !542 limits: bound concurrent pushes; repo download under the pack limit
-: ------- > 1: 9737977 config: push_concurrency, push_per_principal, push_queue, push_queue_wait
1: c855a67 ! 2: 568b879 sshd: receive-pack takes a slot from its own push limit
@@ cmd/gitbayd/main.go: func serveCmd() *cobra.Command {
if packMax > 1 {
packs.CapClass("ip:", packMax-1)
}
-+ // receive-pack has a budget of its own.
-+ pushes := packlimit.New(cfg.Limits.PushLimits())
++ // receive-pack has a budget of its own. Parallel pushes
++ // from one account (scripts, bots, several terminals)
++ // queue, up to half the queue, rather than being refused
++ // once one is waiting.
++ pushMax, pushPer, pushQueue, pushWait := cfg.Limits.PushLimits()
++ pushes := packlimit.New(pushMax, pushPer, pushQueue, pushWait)
+ pushes.Name("push")
++ pushes.CapQueue(pushPerQueue(pushQueue))
errCh := make(chan error, 3)
var sshSrv *sshd.Server
@@ cmd/gitbayd/main.go: func serveCmd() *cobra.Command {
if err != nil {
return err
}
+@@ cmd/gitbayd/main.go: func reapPending(ctx context.Context, st *store.Store, maxAge time.Duration) {
+ }
+ }
+ }
++
++// pushPerQueue is how many pushes one principal may have waiting: half
++// the queue, at least one.
++func pushPerQueue(queue int) int {
++ return max(1, queue/2)
++}
## cmd/gitbayd/system.go ##
@@ cmd/gitbayd/system.go: func shellCmd() *cobra.Command {
@@ cmd/gitbayd/system.go: func shellCmd() *cobra.Command {
os.Exit(code)
return nil
+ ## internal/packlimit/packlimit.go ##
+@@ internal/packlimit/packlimit.go: var (
+
+ type Limiter struct {
+ max, per, queue int
++ perQueue int // waiting, per principal; per unless set
+ wait time.Duration
+ name string // what is limited, for the refusal log
+
+@@ internal/packlimit/packlimit.go: func New(max, per, queue int, wait time.Duration) *Limiter {
+ if max <= 0 {
+ return nil
+ }
+- return &Limiter{max: max, per: per, queue: queue, wait: wait, name: "pack",
++ return &Limiter{max: max, per: per, perQueue: per, queue: queue, wait: wait, name: "pack",
+ held: map[string]int{}, waiting: map[string]int{}, changed: make(chan struct{}),
+ warned: map[string]time.Time{}}
+ }
+@@ internal/packlimit/packlimit.go: func (l *Limiter) Name(name string) {
+ l.name = name
+ }
+
++// CapQueue lets one principal have up to n requests waiting, where by
++// default it may have as many as it may run. It applies only while a
++// per-principal cap is set. Call it before the limiter is in use.
++func (l *Limiter) CapQueue(n int) {
++ if l == nil {
++ return
++ }
++ l.perQueue = n
++}
++
+ // CapClass caps the slots that principals starting with prefix may hold
+ // between them. Call it before the limiter is in use.
+ func (l *Limiter) CapClass(prefix string, n int) {
+@@ internal/packlimit/packlimit.go: func (l *Limiter) Acquire(done <-chan struct{}, principal string) (release func(
+ l.mu.Unlock()
+ return l.releaser(principal), nil
+ }
+- if l.queued >= l.queue || (l.per > 0 && l.waiting[principal] >= l.per) {
++ if l.queued >= l.queue || (l.per > 0 && l.waiting[principal] >= l.perQueue) {
+ l.mu.Unlock()
+ return nil, ErrBusy
+ }
+
+ ## internal/packlimit/packlimit_test.go ##
+@@ internal/packlimit/packlimit_test.go: func TestRefusedNamesTheLimit(t *testing.T) {
+ var none *Limiter
+ none.Name("push")
+ }
++
++// With a waiting cap above per, one principal runs per and queues up to
++// perQueue, and is refused past that.
++func TestPerPrincipalQueueCap(t *testing.T) {
++ l := New(4, 1, 16, 5*time.Second)
++ l.CapQueue(4)
++ r, err := l.Acquire(nil, "a")
++ if err != nil {
++ t.Fatal(err)
++ }
++ for i := 1; i <= 4; i++ {
++ go func() {
++ if r, err := l.Acquire(nil, "a"); err == nil {
++ r()
++ }
++ }()
++ waitQueued(t, l, i)
++ }
++ if _, err := l.Acquire(nil, "a"); !errors.Is(err, ErrBusy) {
++ t.Fatalf("sixth for a: %v", err)
++ }
++ rb, err := l.Acquire(nil, "b")
++ if err != nil {
++ t.Fatalf("b blocked by a's queue: %v", err)
++ }
++ rb()
++ r()
++}
+
## internal/sshd/refusal_test.go ##
@@ internal/sshd/refusal_test.go: package sshd
2: 46a6efd = 3: 1ac5d04 control: repo download takes a pack slot
3: 54db77f = 4: 03e34fb sshd: cut a push idle for push_idle or not at pre-receive by push_receive_timeout
4: 932b8bb = 5: d4ea40c httpd: a command a limiter turned away is 503 with Retry-After on the API
5: 59a7d29 ! 6: db01bf4 wiki, changelog: push limit and repo download under the pack limit
@@ .gitbay/wiki/Admin.org: push=.
+ =push_queue_wait= (="60s"=) — =git receive-pack= over SSH, on a
+ budget of its own so clones cannot starve pushes or the reverse:
+ this many at once, this many per account or deploy key, and this
-+ many waiting for at most the wait, with no more than
-+ =push_per_principal= of them from one principal. A deploy key is its
-+ own principal, apart from the account that registered it, so an
-+ account with deploy keys can hold =push_per_principal= slots per key;
++ many waiting for at most the wait. One principal may have up to half
++ of =push_queue= (at least one) waiting, so parallel pushes from one
++ account queue rather than being refused, but cannot fill the queue.
++ A deploy key is its own principal, apart from the account that
++ registered it, so an account with deploy keys can hold
++ =push_per_principal= slots per key;
+ the global cap still holds. A bot account such as a runner's counts
+ like any other account. The slot is taken after the access checks and held
+ until receive-pack exits, which is after =post-receive= (merge
@@ CHANGELOG.org: Versioning follows semver from v0.1.0. Database migrations run
+- Pushes have a concurrency limit of their own, separate from the pack
+ budget: =[limits] push_concurrency= (2), =push_per_principal= (1),
+ =push_queue= (16) and =push_queue_wait= ("60s"), read like the
-+ =pack_*= settings. A deploy key counts apart from the account that
-+ registered it. The slot is held until receive-pack and its
++ =pack_*= settings. One principal may have up to half of =push_queue=
++ waiting, so parallel pushes from one account queue. A deploy key
++ counts apart from the account that registered it. The slot is held until receive-pack and its
+ post-receive hook have finished; a busy push exits 1 with "the server
+ is busy: it is at its limit of concurrent pushes…", and the daemon
+ logs a =push limit= warning at most once a minute. (#308)