Watch
1
0
Fork
You've already forked pkg-proxy
1
mirror of https://github.com/git-pkgs/proxy.git synced 2026-11-09 07:39:19 -05:00
pkg-proxy/internal/gitcache/budget.go
Simon Chrzanowski 3fdc53d39a Stop relaying for a fetch once it has ended
exec.CommandContext kills only the git parent. Its git-remote-http helper
kept its relay connection and went on downloading for up to
http.lowSpeedTime after a client disconnect, Close or fetchTimeout. Each
read it relayed called cover, which reserved the relayed total again on a
reservation already settled or released, so c.reserved grew for good and
could evict repositories, and the helpers outlived the fetch cap.

Tie each relay route to the fetch's context and cancel it when the fetch
returns, so the upstream request ends and the helper's connection is cut.
Make cover refuse a reservation that has been given back.
2026-11-08 08:02:30 +01:00

139 lines
3.4 KiB
Go

package gitcache
import (
"context"
"errors"
"sync/atomic"
"time"
)
const (
// maxReserveChunk caps the space a fetch reserves ahead of what it has
// received, so the cache is not locked for every read. Each fetch takes
// at most a sixteenth of MaxSize ahead, so fetches share a small cache.
maxReserveChunk = 4 << 20
reserveChunks = 16
// cutWait bounds how long a fetch waits for one that was cut off to
// give back its space.
cutWait = time.Minute
)
// reservation holds space within MaxSize for the bytes a fetch receives,
// until they are counted in the repository's size. Fetches still running
// therefore count toward MaxSize, so the cache stays within it however many
// run at once.
type reservation struct {
c *Cache
// reserved is written under c.mu; reads that find it large enough skip
// the lock.
reserved atomic.Int64
// cut, guarded by c.mu, marks a fetch stopped because the cache could
// not make room for it.
cut bool
// ended, guarded by c.mu, marks a reservation given back. A read
// relayed after that, by a git helper the fetch left behind, reserves
// nothing.
ended bool
}
// errReservationEnded is returned by cover once the fetch has ended.
var errReservationEnded = errors.New("fetch has ended")
func (c *Cache) newReservation() *reservation {
return &reservation{c: c}
}
// cover reserves space for a fetch that has received relayed bytes,
// evicting idle repositories as needed. It returns ErrCacheFull when evicting
// every idle repository would not make room, unless another fetch has just
// been cut off, whose space it waits for: one cut off waits for nothing, so
// two fetches that do not fit together do not both fail.
func (r *reservation) cover(ctx context.Context, relayed int64) error {
c := r.c
if c.opts.MaxSize <= 0 || relayed <= r.reserved.Load() {
return nil
}
var timeout <-chan time.Time
for {
c.mu.Lock()
if r.ended {
c.mu.Unlock()
return errReservationEnded
}
if r.cut {
c.mu.Unlock()
return ErrCacheFull
}
need := relayed - r.reserved.Load()
removed, total := c.evictLocked(need)
fits := total+need <= c.opts.MaxSize
var freed chan struct{}
switch {
case fits:
chunk := min(maxReserveChunk, c.opts.MaxSize/reserveChunks)
n := min(need+chunk, c.opts.MaxSize-total)
r.reserved.Add(n)
c.reserved += n
case c.cutPending > 0:
freed = c.freed
default:
r.cut = true
c.cutPending++
}
c.mu.Unlock()
c.removeEvicted(removed)
if fits {
return nil
}
if freed == nil {
return ErrCacheFull
}
if timeout == nil {
timeout = time.After(cutWait)
}
select {
case <-freed:
case <-timeout:
c.mu.Lock()
r.cut = true
c.cutPending++
c.mu.Unlock()
return ErrCacheFull
case <-ctx.Done():
return ctx.Err()
}
}
}
// settle counts what the fetch left in the repository's size and gives back
// its reservation together, so the bytes are never counted twice or not at
// all.
func (r *reservation) settle(rp *repo, size int64) {
r.c.mu.Lock()
rp.size.Store(size)
r.releaseLocked()
r.c.mu.Unlock()
}
// release gives back the reservation of a fetch that left nothing behind.
// It may follow settle.
func (r *reservation) release() {
r.c.mu.Lock()
r.releaseLocked()
r.c.mu.Unlock()
}
func (r *reservation) releaseLocked() {
c := r.c
r.ended = true
n := r.reserved.Swap(0)
if r.cut {
r.cut = false
c.cutPending--
} else if n == 0 {
return
}
c.reserved -= n
close(c.freed)
c.freed = make(chan struct{})
}