internal/packlimit/packlimit.go

bd49b87fce895e9f0a7588152548fb6e1821ac7d
gitbay/internal/packlimit/packlimit.go history · blame · raw

177 lines · 4775 bytes

  1// Package packlimit bounds concurrent git pack generation. upload-pack
  2// and upload-archive over SSH, smart HTTP and git:// draw on one
  3// budget: a global cap, a cap per principal (an account, or a client
  4// address on the anonymous transports), and a bounded queue whose
  5// waiters give up after a fixed wait or when the client goes away.
  6// Waiters are not served in order; a new arrival can take a freed slot
  7// ahead of them, and the wait bounds how long any one of them waits.
  8package packlimit
  9
 10import (
 11	"errors"
 12	"net/netip"
 13	"strings"
 14	"sync"
 15	"time"
 16)
 17
 18var (
 19	ErrBusy = errors.New("the server is busy generating packs for other clients; try again in a minute")
 20	ErrGone = errors.New("client went away while queued")
 21)
 22
 23type Limiter struct {
 24	max, per, queue int
 25	wait            time.Duration
 26
 27	// Principals starting with class may hold at most classCap slots
 28	// between them; classCap 0 is no class cap.
 29	class    string
 30	classCap int
 31
 32	mu        sync.Mutex
 33	running   int
 34	classHeld int
 35	queued    int
 36	held      map[string]int // running, per principal
 37	waiting   map[string]int // queued, per principal
 38	changed   chan struct{}  // closed and replaced on every release
 39}
 40
 41// New returns a limiter, or nil — no limit — when max is not positive.
 42func New(max, per, queue int, wait time.Duration) *Limiter {
 43	if max <= 0 {
 44		return nil
 45	}
 46	return &Limiter{max: max, per: per, queue: queue, wait: wait,
 47		held: map[string]int{}, waiting: map[string]int{}, changed: make(chan struct{})}
 48}
 49
 50// CapClass caps the slots that principals starting with prefix may hold
 51// between them. Call it before the limiter is in use.
 52func (l *Limiter) CapClass(prefix string, n int) {
 53	if l == nil {
 54		return
 55	}
 56	l.class, l.classCap = prefix, n
 57}
 58
 59// AddrPrincipal is the principal for an unauthenticated client at addr:
 60// an IPv4 address as is, an IPv6 address by its /64, since one host
 61// commonly holds a whole /64. An address that does not parse is used
 62// as given.
 63func AddrPrincipal(addr string) string {
 64	a, err := netip.ParseAddr(addr)
 65	if err != nil {
 66		return "ip:" + addr
 67	}
 68	a = a.WithZone("").Unmap()
 69	if a.Is4() {
 70		return "ip:" + a.String()
 71	}
 72	return "ip:" + netip.PrefixFrom(a, 64).Masked().String()
 73}
 74
 75// Acquire takes a slot for principal, queueing when none is free.
 76// done, when it closes, ends the wait. Once Acquire returns a nil
 77// error, the caller holds the slot and must call release — once git
 78// has exited — regardless of what its own context has done since:
 79// done closing after that point does not release the slot on the
 80// caller's behalf.
 81func (l *Limiter) Acquire(done <-chan struct{}, principal string) (release func(), err error) {
 82	if l == nil {
 83		return func() {}, nil
 84	}
 85	l.mu.Lock()
 86	if l.fits(principal) {
 87		l.take(principal)
 88		l.mu.Unlock()
 89		return l.releaser(principal), nil
 90	}
 91	if l.queued >= l.queue || (l.per > 0 && l.waiting[principal] >= l.per) {
 92		l.mu.Unlock()
 93		return nil, ErrBusy
 94	}
 95	l.queued++
 96	l.waiting[principal]++
 97	l.mu.Unlock()
 98	defer func() {
 99		l.mu.Lock()
100		l.queued--
101		if l.waiting[principal]--; l.waiting[principal] == 0 {
102			delete(l.waiting, principal)
103		}
104		l.mu.Unlock()
105	}()
106
107	timer := time.NewTimer(l.wait)
108	defer timer.Stop()
109	for {
110		l.mu.Lock()
111		// changed and done can both be ready at once — a slot can
112		// free up at the same moment the caller gives up. select
113		// among the wake sources would then pick between them at
114		// random, so re-check done first, under the lock, on every
115		// pass: this makes the limiter prefer ErrGone whenever both
116		// are ready, instead of leaving it to chance which one a
117		// given pass observes.
118		select {
119		case <-done:
120			l.mu.Unlock()
121			return nil, ErrGone
122		default:
123		}
124		if l.fits(principal) {
125			l.take(principal)
126			l.mu.Unlock()
127			return l.releaser(principal), nil
128		}
129		changed := l.changed
130		l.mu.Unlock()
131		select {
132		case <-changed:
133		case <-timer.C:
134			return nil, ErrBusy
135		case <-done:
136			return nil, ErrGone
137		}
138	}
139}
140
141func (l *Limiter) fits(principal string) bool {
142	if l.inClass(principal) && l.classHeld >= l.classCap {
143		return false
144	}
145	return l.running < l.max && (l.per <= 0 || l.held[principal] < l.per)
146}
147
148func (l *Limiter) inClass(principal string) bool {
149	return l.classCap > 0 && strings.HasPrefix(principal, l.class)
150}
151
152func (l *Limiter) take(principal string) {
153	l.running++
154	l.held[principal]++
155	if l.inClass(principal) {
156		l.classHeld++
157	}
158}
159
160func (l *Limiter) releaser(principal string) func() {
161	var once sync.Once
162	return func() {
163		once.Do(func() {
164			l.mu.Lock()
165			defer l.mu.Unlock()
166			l.running--
167			if l.inClass(principal) {
168				l.classHeld--
169			}
170			if l.held[principal]--; l.held[principal] == 0 {
171				delete(l.held, principal)
172			}
173			close(l.changed)
174			l.changed = make(chan struct{})
175		})
176	}
177}