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)